YOGYUI

현대통신 월패드 '디밍조명' 제어 기능 추가 (깃허브, HA 애드온) 본문

홈네트워크(IoT)/힐스테이트 광교산

현대통신 월패드 '디밍조명' 제어 기능 추가 (깃허브, HA 애드온)

요겨 2024. 7. 29. 14:55
반응형

Add Hyundai HT Wallpad 'dimming light' device type

1. 개요

3주 전인 2024년 7월 11일, 현대통신 월패드 RS-485 연동 깃허브 소스코드에 처음으로 Pull-Request가 등록됐다

https://github.com/YOGYUI/HomeNetwork/pull/12

 

Add support for dimming lights by serialx · Pull Request #12 · YOGYUI/HomeNetwork

안녕하세요. 처음 인사드립니다. 우선 코드 공개해주셔서 감사드립니다. 굉장히 도움이 많이 되었습니다. 디에이치 브랜드 아파트에 살고있는데, 메인 거실 조명이 '디밍조명' 으로 되어있어서

github.com

PR 주제는 현대통신 월패드의 '디밍조명' 디바이스 타입 추가 및 제어 기능 추가

디밍(dimming) 조명: 밝기(brightness)를 동적으로 변경할 수 있는 조명

 

고급 개발자(?)답게 코드 추가/수정 및 테스트까지 완료하신 후에 PR을 올려주셨기에 별 고민없이 merge를 진행했다 (사실 우리집에는 디밍조명 타입이 없어서 테스트를 해볼 수도 없다 ㅠ)

 

주요변경사항은 패킷의 4번째 바이트가 0x1A이면 디밍조명과 관련된 패킷이라는 점 

(참고로 0x19는 일반조명, 0x15는 감성조명)

그리고 On/Off 상태 패킷 파싱 및 On/Off 제어는 일반 조명과 동일하다는 점으로 일반조명의 파서/제어 코드를 그대로 썼다는 점이다

흥미로운 점은 작성자분의 거주 환경에서는 월패드에 디밍조명 타입으로 디바이스가 등록되었음에도 불구하고 월패드 및 스마트폰 앱에서 밝기 제어 인터페이스가 제공되지 않고 있다고 한다 (흠터레스팅...)

2. 패킷 상세 (밝기 제어)

실컷 디밍조명 타입으로 설정했는데도 불구하고 밝기 조절이 안되면 일반 조명과 다를 바가 없기에 웹을 조금 뒤져서 디밍조명의 밝기 변경과 관련된 패킷 상세를 허술하게나마 알아낼 수 있었다 (직접 테스트할 수 있는 장치가 없으니 많이 답답하다 ㅠ)

패킷 상세에 잘못된 부분이 있다면 댓글로 알려주시기 바랍니다~

2.1. 조명 전원 On/Off

2.1.1. 상태 쿼리

Index 0 1 2 3 4 5 6 7 8 9 10
Value F7 0B 01 1A 01 40 XX 00 00 YY EE
  • 패킷 길이 11
  • 4번째 바이트: 디바이스 타입 = 0x1A
  • 5번째 바이트: 패킷 타입 = '상태 쿼리' = 0x01 (통신방향: 월패드→디바이스)
  • 6번째 바이트: 명령/상태 타입 = On/Off = 0x40
  • 7번째 바이트: 상위 4비트 = 공간 인덱스, 하위 4비트 = 장치 인덱스 (1-based)
    예시) 2번째 방의 3번째 장치 = 0x23
  • 10번째 바이트: XOR Checksum

2.1.2. 상태 응답

Index 0 1 2 3 4 5 6 7 8 9 10
Value F7 0B 01 1A 04 40 XX -- YY ZZ EE
  • 패킷 길이 11
  • 4번째 바이트: 디바이스 타입 = 0x1A
  • 5번째 바이트: 패킷 타입 = '상태 응답' = 0x04 (통신방향: 월패드←디바이스)
  • 6번째 바이트: 명령/상태 타입 = On/Off = 0x40
  • 7번째 바이트: 상위 4비트 = 공간 인덱스, 하위 4비트 = 장치 인덱스 (1-based)
  • 9번째 바이트: 현재 조명 On/Off 상태, 0x02=OFF, 0x01=ON (일반 조명과 동일)
  • 10번째 바이트: XOR Checksum

