YOGYUI

힐스테이트 광교산::시스템에어컨 제어 RS-485 패킷 분석 본문

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

힐스테이트 광교산::시스템에어컨 제어 RS-485 패킷 분석

요겨 2022. 6. 19. 17:01
반응형

 

분양받을 때 옵션으로 거실이랑 방3개 모두 천장형 시스템에어컨 (공기청정 미적용) 설치하기로 계약했다

원래 가지고 있던 에어컨이 없었을 뿐더러 이제껏 천장에 달린 시스템에어컨이 있는 집에서만 살아왔기 때문에 이게 익숙해서 별고민없이 설치할 수 있는 공간엔 죄다 설치했다

(에어컨 옵션 금액만 638만원 부들부들... 돈이 이렇게 많이 들줄은 몰랐지 ㅠㅠ)

 

거실에 있는 월패드로 각 방별로 에어컨 4가지의 상태를 설정할 수 있다

  • 가동상태: On/Off
  • 희망온도: 1도 단위, 최소 18도 ~ 최대 30도
  • 풍량: 자동/미풍/약풍/강풍
  • 모드: 자동/냉방/제습/공기청정

공기청정은 미적용 옵션으로 했는데, 월패드에서는 설정할 수 있는 점이 신기했다

(물론 Hi-oT 앱으로도 가능한데, 앱에서는 여러 상태에 대한 명령을 한꺼번에 보내는 바람에 패킷 해석이 좀 힘들었다)

 

가스밸브, 난방, 환기(전열교환기)가 같이 물려있는 RS-485 포트에서 역시나 패킷 캡쳐작업을 해봤다

(아직 4번째 바이트가 0x1C, 0x2A, 0x34, 0x48인 패킷과 관련된 기기가 어떤건지 알아내는 과정이다)

1. 에어컨 가동 상태 변경 패킷 분석

class ParserVarious(SerialParser):    
    def interpretPacket(self, packet: bytearray):
        try:
            if packet[2:4] == bytearray([0x01, 0x1B]):  # 가스차단기
                self.handleGasValve(packet)
            elif packet[2:4] == bytearray([0x01, 0x18]):  # 난방
                self.handleThermostat(packet)
            elif packet[2:4] == bytearray([0x01, 0x2B]):  # 환기 (전열교환기)
                self.handleVentilator(packet)
            else:
                if packet[4] == 0x02:
                    print(self.prettifyPacket(packet))
        except Exception as e:
            writeLog('interpretPacket::Exception::{} ({})'.format(e, packet), self)

F7 0B 01 1C 02 40 11 01 00 B3 EE : 거실 에어컨 가동 시작 명령

F7 0B 01 1C 02 40 11 02 00 B0 EE : 거실 에어컨 가동 중단 명령

F7 0B 01 1C 02 40 21 01 00 83 EE : 침실(방1) 에어컨 가동 시작 명령

F7 0B 01 1C 02 40 21 02 00 80 EE : 침실(방1) 에어컨 가동 중단 명령

F7 0B 01 1C 02 40 31 01 00 93 EE : 서재(방2) 에어컨 가동 시작 명령

F7 0B 01 1C 02 40 31 02 00 90 EE : 서재(방2) 에어컨 가동 중단 명령

F7 0B 01 1C 02 40 41 01 00 E3 EE : 컴퓨터방(방3) 에어컨 가동 시작 명령

F7 0B 01 1C 02 40 41 02 00 E0 EE : 컴퓨터방(방3) 에어컨 가동 중단 명령


에어컨 가동 명령은 길이 11(=0x0B)에 네번째 바이트가 0x1C로 구성되어 있는 것을 알 수 있다

7번째 바이트는 조명/아울렛과 마찬가지로 상위 4비트가 공간 인덱스 값이며 8번째 바이트가 0x01이면 가동 시작, 0x02이면 가동 종료 명령인 것으로 보인다

 

네번째 바이트가 0x1C가 에어컨과 연관된 패킷인 것을 알아냈으므로, 코드를 조금 수정해서 컴퓨터방(방3)의 희망온도, 풍량, 운전모드를 변경하면서 명령 패킷을 캡쳐해보자

