YOGYUI

힐스테이트 광교산::조명 - 애플 홈킷 + 구글 어시스턴트 연동 본문

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

힐스테이트 광교산::조명 - 애플 홈킷 + 구글 어시스턴트 연동

요겨 2022. 6. 13. 20:52
반응형

 

지난 글(링크)에서 월패드의 조명 관련 '상태 조회', '응답', '명령'에 관한 RS-485 패킷을 파싱하는 방법 및 패킷을 생성하는 방법에 대해 조사해봤다

이제 이를 토대로 애플 홈킷 및 구글 어시스턴트와 연동하여 스마트폰 및 AI스피커를 통해 조명을 제어해보자

시스템은 광교 아이파크때와 마찬가지로 필요한 모든 기능을 라즈베리파이4 단일 HW에서 모두 구동하도록 구현했다

  • 홈네트워크 플랫폼 - Homebridge 및 Home Assistant(컨테이너)
  • MQTT broker(Mosquitto) - 모든 홈네트워크 디바이스는 MQTT publish, subscribe 형식으로 상호작용
  • USB to RS485 컨버터
  • Python3 기반 프로그래밍: Flask로 이벤트 루프 생성 및 웹서버 구동
  • duckdns로 무료 도메인 생성 및 Let's Encrypt로 SSL 인증서 발급 (구글 어시스턴트 HTTPS 연동)

조명 연동 관련 코드를 깃헙 저장소에 커밋 완료 (브랜치 이름: hillstate-light)

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

 

GitHub - YOGYUI/HomeNetwork: HomeNetwork(Homebridge) Repo

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

github.com

 

월패드 벽면 안쪽 공간이 협소해 라즈베리파이 보드 하나조차 넣는게 쉽지가 않다
위 사진처럼 미관을 해치게 선이 튀어나온 상태로 살지는 않을거라, 어떻게 할지 고민중이다
일단 RS-485 제어 관련 코딩 작업은 급한대로 지금처럼 한 뒤에, 코딩 작업 마무리되면 하드웨어는 어떻게 할지 고민중..
(따라서 깃헙에 올라간 코드도 확정적인 코드가 아니라서, 코드에 대한 자세한 내용은 나중에 모든게 확정되면 포스팅할 예정)

 

제일 핵심은 아무래도 RS-485 패킷 해석 및 명령 패킷 생성 구문이다

수신되는 바이트스트림은 다음과 같이 해석하면 된다

  • 시작 바이트 0xF7과 종료 바이트 0xEE로 끊어서 패킷화
  • 각 패킷의 3~4번 바이트가 [0x01, 0x19]일 경우 조명 관련 패킷
    확실하진 않지만, 0x01은 월패드 ID, 0x19는 디바이스 아이디(조명은 0x19)를 가리키지 않나 생각해본다
  • 패킷의 5번째 바이트가 0x04일 경우 각 방의 조명 상태를 담은 응답 패킷
    7번째 바이트의 하위 4비트가 0일 경우 모든 조명에 대한 상태 정보를 담고 있으며, 0이 아닐 경우 특정 조명 1개에 대한 상태 정보를 담고 있다
class ParserLight(SerialParser):
    def handlePacket(self):
        idx = self.buffer.find(0xF7)
        if idx > 0:
            self.buffer = self.buffer[idx:]
        if len(self.buffer) >= 3:
            packet_length = self.buffer[1]
            if len(self.buffer) >= packet_length:
                if self.buffer[packet_length - 1] == 0xEE:
                    packet = self.buffer[:packet_length]
                    self.interpretPacket(packet)
                    # self.sig_parse.emit(packet)
                    self.buffer = self.buffer[packet_length:]
    
    def interpretPacket(self, packet: bytearray):
        try:
            if packet[2] == 0x01 and packet[3] == 0x19:
                room_idx = packet[6] >> 4
                if packet[4] == 0x01:  # 조명 상태 쿼리
                    pass
                elif packet[4] == 0x02:  # 조명 상태 변경 명령
                    pass
                elif packet[4] == 0x04:  # 각 방별 조명 On/Off 상태
                    dev_idx = packet[6] & 0x0F
                    if dev_idx == 0:  # 일반 쿼리 (존재하는 모든 디바이스)
                        light_count = len(packet) - 10
                        for idx in range(light_count):
                            state = 0 if packet[8 + idx] == 0x02 else 1
                            self.sig_parse_result.emit({
                                'device': 'light', 
                                'index': idx,
                                'room_index': room_idx,
                                'state': state
                            })
                    else:  # 상태 변경 명령 직후 응답
                        state = 0 if packet[8] == 0x02 else 1
                        self.sig_parse_result.emit({
                            'device': 'light', 
                            'index': dev_idx - 1,
                            'room_index': room_idx,
                            'state': state
                        })
        except Exception as e:
            writeLog('interpretPacket::Exception::{} ({})'.format(e, data), self)

 

제어 명령 패킷은 체크섬 (XOR SUM) 계산 공식을 알기 때문에 쉽게 구현할 수 있었다

(아이파크 bestin은 당최 알수 없는 방식으로 만들어내는 바람에 명령 패킷을 하나하나 정성들여 기록해야 했다...)

조명 객체는 조명 자체의 인덱스(특정 공간의 몇번째 조명인지)와 해당 조명이 속해있는 공간의 인덱스만 알면 쉽게 다음과 같이 명령 및 조회 패킷을 만들 수 있다

