YOGYUI

힐스테이트 광교산::엘리베이터 호출 패킷 추가 분석 및 코드 적용 본문

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

힐스테이트 광교산::엘리베이터 호출 패킷 추가 분석 및 코드 적용

요겨 2022. 9. 8. 17:38
반응형

지난번 엘리베이터 호출 RS-485 패킷 분석을 할 때, 정체를 알 수 없는 값들은 심층 분석하지 않고 야매로 '도착' 신호만 활용해서 애플 홈킷/구글 어시스턴트와 연동을 마무리했었다

힐스테이트 광교산::엘리베이터 호출 제어 RS-485 패킷 분석

 

힐스테이트 광교산::엘리베이터 호출 제어 RS-485 패킷 분석

한동안 1일 1디바이스씩 조지다가(?) 21일~23일 거제도 출장 일정으로 잠깐 휴식을 가졌다 ㅎㅎ 다시 열심히 달려보자.. 거실 월패드랑 복도 쪽 제어패드에는 엘리베이터 호출 기능이 있다 월패드

yogyui.tistory.com

 

그런데 최근에 해당 글에 패킷 정보에 대한 댓글이 달렸다

댓글달린 뒤 얼마 지나지 않아 얼마 뒤에 이메일을 받았는데

오호라~ 지난번에 주방 비디오폰 RS-485 통신선 파형 측정을 의뢰하신 분께서 친히 댓글까지 달아주신 것이었다 (홈IoT 고수이신듯 ㅎㅎ)

 

귀중한 정보를 주셨으니, 바로바로 적용해보자

1. Remind

엘리베이터 호출 관련 패킷은 현관 초입 복도에 설치된 소형 제어 패드(편의상 미니패드라고 한다)와 거실 메인 월패드간 RS-485 통신라인을 후킹해서 가져왔다

뇌피셜로 패킷을 해석한 바, 거실 월패드(W)가 복도 미니패드(W)로 현재 상태(엘리베이터 호출 버튼이 눌렸는지 여부)를 조회(쿼리)하면, 미니패드가 이에 대한 응답패킷을 보내는 형태가 기본이다


평소 상태 (IDLE, 호출이 되지 않은 상태)

[W→M] F7 0D 01 34 01 41 10 00 00 00 00 9F EE

[M→W] F7 0B 01 34 04 41 10 00 00 9C EE


현대통신의 RS-485 패킷 기본 구조는 0xF7이 시작 바이트, 0xEE가 끝 바이트로 이루어져 있으며, 2번째 바이트는 패킷의 길이, 4번째 바이트는 디바이스 ID를 가리킨다 (4번째 패킷이 0x34이면 엘리베이터 패킷)
또한 5번째 바이트가이 0x01이면 상태 조회, 0x02이면 명령, 0x04이면 응답 패킷으로 해석할 수 있다

특이한 점은 거실 월패드가 쿼리 패킷을 보낼 때, 만약 엘리베이터가 호출되어 이동중인 상태라면 엘리베이터의 현재 상태값들을 실어보낸다는 사실이다


엘리베이터 호출된 상태 (하행)

[W→M] F7 0D 01 34 01 41 10 00 00 00 00 9F EE

[M→W] F7 0B 01 34 04 41 10 06 06 9C EE

[M→W] F7 0B 01 34 04 41 10 06 06 9C EE

[M→W] F7 0B 01 3404 41 10 06 06 9C EE

[W→M] F7 0D 01 34 01 41 10 00 B6 06 07 28 EE
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 B6 06 89 EE
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 B6 06 07 28 EE
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 B6 06 89 EE
...

엘리베이터 도착 후

[W→M] F7 0D 01 34 01 41 10 00 01 XX 07 8F EE

[M→W] F7 0B 01 34 04 41 10 00 00 9C EE


거실 월패드는 아파트 단지 서버와의 통신 (혹은 엘리베이터와의 통신)을 통해 현재 엘리베이터의 상태를 가져와 미니 패드에 보내는 쿼리 패킷에 관련 정보를 실어보내는게 아닌가 추측해봤다

저번에 분석할 때는 10번째 바이트가 엘리베이터의 '층 수'인 것과, 도착했을 때 9번째 바이트가 0x01로 값이 바뀐다는 점만 파악하고 나머지 정보에 대해서는 패킷 parser에 적용하지 않았다 

(정보 파악한답시고 엘리베이터 자꾸 호출하는게 약간 부담스러웠달까...)

 

참고로 거실 월패드에서 상행 호출 시에는 쿼리 패킷에 엘리베이터 정보가 실리기는 하나 미니패드의 응답 패킷은 값이 변하지 않는 것을 알 수 있었다


