HomeAssistant (이하 HA) 작업을 수행하던 중 가장 짜증나는 부분이 LG 에어컨을 연동하는 부분이었다. 최근 출시된 LG 가전제품은 SmartThinq라는 IoT 솔루션을 제공한다. 하지만 가장 중요한 API는 공개를 하지 않고 있다.
그래도 그나마 다행인 부분은 SmartThinq를 리버스 엔지니어링을 통하여 라이브러리로 만든 Wideq라는 라이브러리와 그 라이브러리를 바탕으로 만들어진 hass-smartthinq라는 라이브러리가 있다는 것이다.
설치 방법은 아래 링크를 따라 가면 설치가 가능하다.
설치시 wideq 라이브러리는 별도로 설치를 해야한다.
wideq와 smapsyo smartthinq, 여기까지만 하면 초입은 통과하게 된다.
초입은 통과했다는 말은 운이 좋으면 별 다른 수정 없이 적용이 가능하다는 이야기다.
먼저 LG 에어컨 리모컨을 이용하여 우선 테스트를 수행한다. 리모컨을 이용하여 바람 세기, 모드를 변경하다보면 HA에 로그가 쌓인다. 만약 쌓이지 않는다면 이 글을 읽을 필요없이 바로 사용이 가능하다. 하지만 에러가 발생한다면, 삽질을 시작할 시간이다.
LG 에어컨과 HA 연동의 가장 큰 문제는… LG에서 나오는 모델에 따라서 신호를 지칭하는 명칭이 다르다는 것이다! (좀…. 통일 좀…)
그래서 HA 로그를 보면서 LG 에어컨 리모컨을 눌러 이래저래 세팅하면서 오류 로그를 모아두었다가 수정을 해야 한다.
먼저, 간단한 테스트를 수행해보았다.
테스트 환경은 두개의 에어컨이 설치되어 있다. 거실에는 스탠드형으로 메인이 설치되어 있고, 안방에는 보조용으로 하나 더 설치되어 있다.
일단 안방의 경우 무난하게 바람 세기 테스트는 통과했다. 그런데 거실은 정상 동작하지 않는다.
거실의 에어컨은 일명 듀얼형 에어컨(오른쪽, 왼쪽 구분되어 동작하는 에어컨)인데 전원을 켜면 오른쪽, 왼쪽 팬이 동시에 동작하는게 아니라 오른쪽만 동작하는 것을 확인했다.
그래서 원인을 찾고자 wideq.py 파일을 열었다.
## WIDEQ AC.py
class ACOp(enum.Enum):
"""Whether a device is on or off."""
OFF = "@AC_MAIN_OPERATION_OFF_W"
ALL_ON = "@AC_MAIN_OPERATION_ALL_ON_W"
RIGHT_ON = "@AC_MAIN_OPERATION_RIGHT_ON_W" # This one seems to mean "on"?
LEFT_ON = "@AC_MAIN_OPERATION_LEFT_ON_W"
이라는 enum 클래스를 상속한 녀석을 찾을 수 있었다. 오… ALL_ON, RIGHT_ON, LEFT_ON 다 있다. 근데 왜? 라고 생각하고 ACOp로 코드를 검색해보니…
## WIDEQ AC.py
def set_on(self, is_on):
"""Turn on or off the device (according to a boolean).
"""
op = ACOp.RIGHT_ON if is_on else ACOp.OFF
op_value = self.model.enum_value('Operation', op.value)
self._set_control('Operation', op_value)
set_on이라는 함수를 찾을 수 있었다.
op = ACOp.RIGHT_ON if is_on else ACOp.OFF 부분이 RIGHT_ON 이다. 아마 이 라이브러리를 개발하신 분은 듀얼형 에어컨이 아닌 에어컨으로 테스트를 수행한 모양이다. 그래서 아래처럼 코드를 수정했다.
## WIDEQ AC.py
def set_on(self, is_on):
"""Turn on or off the device (according to a boolean).
"""
#op = ACOp.RIGHT_ON if is_on else ACOp.OFF
op = ACOp.ALL_ON if is_on else ACOp.OFF # 요부분 수정
op_value = self.model.enum_value('Operation', op.value)
self._set_control('Operation', op_value)
HA를 재시작하고 로그를 다시보니 ‘@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W’는 없는 바람 세기라고 한다. 그래서 다시 코드를 열어 본다.
## WIDEQ AC.py
class ACFanSpeed(enum.Enum):
"""The fan speed for an AC/HVAC device."""
SLOW = '@AC_MAIN_WIND_STRENGTH_SLOW_W'
SLOW_LOW = '@AC_MAIN_WIND_STRENGTH_SLOW_LOW_W'
LOW = '@AC_MAIN_WIND_STRENGTH_LOW_W'
LOW_MID = '@AC_MAIN_WIND_STRENGTH_LOW_MID_W'
MID = '@AC_MAIN_WIND_STRENGTH_MID_W'
MID_HIGH = '@AC_MAIN_WIND_STRENGTH_MID_HIGH_W'
HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_W'
POWER = '@AC_MAIN_WIND_STRENGTH_POWER_W'
AUTO = '@AC_MAIN_WIND_STRENGTH_AUTO_W'
NATURE = '@AC_MAIN_WIND_STRENGTH_NATURE_W'
라는 코드를 찾을 수 있었다. 해당 코드와 에러에 나온 코드를 보니… 듀얼형 에어컨의 경우 파이프라인을 통해 왼쪽 오른쪽을 동시에 관리한다는 것을 알게되었다.
그래서 아래와 같이 코드를 추가하였다.
## WIDEQ AC.py
class ACFanSpeed(enum.Enum):
"""The fan speed for an AC/HVAC device."""
SLOW = '@AC_MAIN_WIND_STRENGTH_SLOW_W'
SLOW_LOW = '@AC_MAIN_WIND_STRENGTH_SLOW_LOW_W'
LOW = '@AC_MAIN_WIND_STRENGTH_LOW_W'
LOW_MID = '@AC_MAIN_WIND_STRENGTH_LOW_MID_W'
MID = '@AC_MAIN_WIND_STRENGTH_MID_W'
MID_HIGH = '@AC_MAIN_WIND_STRENGTH_MID_HIGH_W'
HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_W'
POWER = '@AC_MAIN_WIND_STRENGTH_POWER_W'
AUTO = '@AC_MAIN_WIND_STRENGTH_AUTO_W'
NATURE = '@AC_MAIN_WIND_STRENGTH_NATURE_W'
## 여기서부터 추가
R_LOW = '@AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
R_MID = '@AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
R_HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
L_LOW = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W'
L_MID = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W'
L_HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W'
L_LOWR_LOW = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
L_LOWR_MID = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
L_LOWR_HIGH = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
L_MIDR_LOW = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
L_MIDR_MID = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
L_MIDR_HIGH = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
L_HIGHR_LOW = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
L_HIGHR_MID = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
L_HIGHR_HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
AUTO_2 = '@AC_MAIN_WIND_STRENGTH_AUTO_LEFT_W|AC_MAIN_WIND_STRENGTH_AUTO_RIGHT_W'
POWER_2 = '@AC_MAIN_WIND_STRENGTH_POWER_LEFT_W|AC_MAIN_WIND_STRENGTH_POWER_RIGHT_W'
LONGPOWER = '@AC_MAIN_WIND_STRENGTH_LONGPOWER_LEFT_W|AC_MAIN_WIND_STRENGTH_LONGPOWER_RIGHT_W'
해당 코드는 일일이 에어컨 리모컨을 조작하여 알아낸 코드이다. 결국 왼쪽, 오른쪽 팬을 다른 속도로 돌릴 수 있게 설계하면서 파이프라인을 써서 분리 시킨 것이었다.
자 위의 코드를 수정하고, HA를 재시작하였다. 다행스럽게도 정상적으로 정보를 가지고 왔다.
그런데… 단순히 정보만 볼거면 굳이… HA와 연동을 안시켰을 것이다. 그래서 이제 명령을 내리는 부분을 찾기 위해 smartthinq 라이브러리를 확인하였다.
## SmartThinq Climate.py
FAN_MODES = {
'LOW': c_const.FAN_LOW,
'LOW_MID': 'low-mid',
'MID': c_const.FAN_MEDIUM,
'MID_HIGH': 'mid-high',
'HIGH': c_const.FAN_HIGH,
}
smartthinq 라이브러리의 climate 코드를 열어보니 팬모드라는 것이 떡하니 있었다. 그래서 아래와 같이 먼저 수정을 해보았다.
## SmartThinq Climate.py
FAN_MODES = {
'LOW': c_const.FAN_LOW,
'LOW_MID': 'low-mid',
'MID': c_const.FAN_MEDIUM,
'MID_HIGH': 'mid-high',
'HIGH': c_const.FAN_HIGH,
'AUTO': 'AUTO',
'NATURE': 'NATURE',
'R_LOW' : 'R[L]',
'R_MID' : 'R[M]',
'R_HIGH' : 'R[H]',
'L_LOW' : 'L[L]',
'L_MID' : 'L[M]',
'L_HIGH' : 'L[H]',
'L_LOWR_LOW' : 'L[L]-R[L]',
'L_LOWR_MID' : 'L[L]-R[M]',
'L_LOWR_HIGH' : 'L[L]-R[H]',
'L_MIDR_LOW' : 'L[M]-R[L]',
'L_MIDR_MID' : 'L[M]-R[M]',
'L_MIDR_HIGH' : 'L[M]-R[H]',
'L_HIGHR_LOW' : 'L[H]-R[L]',
'L_HIGHR_MID' : 'L[H]-R[M]',
'L_HIGHR_HIGH' : 'L[H]-R[H]',
'AUTO_2' : 'AUTO',
'POWER_2' : 'POWER',
'LONGPOWER' : 'LONG-POWER',
}
적용하고 다시 시작, 오… 잘된다. 그러나 여기서 멈추기에는… 보기 싫었다. 왜냐하면 안방 에어컨과 거실 에어컨 모두 컨트롤 해야 하는데, L_LOW 아래부터는 사실 거실 에어컨에만 사용되는 명령어기 때문이었다. 안방 에어컨에서는 알아듣지도 못하는 코드를 그냥 두기 애매해서 코드를 조금 더 뜯어보았다.
그랬더니 바람 세기를 가져오는 fan_modes 함수와 바람 세기를 세팅하는 set_fan_mode 함수를 확인할 수 있었다. 이 두가지만 모델명으로 분리해서 동작시키면 되지 않을까? 하고 HA를 디버그 모드로 동작시키고, 모델명을 추출하여 아래와 같은 코드를 완성 시켰다.
## SmartThinq Climate.py
FAN_MODES = {
'LOW': c_const.FAN_LOW,
'LOW_MID': 'low-mid',
'MID': c_const.FAN_MEDIUM,
'MID_HIGH': 'mid-high',
'HIGH': c_const.FAN_HIGH,
'AUTO': 'AUTO',
'NATURE': 'NATURE',
}
# 2019.10.23. teshi85 fix.
# PAC_910604_WW model works with seperate wind power
PAC_910604_WW_FAN_MODES = {
'R_LOW' : 'R[L]',
'R_MID' : 'R[M]',
'R_HIGH' : 'R[H]',
'L_LOW' : 'L[L]',
'L_MID' : 'L[M]',
'L_HIGH' : 'L[H]',
'L_LOWR_LOW' : 'L[L]-R[L]',
'L_LOWR_MID' : 'L[L]-R[M]',
'L_LOWR_HIGH' : 'L[L]-R[H]',
'L_MIDR_LOW' : 'L[M]-R[L]',
'L_MIDR_MID' : 'L[M]-R[M]',
'L_MIDR_HIGH' : 'L[M]-R[H]',
'L_HIGHR_LOW' : 'L[H]-R[L]',
'L_HIGHR_MID' : 'L[H]-R[M]',
'L_HIGHR_HIGH' : 'L[H]-R[H]',
'AUTO_2' : 'AUTO',
'POWER_2' : 'POWER',
'LONGPOWER' : 'LONG-POWER',
}
...
class LGDevice(climate.ClimateDevice):
...
@property
def fan_modes(self):
if self._device.model_id == 'PAC_910604_WW':
return list(PAC_910604_WW_FAN_MODES.values())
return list(FAN_MODES.values())
...
def set_fan_mode(self, fan_mode):
import wideq
if self._device.model_id == 'PAC_910604_WW':
fan_modes_inv = {v: k for k, v in PAC_910604_WW_FAN_MODES.items()}
mode = wideq.ACFanSpeed[fan_modes_inv[fan_mode]]
LOGGER.info('Setting fan mode to %s', fan_mode)
self._ac.set_fan_speed(mode)
LOGGER.info('Fan mode set.')
else:
# Invert the fan modes mapping.
fan_modes_inv = {v: k for k, v in FAN_MODES.items()}
mode = wideq.ACFanSpeed[fan_modes_inv[fan_mode]]
LOGGER.info('Setting fan mode to %s', fan_mode)
self._ac.set_fan_speed(mode)
LOGGER.info('Fan mode set.')
코드 자체는 심플하다. 모델명을 가져와서 해당 모델일 경우 다른 팬모드를 불러오는 것이다.
이렇게 Wideq, SmartThinq를 이요한 HA 연동을 성공적으로 수행할 수 있었다. 하지만… 아마도… 여기에 더 많은 문제가 있을 것으로 보인다. 예를들어 알지 못한 바람 세기가 있을 수도 있다.
차근 차근 수정해 나가면 좋겠지만… 이왕이면… LG 측에서 API를 공개해주면 참 좋을 것 같다는 생각이 든다.