class Device:
    @staticmethod
    def calcXORChecksum(data: Union[bytearray, bytes, List[int]]) -> int:
        return reduce(lambda x, y: x ^ y, data, 0)

class Light(Device):
    def __init__(self, name: str = 'Light', index: int = 0, **kwargs):
        self.index = index  # light device order index
        super().__init__(name, **kwargs)

    def __repr__(self):
        repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})'
        repr_txt += f' Room Idx: {self.room_index}, Dev Idx: {self.index}'
        repr_txt += '>'
        return repr_txt

    def publish_mqtt(self):
        obj = {"state": self.state}
        if self.mqtt_client is not None:
            self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)

    def makePacketQueryState(self) -> bytearray:
        # F7 0B 01 19 01 40 XX 00 00 YY EE
        # XX: 상위 4비트 = Room Index, 하위 4비트 = 0
        # YY: Checksum (XOR SUM)
        packet = bytearray([0xF7, 0x0B, 0x01, 0x19, 0x01, 0x40])
        packet.append((self.room_index << 4))
        packet.extend([0x00, 0x00])
        packet.append(self.calcXORChecksum(packet))
        packet.append(0xEE)
        return packet

    def makePacketSetState(self, state: bool):
        # F7 0B 01 19 02 40 XX YY 00 ZZ EE
        # XX: 상위 4비트 = Room Index, 하위 4비트 = Device Index (1-index)
        # YY: 02 = OFF, 01 = ON
        # ZZ: Checksum (XOR SUM)
        packet = bytearray([0xF7, 0x0B, 0x01, 0x19, 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

 

각 디바이스별 MQTT 토픽은 로컬의 XML파일(config.xml)에서 읽을 수 있도록 만들었다 (하드코딩 방지)

    <rooms>
        <room1>
            <name>Livingroom</name>
            <index>1</index>
            <lights>
                <light1>
                    <name>ceil 1</name>
                    <mqtt>
                        <publish>home/hillstate/light/state/1/1</publish>
                        <subscribe>home/hillstate/light/command/1/1</subscribe>
                    </mqtt>
                </light1>

 

Mosquitto, Homebridge, Home Assistant는 이미 구축해둔 환경을 활용했다

(나중에 하드웨어까지 모두 확정되면 그 때 설치방법 A to Z를 상세히 포스팅할 예정)


구현한 조명은 총 9개

  • 거실(공간 인덱스 1) - 천장 조명 2개 + 복도 천장 조명 (복도 천장은 3개 조명이 하나의 그룹으로 제어된다)
  • 침실(공간 인덱스 2) - 천장 조명 1개 + 실외기실 천장 조명
  • 서재(공간 인덱스 3) - 천장 조명 1개
  • 컴퓨터방(공간 인덱스 4) - 천장 조명 1개
  • 주방(공간 인덱스 6) - 싱크대 및 조리대 조명 (3개 조명이 하나의 그룹) + 식탁 천장 조명 1개

홈브릿지에 다음과 같이 액세서리들을 추가했다

 

홈어시스턴트(HA)에도 액세서리를 추가했다

 

아이폰과 같은 애플 기기에서 바로 액세서리들을 제어할 수 있다


거실 조명 제어 예시를 동영상으로 찍어봤다

 

마찬가지로 주방 조명 제어 예시도 찍어봤다

 

제어 패드에서 조명을 끄면 3초 정도 딜레이 후에 꺼지는데 (아마 사용자가 잘못 눌렀을 경우 취소할 수 있는 시간 개념인듯), RS-485를 통해 명령 패킷을 전송하면 즉시 꺼지는 것을 알 수 있다

(힐스테이트 앱을 사용해도 즉시 꺼지긴 하는데, 앱 자체가 네트워크 응답 기다리는 레이턴시가 있어서 연속적인 제어를 할 수 없는 단점이 있다 - 물론 홈킷은 그런거 없음 ㅎㅎ)

사용자가 패드로 제어해서 상태를 변경했을 경우, 힐스테이트 앱은 수동으로 새로고침해야 변경 상태가 적용되지만, 내가 짠 코드는 상태가 변경되면 즉시 mqtt 토픽 발생을 하기 때문에 즉각적으로 Home 앱에 변경 내용이 적용된다 ^^ very good~

 

HA랑 구글 어시스턴트 연동도 해뒀으니, 구글 홈 미니 AI 스피커로 음성 제어도 가능하다~!


Bestin때 노가다 열심히 한 짬바가 있어서 그런지, 힐스테이트 홈네트워크는 굉장히 진도가 빠르다 (RS485 패킷 해석하고 플랫폼 연동하는데까지 다 합쳐서 4시간 정도? 걸린듯)

이 기세를 몰아 다른 디바이스들도 빠르게 연동해보자

남은 디바이스 종류는

  • 아울렛(전원 콘센트) 제어 및 실시간 전기 사용량
  • 도시가스 차단기
  • 난방 제어 및 현재 온도 (허니웰 thermostat)
  • 시스템 에어컨
  • 환기 (전열교환기)
  • 엘리베이터 호출
  • 도어락 해제
  • Optional: 현관 비디오폰 영상, 거실 천장 모션 센서

퇴근하고 저녁 시간이랑 주말 등 짬나는 대로 들이박으면 2달 정도면 다 할 수 있지 않을까 희망해본다

반응형