거실 월패드에서 엘리베이터 상행 호출 시

[W→M] F7 0D 01 34 01 41 10 00 B5 B6 06 9A EE

[M→W] F7 0B 01 34 04 41 10 00 00 9C EE

[W→M] F7 0D 01 34 01 41 10 00 A5 B5 06 89 EE
[M→W] F7 0B 01 34 04 41 10 00 00 9C EE
[W→M] F7 0D 01 34 01 41 10 00 A5 B4 07 89 EE
[M→W] F7 0B 01 34 04 41 10 00 00 9C EE


상행 호출 시 쿼리 패킷의  9번째 바이트 하위 4비트가 0x5로, 하행 호출 시 하위 4비트가 0x6인 것과 차이를 보였다

(아무래도 미니패드는 엘리베이터 호출 버튼이 눌렸는지 여부만 응답하는 것으로 보이며, 상행 호출 기능이 없는 미니패드는 이에 대한 값 변경이 없는게 아닌가 하는 추측~)


댓글을 보니 11번째 바이트가 엘리베이터 호기 정보를 담고 있고, 9번째 바이트의 상위 4비트는 0xA일 경우 '올라가는 중', 0xB일 경우 '내려가는 중' 정보를 나타낸다고 한다 (댓글 다신분은 zero-index based로 패킷 순서를 말씀하신 것 같다)

<예시>

  • [W→M] F7 0D 01 34 01 41 10 00 B6 06 07 28 EE: 7호기의 현재 층수=6층(06), 하행 호출 상태/내려가는 중
  • [W→M] F7 0D 01 34 01 41 10 00 A5 B5 06 89 EE: 6호기의 현재 층수=지하5층(B5), 상행 호출 상태/올라가는 중

이 정보를 토대로 패킷 파서 구문과 Elevator 상태 update 구문을 수정해보자

2. 코드 수정

패킷 파서(ParserVarious.py) 스크립트의 엘리베이터 관련 코드에는 9, 10, 11번째 바이트값을 해석하는 구문을 다음과 같이 추가했다

class ParserVarious(PacketParser):
    def interpretPacket(self, packet: bytearray):
        if packet[3] == 0x34:
            self.handleElevator(packet)
    
    def handleElevator(self, packet: bytearray):
        if packet[4] == 0x01:  # 상태 쿼리 (월패드 -> 복도 미니패드)
            # F7 0D 01 34 01 41 10 00 XX YY ZZ ** EE
            # XX: 00=Idle, 01=Arrived, 하위4비트가 6이면 하행 호출중, 5이면 상행 호출 중, 
            #     상위4비트 A: 올라가는 중, B: 내려가는 중
            # YY: 현재 층수 (string encoded), ex) 지하3층 = B3, 5층 = 05
            # ZZ: 호기, ex) 01=1호기, 02=2호기, ...
            # **: Checksum (XOR SUM)
            state_h = (packet[8] & 0xF0) >> 4  # 상위 4비트, 0x0: stopped, 0xA: moving (up), 0x0B: moving (down)
            state_l = packet[8] & 0x0F  # 하위 4비트, 0x0: idle, 0x1: arrived, 0x5: command (up), 0x6: command (down)
            floor = '{:02X}'.format(packet[9])
            dev_idx = packet[10]  # 엘리베이터 n호기, 2대 이상의 정보가 교차로 들어오게 됨, idle일 경우 0

            state = 0  # idle (command done, command off)
            if state_h == 0x0:
                direction = 0
                if state_l == 0x1:
                    state = 1  # arrived
            else:
                direction = 0
                if state_h == 0xA:  # Up
                    direction = 5
                elif state_h == 0xB:  # Down
                    direction = 6
                if state_l == 0x5:  # Up
                    state = 5
                elif state_l == 0x6:  # Down
                    state = 6
                
            result = {'device': 'elevator', 'state': state, 'dev_idx': dev_idx, 'direction': direction, 'floor': floor}
            self.sig_parse_result.emit(result)
        elif packet[4] == 0x02:
            pass
        elif packet[4] == 0x04:  # 상태 응답 (복도 미니패드 -> 월패드)
            # F7 0B 01 34 04 41 10 00 XX YY EE
            # XX: 하위 4비트: 6 = 하행 호출  ** 상행 호출에 해당하는 5 값은 발견되지 않는다
            # YY: Checksum (XOR SUM)
            # 미니패드의 '엘리베이터 호출' 버튼의 상태를 반환함
            state = packet[8] & 0x0F  # 0 = idle, 6 = command (하행) 호출
            result = {'device': 'elevator', 'state': state}
            self.sig_parse_result.emit(result)