class ParserVarious(SerialParser):    
    def interpretPacket(self, packet: bytearray):
        try:
            if packet[2:4] == bytearray([0x01, 0x1B]):  # 가스차단기
                self.handleGasValve(packet)
            elif packet[2:4] == bytearray([0x01, 0x18]):  # 난방
                self.handleThermostat(packet)
            elif packet[2:4] == bytearray([0x01, 0x2B]):  # 환기 (전열교환기)
                self.handleVentilator(packet)
            elif packet[2:4] == bytearray([0x01, 0x1C]):  # 시스템에어컨
                room_idx = packet[6] >> 4
                if room_idx == 4:
                    if packet[4] == 0x02:
                        print(self.prettifyPacket(packet))
        except Exception as e:
            writeLog('interpretPacket::Exception::{} ({})'.format(e, packet), self)

F7 0B 01 1C 02 45 41 18 00 FF EE : 컴퓨터방(방3) 희망온도 24도 설정 명령

F7 0B 01 1C 02 45 41 17 00 F0 EE : 컴퓨터방(방3) 희망온도 23도 설정 명령

F7 0B 01 1C 02 45 41 11 00 F6 EE : 컴퓨터방(방3) 희망온도 18도 설정 명령 (최소)

F7 0B 01 1C 02 45 41 1E 00 F9 EE : 컴퓨터방(방3) 희망온도 30도 설정 명령 (최대)

 

F7 0B 01 1C 02 5C 41 00 00 FE EE : 컴퓨터방(방3) 자동 모드 설정 명령

F7 0B 01 1C 02 5C 41 01 00 FF EE : 컴퓨터방(방3) 냉방 모드 설정 명령

F7 0B 01 1C 02 5C 41 02 00 FC EE : 컴퓨터방(방3) 제습 모드 설정 명령

F7 0B 01 1C 02 5C 41 03 00 FD EE : 컴퓨터방(방3) 공기청정 모드 설정 명령 

 

F7 0B 01 1C 02 5D 41 01 00 FE EE : 컴퓨터방(방3) 풍량 자동(Auto) 설정 명령

F7 0B 01 1C 02 5D 41 02 00 FD EE : 컴퓨터방(방3) 풍량 미풍 설정 명령

F7 0B 01 1C 02 5D 41 03 00 FC EE : 컴퓨터방(방3) 풍량 약풍 설정 명령

F7 0B 01 1C 02 5D 41 04 00 FB EE : 컴퓨터방(방3) 풍량 강풍 설정 명령


설정하고자 하는 카테고리가 패킷의 6번째 바이트 값에 따라 다른 것을 알 수 있다

  • 0x45: 희망온도 설정
    8번째 바이트 = 온도값 (정수형)
    0x18 = 24, 0x17 = 23, 0x11 = 11, 0x1E = 30
  • 0x5C: 운전모드 설정
    8번째 바이트가 0x00이면 자동, 0x01이면 냉방, 0x02이면 제습, 0x03이면 공기청정
  • 0x5D: 풍량 설정
    8번째 바이트가 0x01이면 자동, 0x02이면 미풍, 0x03이면 약풍, 0x04이면 강풍

난방과 마찬가지로 온도값이 1바이트에 정수형으로 인코딩되어 있어서 파싱하기 쉽다

2. 쿼리 및 응답 패킷 분석

평소에 월패드가 에어컨과 주고받는 패킷을 캡쳐해보자 (방3에 한정해서..)

class ParserVarious(SerialParser):    
    def interpretPacket(self, packet: bytearray):
        try:
            if packet[2:4] == bytearray([0x01, 0x1B]):  # 가스차단기
                self.handleGasValve(packet)
            elif packet[2:4] == bytearray([0x01, 0x18]):  # 난방
                self.handleThermostat(packet)
            elif packet[2:4] == bytearray([0x01, 0x2B]):  # 환기 (전열교환기)
                self.handleVentilator(packet)
            elif packet[2:4] == bytearray([0x01, 0x1C]):  # 시스템에어컨
                room_idx = packet[6] >> 4
                if room_idx == 4:
                    print(self.prettifyPacket(packet))
        except Exception as e:
            writeLog('interpretPacket::Exception::{} ({})'.format(e, packet), self)

