일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- ConnectedHomeIP
- 홈네트워크
- 애플
- matter
- 오블완
- RS-485
- 매터
- cluster
- 현대통신
- Espressif
- 공모주
- 국내주식
- 티스토리챌린지
- Python
- 파이썬
- Bestin
- esp32
- SK텔레콤
- raspberry pi
- 월패드
- homebridge
- Home Assistant
- Apple
- 나스닥
- MQTT
- 해외주식
- 코스피
- 미국주식
- 배당
- 힐스테이트 광교산
- Today
- Total
YOGYUI
현대통신 월패드 RS-485 연동 소스코드(python) 개선 작업 본문
Hyundai Wallpad RS-485 Python Source Code Enhancement
지난주 목요일 (6월 15일) 힐스테이트 소스코드 관련 지원 요청 이메일을 받았다
소스코드가 워낙에 조악하게 기능 구현에만 충실하게 짜놨다보니 디버깅 혹은 원격지원 관련해서는 이렇게 이메일로 받아볼 수 밖에 없는 안타까운 현실..
그렇다 하더라도 괜히 AWS같은 클라우드를 도입하기에는 딱히 코드로 부가 수익을 내는게 아니기때문에 부담스럽... 뭔가 정식으로 제품을 출시하지 않을 바에야 그냥 앞으로도 이렇게 유저분들이 보내주시는 수동 에러 리포트에 대응하면서 코드를 개선해나갈 생각 ㅋㅋ
1. 문제점 파악
에러 리포트 이메일에 첨부된 로그는 다음과 같다
<23:22:10.406552> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 19 01 40 10 00 00 B5 EE
<23:22:10.730232> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0D 01 19 04 40 10 00 02 02 02 B4 EE
<23:22:10.890279> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 1F 01 40 20 00 00 83 EE
<23:22:10.891901> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0D 01 34 01 41 10 00 00 00 00 9F EE
<23:22:10.953304> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 1C 01 1F 04 40 20 00 21 01 00 00 00 00 00 00 02 22 01 00 00 00 00 00 00 02 92 EE
<23:22:11.348932> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0B 01 1B 01 43 11 00 00 B5 EE
<23:22:11.412824> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0D 01 1B 04 43 11 00 03 00 00 B5 EE
<23:22:11.484604> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0E 01 2A 01 40 10 00 19 00 1B 03 82 EE
<23:22:11.485020> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 19 01 40 20 00 00 85 EE
<23:22:11.814052> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0C 01 19 04 40 20 00 02 02 87 EE
<23:22:12.040436> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 1F 01 40 30 00 00 93 EE
<23:22:12.041157> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0D 01 34 01 41 10 00 00 00 00 9F EE
<23:22:12.117436> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 1C 01 1F 04 40 30 00 31 01 00 3C 00 00 00 00 02 32 01 00 31 00 00 00 00 02 8F EE
<23:22:12.439258> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0B 01 18 01 46 10 00 00 B2 EE
<23:22:12.531168> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 22 01 18 04 46 10 00 04 1B 1C 04 1C 15 04 1C 19 04 1C 17 00 00 00 00 00 00 00 00 00 00 00 00 9E EE
<23:22:12.665028> [Home (0x7F36C42C6FD0)] handlePacketParseResult::Exception::list index out of range ({'device': <DeviceType.LIGHT: 1>, 'index': 1, 'room_index': 3, 'state': 0})
<23:22:12.737188> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0D 01 34 01 41 10 00 00 00 00 9F EE
<23:22:13.070259> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 19 01 40 30 00 00 95 EE
<23:22:13.144309> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0C 01 19 04 40 30 00 02 02 97 EE
<23:22:13.206216> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0B 01 1B 01 43 11 00 00 B5 EE
<23:22:13.269856> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0D 01 1B 04 43 11 00 03 00 00 B5 EE
<23:22:13.271038> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 1F 01 40 40 00 00 E3 EE
<23:22:13.340985> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0E 01 2A 01 40 10 00 19 00 1B 03 82 EE
<23:22:13.341555> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 1C 01 1F 04 40 40 00 41 01 00 00 00 00 00 00 02 42 01 00 00 00 00 00 00 02 F2 EE
<23:22:13.731892> [Home (0x7F36C42C6FD0)] handlePacketParseResult::Exception::list index out of range ({'device': <DeviceType.LIGHT: 1>, 'index': 1, 'room_index': 4, 'state': 0})
<23:22:13.803251> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0D 01 34 01 41 10 00 00 00 00 9F EE
<23:22:14.136822> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 19 01 40 40 00 00 E5 EE
<23:22:14.208030> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0C 01 19 04 40 40 00 02 02 E7 EE
<23:22:14.208347> [Home (0x7F36C42C6FD0)] handlePacketParseResult::Exception::'NoneType' object has no attribute 'outlets' ({'device': <DeviceType.OUTLET: 2>, 'index': 0, 'room_index': 5, 'state': 1})
<23:22:14.208502> [Home (0x7F36C42C6FD0)] handlePacketParseResult::Exception::'NoneType' object has no attribute 'outlets' ({'device': <DeviceType.OUTLET: 2>, 'index': 1, 'room_index': 5, 'state': 1})
<23:22:14.284661> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0B 01 1B 01 43 11 00 00 B5 EE
<23:22:14.351339> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0D 01 1B 04 43 11 00 03 00 00 B5 EE
<23:22:14.351739> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 0B 01 1F 01 40 50 00 00 F3 EE
<23:22:14.421201> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0E 01 2A 01 40 10 00 19 00 1B 03 82 EE
<23:22:14.422636> [ParserVarious (0x7F36C42FABB0)] Unknown packet (??): F7 1C 01 1F 04 40 50 00 51 01 00 00 00 00 00 00 02 52 01 00 5F 00 00 00 00 02 BD EE
<23:22:14.748416> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 0B 01 18 01 46 10 00 00 B2 EE
<23:22:14.841085> [ParserLight (0x7F36C42FA5E0)] Unknown packet: F7 22 01 18 04 46 10 00 04 1B 1C 04 1C 15 04 1C 19 04 1C 17 00 00 00 00 00 00 00 00 00 00 00 00 9E EE
<23:22:14.974426> [Home (0x7F36C42C6FD0)] handlePacketParseResult::Exception::'NoneType' object has no attribute 'lights' ({'device': <DeviceType.LIGHT: 1>, 'index': 0, 'room_index': 5, 'state': 0})
<23:22:14.975034> [Home (0x7F36C42C6FD0)] handlePacketParseResult::Exception::'NoneType' object has no attribute 'lights' ({'device': <DeviceType.LIGHT: 1>, 'index': 1, 'room_index': 5, 'state': 0})
<23:22:14.975284> [Home (0x7F36C42C6FD0)] handlePacketParseResult::Exception::'NoneType' object has no attribute 'lights' ({'device': <DeviceType.LIGHT: 1>, 'index': 2, 'room_index': 5, 'state': 0})
- 두 개의 RS-485 포트에 후킹을 위해 장착한 컨버터 모듈(USB-to-RS485 혹은 EW-11)에서 수신되는 패킷을 해석하는 'ParserLight'와 'ParserVarious' 객체 모두 'Unknown packet' 로그 발생
- 'NoneType' object has no attribute 'lights' 로그 발생
두 번째 이슈는 config.xml 파일을 수정하면 해결될 것으로 보이는데, 첫번째 문제는 상당히 문제가 심각했다
(실제로 Unknown packet 로그 뒤에 붙는 패킷 raw bytes를 보면 정상적인 패킷으로 보이기 때문)
좀 더 현상 파악을 위해 유저분과 이메일을 주고받으면서 알게된 약간은 충격적인 사실
RS-485 포트를 하나만 사용하는 환경이 있다!
조명, 난방, 가스(밸브), 대기전력(아울렛), 일괄소등 스위치, 엘리베이터, HEMS 모두 월패드 뒷면의 하나의 RS-485 포트('1번 포트')만 사용하고 있는 상황..
우리집 (힐스테이트 광교산)은 월패드 뒷면의 RS-485 포트 두개가 모두 사용되고 있는 환경이라 각 포트별로 특화된 패킷 파서 클래스를 구현해둔 상태
※ 각 패킷 파서는 패킷의 4번째 바이트(buffer[3])의 값에 따라 디바이스 종류를 판별
- ParserLight: 조명/콘센트(아울렛) 관련 패킷을 해석
buffer[3] == 0x19: 조명 → handleLight 메서드 호출
buffer[3] == 0x1E: 현관 도어락
buffer[3] == 0x1F: 콘센트(아울렛) → handleOutlet 메서드 호출
buffer[3] == 0x43: 에너지 사용량(콘센트) 쿼리
이외에는 모두 'Unknown packet'으로 처리 - ParserVarious: 기타 디바이스 패킷 해석
buffer[3] == 0x18: 난방 → handleThermostat 메서드 호출
buffer[3] == 0x1B: 가스차단기 → handleGasValve 메서드 호출
buffer[3] == 0x1C: 시스템에어컨 → handleAirconditioner 메서드 호출
buffer[3] == 0x2A: 일괄소등 스위치 → handleBatchOffSwitch 메서드 호출
buffer[3] == 0x2B: 환기(전열교환기) → handleVentilator 메서드 호출
buffer[3] == 0x34: 엘리베이터 → handleElevator 메서드 호출
buffer[3] == 0x43: HEMS → handleEnergyMonitoring 메서드 호출
buffer[3] == 0x44: 현재 시간
buffer[3] == 0x48: ??? (정체 파악 못함)
이외에는 모두 'Unknown packet'으로 처리 - ParserSubPhone: 주방 서브폰 (baud 3840) 패킷 해석
주방 서브폰 RS-485 포트 사용하지 않을 경우 의미없음
결국 1개의 RS-485 포트에 'ParserLight'와 'ParserVarious' 두 객체를 모두 연동할 경우 객체가 각각 처리할 수 있는 패킷 외에는 모두 'Unknown packet' 오류를 출력할 수 밖에 없는 구조...
무엇이 문제인지는 판별 완료!
2. 코드 구조 개선 방안
메일 주신 분께서는 급한대로 '조명'이라도 사용을 원하셨다
그런데 아무리 짱구를 굴려봐도 기존 코드 구조를 조금 수정해서는 위에서 발견한 문제를 해결할 수 없겠다는 생각이 들어서 소스코드를 대폭 수정하기로 결심했다
가장 시급한 건 패킷 파서 클래스를 하나로 통합하는 것이다
각 포트별로 해석되는 패킷이 모두 동일한 구조를 가지며, 4번째 바이트값이 디바이스 종류를 결정하는데 겹치는 값이 없으므로 손쉽게 다음과 같이 통합할 수 있다
※ 엄밀히 말하면 패킷 구조가 동일한 것은 아니다
조명, 가스 등의 9600 baud를 사용하는 패킷은 F7 XX XX ... EE 구조이며, 3840 baud를 사용하는 주방 서브폰 패킷은 7F XX ... EE 구조를 가지기 때문에 패킷 파서에서 이를 유의해서 처리해줘야 한다
패킷 자체의 '타입'을 지정해주면 될듯?
또한, 기존에는 'Room' 클래스를 구현해 Room 안에 조명, 아울렛, 난방, 에어컨 객체를 멤버변수로 가지게 구현해뒀었다
(Light 및 Outlet과 Room의 릴레이션은 N:1, Thermostat 및 Airconditioner와 Room의 릴레이션은 1:1)
또한 Home은 Room에 포함되지 않은 디바이스 (엘리베이터, 가스밸브, 전열교환기 등)들을 각각 멤버변수로 가지고 있어 실제 환경에서 '무조건' 연동되어 있다는 가정이 전제된 구조로 구현되어 있었다
(사용하지 않게 하기 위해서는 별도로 수정이 필요했음)
힐스테이트 광교산과는 구조가 다른 환경 (방 개수가 다름 / 환기나 일괄 소등 등 디바이스의 유무 등)에서도 유연하게 돌아가게 하기 위해 다음과 같이 수정해줘야 한다
- Home 객체가 별도로 클래스 종속적인 멤버변수들을 가지지 않고, 디바이스들의 부모 클래스인 Device를 리스트로 가짐 (Home과 Device의 릴레이션의 1:N)
- 'Room' 클래스는 제거
- 각 Device는 공통적으로 'room_index' 멤버변수를 가짐
3. 코드 구현
2.1. 패킷 파서 클래스 통합
기존의 PacketLight와 PacketVarious 및 PacketSubphone 3 클래스 내부 구현 코드를 PacketParser 클래스 하나로 통합해줬다 (코드 자체가 길어질 뿐이지 그다지 어려운 작업은 아님)
체크포인트는 'ParserType' 열거형 클래스로 RS-485 포트의 타입을 지정할 수 있게 했다는 정도?
(9600 baud 패킷 형태와 3840 baud 주방 서브폰의 패킷 형태가 상이함)
- handlePacket 메서드에서 각 타입 분기에 따라 raw byte stream을 어떻게 끊어서 해석할 지가 결정된다
@unique
class ParserType(IntEnum):
REGULAR = 0 # 일반 RS-485 (baud 9600)
SUBPHONE = auto() # 주방 서브폰 (baud 3840)
UNKNOWN = auto()
class PacketParser:
name: str = "Parser"
rs485: RS485Comm
buffer: bytearray
enable_console_log: bool = False
chunk_cnt: int = 0
max_chunk_cnt: int = 1e6
max_buffer_size: int = 200
line_busy: bool = False
type_interpret: ParserType = ParserType.REGULAR
packet_storage: List[dict]
max_packet_store_cnt: int = 100
def __init__(self, rs485_instance: RS485Comm, name: str, type_interpret: ParserType = ParserType.REGULAR):
self.buffer = bytearray()
self.sig_parse_result = Callback(dict)
self.name = name
self.rs485 = rs485_instance
self.rs485.sig_send_data.connect(self.onSendData)
self.rs485.sig_recv_data.connect(self.onRecvData)
self.type_interpret = type_interpret
self.packet_storage = list()
def __repr__(self):
repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})>'
return repr_txt
def release(self):
self.buffer.clear()
def sendPacket(self, packet: Union[bytes, bytearray], log: bool = True):
self.log_send_result = log
self.rs485.sendData(packet)
def sendString(self, packet_str: str):
self.rs485.sendData(bytearray([int(x, 16) for x in packet_str.split(' ')]))
def onSendData(self, data: bytes):
if self.log_send_result:
msg = ' '.join(['%02X' % x for x in data])
self.log(f"Send >> {msg}")
self.log_send_result = True
def onRecvData(self, data: bytes):
self.line_busy = True
if len(self.buffer) > self.max_buffer_size:
self.buffer.clear()
self.line_busy = False
self.buffer.extend(data)
self.handlePacket()
def log(self, message: str):
writeLog(f"<{self.name}> {message}", self)
def handlePacket(self):
if self.type_interpret is ParserType.REGULAR:
idx = self.buffer.find(0xF7)
if idx > 0:
self.buffer = self.buffer[idx:]
if len(self.buffer) >= 2:
packet_length = self.buffer[1]
if len(self.buffer) >= packet_length:
if self.buffer[0] == 0xF7 and self.buffer[packet_length - 1] == 0xEE:
self.line_busy = False
packet = self.buffer[:packet_length]
try:
checksum_calc = self.calcXORChecksum(packet[:-2])
checksum_recv = packet[-2]
if checksum_calc == checksum_recv:
self.interpretPacket(packet)
else:
pacstr = self.prettifyPacket(packet)
self.log(f'Checksum Error (calc={checksum_calc}, recv={checksum_recv}) ({pacstr})')
self.buffer = self.buffer[packet_length:]
except IndexError:
buffstr = self.prettifyPacket(self.buffer)
pacstr = self.prettifyPacket(packet)
self.log(f'Index Error (buffer={buffstr}, packet_len={packet_length}, packet={pacstr})')
elif self.type_interpret is ParserType.SUBPHONE:
idx = self.buffer.find(0x7F)
if idx > 0:
self.buffer = self.buffer[idx:]
if len(self.buffer) >= 2:
idx2 = self.buffer.find(0xEE)
if idx2 > 0:
self.line_busy = False
packet = self.buffer[:idx2 + 1]
self.interpretPacket(packet)
self.buffer = self.buffer[idx2 + 1:]
elif len(self.buffer) == 1 and self.buffer[0] == 0xEE:
self.line_busy = False
self.buffer.clear()
elif self.type_interpret is ParserType.UNKNOWN:
self.buffer.clear()
self.log(f'Invalid Parser Type ({self.type_interpret})')
def interpretPacket(self, packet: bytearray):
store: bool = True
packet_info = {'packet': packet, 'timestamp': datetime.datetime.now()}
try:
if self.type_interpret == ParserType.REGULAR:
if packet[3] == 0x18: # 난방
self.handleThermostat(packet)
packet_info['device'] = 'thermostat'
store = self.enable_store_packet_header_18
elif packet[3] == 0x19: # 조명
self.handleLight(packet)
packet_info['device'] = 'light'
store = self.enable_store_packet_header_19
elif packet[3] == 0x1B: # 가스차단기
self.handleGasValve(packet)
packet_info['device'] = 'gasvalve'
store = self.enable_store_packet_header_1B
elif packet[3] == 0x1C: # 시스템에어컨
self.handleAirconditioner(packet)
store = self.enable_store_packet_header_1C
packet_info['device'] = 'airconditioner'
elif packet[3] == 0x1E: # 현관 도어락 (?)
self.log(f'Doorlock Packet: {self.prettifyPacket(packet)}')
packet_info['device'] = 'doorlock'
store = self.enable_store_packet_header_1E
elif packet[3] == 0x1F: # 아울렛 (콘센트)
self.handleOutlet(packet)
packet_info['device'] = 'outlet'
store = self.enable_store_packet_header_1F
elif packet[3] == 0x2A: # 일괄소등 스위치
self.handleBatchOffSwitch(packet)
packet_info['device'] = 'multi function switch'
store = self.enable_store_packet_header_2A
elif packet[3] == 0x2B: # 환기 (전열교환기)
self.handleVentilator(packet)
packet_info['device'] = 'ventilator'
store = self.enable_store_packet_header_2B
elif packet[3] == 0x34: # 엘리베이터
self.handleElevator(packet)
packet_info['device'] = 'elevator'
store = self.enable_store_packet_header_34
elif packet[3] == 0x43: # 에너지 모니터링
self.handleEnergyMonitoring(packet)
packet_info['device'] = 'hems'
store = self.enable_store_packet_header_43
elif packet[3] == 0x44: # maybe current date-time?
if packet[4] == 0x0C: # broadcasting?
packet_info['device'] = 'timestamp'
if self.enable_trace_timestamp_packet:
year, month, day = packet[8], packet[9], packet[10]
hour, minute, second = packet[11], packet[12], packet[13]
millis = packet[14] * 100 + packet[15] * 10 + packet[16]
dt = datetime.datetime(year, month, day, hour, minute, second, millis * 1000)
self.log(f'Timestamp Packet: {self.prettifyPacket(packet)}')
self.log(f'>> {dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}')
else:
packet_info['device'] = 'unknown'
self.log(f'Unknown packet (44): {self.prettifyPacket(packet)}')
store = self.enable_store_packet_header_44
elif packet[3] == 0x48: # ??
packet_info['device'] = 'unknown'
store = self.enable_store_packet_header_48
else:
self.log(f'Unknown packet: {self.prettifyPacket(packet)}')
packet_info['device'] = 'unknown'
store = self.enable_store_packet_unknown
elif self.type_interpret == ParserType.SUBPHONE:
if (packet[1] & 0xF0) == 0xB0: # 현관 도어폰 호출
self.handleFrontDoor(packet)
elif (packet[1] & 0xF0) == 0x50: # 공동 현관문 호출
self.handleCommunalDoor(packet)
elif (packet[1] & 0xF0) == 0xE0: # HEMS
self.handleHEMS(packet)
else:
self.log(f'{self.prettifyPacket(packet)} >> ???')
if store:
if len(self.packet_storage) > self.max_packet_store_cnt:
self.packet_storage.pop(0)
self.packet_storage.append(packet_info)
except Exception as e:
self.log(f'interpretPacket::Exception::{e} ({self.prettifyPacket(packet)})')
def startRecv(self, count: int = 64):
self.buffer.clear()
self.chunk_cnt = 0
self.enable_console_log = True
while self.chunk_cnt < count:
pass
self.enable_console_log = False
def setRS485LineBusy(self, value: bool):
self.line_busy = value
def isRS485LineBusy(self) -> bool:
if self.rs485.getType() == RS485HwType.Socket:
return False # 무선 송신 레이턴시때문에 언제 라인이 IDLE인지 정확히 파악할 수 없다
return self.line_busy
def getRS485HwType(self) -> RS485HwType:
return self.rs485.getType()
def clearPacketStorage(self):
self.packet_storage.clear()
def setBufferSize(self, size: int, clear_buffer: bool = True):
self.max_buffer_size = size
if clear_buffer:
self.buffer.clear()
self.log(f'Recv Buffer Size: {self.max_buffer_size}')
@staticmethod
def prettifyPacket(packet: bytearray) -> str:
return ' '.join(['%02X' % x for x in packet])
@staticmethod
def calcXORChecksum(data: Union[bytearray, bytes, List[int]]) -> int:
return reduce(lambda x, y: x ^ y, data, 0)
def handleLight(self, packet: bytearray):
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
result = {
'device': DeviceType.LIGHT,
'index': idx,
'room_index': room_idx,
'state': state
}
self.sig_parse_result.emit(result)
else: # 상태 변경 명령 직후 응답
state = 0 if packet[8] == 0x02 else 1
result = {
'device': DeviceType.LIGHT,
'index': dev_idx - 1,
'room_index': room_idx,
'state': state
}
self.sig_parse_result.emit(result)
def handleOutlet(self, packet: bytearray):
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: # 일반 쿼리 (모든 디바이스)
outlet_count = (len(packet) - 10) // 9
for idx in range(outlet_count):
dev_packet = packet[8 + idx * 9: 8 + (idx + 1) * 9]
state = 0 if dev_packet[1] == 0x02 else 1
result = {
'device': DeviceType.OUTLET,
'index': idx,
'room_index': room_idx,
'state': state
}
self.sig_parse_result.emit(result)
else: # 상태 변경 명령 직후 응답
state = 0 if packet[8] == 0x02 else 1
result = {
'device': DeviceType.OUTLET,
'index': dev_idx - 1,
'room_index': room_idx,
'state': state
}
self.sig_parse_result.emit(result)
def handleGasValve(self, packet: bytearray):
if packet[4] == 0x01: # 상태 쿼리
pass
elif packet[4] == 0x02: # 상태 변경 명령
pass
elif packet[4] == 0x04: # 상태 응답
state = 0 if packet[8] == 0x03 else 1
result = {
'device': DeviceType.GASVALVE,
'state': state
}
self.sig_parse_result.emit(result)
def handleThermostat(self, packet: bytearray):
room_idx = packet[6] & 0x0F
if packet[4] == 0x01: # 상태 쿼리
pass
elif packet[4] == 0x02: # On/Off, 온도 변경 명령
pass
elif packet[4] == 0x04: # 상태 응답
if room_idx == 0: # 일반 쿼리 (존재하는 모든 디바이스)
thermostat_count = (len(packet) - 10) // 3
for idx in range(thermostat_count):
dev_packet = packet[8 + idx * 3: 8 + (idx + 1) * 3]
if dev_packet[0] != 0x00: # 0이면 존재하지 않는 디바이스
state = 0 if dev_packet[0] == 0x04 else 1
temp_current = dev_packet[1] # 현재 온도
temp_config = dev_packet[2] # 설정 온도
result = {
'device': DeviceType.THERMOSTAT,
'room_index': idx + 1,
'state': state,
'temp_current': temp_current,
'temp_config': temp_config
}
self.sig_parse_result.emit(result)
else: # 상태 변경 명령 직후 응답
if packet[5] in [0x45, 0x46]: # 0x46: On/Off 설정 변경에 대한 응답, 0x45: 온도 설정 변경에 대한 응답
state = 0 if packet[8] == 0x04 else 1
temp_current = packet[9] # 현재 온도
temp_config = packet[10] # 설정 온도
result = {
'device': DeviceType.THERMOSTAT,
'room_index': room_idx,
'state': state,
'temp_current': temp_current,
'temp_config': temp_config
}
self.sig_parse_result.emit(result)
def handleVentilator(self, packet: bytearray):
if packet[4] == 0x01:
pass
elif packet[4] == 0x02:
pass
elif packet[4] == 0x04:
state = 0 if packet[8] == 0x02 else 1
rotation_speed = packet[9] # 0x01=약, 0x03=중, 0x07=강
result = {
'device': DeviceType.VENTILATOR,
'state': state
}
if rotation_speed != 0:
result['rotation_speed'] = rotation_speed
self.sig_parse_result.emit(result)
def handleAirconditioner(self, packet: bytearray):
room_idx = packet[6] >> 4
if packet[4] == 0x01: # 상태 쿼리
pass
elif packet[4] == 0x02: # On/Off, 온도 변경 명령
pass
elif packet[4] == 0x04: # 상태 응답
state = 0 if packet[8] == 0x02 else 1
temp_current = packet[9] # 현재 온도
temp_config = packet[10] # 설정 온도
mode = packet[11] # 모드 (0=자동, 1=냉방, 2=제습, 3=공기청정)
rotation_speed = packet[12] # 풍량 (1=자동, 2=미풍, 3=약풍, 4=강풍)
result = {
'device': DeviceType.AIRCONDITIONER,
'room_index': room_idx,
'state': state,
'temp_current': temp_current,
'temp_config': temp_config,
'mode': mode,
'rotation_speed': rotation_speed
}
self.sig_parse_result.emit(result)
def handleElevator(self, packet: bytearray):
if packet[4] == 0x01: # 상태 쿼리 (월패드 -> 복도 미니패드)
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])
ev_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': DeviceType.ELEVATOR,
'data_type': 'query',
'state': state,
'ev_dev_idx': ev_dev_idx,
'direction': direction,
'floor': floor
}
self.sig_parse_result.emit(result)
elif packet[4] == 0x02:
pass
elif packet[4] == 0x04: # 상태 응답 (복도 미니패드 -> 월패드)
state = packet[8] & 0x0F # 0 = idle, 6 = command (하행) 호출
result = {
'device': DeviceType.ELEVATOR,
'data_type': 'response',
'state': state
}
# print(f'Response: {self.prettifyPacket(packet)}, {result}')
self.sig_parse_result.emit(result)
def handleEnergyMonitoring(self, packet: bytearray):
if packet[4] == 0x01: # 상태 쿼리
pass
elif packet[4] == 0x04:
# 값들이 hexa encoding되어있다!
if packet[5] == 0x11: # 전기 사용량
value = int(''.join('%02X' % x for x in packet[7:12]))
# self.log(f'EMON - Electricity: {value}')
elif packet[5] == 0x13: # 가스 사용량
value = int(''.join('%02X' % x for x in packet[7:12]))
# self.log(f'EMON - Gas: {value}')
elif packet[5] == 0x14: # 수도 사용량
value = int(''.join('%02X' % x for x in packet[7:12]))
# self.log(f'EMON - Water: {value}')
elif packet[5] == 0x15: # 온수 사용량
value = int(''.join('%02X' % x for x in packet[7:12]))
# self.log(f'EMON - Hot Water: {value}')
elif packet[5] == 0x16: # 난방 사용량
value = int(''.join('%02X' % x for x in packet[7:12]))
# self.log(f'EMON - Heating: {value}')
else:
self.log(f'> {self.prettifyPacket(packet)}')
def handleBatchOffSwitch(self, packet: bytearray):
if packet[4] == 0x01: # 상태 쿼리
pass
elif packet[4] == 0x04:
state = 0 if packet[9] == 0x02 else 1
result = {
'device': DeviceType.BATCHOFFSWITCH,
'state': state
}
self.sig_parse_result.emit(result)
def handleFrontDoor(self, packet: bytearray):
result = {'device': DeviceType.SUBPHONE}
notify: bool = True
if packet[1] == 0xB5:
# 현관 도어폰 초인종 호출 (월패드 -> 서브폰)
result['ringing_front'] = 1
self.log(f'{self.prettifyPacket(packet)} >> Front door ringing started')
elif packet[1] == 0xB6:
# 현관 도어폰 초인종 호출 종료 (월패드 -> 서브폰)
result['ringing_front'] = 0
self.log(f'{self.prettifyPacket(packet)} >> Front door ringing terminated')
elif packet[1] == 0xB9:
# 서브폰에서 현관 통화 시작 (서브폰 -> 월패드)
result['streaming'] = 1
self.log(f'{self.prettifyPacket(packet)} >> Streaming (front door) started from Subphone')
elif packet[1] == 0xBA:
# 서브폰에서 현관 통화 종료 (서브폰 -> 월패드)
result['streaming'] = 0
self.log(f'{self.prettifyPacket(packet)} >> Streaming (front door) terminated from Subphone')
elif packet[1] == 0xB4:
# 서브폰에서 현관문 열림 명령 (서브폰 -> 월패드)
result['doorlock'] = 0 # Unsecured
self.log(f'{self.prettifyPacket(packet)} >> Open front door from Subphone')
elif packet[1] in [0xBB, 0xB8]:
# 현관 도어폰 통화 종료
result['ringing_front'] = 0
result['streaming'] = 0
result['doorlock'] = 1 # Secured
self.log(f'{self.prettifyPacket(packet)} >> Streaming finished')
else:
notify = False
self.log(f'{self.prettifyPacket(packet)} >> ???')
if notify:
self.sig_parse_result.emit(result)
def handleCommunalDoor(self, packet: bytearray):
result = {'device': DeviceType.SUBPHONE}
notify: bool = True
if packet[1] == 0x5A:
# 공동현관문 호출 (월패드 -> 서브폰)
result['ringing_communal'] = 1
self.log(f'{self.prettifyPacket(packet)} >> Communal door ringing started')
elif packet[1] == 0x5C:
# 공동현관문 호출 종료 (월패드 -> 서브폰)
result['ringing_communal'] = 0
self.log(f'{self.prettifyPacket(packet)} >> Communal door ringing terminated')
elif packet[1] == 0x5E:
# 공동현관문 통화 종료
result['ringing_communal'] = 0
result['streaming'] = 0
result['doorlock'] = 1 # Secured
self.log(f'{self.prettifyPacket(packet)} >> Streaming finished')
else:
notify = False
self.log(f'{self.prettifyPacket(packet)} >> ???')
if notify:
self.sig_parse_result.emit(result)
def handleHEMS(self, packet: bytearray):
if packet[1] == 0xE0:
# 쿼리 패킷 (서브폰 -> 월패드)
pass
elif packet[1] == 0xE1:
result = {'device': DeviceType.HEMS, 'packet': packet}
notify: bool = True
# 응답 패킷 (월패드 -> 서브폰)
devtype = HEMSDevType((packet[2] & 0xF0) >> 4)
category = HEMSCategory(packet[2] & 0x0F)
if category.value in [1, 2, 3, 4]:
# 7F E1 XY 09 P1 P2 P3 Q1 Q2 Q3 R1 R2 R3 ZZ EE
# X: 디바이스 타입
# Y: 쿼리 타입
# P1 P2 P3 : 당월 이력 값
# Q1 Q2 Q3 : 전월 이력 값
# R1 R2 R3 : 전전월 이력 값
# ZZ : XOR Checksum
v1 = int.from_bytes(packet[4:7], byteorder='big', signed=False)
v2 = int.from_bytes(packet[7:10], byteorder='big', signed=False)
v3 = int.from_bytes(packet[10:13], byteorder='big', signed=False)
result[f'{devtype.name.lower()}_{category.name.lower()}_cur_month'] = v1
result[f'{devtype.name.lower()}_{category.name.lower()}_1m_ago'] = v2
result[f'{devtype.name.lower()}_{category.name.lower()}_2m_ago'] = v3
elif category.value in [5, 7]:
# 7F E1 XY 03 P1 P2 P3 ZZ EE
# X: 디바이스 타입
# Y: 쿼리 타입
# P1~P3: 값
# ZZ : XOR Checksum
v = int.from_bytes(packet[4:7], byteorder='big', signed=False)
result[f'{devtype.name.lower()}_{category.name.lower()}'] = v
else:
notify = False
self.log(f'{self.prettifyPacket(packet)} >> ???')
if notify:
self.sig_parse_result.emit(result)
elif packet[1] == 0xE2:
if self.enable_trace_timestamp_packet:
year, month, day = int('%02X' % packet[2]), int('%02X' % packet[3]), int('%02X' % packet[4])
hour, minute, second = int('%02X' % packet[5]), int('%02X' % packet[6]), int('%02X' % packet[7])
dt = datetime.datetime(year, month, day, hour, minute, second)
self.log(f'Timestamp Packet: {self.prettifyPacket(packet)}')
self.log(f'>> {dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}')
else:
self.log(f'{self.prettifyPacket(packet)} >> ???')
2.2. Room 클래스 제거 및 Device 클래스 수정
수정 사항이 너무 많아 깃허브 커밋으로 대체 (ㅠ)
https://github.com/YOGYUI/HomeNetwork/commit/4cba376258465608b1dda0ed650d536920f6f95b
메일을 받자마자 코드 설계 및 구현 작업을 해서 3일 정도 걸려서 마무리했다
생각보다 Home 클래스 쪽에 개선해야 할 작업이 많아서 코드가 상당 부분 바뀌었다
처음에는 리팩터링 정도로 생각하고 접근했지만, 내부 구조가 완전히 달라져서 이전 버전의 config와는 호환이 불가능하게 되었다는... ㅠ_ㅠ
그래도 Readme 마크다운으로 꽤나 자세하게 설명해두긴 했다..
4. 마무리
일단 우리집인 힐스테이트 광교산에서는 모든 디바이스 동작이 원활하게 잘 되는 걸 확인했다
(mqtt topic 구조도 바꾸는 바람에 homebridge, home assistant config도 대폭 수정해야 했지만...)
메일 주신 분의 테스트 결과를 보고 잘 돌아간다 싶으면 바꾼 코드를 토대로 숙원사업(?)이던 'automatic device discovery' 기능까지 마무리해봐야겠다
그동안 손놓고 있었는데, 코드 구조를 확 개선하고나니 discovery 기능도 더 쉽게 구현할 수 있을 것 같다..
진작에 Room 객체를 좀 지웠어야했는데 ㅋㅋ
'홈네트워크(IoT) > 힐스테이트 광교산' 카테고리의 다른 글
HAOS에서 현대통신 RS485 연동 GitHub python 코드 실행하기 (20) | 2024.01.02 |
---|---|
현대통신 월패드 RS-485 디바이스 자동 탐지 및 HA MQTT Discovery 지원 기능 추가 (9) | 2023.06.24 |
힐스테이트 광교산::일괄소등 스위치 RS-485 패킷 분석 및 애플 홈 연동 (4) | 2023.06.04 |
힐스테이트 광교산::난방/냉방 (반복) 타이머 기능 구현 (4) | 2022.12.20 |
힐스테이트 광교산::공동출입문용 RFID 스티커형 태그 복사 (스마트폰) (1) | 2022.11.21 |