응답 패킷의 9번째 바이트 하위 4비트가 0x0에서 0x6으로 변하면 하행 호출이 된 것으로 판단할 수 있다

엘리베이터 호출이 시작된 것과는 별개로, 쿼리 패킷은 1대 이상의 엘리베이터에 대한 현재 상태를 각각 다루어야하므로 엘리베이터 클래스 (Elevator.py) 코드는 살짝 복잡해졌다 (여러 기기의 상태 관리를 위해 Dict-List 자료형을 사용)

class Direction(IntEnum):
    UNKNOWN = 0
    UP = 5
    DOWN = 6

class State(IntEnum):
    IDLE = 0
    ARRIVED = 1
    MOVINGUP = 5
    MOVINGDOWN = 6

class DevInfo:
    index: int  # n호기
    state: State
    direction: Direction
    floor: str

    def __init__(self, index: int):
        self.index = index
        self.state = State.IDLE
        self.direction = Direction.UNKNOWN
        self.floor = ''

    def __repr__(self) -> str:
        return f'<Elevator({self.index}) - STAT:{self.state.name}, DIR:{self.direction.name}, FLOOR:{self.floor}>'

class Elevator(Device):
    time_arrived: float = 0.
    time_threshold_arrived_change: float = 10.
    dev_info_list: List[DevInfo]
    ready_to_clear: bool = True

    def __init__(self, name: str = 'Elevator', **kwargs):
        super().__init__(name, **kwargs)
        self.dev_info_list = list()
    
    def __repr__(self):
        repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})'
        repr_txt += '>'
        return repr_txt
    
    def publish_mqtt(self):
        obj = {
            "state": self.state, 
            "index": [x.index for x in self.dev_info_list],
            "direction": [x.direction.value for x in self.dev_info_list],
            "floor": [x.floor for x in self.dev_info_list]
        }
        if self.mqtt_client is not None:
            self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)
    
    def updateState(self, state: int, **kwargs):
        dev_idx = kwargs.get('dev_idx')
        if dev_idx is not None:
            # 월패드 -> 복도 미니패드 상태 쿼리 패킷 (packet[4] == 0x01)
            direction = kwargs.get('direction')
            floor = kwargs.get('floor')
            if dev_idx == 0:  # idle 상태
                if self.ready_to_clear:
                    self.dev_info_list.clear()
                if self.state_prev in [5, 6]:  # '도착' 정보가 담긴 패킷을 놓치는 경우에 대한 처리
                    writeLog(f"Arrived (Missing Packet)", self)
                    self.state = 1
                else:
                    self.state = 0
            else:
                find = list(filter(lambda x: x.index == dev_idx, self.dev_info_list))
                if len(find) == 0:
                    dev_info = DevInfo(dev_idx)
                    self.dev_info_list.append(dev_info)
                    self.dev_info_list.sort(key=lambda x: x.index)
                else:
                    dev_info = find[0]
                dev_info.state = State(state)
                dev_info.direction = Direction(direction)
                dev_info.floor = floor
            
            for e in self.dev_info_list:
                # 여러대의 엘리베이터 중 한대라도 '도착'이면 state를 1로 전환
                if e.state == State.ARRIVED:
                    if self.state_prev != 1:
                        writeLog(f"#{e.index} - Arrived", self)
                    self.state = 1
                    break
        else:
            # 복도 미니패드 -> 월패드 상태 응답 패킷 (packet[4] == 0x04)
            # state값은 0(idle) 혹은 6(하행 호출)만 전달됨
            self.state = state
        
        if not self.init:
            self.publish_mqtt()
            self.init = True

        if self.state != self.state_prev:
            if self.state == 1:  # Arrived
                self.ready_to_clear = False
                self.time_arrived = time.perf_counter()
                self.publish_mqtt()
                self.state_prev = self.state
            else:
                if self.state_prev == 1:
                    # 도착 후 상태가 다시 idle로 바뀔 때 시간차가 적으면 occupancy sensor가 즉시 off가 되어
                    # notification이 제대로 되지 않는 문제가 있어, 상태 변화 딜레이를 줘야한다
                    time_elapsed_last_arrived = time.perf_counter() - self.time_arrived
                    if time_elapsed_last_arrived > self.time_threshold_arrived_change:
                        writeLog("Ready to rollback state (idle)", self)
                        self.ready_to_clear = True
                        self.publish_mqtt()
                        self.state_prev = self.state
                else:
                    self.publish_mqtt()
                    self.state_prev = self.state