2.1.3. 명령

Index 0 1 2 3 4 5 6 7 8 9 10
Value F7 0B 01 1A 02 40 XX YY 00 ZZ EE
  • 패킷 길이 11
  • 4번째 바이트: 디바이스 타입 = 0x1A
  • 5번째 바이트: 패킷 타입 = '명령' = 0x02 (통신방향: 월패드→디바이스)
  • 6번째 바이트: 명령/상태 타입 = On/Off = 0x40
  • 7번째 바이트: 상위 4비트 = 공간 인덱스, 하위 4비트 = 장치 인덱스 (1-based)
  • 8번째 바이트: 현재 조명 On/Off 상태, 0x02=OFF, 0x01=ON (일반 조명과 동일)
  • 10번째 바이트: XOR Checksum

2.2. 조명 밝기

2.2.1. 상태 쿼리

Index 0 1 2 3 4 5 6 7 8 9 10
Value F7 0B 01 1A 01 42 XX 00 00 YY EE
  • 패킷 길이 11
  • 4번째 바이트: 디바이스 타입 = 0x1A
  • 5번째 바이트: 패킷 타입 = '상태 쿼리' = 0x01 (통신방향: 월패드→디바이스)
  • 6번째 바이트: 명령/상태 타입 = Brightness = 0x42
  • 7번째 바이트: 상위 4비트 = 공간 인덱스, 하위 4비트 = 장치 인덱스 (1-based)
  • 10번째 바이트: XOR Checksum

2.2.2. 상태 응답

Index 0 1 2 3 4 5 6 7 8 9 10
Value F7 0B 01 1A 04 42 XX -- YY ZZ EE
  • 패킷 길이 11
  • 4번째 바이트: 디바이스 타입 = 0x1A
  • 5번째 바이트: 패킷 타입 = '상태 응답' = 0x04 (통신방향: 월패드←디바이스)
  • 6번째 바이트: 명령/상태 타입 = Brightness = 0x42
  • 7번째 바이트: 상위 4비트 = 공간 인덱스, 하위 4비트 = 장치 인덱스 (1-based)
  • 9번째 바이트: 현재 조명 밝기값
  • 10번째 바이트: XOR Checksum

2.2.3. 명령

Index 0 1 2 3 4 5 6 7 8 9 10
Value F7 0B 01 1A 02 42 XX YY 00 ZZ EE
  • 패킷 길이 11
  • 4번째 바이트: 디바이스 타입 = 0x1A
  • 5번째 바이트: 패킷 타입 = '명령' = 0x02 (통신방향: 월패드→디바이스)
  • 6번째 바이트: 명령/상태 타입 = Brightness = 0x42
  • 7번째 바이트: 상위 4비트 = 공간 인덱스, 하위 4비트 = 장치 인덱스 (1-based)
  • 8번째 바이트: 설정하고자 하는 조명 밝기값
  • 10번째 바이트: XOR Checksum

3. 코드 수정

3.1. DimmingLight 클래스 수정

class DimmingLight(Device):
    brightness: int = 0  # 현재 밝기 레벨
    brightness_prev: int = 0  # 현재 밝기 레벨 버퍼
    max_brightness_level: int = 7
    
    def publishMQTT(self):
        brightness_conv = self.convert_word_to_level(self.brightness)
        obj = {
            "state": self.state,
            "brightness": brightness_conv
        }
        if self.mqtt_client is not None:
            self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)
    
    def setMaxBrightnessLevel(self, level: int):
        self.max_brightness_level = level
        writeLog(f"{str(self)} Set Max Brightness Level: {self.max_brightness_level}", self)
    
    def updateState(self, state: int, **kwargs):
        self.state = state
        if not self.init:
            self.publishMQTT()
            self.init = True
        if self.state != self.state_prev:
            self.publishMQTT()
        self.state_prev = self.state
        # 밝기 레벨
        brightness = kwargs.get('brightness')
        if brightness is not None:
            self.brightness = brightness
            if self.brightness != self.brightness_prev:
                self.publishMQTT()
            self.brightness_prev = self.brightness