F7 0B 01 1C 01 40 41 00 00 E1 EE : 평소 쿼리

F7 0F 01 1C 04 40 41 00 02 1A 1A 03 01 E0 EE : 평소 응답

F7 0B 01 1C 02 40 41 01 00 E3 EE : 가동 명령

F7 0F 01 1C 04 40 41 01 01 1A 1A 03 01 E2 EE : 가동 명령 후 응답

F7 0B 01 1C 02 40 41 02 00 E0 EE : 가동 중단 명령

F7 0F 01 1C 04 40 41 02 02 1A 1A 03 01 E2 EE : 중단 명령 응답

 

F7 0B 01 1C 02 45 41 18 00 FF EE : 희망온도 24도 설정 명령

F7 0F 01 1C 04 45 41 18 01 1A 18 03 01 FC EE : 온도 설정 명령 응답

F7 0F 01 1C 04 40 41 00 01 1A 18 03 01 E1 EE : 평소 응답

 

F7 0B 01 1C 02 5D 41 02 00 FD EE : 풍량 미풍 설정 명령

F7 0F 01 1C 04 5D 41 02 01 1A 18 03 02 FD EE : 풍량 설정 명령 응답

F7 0F 01 1C 04 40 41 00 01 1A 18 03 02 FD EE : 평소 응답

 

F7 0B 01 1C 02 5C 41 00 00 FE EE : 자동 모드 설정 명령

F7 0F 01 1C 04 5C 41 00 01 1A 18 00 02 FD EE : 설정 명령 응답

F7 0F 01 1C 04 40 41 00 01 1A 18 00 02 E1 EE : 평소 응답


평소 쿼리에 대한 응답과, 명령에 대한 응답 패킷의 구조가 6번째, 8번째 바이트 그리고 체크섬을 제외하고는 값이 동일한 것을 알 수 있다 (= 상태 파싱 구문 구현하는게 어렵지 않다)

아마도 8번째 바이트는 명령 패킷의 명령값을 반복(어떤 명령이 들어왔는지 확인하는 용도)해주는게 아닌가하는 추측~

 

이제 패킷 명세를 정리해보자

  • 4번째 바이트가 0x18이면 시스템에어컨 관련 패킷
  • 5번째 바이트가 0x01이면 쿼리, 0x02이면 명령, 0x04이면 응답 패킷
  • 6번째 바이트가
    0x40이면 가동 시작/중단 상태 및 평소 쿼리-응답
    0x45이면 희망온도
    0x5C이면 운전모드
    0x5D이면 풍량 관련 설정 패킷
  • 7번째 바이트의 상위 4비트는 공간 인덱스 (1=거실, 2=침실, 3=서재, 4=컴퓨터방)
  • 명령 패킷의 8번째 바이트는 설정값
    운전모드 설정 시: 0=자동, 1=냉방, 2=제습, 3=공기청정
    풍량 설정 시: 1=자동, 2=미풍, 3=약풍, 4=강풍
    희망온도 설정 시: 온도값 (정수)
  • 응답 패킷의 9번째 바이트는 가동 상태: 2=OFF, 1=ON
  • 응답 패킷의 10번째 바이트는 현재 온도값
  • 응답 패킷의 11번째 바이트는 희망 온도값
  • 응답 패킷의 12번째 바이트는 운전모드
  • 응답 패킷의 13번째 바이트는 풍량

가동상태, 현재온도, 희망온도, 운전모드, 풍량 등 5개 상태를 다뤄야하므로 코드가 약간 복잡해질 것 같긴 하지만... 어쨌든 홈네트워크 플랫폼 (Homebridge, Home Assistant)랑 연동할 준비는 끝~!

 

 

 

반응형
Comments