여러 기기 중 한대라도 쿼리 패킷9번째 바이트0x01이 되면 엘리베이터 객체의 상태는 '도착(=1)'이 되게 만들며, 도착 이후 응답 패킷의 9번째 바이트가 0x00이 됨으로 인해 인해 바로 IDLE 상태가 되면 홈킷에 도착 Notification이 제대로 되지 않는 문제가 있어 state 변경 시 시간 지연 알고리즘을 추가한 것 정도가 핵심이 되겠다

사전 설계하지 않고 agile하게 되는대로 헤딩하면서 30분만에 짠거라 굉장히 지저분한 코드가 되어버렸는데, 딱히 엘리베이터 현재 상태 자체를 활용할 계획은 없어서 그냥 이대로 놔두기로 한다 ㅋㅋ (HA에서는 엘리베이터 층수 관련해서 재미있는 악세사리들을 붙일 수 있는거 같은데, Homebridge는 뭐... 그냥 기본 기능에 충실하게 사용하는걸로 만족하기로~)

3. 테스트

거실 월패드에 표시되는 엘리베이터 상태와 상호 비교를 쉽게 하기위해 간단하게 flask 웹서버를 구현해 비교해봤다

거실 월패드에 상태가 업데이트되는 속도에 비해, 거실 월패드가 미니패드에 보내는 쿼리 패킷에 정보가 반영되는 속도가 다소 느리고, 층수 정보가 누락되는 경우가 보인다

 

실제로 Raw Packet을 콘솔에 로깅해봐도 거실 월패드에는 표시되는 층수가 패킷에는 표시되지 않는 경우가 다수 발생했다... 내가 뭔가 잘못하고 있는건 아니겠지? ㅋㅋ


[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[M→W] F7 0B 01 34 04 41 10 06 06 9C EE
[W→M] F7 0D 01 34 01 41 10 00 B6 06 07 28 EE : 7호기 현재층수 = 6
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 B6 06 89 EE : 6호기 현재층수 = B6
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 B6 06 07 28 EE: 7호기 현재층수 = 6
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 B6 06 89 EE: 6호기 현재층수=B6
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 B5 06 8A EE: 6호기 현재층수=B5
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 06 07 38 EE: 7호기 현재층수=6
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 B4 06 8B EE: 6호기 현재층수=B4
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 00 06 3F EE: 6호기 현재층수=00 (??)

※ 6호기의 현재층수가 00으로 표시되는 경우는, 아파트 BM층 (지하3층과 지하4층 사이)에 엘리베이터가 위치할 때인 것 같다.. (딱히 16진수로 표시할 방법이 없었던 듯? ㅋㅋㅋ) 월패드에도 B- 와 같이 표시된다

[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 08 07 36 EE: 7호기 현재층수=8
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 00 06 3F EE: 6호기 현재층수=00 (??)
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 09 07 37 EE: 7호기 현재층수=9
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 B3 06 8C EE: 6호기 현재층수=B3
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 11 07 2F EE: 7호기 현재층수=11
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 02 06 3D EE: 6호기 현재층수=2

※ 월패드에는 7호기 12층이 표시되나, 패킷에는 보이지 않았다
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 13 07 2D EE: 7호기 현재층수=13
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 14 07 2A EE: 7호기 현재층수=14
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 03 06 3C EE: 6호기 현재층수=3
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE
[W→M] F7 0D 01 34 01 41 10 00 A6 06 06 39 EE: 6호기 현재층수=6
[M→W] F7 0B 01 34 04 41 10 00 06 9A EE

 


아무래도 미니패드가 연결된 RS-485 포트에는 난방, 에어컨, 전열교환기, 도시가스차단기 등 다수의 기기가 함께 연결되어 있어서 엘리베이터 관련 패킷만 단독으로 송/수신하는데는 한계가 있는 것으로 추측해본다

※ 실제로 위의 엘리베이터 패킷들 사이사이에 다른 디바이스들과 관련된 패킷들이 많이 오가고 있다

4. 깃허브 커밋

커밋 아이디 45738ab28ae1bb675f379a02ad75ed030ec4db74로 변경 내용 업데이트 완료~

https://github.com/YOGYUI/HomeNetwork/commit/45738ab28ae1bb675f379a02ad75ed030ec4db74

 

힐스테이트 - 엘리베이터 파서 상태 조회 기능 추가 · YOGYUI/HomeNetwork@45738ab

Show file tree Showing 28 changed files with 195 additions and 100 deletions.

github.com

나중에 Home Assistant 고도화할 때 요긴하게 쓸 수 있을 것 같은 느낌이 들지만, 오늘은 이대로 마무리하기로~ 

 

끝~!

 

P.S) HomeAssistant 네이버카페 루트2님에게 감사의 인사를 드립니다~ ^^

 

반응형
Comments