์ ๋ฒ ๊ฒ์๊ธ์์ Home Assistant ๊ธฐ๋ณธ ๊ตฌ์ถ๊ณผ ๋๋ถ์ด, ๊ธฐ๋ณธ์ ์ธ ๋์๋ณด๋ ์ค์ ์ ํด์ฃผ์๋ค.
์ด๋ฒ์, Smartthings ํ๋ฌ๊ทธ์ธ์ด ์ปค๋ฒํ์ง ๋ชปํ๋ Galaxy Home mini์ IR ์ผ์ ๋์๋ค์ ๊ตฌํํด๋ณด๊ฒ ๋ค.
Smartthings Extra
ํ๋ฌ๊ทธ์ธ์ ์ ์ํ๊ธฐ์ ์ ๋ฒค์น๋งํน ํ๊ธฐ ์ข์ ํ๋ฌ๊ทธ์ธ์ ๋ฐ๊ฒฌํ๋ค.
Smartthings Extra๋ผ๋ ํ๋ฌ๊ทธ์ธ์ด์๋๋ฐ.
Smartthings ๊ณต์ ํ๋ฌ๊ทธ์ธ์ OAuth ์ธ์ฆ์ ๋ณด๋ฅผ ๊ทธ๋๋ก ํ์ฉํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ๋ค.
https://github.com/mik-laj/ha_smartthing_extra
GitHub - mik-laj/ha_smartthing_extra
Contribute to mik-laj/ha_smartthing_extra development by creating an account on GitHub.
github.com
์ด ๋ฐฉ์์ ๊ทธ๋๋ก ์ฌ์ฉํ๋ ค ํ์ง๋ง, ์ด ํ๋ฌ๊ทธ์ธ ์ญ์ deprecated๋ IR ๋์์ ์ง์ํ์ง ์์๋ค.
๋ฐ๋ผ์ ํ์ํ ๋ถ๋ถ๋ง ๋ฒค์น๋งํนํด์ ์ฌ์ฉํ๋ ค๊ณ ํ๋ค.
์ฝ๋์์ ํ์ํ ๋ถ๋ถ์ ์ฐพ์๋ณด์
์๋๋ ๊ณต์ ํ๋ฌ๊ทธ์ธ์ OAuth ํด๋ผ์ด์ธํธ ์ ์ฅ์์น๋ฅผ ์ฐพ๋ ๋ก์ง์ด๋ค.
st_entry_id: Optional[str] = None
for entry_id in device_entry.config_entries:
entry = hass.config_entries.async_get_entry(entry_id)
if entry and entry.domain == ST_DOMAIN and entry.state == ConfigEntryState.LOADED:
st_entry_id = entry.entry_id
break
๊ทธ๋ฆฌ๊ณ OAuth ํด๋ผ์ด์ธํธ๋ฅผ ํ๋ํ๋ ๋ก์ง
entry: ConfigEntry | None = hass.config_entries.async_get_entry(st_entry_id)
if not entry or not entry.runtime_data:
raise HomeAssistantError("SmartThings entry not available or not loaded.")
client = entry.runtime_data.client
๋ง์ง๋ง์ผ๋ก, device id์ ๋ช ๋ น ์ ์ก ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์๋ค.
async def _resolve_st_ids(ha_device_id: str) -> Tuple[str, str]:
dev_reg = dr.async_get(hass)
device_entry = dev_reg.async_get(ha_device_id)
if not device_entry:
raise HomeAssistantError(f"No HA device found: {ha_device_id}")
st_device_id: Optional[str] = None
for domain, ident in device_entry.identifiers:
if domain == ST_DOMAIN:
st_device_id = ident
break
st_entry_id: Optional[str] = None
for entry_id in device_entry.config_entries:
entry = hass.config_entries.async_get_entry(entry_id)
if entry and entry.domain == ST_DOMAIN and entry.state == ConfigEntryState.LOADED:
st_entry_id = entry.entry_id
break
return st_device_id, st_entry_id
์ด์ ํ์ํ ๋ก์ง์ ํ์ ํ์ผ๋ ์ปค์คํ ํ๋ฌ๊ทธ์ธ ์ ์์ ํด๋ณด์
ํ๋ฌ๊ทธ์ธ ๊ตฌํ
๋จผ์ ํตํฉ ์ ๋ณด์ธ manifest.json์ ๊ตฌํํ์
ํ์ํ aiohttp์, ๋ช ๋ น ์ ์ก์ ์ํ pysmartthings ํจํค์ง๋ฅผ ์์ฑํด ์ฃผ์๋ค.
{
"domain": "smartthings_ir",
"name": "SmartThings IR Controller",
"version": "1.0.0",
"requirements": ["aiohttp>=3.8.0", "pysmartthings>=0.8.2"],
"dependencies": [],
"codeowners": ["@101Sean"],
"config_flow": true,
"iot_class": "cloud_polling"
}
์ง์ ์ ์ธ __init__.py, Home Assistant๊ฐ ๋ก๋ํ ๋ ๊ฐ์ฅ ๋จผ์ ์คํํ๋ ํ์ผ์ด๋ค.
async_setup_entry ๋ฉ์๋๋ก climate์ media_player๋ฅผ ๋ก๋ํ๊ฒ ํด์ฃผ์๋ค. (ํ์ํ ์ ์ธ์๋ฆฌ)
PLATFORMS = [Platform.CLIMATE, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = entry.data
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
์ด์ Extra์ ํต์ฌ ๋ก์ง์ ๊ตฌํํ api.py๋ฅผ ๊ตฌํํ์
๊ณต์ ํ๋ฌ๊ทธ์ธ์ OAuth ์ธ์ฆ์ ์ฌ์ฌ์ฉ ํ๊ณ , device_id ๋ณํ๊ณผ capability ๋งคํ์ ํ ๊ฒ์ด๋ค.
device_id๋ฅผ Smartthings UUID๋ก ๋ณํํ๋ ๋ฉ์๋์ด๋ค.
๊ธฐ๊ธฐ ์ ๋ณด ์กฐํ > UUID ์ถ์ถ > Entry ID๋ฅผ ์ฐพ์ ๋ณํ ํ๋ ์์์ด๋ค.
async def _resolve_st_ids(self, ha_device_id: str) -> Tuple[str, str]:
dev_reg = dr.async_get(self.hass)
device_entry = dev_reg.async_get(ha_device_id)
if not device_entry:
raise HomeAssistantError(f"No HA device found: {ha_device_id}")
st_device_id: Optional[str] = None
for domain, ident in device_entry.identifiers:
if domain == ST_DOMAIN:
st_device_id = ident
break
if not st_device_id:
raise HomeAssistantError("Device is not linked to SmartThings.")
st_entry_id: Optional[str] = None
for entry_id in device_entry.config_entries:
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry and entry.domain == ST_DOMAIN and entry.state == ConfigEntryState.LOADED:
st_entry_id = entry.entry_id
break
if not st_entry_id:
raise HomeAssistantError("No loaded SmartThings config entry for this device.")
return st_device_id, st_entry_id
๋ค์์ผ๋ก๋, ๊ณต์ ํ๋ฌ๊ทธ์ธ์์ OAuth ์ธ์ฆ๋ API ํด๋ผ์ด์ธํธ๋ฅผ ํ๋ํ๋ ๋ฉ์๋์ด๋ค.
async def _get_client(self, st_entry_id: str):
entry = self.hass.config_entries.async_get_entry(st_entry_id)
if not entry or not entry.runtime_data:
raise HomeAssistantError("SmartThings entry not available or not loaded.")
return entry.runtime_data.client
๋ง์ง๋ง์ผ๋ก๋, ์ค์ Smartthings ๊ธฐ๊ธฐ์ ๋ช ๋ น ์ ์กํ๋ ๋ฉ์๋์ด๋ค.
special_caps ๋งคํ์ผ๋ก, deprecated๋ IR ๋ช ๋ น๋ค๋ ์ฌ์ฉํ ์ ์๊ฒ ํด์ฃผ์๋ค.
async def command(
self,
ha_device_id: str,
capability: str,
command: str,
arguments: Optional[list] = None,
component: str = "main",
) -> bool:
try:
st_device_id, st_entry_id = await self._resolve_st_ids(ha_device_id)
client = await self._get_client(st_entry_id)
special_caps = {
"STATELESS_AUDIO_VOLUME_BUTTON": "statelessAudioVolumeButton",
"STATELESS_CHANNEL_BUTTON": "statelessChannelButton",
"STATELESS_AUDIO_MUTE_BUTTON": "statelessAudioMuteButton",
"STATELESS_POWER_TOGGLE_BUTTON": "statelessPowerToggleButton",
}
upper_input = capability.upper()
if upper_input in special_caps:
mapped_capability = special_caps[upper_input]
else:
mapped_capability = getattr(Capability, upper_input, capability)
mapped_command = getattr(Command, command.upper(), command)
_LOGGER.debug(
"Command -> device=%s, capability=%s, command=%s, args=%s",
st_device_id, mapped_capability, mapped_command, arguments
)
await client.execute_device_command(
device_id=st_device_id,
capability=mapped_capability,
command=mapped_command,
component=component,
argument=arguments or [],
)
return True
except Exception as e:
_LOGGER.error("Command failed: %s", e)
return False
์ด์ ์์ด์ปจ์ ์ ์ด ํ climate.py๋ฅผ ๊ตฌํํ์.
IR๋ก ์ ์ดํ๋ ์์ด์ปจ์ Home Assistant์ Climate ์ํฐํฐ๋ก ๋ณํ ํ ๊ฒ์ด๋ค.
https://www.home-assistant.io/integrations/climate/
Climate
Instructions on how to set up climate control devices within Home Assistant.
www.home-assistant.io
๊ธฐ๋ณธ ๊ธฐ๊ธฐ ์ ๋ณด ์ ์ฅ์ ํ ํด๋์ค๋ฅผ ์ ์ธํ๊ณ
class SmartThingsIRClimate(ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_min_temp = 18
_attr_max_temp = 30
_attr_target_temperature_step = 1
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
_attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.DRY, HVACMode.AUTO]
_attr_fan_modes = ["low", "medium", "high"]
def __init__(self, hass, device_id, name):
self.hass = hass
self._device_id = device_id
self._attr_name = name
self._attr_unique_id = f"smartthings_ir_climate_{device_id}"
self._attr_target_temperature = 24
self._attr_hvac_mode = HVACMode.OFF
self._attr_fan_mode = "medium"
self._api = SmartThingsIRAPI(hass)
์์ด์ปจ ๋ชจ๋ ๋ณ๊ฒฝ ๋ฉ์๋ ๋ถ๋ถ์ ๊ตฌํํ๋ค.
์์ด์ปจ์ ๋ชจ๋(๋๋ฐฉ, ์ ์ต, ์๋)๋ฑ๊ณผ on, off๋ฅผ ๋งคํ
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
if hvac_mode == HVACMode.OFF:
await self._send_command("switch", "off")
else:
await self._send_command("switch", "on")
mode_map = {
HVACMode.COOL: "cool",
HVACMode.DRY: "dry",
HVACMode.AUTO: "auto"
}
await self._send_command("AIR_CONDITIONER_MODE", "SET_AIR_CONDITIONER_MODE", [mode_map.get(hvac_mode, "cool")])
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()
๋ค์์ผ๋ก๋ ์จ๋ ์ค์ ๋ฉ์๋
async def async_set_temperature(self, **kwargs) -> None:
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is not None:
self._attr_target_temperature = temp
await self._send_command("THERMOSTAT_COOLING_SETPOINT", "SET_COOLING_SETPOINT", [temp])
self.async_write_ha_state()
๊ทธ๋ฆฌ๊ณ ํฌ ์๋ ๋ณ๊ฒฝ ๋ฉ์๋๊น์ง ๊ตฌํ ํ๋ฉด, ๋์ ๊ตฌํ์ ๋์ด๋ค.
async def async_set_fan_mode(self, fan_mode: str) -> None:
self._attr_fan_mode = fan_mode
await self._send_command("AIR_CONDITIONER_FAN_MODE", "SET_FAN_MODE", [fan_mode])
self.async_write_ha_state()
๋ง์ง๋ง์ผ๋ก API ํธ์ถ์ ์ํ ๋ฉ์๋ ๊น์ง ๊ตฌํ
capability, command, argument๋ก Smartthings API ๊ท์ฝ์ ๋ง๊ฒ ๊ตฌํํ๋ค.
async def _send_command(self, capability, command, arguments=None):
await self._api.command(
ha_device_id=self._device_id,
capability=capability,
command=command,
arguments=arguments or [],
)
์ด์ , TV์ Settop์ ๋งคํํ media_player.py์ด๋ค.
IR๋ก ์ ์ดํ๋ ๋ ๊ธฐ๊ธฐ๋ฅผ Home Assistant์ Media Player ์ํฐํฐ๋ก ๋ณํํ ๊ฒ์ด๋ค.
https://www.home-assistant.io/integrations/media_player/
Media player
Instructions on how to set up your media players with Home Assistant.
www.home-assistant.io
์ญ์, ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๊ธฐ ์ ๋ณด๋ฅผ ์ ์ฅ ํ ํด๋์ค๋ฅผ ์์ฑ
ON/OFF์ ๊ตฌ๋ถ์ ํ์ ์์ด์ TURN_ON์ ์ ๊ฑฐ ํ๋ค.
class SmartThingsIRMediaPlayer(MediaPlayerEntity):
_attr_supported_features = (
MediaPlayerEntityFeature.TURN_OFF |
MediaPlayerEntityFeature.VOLUME_STEP |
MediaPlayerEntityFeature.VOLUME_MUTE |
MediaPlayerEntityFeature.NEXT_TRACK |
MediaPlayerEntityFeature.PREVIOUS_TRACK
)
_attr_assumed_state = True
def __init__(self, hass, device_id, name, device_type):
self.hass = hass
self._device_id = device_id
self._attr_name = name
self._attr_device_class = "tv"
self._attr_unique_id = f"smartthings_ir_media_player_{device_id}"
self._attr_state = MediaPlayerState.ON
self._device_type = device_type
self._api = SmartThingsIRAPI(hass)
์ ์ ํ ๊ธ์ ์ํ ๋ฉ์๋๋ฅผ ๊ตฌํํ๊ณ
async def async_turn_off(self):
await self._send_command("STATELESS_POWER_TOGGLE_BUTTON", "SET_BUTTON", ["powerToggle"])
self._attr_state = MediaPlayerState.ON
self.async_write_ha_state()
๋ณผ๋ฅจ ์กฐ์ ๋ ๊ตฌํ
async def async_volume_up(self):
await self._send_command("STATELESS_AUDIO_VOLUME_BUTTON", "SET_BUTTON", ["volumeUp"])
async def async_volume_down(self):
await self._send_command("STATELESS_AUDIO_VOLUME_BUTTON", "SET_BUTTON", ["volumeDown"])
์ฑ๋ ๋ณ๊ฒฝ์, Media Player์ ํธ๋ ๋ณ๊ฒฝ์๋ค๊ฐ ๋งคํ์ ํด์ฃผ์๋ค.
async def async_media_next_track(self):
await self._send_command("STATELESS_CHANNEL_BUTTON", "SET_BUTTON", ["channelUp"])
async def async_media_previous_track(self):
await self._send_command("STATELESS_CHANNEL_BUTTON", "SET_BUTTON", ["channelDown"])
๋์ผํ๊ฒ ์ค์ API๋ก ์ ์กํ๋ ๋ฉ์๋๊น์ง ๊ตฌํ ์๋ฃ
async def _send_command(self, capability, command, arguments=None):
await self._api.command(
ha_device_id=self._device_id,
capability=capability,
command=command,
arguments=arguments or [],
)
์ด์ device_id๊ฐ ํ์ํ๋ฐ, ๊ณต์ ํ๋ฌ๊ทธ์ธ์ด ์ด๋ค id๋ฅผ ์ฌ์ฉ์ค์ธ์ง ์ง์ ์ ์ ์์ด์
extra ํ๋ฌ๊ทธ์ธ์ ๋ฐฉ์์ ์ฐฉ์ํด์ ์๋น์ค๋ฅผ ํ๋ ๋ง๋๋ ค๊ณ ํ๋ค.
services.py ์์ฑ ํ, ๊ณต์ ํ๋ฌ๊ทธ์ธ์ ๊ธฐ๊ธฐ ์ ํ๊ณผ, UUID ๋ณํ/ํ์ธ์ ๊ตฌํํ๋ค.
async def async_register_services(hass: HomeAssistant) -> None:
async def handle_show_device_id(call: ServiceCall) -> None:
ha_device_id = call.data.get("device_id")
if not ha_device_id:
raise HomeAssistantError("device_id is required")
api = SmartThingsIRAPI(hass)
st_device_id, st_entry_id = await api._resolve_st_ids(ha_device_id)
hass.services.async_register(DOMAIN, "show_device_id", handle_show_device_id)
async def async_unregister_services(hass: HomeAssistant) -> None:
hass.services.async_remove(DOMAIN, "show_device_id")
๊ทธ๋ฆฌ๊ณ ์๋น์ค ํ๊น์ง ์ ์ ํ๋ฉด ๋
show_device_id:
name: Show Device UUID
description: Display the SmartThings UUID for a selected device
fields:
device_id:
name: Device
description: Select a SmartThings device
required: true
selector:
device:
filter:
- integration: smartthings
๋ง์ง๋ง์ผ๋ก config_flow.py๋ฅผ ๊ตฌํํด์,
ํตํฉ๊ตฌ์ฑ์์๋ฅผ ์ถ๊ฐํ ๋ ๋์ค๋ ์ค์ ํ๋ฉด๊น์ง ๋ง๋ค์๋ค. (์ฝ๋์๋ต)
https://github.com/101Sean/hass_smartthings_ir
ํ๋ฌ๊ทธ์ธ ๋ฑ๋ก
์ด์ ์์ฑ๋ ํ๋ฌ๊ทธ์ธ์ HACS๋ฅผ ํตํด ์ ์ฅ์ ์ถ๊ฐ๋ฅผ ํด๋ณด์
HACS > ์ฐ์ธก ์๋จ ๋ฉ๋ด > Custom repositories ๋ฉ๋ด ์ ํ
Repository ์ถ๊ฐ์ ํ์ ์ ํตํฉ์ผ๋ก ์ถ๊ฐ

์ถ๊ฐ ํ, ํตํฉ๊ตฌ์ฑ์์์์ ๊ฒ์ํ๋ฉด ๋ฑ๋กํ ํ๋ฌ๊ทธ์ธ์ด ๋ํ๋๋ค.

์ด์ ์ค์ > ๊ฐ๋ฐ์ ๋๊ตฌ > ๋์
services.py์์ ๊ตฌํํ show_device_id ์ ํ

๊ธฐ๊ธฐ ์ ํ ํ, YAML ๋ชจ๋๋ฅผ ํด๋ฆญํ๋ฉด device_id๋ฅผ ๋ณผ ์ ์๋ค.

์ด์ ๊ธฐ๊ธฐ ์ถ๊ฐ์ device_id๋ฅผ ๋ฃ๊ณ ๋ฑ๋กํ๋ฉด ๋

์ฌ์ฉ์ค์ธ ๋๋ฐ์ด์ค 3๊ฐ๋ฅผ ์ ์์ ์ผ๋ก ๋ฑ๋ก ํ๋ค.

๋ง์ง๋ง์ผ๋ก ๋์๋ณด๋์ ํ์ผ๋ก ์ถ๊ฐ ์๋ฃ

์ด์ Home Assistant์์๋ Smartthings์ ๋ชจ๋ ๋์์ ์ฌ์ฉ ํ ์ ์๊ฒ ๋์๋ค.
์ฐจ๊ทผ์ฐจ๊ทผ Homebridge์ ํ๋ฌ๊ทธ์ธ๋ค์ ์ฎ๊ธฐ๋ฉด์ ์์กด๋๋ฅผ ์ค์ฌ ๋๊ฐ๋๋ก ํ๊ฒ ๋ค.
์๋ณธ ๊ฒ์๊ธ
[Python] Home Assistant Smarthings IR ํ๋ฌ๊ทธ์ธ ์ ์
์ ๋ฒ ๊ฒ์๊ธ์์ Home Assistant ๊ธฐ๋ณธ ๊ตฌ์ถ๊ณผ ๋๋ถ์ด, ๊ธฐ๋ณธ์ ์ธ ๋์๋ณด๋ ์ค์ ์ ํด์ฃผ์๋ค. ์ด๋ฒ์, Sma...
blog.naver.com
'IoT ๐ > HomeAssistant' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| Home Assistant ๊ตฌ์ถ ๋ฐ ๊ธฐ๊ธฐ ์ฐ๋ (0) | 2026.07.07 |
|---|