DimmingLight 클래스에 밝기(brightness) 관련 멤버변수를 추가해줬다

또한, 디밍조명 디바이스 타입이 우리집 월패드에는 없기 때문에 월패드 상의 최대 밝기 레벨을 하드코딩할 수가 없어 max_brightness_level 멤버변수를 추가한 뒤 이를 config.xml로 수정할 수 있게 구현했다

3.2. 쿼리/명령 패킷 생성 구문 추가

class DimmingLight(Device):
    def makePacketQueryState(self) -> bytearray:
        packet = bytearray([0xF7, 0x0B, 0x01, 0x1A, 0x01, 0x40])
        packet.append((self.room_index << 4) + (self.index + 1))
        packet.extend([0x00, 0x00])
        packet.append(self.calcXORChecksum(packet))
        packet.append(0xEE)
        return packet

    def makePacketQueryBrightness(self) -> bytearray:
        packet = bytearray([0xF7, 0x0B, 0x01, 0x1A, 0x01, 0x42])
        packet.append((self.room_index << 4) + (self.index + 1))
        packet.extend([0x00, 0x00])
        packet.append(self.calcXORChecksum(packet))
        packet.append(0xEE)
        return packet

    def makePacketSetState(self, state: bool) -> bytearray:
        packet = bytearray([0xF7, 0x0B, 0x01, 0x1A, 0x02, 0x40])
        packet.append((self.room_index << 4) + (self.index + 1))
        if state:
            packet.extend([0x01, 0x00])
        else:
            packet.extend([0x02, 0x00])
        packet.append(self.calcXORChecksum(packet))
        packet.append(0xEE)
        return packet

    def makePacketSetBrightness(self, brightness: int) -> bytearray:
        packet = bytearray([0xF7, 0x0B, 0x01, 0x1A, 0x02, 0x42])
        packet.append((self.room_index << 4) + (self.index + 1))
        packet.extend([max(0, min(brightness, self.max_brightness_level)), 0x00])
        packet.append(self.calcXORChecksum(packet))
        packet.append(0xEE)
        return packet

패킷 생성 구문은 앞서 작성한 패킷명세에 맞춰 수정~

3.3. 파싱 구문 수정

class PacketParser:
    def interpretPacket(self, packet: bytearray):
        # 생략
        if packet[3] == 0x1A:  # 디밍조명
            self.handleDimmingLight(packet)
            packet_info['device'] = 'dimming light'
        # 생략
    
    def handleDimmingLight(self, packet: bytearray):
        room_idx = packet[6] >> 4
        if packet[4] == 0x04:
            state_type = packet[5]
            dev_idx = packet[6] & 0x0F
            if state_type == 0x40:
                state = 0 if packet[8] == 0x02 else 1
                result = {
                    'device': DeviceType.DIMMINGLIGHT, 
                    'index': dev_idx - 1,
                    'room_index': room_idx,
                    'state': state,
                    'brightness': None
                }
                self.updateDeviceState(result)
            elif state_type == 0x42:
                brightness = packet[8]
                result = {
                    'device': DeviceType.DIMMINGLIGHT, 
                    'index': dev_idx - 1,
                    'room_index': room_idx,
                    'state': None,
                    'brightness': brightness
                }
                self.updateDeviceState(result)

상태 패킷 쿼리 구문은 On/Off 상태와 Brightness 상태 패킷을 별도로 다룰 수 있게 if-else 조건문으로 구현~

3.4. Home Assistant Discovery용 json 템플릿 수정

Home Assistant의 MQTT Light 관련 템플릿은 공식 문서를 참고해 3번의 삽질 끝에 구현 성공!

https://www.home-assistant.io/integrations/light.mqtt/

 

MQTT Light

Instructions on how to setup MQTT lights using default schema within Home Assistant.

www.home-assistant.io

class DimmingLight(Device):
    def configMQTT(self, retain: bool = False):
        topic = f'{self.ha_discovery_prefix}/light/{self.unique_id}/config'
        obj = {
            "name": self.name,
            "object_id": self.unique_id,
            "unique_id": self.unique_id,
            "state_topic": self.mqtt_publish_topic,
            "command_topic": self.mqtt_subscribe_topic,
            "schema": "template",
            "command_on_template": '{'\
                '"state": 1'\
                '{%- if brightness is defined -%}'\
                ', "brightness": {{ brightness }}'\
                '{%- endif -%}'\
            '}',
            "command_off_template": '{"state": 0}',
            "state_template": "{% if value_json.state %} on {% else %} off {% endif %}",
            "brightness_template": '{{ value_json.brightness }}'
        }
        self.mqtt_client.publish(topic, json.dumps(obj), 1, retain)

테스트삼아 위 템플릿대로 HA configuration.yaml 파일을 수정한 뒤 설정 파일을 reload해보니

HA에서 전원과 밝기 제어가 가능한 액세서리가 추가되는 것을 확인!

당연하게도 실제 장치 테스트는 진행하지 못하고 단순하게 unit test로 상태 반영 및 명령 패킷 전송에 대해서만 검증 완료 ㅠ

3.5. GitHub 소스코드 커밋

PR merge 후 6번의 커밋을 추가로 진행했다

디밍조명이 추가된 소스코드는 main 브랜치에 모두 적용해뒀다

https://github.com/YOGYUI/HomeNetwork/tree/main/Hillstate-Gwanggyosan

 

HomeNetwork/Hillstate-Gwanggyosan at main · YOGYUI/HomeNetwork

HomeNetwork(Homebridge) Repo. Contribute to YOGYUI/HomeNetwork development by creating an account on GitHub.

github.com

3.6. 간접 테스트(?) 결과

PR 올려주신 개발자분께 과감히 실제 장치 테스트를 의뢰했다!

다행히도 밝기 제어 관련 패킷이 의도했던 대로 동작하는 것 같다

원래는 월패드의 최대 밝기 레벨을 10으로 커밋했었는데, 다양한 환경에서 테스트하지 못했기에 그냥 디폴트값을 7로 다시 커밋했다 ^^;;

월패드/스마트폰 앱에서 제공하지 않는 디밍 조명 밝기 제어 인터페이스를 뚫었(??)다는데 의의가 있다고 볼 수 있겠다 ㅋㅋㅋ

 

[24.07.31] 수정

월패드에서는 디밍 제어 인터페이스가 제공된다고 한다 ㅎㅎ

4. HA 애드온 버전 업데이트

HA 애드온도 디밍 조명 사용 시 discovery 후 제어가 가능하도록 버전을 1.1.2로 업데이트했다

https://github.com/YOGYUI/homeassistant-addons/tree/main/homenet-hillstate

 

homeassistant-addons/homenet-hillstate at main · YOGYUI/homeassistant-addons

My Home Assistant Addon(s). Contribute to YOGYUI/homeassistant-addons development by creating an account on GitHub.

github.com

애드온 기타 설정(etc)에 dimminglight 관련 최대 밝기 및 소수점 처리 방법에 대한 파라미터를 추가해뒀다

5. 마무리

디밍조명 디바이스 타입이 있는 유저라면 사용하면서 발생하는 문제점이나 개선사항을 블로그 댓글이나 방명록 혹은 이메일 (lee2002w@gmail.com)으로 알려주시기 바랍니다~

※ 실제 장치가 없어서 문제 파악이 힘든 상황

반응형