일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 파이썬
- 힐스테이트 광교산
- matter
- 퀄컴
- 배당
- 나스닥
- ConnectedHomeIP
- 티스토리챌린지
- 미국주식
- 월패드
- raspberry pi
- homebridge
- Python
- 코스피
- 엔비디아
- 현대통신
- esp32
- 공모주
- 애플
- 홈네트워크
- MQTT
- 해외주식
- 오블완
- Apple
- Home Assistant
- Bestin
- Espressif
- 국내주식
- RS-485
- 매터
- Today
- Total
YOGYUI
광교아이파크::Bestin ↔ Apple HomeKit 연동 Summary (1) 본문
Bentin 홈네트워크 월패드에서 제어 가능한 디바이스 대부분을 Apple Homekit에 액세서리로 연동하는데 성공했다
- 주방 및 서재, 침실, 컴퓨터방 조명 On/Off
- 거실, 침실, 컴퓨터방 난방 On/Off 및 온도 설정
- 환기(전열교환기) On/Off 및 풍량 설정
- 가스레인지 밸브 잠금
- 엘리베이터 호출
- 거실 조명 On/Off
2020년 12월 초부터 시작해서 2021년 3월까지 대충 4개월정도 걸린 것 같다
(재택근무하는 와중에 짬짬이 시간을 내서 하다보니 생각보다 길어졌다)
Raspberry Pi 4도 새걸로 한개 마련하고, USB-Serial 컨버터도 여러개 구매하고 PCB도 제작하다보니 돈이 꽤 많이 들었다 (대략 30만원? ㅠㅠ 다음번 포스팅 때 정확한 금액을 산출해 볼 예정)
돈이 들어서 아깝다기보다는 홈네트워크 관련 경험을 쌓았다는 데 의의를 두자... 정신승리
내년에 입주하는 힐스테이트(내 집!!!)는 과연 어떤 네트워크를 쓸지 벌써부터 기대된다
대장정의 막을 내릴 겸, 코드 정리도 할 겸 마무리 포스팅을 작성해보기로 했다
Apple Homekit 연동을 위해 Raspberry Pi 4에 Homebridge를 구동하고, 디바이스간 통신 프로토콜을 MQTT로 결정한 뒤 broker인 Moqsuitto도 함께 구동하고 있다
거실 조명 제어를 제외하면 모두 신발장에 있는 홈네트워크 게이트웨이의 RS-485 포트에 USB-RS485 컨버터를 통해 통신라인을 후킹해서 시리얼패킷 파싱 및 송신까지 Raspberry Pi에서 담당하게 하였다
거실 조명 제어는 외부에서 접근할 수 있는 통신 라인을 찾을 수가 없어서, 거실 월패드의 터치패드 부분의 PCB를 역공학해서 하드웨어단에서 접근할 수 있게 WiFi Module (ESP-12F)을 기반으로 한 PCB를 직접 설계 및 제작해서 제어가 가능하도록 구현했다
Raspberry Pi 내에서 RS-485 패킷 핸들링 및 Homebridge와의 연동은 Python 언어로 작성된 스크립트도 구동하며, 웹 연동을 위해 Flask 라이브러리를 사용해 웹서버를 개설해 동작하고 있다
전체 시스템 다이어그램을 그려보면 다음과 같다
정리해보니 뭔가 한 일에 비해 시스템 자체는 간단해보여서 조금은 허무하다 ㅋㅋ
코드 유지보수 편의성 향상을 위해 코드를 객체 지향적으로 깔끔하게 정리하고 가능하면 깃허브까지 연동해보도록 하자
1. 클래스 설계
draw.io 를 통해 간단하게 UML 다이어그램을 그리고 전체적인 코드 구조를 설계해보자
- 각 제어 디바이스 객체들은 공통적으로 On/Off 제어가 핵심이므로 부모 클래스인 Device는 On/Off 상태에 대한 변수 및 mqtt 관련 객체를 멤버로 갖고 있도록 설계
- On/Off 제어 외 다양한 제어들은 자식 클래스들이 자체 멤버 메서드로 접근할 수 있도록 설계
(arguments로 공통화하려고도 해봤는데, 메서드 이름에 기능이 명시화되는게 더 유지 및 보수에 더 좋다고 판단) - 방(Room) 하나에 동일한 종류의 디바이스가 여러 개 존재할 경우 (ex: 조명)을 위해 인덱스 멤버변수는 필수
- 상태값이 변경되었을 경우 즉각 mqtt publish할 수 있도록 과거 상태값을 저장하고 있는 멤버변수 구현 (_prev)
- 거실 조명은 별도의 MQTT Client가 구동되는 하드웨어에서 관리하므로 구현할 필요 없음
- 1개씩만 존재하는 디바이스는 별도로 방에 배정하지 않고 Home에 객체 멤버 배치
- 여러 명령이 동시에 전달되는 경우를 대비하여 Queue 방식으로 명령을 쌓은 뒤 처리하도록 구현 (쓰레드)
- 일정 간격으로 전체 디바이스들의 현재 상태값을 publish하도록 쓰레드 구현
- 보안 관련 정보 (MQTT broker 계정)와 시리얼 패킷값은 모두 XML 형식 파일을 로컬에서 불러오도록 설정
(시리얼 패킷 하드코딩 방지)
2. 코드 구현
클래스 설계를 토대로 차례차례 구현해보자
필요한 파이썬 라이브러리는 다음과 같다
import os
import time
import json
import queue
import ctypes
import requests
import threading
import datetime
import threading
from abc import ABCMeta, abstractmethod
from typing import List, Union
import paho.mqtt.client as mqtt
import xml.etree.ElementTree as ET
from Serial485.SerialComm import SerialComm
from Serial485.EnergyParser import EnergyParser
from Serial485.ControlParser import ControlParser
from Serial485.SmartParser import SmartParser
def checkAgrumentType(obj, arg):
if type(obj) == arg:
return True
if arg == object:
return True
if arg in obj.__class__.__bases__:
return True
return False
class Callback(object):
_args = None
_callback = None
def __init__(self, *args):
self._args = args
def connect(self, callback):
self._callback = callback
def emit(self, *args):
if len(args) != len(self._args):
raise Exception('Callback::Argument Length Mismatch')
arglen = len(args)
if arglen > 0:
validTypes = [checkAgrumentType(args[i], self._args[i]) for i in range(arglen)]
if sum(validTypes) != arglen:
raise Exception('Callback::Argument Type Mismatch (Definition: {}, Call: {})'.format(self._args, args))
if self._callback is not None:
self._callback(*args)
def timestampToString(timestamp: datetime.datetime):
h = timestamp.hour
m = timestamp.minute
s = timestamp.second
us = timestamp.microsecond
return '%02d:%02d:%02d.%06d' % (h, m, s, us)
def getCurTimeStr():
return '<%s>' % timestampToString(datetime.datetime.now())
def writeLog(strMsg: str, obj: object = None):
strTime = getCurTimeStr()
if obj is not None:
if isinstance(obj, threading.Thread):
if obj.ident is not None:
strObj = ' [%s (0x%X)]' % (type(obj).__name__, obj.ident)
else:
strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj))
else:
strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj))
else:
strObj = ''
msg = strTime + strObj + ' ' + strMsg
print(msg)
여기서 Serial485 패키지는 pyserial 라이브러리를 기반으로 시리얼 통신 및 패킷 파싱 관련 구현 스크립트로, 포스트 중간중간에 구현 결과물이 있으므로 여기서는 생략하도록 한다
Device 부모클래스는 다음과 같이 구현할 수 있다
class Device:
__metaclass__ = ABCMeta
name: str = 'Device'
room_index: int = 0
init: bool = False
state: int = 0 # mostly, 0 is OFF and 1 is ON
state_prev: int = 0
packet_set_state_on: str = ''
packet_set_state_off: str = ''
packet_get_state: str = ''
mqtt_client: mqtt.Client = None
mqtt_publish_topic: str = ''
mqtt_subscribe_topics: List[str]
def __init__(self, name: str = 'Device', **kwargs):
self.name = name
if 'room_index' in kwargs.keys():
self.room_index = kwargs['room_index']
self.mqtt_client = kwargs.get('mqtt_client')
self.mqtt_subscribe_topics = list()
writeLog('Device Initialized >> Name: {}, Room Index: {}'.format(self.name, self.room_index), self)
@abstractmethod
def publish_mqtt(self):
pass
실제 디바이스에 해당하는 객체들은 다음과 같이 구현
class Light(Device):
def __init__(self, name: str = 'Device', index: int = 0, **kwargs):
self.index = index
super().__init__(name, **kwargs)
def publish_mqtt(self):
obj = {"state": self.state}
self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)
class GasValve(Device):
def publish_mqtt(self):
# 0 = closed, 1 = opened, 2 = opening/closing
obj = {"state": int(self.state == 1)}
self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)
class Thermostat(Device):
temperature_current: float = 0.
temperature_current_prev: float = 0.
temperature_setting: float = 0.
temperature_setting_prev: float = 0.
packet_set_temperature: List[str]
def __init__(self, name: str = 'Device', **kwargs):
super().__init__(name, **kwargs)
self.packet_set_temperature = [''] * 71 # 5.0 ~ 40.0, step=0.5
def publish_mqtt(self):
obj = {
"state": 'HEAT' if self.state == 1 else 'OFF',
"currentTemperature": self.temperature_current,
"targetTemperature": self.temperature_setting
}
self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)
class Ventilator(Device):
state_natural: int = 0
rotation_speed: int = 0
rotation_speed_prev: int = 0
timer_remain: int = 0
packet_set_rotation_speed: List[str]
def __init__(self, name: str = 'Ventilator', **kwargs):
super().__init__(name, **kwargs)
self.packet_set_rotation_speed = [''] * 3
def publish_mqtt(self):
obj = {
"state": self.state,
"rotationspeed": int(self.rotation_speed / 3 * 100)
}
self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)
class Elevator(Device):
my_floor: int = -1
current_floor: int = -1
current_floor_prev: int = -1
def __init__(self, name: str = 'Elevator', **kwargs):
super().__init__(name, **kwargs)
self.sig_call_up = Callback()
self.sig_call_down = Callback()
def call_up(self):
self.sig_call_up.emit()
def call_down(self):
self.sig_call_down.emit()
def publish_mqtt(self):
obj = {
"state": int(self.state == 4 and self.current_floor == self.my_floor)
}
self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)
self.mqtt_client.publish("home/ipark/elevator/state/occupancy", json.dumps(obj), 1)
조명이나 난방 디바이스가 포함된 Room 클래스는 다음과 같이 구현
class Room:
name: str = 'Room'
# 각 방에는 조명 모듈 여러개와 난방 모듈 1개 존재
index: int = 0
lights: List[Light]
thermostat: Thermostat = None
def __init__(self, name: str = 'Room', index: int = 0, light_count: int = 0, has_thermostat: bool = True, **kwargs):
self.name = name
self.index = index
self.lights = list()
for i in range(light_count):
self.lights.append(Light(name='Light {}'.format(i + 1), index=i, room_index=self.index, mqtt_client=kwargs.get('mqtt_client')))
if has_thermostat:
self.thermostat = Thermostat(name='Thermostat', room_index=self.index, mqtt_client=kwargs.get('mqtt_client'))
@property
def light_count(self):
return len(self.lights)
명령 Queue 관리 및 모니터링 관련 쓰레드는 다음과 같이 구현
모니터링 쓰레드는 일정 시간 간격으로 현재 상태를 MQTT 발행하여 액세서리의 상태를 최신값으로 유지할 수 있도록 하며, 커맨드 쓰레드는 Homebridge를 통해 상태 변경에 대한 명령을 받았을 경우 게이트웨이의 RS-485 통신라인에 패킷을 쓰는 가장 핵심적인 구문이라고 할 수 있다
class ThreadMonitoring(threading.Thread):
_keepAlive: bool = True
def __init__(
self,
serial_list: List[SerialComm],
device_list: List[Device],
publish_interval: int = 60,
interval_ms: int = 2000
):
threading.Thread.__init__(self)
self._serial_list = serial_list
self._device_list = device_list
self._publish_interval = publish_interval
self._interval_ms = interval_ms
self.sig_terminated = Callback()
def run(self):
writeLog('Started', self)
tm = time.perf_counter()
while self._keepAlive:
for ser in self._serial_list:
if ser.isConnected():
delta = ser.time_after_last_recv()
if delta > 10:
writeLog('Warning!! Serial <{}> is not receiving for {:.1f} seconds'.format(ser.name, delta), self)
else:
# writeLog('Warning!! Serial <{}> is not connected'.format(ser.name), self)
pass
if time.perf_counter() - tm > self._publish_interval:
writeLog('Regular Publishing Device State MQTT (interval: {} sec)'.format(self._publish_interval), self)
for dev in self._device_list:
dev.publish_mqtt()
tm = time.perf_counter()
time.sleep(self._interval_ms / 1000)
writeLog('Terminated', self)
self.sig_terminated.emit()
def stop(self):
self._keepAlive = False
class ThreadCommand(threading.Thread):
_keepAlive: bool = True
def __init__(self, queue_: queue.Queue):
threading.Thread.__init__(self)
self._queue = queue_
self._retry_cnt = 10
self._delay_response = 0.4
self.sig_send_energy = Callback(str)
self.sig_send_control = Callback(str)
self.sig_send_smart = Callback(str)
self.sig_terminated = Callback()
def run(self):
writeLog('Started', self)
while self._keepAlive:
if not self._queue.empty():
elem = self._queue.get()
writeLog('Get Command Queue: {}'.format(elem), self)
try:
dev = elem['device']
category = elem['category']
target = elem['target']
func = elem['func']
if target is None:
continue
if isinstance(dev, Light):
if category == 'state':
room_idx = elem['room_idx']
dev_idx = elem['dev_idx']
self.set_light_state(dev, target, room_idx, dev_idx, func)
elif isinstance(dev, Thermostat):
if category == 'state':
self.set_state_common(dev, target, func)
elif category == 'temperature':
self.set_thermostat_temperature(dev, target, func)
elif isinstance(dev, GasValve):
if category == 'state':
self.set_gas_state(dev, target, func)
elif isinstance(dev, Ventilator):
if category == 'state':
self.set_state_common(dev, target, func)
elif category == 'rotation_speed':
self.set_ventilator_rotation_speed(dev, target, func)
elif isinstance(dev, Elevator):
if category == 'state':
if target == 1:
func()
dev.publish_mqtt()
except Exception as e:
writeLog(e, self)
else:
time.sleep(1e-3)
writeLog('Terminated', self)
self.sig_terminated.emit()
def stop(self):
self._keepAlive = False
def set_state_common(self, dev: Device, target: int, func):
cnt = 0
packet1 = dev.packet_set_state_on if target else dev.packet_set_state_off
packet2 = dev.packet_get_state
for _ in range(self._retry_cnt):
if dev.state == target:
break
func(packet1)
cnt += 1
time.sleep(0.2)
if dev.state == target:
break
func(packet2)
time.sleep(0.2)
writeLog('set_state_common::send # = {}'.format(cnt), self)
time.sleep(self._delay_response)
dev.publish_mqtt()
def set_light_state(self, dev: Light, target: int, room_idx: int, dev_idx: int, func):
cnt = 0
packet1 = dev.packet_set_state_on if target else dev.packet_set_state_off
packet2 = dev.packet_get_state
for _ in range(self._retry_cnt):
if dev.state == target:
break
func(packet1)
cnt += 1
time.sleep(0.2)
if dev.state == target:
break
func(packet2)
time.sleep(0.2)
writeLog('set_light_state::send # = {}'.format(cnt), self)
time.sleep(self._delay_response)
dev.publish_mqtt()
def set_gas_state(self, dev: GasValve, target: int, func):
cnt = 0
packet1 = dev.packet_set_state_on if target else dev.packet_set_state_off
packet2 = dev.packet_get_state
# only closing is permitted, 2 = Opening/Closing (Valve is moving...)
if target == 0:
for _ in range(self._retry_cnt):
if dev.state in [target, 2]:
break
func(packet1)
cnt += 1
time.sleep(0.5)
if dev.state in [target, 2]:
break
func(packet2)
time.sleep(0.5)
writeLog('set_gas_state::send # = {}'.format(cnt), self)
time.sleep(self._delay_response)
dev.publish_mqtt()
def set_thermostat_temperature(self, dev: Thermostat, target: float, func):
cnt = 0
idx = max(0, min(70, int((target - 5.0) / 0.5)))
packet1 = dev.packet_set_temperature[idx]
packet2 = dev.packet_get_state
for _ in range(self._retry_cnt):
if dev.temperature_setting == target:
break
func(packet1)
cnt += 1
time.sleep(0.2)
if dev.temperature_setting == target:
break
func(packet2)
time.sleep(0.2)
writeLog('set_thermostat_temperature::send # = {}'.format(cnt), self)
time.sleep(self._delay_response)
dev.publish_mqtt()
def set_ventilator_rotation_speed(self, dev: Ventilator, target: int, func):
cnt = 0
packet1 = dev.packet_set_rotation_speed[target - 1]
packet2 = dev.packet_get_state
for _ in range(self._retry_cnt):
if dev.rotation_speed == target:
break
func(packet1)
cnt += 1
time.sleep(0.2)
if dev.rotation_speed == target:
break
func(packet2)
time.sleep(0.2)
writeLog('set_ventilator_rotation_speed::send # = {}'.format(cnt), self)
dev.publish_mqtt()
마지막으로 hierachy 최상단에 존재하는 Home 클래스는 다음과 같이 구현하면 된다
class Home:
name: str = 'Home'
device_list: List[Device]
rooms: List[Room]
gas_valve: GasValve
ventilator: Ventilator
elevator: Elevator
serial_baud: int = 9600
serial_485_energy_port: str = ''
serial_485_control_port: str = ''
serial_485_smart_port1: str = ''
serial_485_smart_port2: str = ''
thread_command: Union[ThreadCommand, None] = None
thread_monitoring: Union[ThreadMonitoring, None] = None
queue_command: queue.Queue
mqtt_client: mqtt.Client
mqtt_host: str = 'localhost'
mqtt_port: int = 1883
def __init__(self, room_info: List, name: str = 'Home'):
self.name = name
self.device_list = list()
self.mqtt_client = mqtt.Client()
self.mqtt_client.on_connect = self.onMqttClientConnect
self.mqtt_client.on_disconnect = self.onMqttClientDisconnect
self.mqtt_client.on_subscribe = self.onMqttClientSubscribe
self.mqtt_client.on_unsubscribe = self.onMqttClientUnsubscribe
self.mqtt_client.on_publish = self.onMqttClientPublish
self.mqtt_client.on_message = self.onMqttClientMessage
self.mqtt_client.on_log = self.onMqttClientLog
self.rooms = list()
for i, info in enumerate(room_info):
name = info['name']
light_count = info['light_count']
has_thermostat = info['has_thermostat']
self.rooms.append(Room(name=name, index=i, light_count=light_count, has_thermostat=has_thermostat, mqtt_client=self.mqtt_client))
self.gas_valve = GasValve(name='Gas Valve', mqtt_client=self.mqtt_client)
self.ventilator = Ventilator(name='Ventilator', mqtt_client=self.mqtt_client)
self.elevator = Elevator(name='Elevator', mqtt_client=self.mqtt_client)
self.elevator.sig_call_up.connect(self.onElevatorCallUp)
self.elevator.sig_call_down.connect(self.onElevatorCallDown)
# device list
for room in self.rooms:
self.device_list.extend(room.lights)
if room.thermostat is not None:
self.device_list.append(room.thermostat)
self.device_list.append(self.gas_valve)
self.device_list.append(self.ventilator)
self.device_list.append(self.elevator)
curpath = os.path.dirname(os.path.abspath(__file__))
xml_path = os.path.join(curpath, 'config.xml')
self.load_config(xml_path)
self.mqtt_client.connect(self.mqtt_host, self.mqtt_port)
self.mqtt_client.loop_start()
self.queue_command = queue.Queue()
self.startThreadCommand()
self.serial_485_energy = SerialComm('Energy')
self.parser_energy = EnergyParser(self.serial_485_energy)
self.parser_energy.sig_parse.connect(self.onParserEnergyResult)
self.serial_485_control = SerialComm('Control')
self.parser_control = ControlParser(self.serial_485_control)
self.parser_control.sig_parse.connect(self.onParserControlResult)
self.serial_485_smart1 = SerialComm('Smart1')
self.serial_485_smart2 = SerialComm('Smart2')
self.parser_smart = SmartParser(self.serial_485_smart1, self.serial_485_smart2)
self.parser_smart.sig_parse1.connect(self.onParserSmartResult1)
self.parser_smart.sig_parse2.connect(self.onParserSmartResult2)
self.startThreadMonitoring()
def release(self):
self.mqtt_client.loop_stop()
self.mqtt_client.disconnect()
self.stopThreadCommand()
self.stopThreadMonitoring()
self.serial_485_energy.release()
self.serial_485_control.release()
self.serial_485_smart1.release()
self.serial_485_smart2.release()
def initDevices(self):
self.serial_485_energy.connect(self.serial_485_energy_port, self.serial_baud)
self.serial_485_control.connect(self.serial_485_control_port, self.serial_baud)
self.serial_485_smart1.connect(self.serial_485_smart_port1, self.serial_baud)
self.serial_485_smart2.connect(self.serial_485_smart_port2, self.serial_baud)
def load_config(self, filepath: str):
root = ET.parse(filepath).getroot()
node = root.find('serial')
self.serial_485_energy_port = node.find('port_energy').text
self.serial_485_control_port = node.find('port_control').text
self.serial_485_smart_port1 = node.find('port_smart1').text
self.serial_485_smart_port2 = node.find('port_smart2').text
node = root.find('mqtt')
username = node.find('username').text
password = node.find('password').text
self.mqtt_host = node.find('host').text
self.mqtt_port = int(node.find('port').text)
self.mqtt_client.username_pw_set(username, password)
node = root.find('thermo_temp_packet')
thermo_setting_packets = node.text.split('\n')
thermo_setting_packets = [x.replace('\t', '').strip() for x in thermo_setting_packets]
thermo_setting_packets = list(filter(lambda x: len(x) > 0, thermo_setting_packets))
node = root.find('rooms')
for i, room in enumerate(self.rooms):
room_node = node.find('room{}'.format(i))
if room_node is None:
continue
for j in range(room.light_count):
light_node = room_node.find('light{}'.format(j))
if light_node is None:
continue
room.lights[j].packet_set_state_on = light_node.find('on').text
room.lights[j].packet_set_state_off = light_node.find('off').text
room.lights[j].packet_get_state = light_node.find('get').text
mqtt_node = light_node.find('mqtt')
room.lights[j].mqtt_publish_topic = mqtt_node.find('publish').text
room.lights[j].mqtt_subscribe_topics.append(mqtt_node.find('subscribe').text)
thermo_node = room_node.find('thermostat')
if thermo_node is None:
continue
room.thermostat.packet_set_state_on = thermo_node.find('on').text
room.thermostat.packet_set_state_off = thermo_node.find('off').text
room.thermostat.packet_get_state = thermo_node.find('get').text
mqtt_node = thermo_node.find('mqtt')
room.thermostat.mqtt_publish_topic = mqtt_node.find('publish').text
room.thermostat.mqtt_subscribe_topics.append(mqtt_node.find('subscribe').text)
for j in range(71):
room.thermostat.packet_set_temperature[j] = thermo_setting_packets[j + 71 * (i - 1)]
node = root.find('gasvalve')
self.gas_valve.packet_set_state_off = node.find('off').text
self.gas_valve.packet_get_state = node.find('get').text
mqtt_node = node.find('mqtt')
self.gas_valve.mqtt_publish_topic = mqtt_node.find('publish').text
self.gas_valve.mqtt_subscribe_topics.append(mqtt_node.find('subscribe').text)
node = root.find('ventilator')
self.ventilator.packet_set_state_on = node.find('on').text
self.ventilator.packet_set_state_off = node.find('off').text
self.ventilator.packet_get_state = node.find('get').text
speed_setting_packets = node.find('speed').text.split('\n')
speed_setting_packets = [x.replace('\t', '').strip() for x in speed_setting_packets]
speed_setting_packets = list(filter(lambda x: len(x) > 0, speed_setting_packets))
self.ventilator.packet_set_rotation_speed = speed_setting_packets
mqtt_node = node.find('mqtt')
self.ventilator.mqtt_publish_topic = mqtt_node.find('publish').text
self.ventilator.mqtt_subscribe_topics.append(mqtt_node.find('subscribe').text)
node = root.find('elevator')
self.elevator.my_floor = int(node.find('myfloor').text)
mqtt_node = node.find('mqtt')
self.elevator.mqtt_publish_topic = mqtt_node.find('publish').text
topic_text = mqtt_node.find('subscribe').text
topics = list(filter(lambda y: len(y) > 0, [x.strip() for x in topic_text.split('\n')]))
self.elevator.mqtt_subscribe_topics.extend(topics)
def startThreadCommand(self):
if self.thread_command is None:
self.thread_command = ThreadCommand(self.queue_command)
self.thread_command.sig_send_energy.connect(self.sendSerialEnergyPacket)
self.thread_command.sig_send_control.connect(self.sendSerialControlPacket)
self.thread_command.sig_send_smart.connect(self.sendSerialSmartPacket)
self.thread_command.sig_terminated.connect(self.onThreadCommandTerminated)
self.thread_command.setDaemon(True)
self.thread_command.start()
def stopThreadCommand(self):
if self.thread_command is not None:
self.thread_command.stop()
def onThreadCommandTerminated(self):
del self.thread_command
self.thread_command = None
def startThreadMonitoring(self):
if self.thread_monitoring is None:
self.thread_monitoring = ThreadMonitoring([
self.serial_485_energy,
self.serial_485_control,
self.serial_485_smart1
# self.serial_485_smart2
], self.device_list)
self.thread_monitoring.sig_terminated.connect(self.onThreadMonitoringTerminated)
self.thread_monitoring.setDaemon(True)
self.thread_monitoring.start()
def stopThreadMonitoring(self):
if self.thread_monitoring is not None:
self.thread_monitoring.stop()
def onThreadMonitoringTerminated(self):
del self.thread_monitoring
self.thread_monitoring = None
def sendSerialEnergyPacket(self, packet: str):
if self.serial_485_energy.isConnected():
self.serial_485_energy.sendData(bytearray([int(x, 16) for x in packet.split(' ')]))
def sendSerialControlPacket(self, packet: str):
if self.serial_485_control.isConnected():
self.serial_485_control.sendData(bytearray([int(x, 16) for x in packet.split(' ')]))
def sendSerialSmartPacket(self, packet: str):
if self.serial_485_smart2.isConnected():
self.serial_485_smart2.sendData(bytearray([int(x, 16) for x in packet.split(' ')]))
def onParserEnergyResult(self, chunk: bytearray):
try:
if len(chunk) < 7:
return
header = chunk[1] # [0x31, 0x41, 0x42, 0xD1]
command = chunk[3]
if header == 0x31 and command in [0x81, 0x91]:
# 방 조명 패킷
room_idx = chunk[5] & 0x0F
room = self.rooms[room_idx]
for i in range(room.light_count):
dev = room.lights[i]
dev.state = (chunk[6] & (0x01 << i)) >> i
# notification
if dev.state != dev.state_prev or not dev.init:
dev.publish_mqtt()
dev.init = True
dev.state_prev = dev.state
except Exception as e:
writeLog('onParserEnergyResult::Exception::{}'.format(e), self)
def onParserControlResult(self, chunk: bytearray):
try:
if len(chunk) < 10:
return
header = chunk[1] # [0x28, 0x31, 0x61]
command = chunk[3]
if header == 0x28 and command in [0x91, 0x92]:
# 난방 관련 패킷 (방 인덱스)
# chunk[3] == 0x91: 쿼리 응답
# chunk[3] == 0x92: 명령 응답
room_idx = chunk[5] & 0x0F
room = self.rooms[room_idx]
dev = room.thermostat
dev.state = chunk[6] & 0x01
dev.temperature_setting = (chunk[7] & 0x3F) + (chunk[7] & 0x40 > 0) * 0.5
dev.temperature_current = chunk[9] / 10.0
# notification
if dev.state != dev.state_prev or dev.temperature_setting != dev.temperature_setting_prev or not dev.init:
dev.publish_mqtt()
dev.init = True
if dev.temperature_current != dev.temperature_current_prev:
# dev.publish_mqtt()
pass
dev.state_prev = dev.state
dev.temperature_setting_prev = dev.temperature_setting
dev.temperature_current_prev = dev.temperature_current
elif header == 0x31 and chunk[2] in [0x80, 0x82]:
# 가스 관련 패킷 (길이 정보 없음, 무조건 10 고정)
# chunk[2] == 0x80: 쿼리 응답
# chunk[2] == 0x82: 명령 응답
dev = self.gas_valve
dev.state = chunk[5]
# notification
if dev.state != dev.state_prev or not dev.init:
dev.publish_mqtt()
dev.init = True
dev.state_prev = dev.state
elif header == 0x61 and chunk[2] in [0x80, 0x81, 0x83, 0x84, 0x87]:
# 환기 관련 패킷
dev = self.ventilator
dev.state = chunk[5] & 0x01
dev.state_natural = (chunk[5] & 0x10) >> 4
dev.rotation_speed = chunk[6]
dev.timer_remain = chunk[7]
# notification
if dev.state != dev.state_prev or dev.rotation_speed != dev.rotation_speed_prev or not dev.init:
dev.publish_mqtt()
dev.init = True
dev.state_prev = dev.state
dev.rotation_speed_prev = dev.rotation_speed
else:
pass
except Exception as e:
writeLog('onParserControlResult Exception::{}'.format(e), self)
def onParserSmartResult1(self, chunk: bytearray):
try:
if len(chunk) >= 4:
header = chunk[1] # [0xC1]
packetLen = chunk[2]
cmd = chunk[3]
if header == 0xC1 and packetLen == 0x13 and cmd == 0x13:
dev = self.elevator
if len(chunk) >= 13:
dev.state = chunk[11]
dev.current_floor = ctypes.c_int8(chunk[12]).value
# notification
if dev.state != dev.state_prev or not dev.init:
dev.publish_mqtt()
dev.init = True
if dev.current_floor != dev.current_floor_prev:
writeLog(f'Elevator Current Floor: {dev.current_floor}', self)
dev.state_prev = dev.state
dev.current_floor_prev = dev.current_floor
except Exception as e:
writeLog('onParserSmartResult1 Exception::{}'.format(e), self)
def onParserSmartResult2(self, chunk: bytearray):
try:
pass
except Exception as e:
writeLog('onParserSmartResult2 Exception::{}'.format(e), self)
def command(self, **kwargs):
writeLog('Command::{}'.format(kwargs), self)
try:
dev = kwargs['device']
if isinstance(dev, Light):
kwargs['func'] = self.sendSerialEnergyPacket
elif isinstance(dev, Thermostat):
kwargs['func'] = self.sendSerialControlPacket
elif isinstance(dev, Ventilator):
kwargs['func'] = self.sendSerialControlPacket
elif isinstance(dev, GasValve):
kwargs['func'] = self.sendSerialControlPacket
elif isinstance(dev, Elevator):
if kwargs['direction'] == 'up':
kwargs['func'] = self.onElevatorCallUp
else:
kwargs['func'] = self.onElevatorCallDown
except Exception as e:
writeLog('command Exception::{}'.format(e), self)
self.queue_command.put(kwargs)
def onElevatorCallUp(self):
self.parser_smart.flag_send_up_packet = True
def onElevatorCallDown(self):
self.parser_smart.flag_send_down_packet = True
def startMqttSubscribe(self):
for dev in self.device_list:
for topic in dev.mqtt_subscribe_topics:
self.mqtt_client.subscribe(topic)
def onMqttClientConnect(self, client, userdata, flags, rc):
writeLog('Mqtt Client Connected: {}, {}, {}'.format(userdata, flags, rc), self)
"""
0: Connection successful
1: Connection refused - incorrect protocol version
2: Connection refused - invalid client identifier
3: Connection refused - server unavailable
4: Connection refused - bad username or password
5: Connection refused - not authorised
"""
if rc == 0:
self.startMqttSubscribe()
def onMqttClientDisconnect(self, client, userdata, rc):
writeLog('Mqtt Client Disconnected: {}, {}'.format(userdata, rc), self)
def onMqttClientPublish(self, client, userdata, mid):
writeLog('Mqtt Client Publish: {}, {}'.format(userdata, mid), self)
def onMqttClientMessage(self, client, userdata, message):
writeLog('Mqtt Client Message: {}, {}'.format(userdata, message), self)
topic = message.topic
msg_dict = json.loads(message.payload.decode("utf-8"))
if 'light/command' in topic:
splt = topic.split('/')
room_idx = int(splt[-2])
dev_idx = int(splt[-1])
if 'state' in msg_dict.keys():
self.command(
device=self.rooms[room_idx].lights[dev_idx],
category='state',
target=msg_dict['state'],
room_idx=room_idx,
dev_idx=dev_idx
)
if 'thermostat/command' in topic:
splt = topic.split('/')
room_idx = int(splt[-1])
if 'state' in msg_dict.keys():
target = 1 if msg_dict['state'] == 'HEAT' else 0
self.command(
device=self.rooms[room_idx].thermostat,
category='state',
target=target
)
if 'targetTemperature' in msg_dict.keys():
self.command(
device=self.rooms[room_idx].thermostat,
category='temperature',
target=msg_dict['targetTemperature']
)
if 'ventilator/command' in topic:
if 'state' in msg_dict.keys():
self.command(
device=self.ventilator,
category='state',
target=msg_dict['state']
)
if 'rotationspeed' in msg_dict.keys():
conv = min(3, max(0, int(msg_dict['rotationspeed'] / 100 * 3) + 1))
self.command(
device=self.ventilator,
category='rotation_speed',
target=conv
)
if 'gasvalve/command' in topic:
if 'state' in msg_dict.keys():
self.command(
device=self.gas_valve,
category='state',
target=msg_dict['state']
)
if 'elevator/command' in topic:
if 'state' in msg_dict.keys():
last_word = topic.split('/')[-1]
self.command(
device=self.elevator,
category='state',
target=msg_dict['state'],
direction=last_word
)
def onMqttClientLog(self, client, userdata, level, buf):
writeLog('Mqtt Client Log: {}, {}, {}'.format(userdata, level, buf), self)
def onMqttClientSubscribe(self, client, userdata, mid, granted_qos):
writeLog('Mqtt Client Subscribe: {}, {}, {}'.format(userdata, mid, granted_qos), self)
def onMqttClientUnsubscribe(self, client, userdata, mid):
writeLog('Mqtt Client Unsubscribe: {}, {}'.format(userdata, mid), self)
3. XML Config 파일 구성
로컬에 저장해둔 설정 파일 (xml)은 다음과 같이 포맷을 짜면 된다
(필요한 패킷 문자열을 전부 써야되다보니 꽤 길어서 접은글로 적었다)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<homenetworkserver>
<serial>
<port_energy>/dev/rs485_energy</port_energy>
<port_control>/dev/rs485_control</port_control>
<port_smart1>/dev/rs485_smart1</port_smart1>
<port_smart2>/dev/rs485_smart2</port_smart2>
</serial>
<mqtt>
<host>MQTT Broker (Mosquitto) Address</host>
<port>MQTT Broker (Mosquitto) Port</port>
<username>MQTT broker (Mosquitto) ID</username>
<password>MQTT broker (Mosquitto) Password</password>
</mqtt>
<rooms>
<room1>
<light0>
<on>02 31 0D 01 D0 01 81 00 00 00 00 04 76</on>
<off>02 31 0D 01 D7 01 01 00 00 00 00 00 F5</off>
<get>02 31 07 11 9B 01 C0</get>
<mqtt>
<publish>home/ipark/light/state/1/0</publish>
<subscribe>home/ipark/light/command/1/0</subscribe>
</mqtt>
</light0>
<light1>
<on>02 31 0D 01 58 01 82 00 00 00 00 04 E9</on>
<off>02 31 0D 01 5F 01 02 00 00 00 00 00 6A</off>
<get>02 31 07 11 9B 01 C0</get>
<mqtt>
<publish>home/ipark/light/state/1/1</publish>
<subscribe>home/ipark/light/command/1/1</subscribe>
</mqtt>
</light1>
<light2>
<on>02 31 0D 01 5C 01 84 00 00 00 00 04 EF</on>
<off>02 31 0D 01 63 01 04 00 00 00 00 00 6C</off>
<get>02 31 07 11 9B 01 C0</get>
<mqtt>
<publish>home/ipark/light/state/1/2</publish>
<subscribe>home/ipark/light/command/1/2</subscribe>
</mqtt>
</light2>
<light3>
<on>02 31 0D 01 2B 01 88 00 00 00 00 04 94</on>
<off>02 31 0D 01 33 01 08 00 00 00 00 00 20</off>
<get>02 31 07 11 9B 01 C0</get>
<mqtt>
<publish>home/ipark/light/state/1/3</publish>
<subscribe>home/ipark/light/command/1/3</subscribe>
</mqtt>
</light3>
<thermostat>
<on>02 28 0E 12 E9 01 01 00 00 00 00 00 00 E3</on>
<off>02 28 0E 12 F7 01 02 00 00 00 00 00 00 C8</off>
<get>02 28 07 11 CC 01 F4</get>
<mqtt>
<publish>home/ipark/thermostat/state/1</publish>
<subscribe>home/ipark/thermostat/command/1</subscribe>
</mqtt>
</thermostat>
</room1>
<room2>
<light0>
<on>02 31 0D 01 8C 02 81 00 00 00 00 04 3F</on>
<off>02 31 0D 01 93 02 01 00 00 00 00 00 B8</off>
<get>02 31 07 11 93 02 B5</get>
<mqtt>
<publish>home/ipark/light/state/2/0</publish>
<subscribe>home/ipark/light/command/2/0</subscribe>
</mqtt>
</light0>
<light1>
<on>02 31 0D 01 7B 02 82 00 00 00 00 04 CB</on>
<off>02 31 0D 01 84 02 02 00 00 00 00 00 C4</off>
<get>02 31 07 11 93 02 B5</get>
<mqtt>
<publish>home/ipark/light/state/2/1</publish>
<subscribe>home/ipark/light/command/2/1</subscribe>
</mqtt>
</light1>
<thermostat>
<on>02 28 0E 12 D3 02 01 00 00 00 00 00 00 EE</on>
<off>02 28 0E 12 DD 02 02 00 00 00 00 00 00 F5</off>
<get>02 28 07 11 D0 02 ED</get>
<mqtt>
<publish>home/ipark/thermostat/state/2</publish>
<subscribe>home/ipark/thermostat/command/2</subscribe>
</mqtt>
</thermostat>
</room2>
<room3>
<light0>
<on>02 31 0D 01 3B 03 81 00 00 00 00 04 97</on>
<off>02 31 0D 01 43 03 01 00 00 00 00 00 8B</off>
<get>02 31 07 11 94 03 B1</get>
<mqtt>
<publish>home/ipark/light/state/3/0</publish>
<subscribe>home/ipark/light/command/3/0</subscribe>
</mqtt>
</light0>
<light1>
<on>02 31 0D 01 76 03 82 00 00 00 00 04 D5</on>
<off>02 31 0D 01 7E 03 02 00 00 00 00 00 49</off>
<get>02 31 07 11 94 03 B1</get>
<mqtt>
<publish>home/ipark/light/state/3/1</publish>
<subscribe>home/ipark/light/command/3/1</subscribe>
</mqtt>
</light1>
<thermostat>
<on>02 28 0E 12 7E 03 01 00 00 00 00 00 00 58</on>
<off>02 28 0E 12 87 03 02 00 00 00 00 00 00 BA</off>
<get>02 28 07 11 D4 03 EA</get>
<mqtt>
<publish>home/ipark/thermostat/state/3</publish>
<subscribe>home/ipark/thermostat/command/3</subscribe>
</mqtt>
</thermostat>
</room3>
</rooms>
<gasvalve>
<off>02 31 02 3C 00 00 00 00 00 11</off>
<get>02 31 00 38 00 00 00 00 00 13</get>
<mqtt>
<publish>home/ipark/gasvalve/state</publish>
<subscribe>home/ipark/gasvalve/command</subscribe>
</mqtt>
</gasvalve>
<ventilator>
<on>02 61 01 E3 00 01 01 00 00 89</on>
<off>02 61 01 4C 00 00 01 00 00 2F</off>
<get>02 61 00 F1 00 00 00 00 00 9A</get>
<speed>
02 61 03 EB 00 00 01 00 00 8A
02 61 03 94 00 00 02 00 00 00
02 61 03 9F 00 00 03 00 00 FC
</speed>
<mqtt>
<publish>home/ipark/ventilator/state</publish>
<subscribe>home/ipark/ventilator/command</subscribe>
</mqtt>
</ventilator>
<elevator>
<myfloor>15</myfloor>
<mqtt>
<publish>home/ipark/elevator/state</publish>
<subscribe>
home/ipark/elevator/command/up
home/ipark/elevator/command/down
</subscribe>
</mqtt>
</elevator>
<thermo_temp_packet>
02 28 0E 12 FE 01 00 05 00 00 00 00 00 D0
02 28 0E 12 11 01 00 45 00 00 00 00 00 69
02 28 0E 12 42 01 00 06 00 00 00 00 00 83
02 28 0E 12 5F 01 00 46 00 00 00 00 00 30
02 28 0E 12 70 01 00 07 00 00 00 00 00 54
02 28 0E 12 82 01 00 47 00 00 00 00 00 02
02 28 0E 12 95 01 00 08 00 00 00 00 00 B0
02 28 0E 12 A4 01 00 48 00 00 00 00 00 E3
02 28 0E 12 B2 01 00 09 00 00 00 00 00 88
02 28 0E 12 00 01 00 49 00 00 00 00 00 76
02 28 0E 12 11 01 00 0A 00 00 00 00 00 32
02 28 0E 12 23 01 00 4A 00 00 00 00 00 58
02 28 0E 12 34 01 00 0B 00 00 00 00 00 14
02 28 0E 12 42 01 00 4B 00 00 00 00 00 36
02 28 0E 12 54 01 00 0C 00 00 00 00 00 6F
02 28 0E 12 62 01 00 4C 00 00 00 00 00 1D
02 28 0E 12 70 01 00 0D 00 00 00 00 00 4A
02 28 0E 12 82 01 00 4D 00 00 00 00 00 FC
02 28 0E 12 93 01 00 0E 00 00 00 00 00 AC
02 28 0E 12 A4 01 00 4E 00 00 00 00 00 E1
02 28 0E 12 B7 01 00 0F 00 00 00 00 00 91
02 28 0E 12 C6 01 00 4F 00 00 00 00 00 BE
02 28 0E 12 D8 01 00 10 00 00 00 00 00 E7
02 28 0E 12 FF 01 00 50 00 00 00 00 00 A2
02 28 0E 12 16 01 00 11 00 00 00 00 00 3C
02 28 0E 12 2D 01 00 51 00 00 00 00 00 51
02 28 0E 12 44 01 00 12 00 00 00 00 00 6D
02 28 0E 12 56 01 00 52 00 00 00 00 00 3B
02 28 0E 12 6A 01 00 13 00 00 00 00 00 76
02 28 0E 12 7D 01 00 53 00 00 00 00 00 1F
02 28 0E 12 8E 01 00 14 00 00 00 00 00 B1
02 28 0E 12 9F 01 00 54 00 00 00 00 00 FE
02 28 0E 12 AC 01 00 15 00 00 00 00 00 8E
02 28 0E 12 BE 01 00 55 00 00 00 00 00 E0
02 28 0E 12 CB 01 00 16 00 00 00 00 00 1C
02 28 0E 12 D9 01 00 56 00 00 00 00 00 BE
02 28 0E 12 EB 01 00 17 00 00 00 00 00 FD
02 28 0E 12 F9 01 00 57 00 00 00 00 00 9F
02 28 0E 12 07 01 00 18 00 00 00 00 00 32
02 28 0E 12 15 01 00 58 00 00 00 00 00 80
02 28 0E 12 23 01 00 19 00 00 00 00 00 07
02 28 0E 12 31 01 00 59 00 00 00 00 00 65
02 28 0E 12 43 01 00 1A 00 00 00 00 00 68
02 28 0E 12 50 01 00 5A 00 00 00 00 00 39
02 28 0E 12 62 01 00 1B 00 00 00 00 00 46
02 28 0E 12 73 01 00 5B 00 00 00 00 00 19
02 28 0E 12 84 01 00 1C 00 00 00 00 00 AF
02 28 0E 12 91 01 00 5C 00 00 00 00 00 00
02 28 0E 12 A3 01 00 1D 00 00 00 00 00 8B
02 28 0E 12 B4 01 00 5D 00 00 00 00 00 DE
02 28 0E 12 CB 01 00 1E 00 00 00 00 00 24
02 28 0E 12 DA 01 00 5E 00 00 00 00 00 B3
02 28 0E 12 EC 01 00 1F 00 00 00 00 00 C8
02 28 0E 12 FD 01 00 5F 00 00 00 00 00 9B
02 28 0E 12 0A 01 00 20 00 00 00 00 00 69
02 28 0E 12 18 01 00 60 00 00 00 00 00 57
02 28 0E 12 2D 01 00 21 00 00 00 00 00 41
02 28 0E 12 47 01 00 61 00 00 00 00 00 1B
02 28 0E 12 5A 01 00 22 00 00 00 00 00 57
02 28 0E 12 6D 01 00 62 00 00 00 00 00 3E
02 28 0E 12 7E 01 00 23 00 00 00 00 00 72
02 28 0E 12 95 01 00 63 00 00 00 00 00 C7
02 28 0E 12 A8 01 00 24 00 00 00 00 00 8B
02 28 0E 12 BB 01 00 64 00 00 00 00 00 FA
02 28 0E 12 CC 01 00 25 00 00 00 00 00 DE
02 28 0E 12 DF 01 00 65 00 00 00 00 00 8F
02 28 0E 12 F0 01 00 26 00 00 00 00 00 F5
02 28 0E 12 01 01 00 66 00 00 00 00 00 56
02 28 0E 12 14 01 00 27 00 00 00 00 00 08
02 28 0E 12 27 01 00 67 00 00 00 00 00 79
02 28 0E 12 38 01 00 28 00 00 00 00 00 3F
02 28 0E 12 32 02 00 05 00 00 00 00 00 0F
02 28 0E 12 45 02 00 45 00 00 00 00 00 36
02 28 0E 12 54 02 00 06 00 00 00 00 00 6A
02 28 0E 12 62 02 00 46 00 00 00 00 00 20
02 28 0E 12 72 02 00 07 00 00 00 00 00 51
02 28 0E 12 81 02 00 47 00 00 00 00 00 04
02 28 0E 12 8E 02 00 08 00 00 00 00 00 CE
02 28 0E 12 9B 02 00 48 00 00 00 00 00 ED
02 28 0E 12 A8 02 00 09 00 00 00 00 00 9D
02 28 0E 12 B7 02 00 49 00 00 00 00 00 D0
02 28 0E 12 D4 02 00 0A 00 00 00 00 00 EE
02 28 0E 12 E3 02 00 4A 00 00 00 00 00 A3
02 28 0E 12 F0 02 00 0B 00 00 00 00 00 D3
02 28 0E 12 FD 02 00 4B 00 00 00 00 00 8C
02 28 0E 12 0A 02 00 0C 00 00 00 00 00 4E
02 28 0E 12 17 02 00 4C 00 00 00 00 00 75
02 28 0E 12 24 02 00 0D 00 00 00 00 00 25
02 28 0E 12 33 02 00 4D 00 00 00 00 00 50
02 28 0E 12 3E 02 00 0E 00 00 00 00 00 24
02 28 0E 12 4C 02 00 4E 00 00 00 00 00 3A
02 28 0E 12 59 02 00 0F 00 00 00 00 00 84
02 28 0E 12 66 02 00 4F 00 00 00 00 00 1D
02 28 0E 12 73 02 00 10 00 00 00 00 00 5D
02 28 0E 12 80 02 00 50 00 00 00 00 00 EC
02 28 0E 12 8F 02 00 11 00 00 00 00 00 B0
02 28 0E 12 9E 02 00 51 00 00 00 00 00 E7
02 28 0E 12 AB 02 00 12 00 00 00 00 00 93
02 28 0E 12 B8 02 00 52 00 00 00 00 00 E2
02 28 0E 12 C7 02 00 13 00 00 00 00 00 E6
02 28 0E 12 D6 02 00 53 00 00 00 00 00 C1
02 28 0E 12 E3 02 00 14 00 00 00 00 00 C9
02 28 0E 12 F2 02 00 54 00 00 00 00 00 9E
02 28 0E 12 01 02 00 15 00 00 00 00 00 32
02 28 0E 12 0E 02 00 55 00 00 00 00 00 1B
02 28 0E 12 1B 02 00 16 00 00 00 00 00 3F
02 28 0E 12 28 02 00 56 00 00 00 00 00 4E
02 28 0E 12 37 02 00 17 00 00 00 00 00 1A
02 28 0E 12 42 02 00 57 00 00 00 00 00 31
02 28 0E 12 50 02 00 18 00 00 00 00 00 84
02 28 0E 12 67 02 00 58 00 00 00 00 00 11
02 28 0E 12 92 02 00 19 00 00 00 00 00 BB
02 28 0E 12 A1 02 00 59 00 00 00 00 00 C6
02 28 0E 12 B2 02 00 1A 00 00 00 00 00 9C
02 28 0E 12 BF 02 00 5A 00 00 00 00 00 D7
02 28 0E 12 CD 02 00 1B 00 00 00 00 00 EC
02 28 0E 12 DB 02 00 5B 00 00 00 00 00 BA
02 28 0E 12 EA 02 00 1C 00 00 00 00 00 FE
02 28 0E 12 F9 02 00 5C 00 00 00 00 00 93
02 28 0E 12 06 02 00 1D 00 00 00 00 00 2B
02 28 0E 12 14 02 00 5D 00 00 00 00 00 85
02 28 0E 12 30 02 00 1E 00 00 00 00 00 1E
02 28 0E 12 47 02 00 5E 00 00 00 00 00 33
02 28 0E 12 55 02 00 1F 00 00 00 00 00 80
02 28 0E 12 75 02 00 5F 00 00 00 00 00 20
02 28 0E 12 86 02 00 20 00 00 00 00 00 9E
02 28 0E 12 93 02 00 60 00 00 00 00 00 CD
02 28 0E 12 A5 02 00 21 00 00 00 00 00 BA
02 28 0E 12 B6 02 00 61 00 00 00 00 00 EF
02 28 0E 12 C3 02 00 22 00 00 00 00 00 DB
02 28 0E 12 D1 02 00 62 00 00 00 00 00 91
02 28 0E 12 E4 02 00 23 00 00 00 00 00 F7
02 28 0E 12 F3 02 00 63 00 00 00 00 00 AA
02 28 0E 12 03 02 00 24 00 00 00 00 00 19
02 28 0E 12 12 02 00 64 00 00 00 00 00 4E
02 28 0E 12 21 02 00 25 00 00 00 00 00 42
02 28 0E 12 30 02 00 65 00 00 00 00 00 69
02 28 0E 12 41 02 00 26 00 00 00 00 00 65
02 28 0E 12 4E 02 00 66 00 00 00 00 00 EC
02 28 0E 12 5C 02 00 27 00 00 00 00 00 53
02 28 0E 12 6B 02 00 67 00 00 00 00 00 3E
02 28 0E 12 7A 02 00 28 00 00 00 00 00 82
02 28 0E 12 CB 03 00 05 00 00 00 00 00 01
02 28 0E 12 DC 03 00 45 00 00 00 00 00 B4
02 28 0E 12 ED 03 00 06 00 00 00 00 00 E0
02 28 0E 12 FE 03 00 46 00 00 00 00 00 9D
02 28 0E 12 0F 03 00 07 00 00 00 00 00 43
02 28 0E 12 1C 03 00 47 00 00 00 00 00 72
02 28 0E 12 2A 03 00 08 00 00 00 00 00 33
02 28 0E 12 38 03 00 48 00 00 00 00 00 4D
02 28 0E 12 4A 03 00 09 00 00 00 00 00 92
02 28 0E 12 5B 03 00 49 00 00 00 00 00 2D
02 28 0E 12 6C 03 00 0A 00 00 00 00 00 57
02 28 0E 12 7D 03 00 4A 00 00 00 00 00 0C
02 28 0E 12 8E 03 00 0B 00 00 00 00 00 D0
02 28 0E 12 9F 03 00 4B 00 00 00 00 00 E7
02 28 0E 12 B0 03 00 0C 00 00 00 00 00 91
02 28 0E 12 C1 03 00 4C 00 00 00 00 00 BA
02 28 0E 12 D2 03 00 0D 00 00 00 00 00 E6
02 28 0E 12 E3 03 00 4D 00 00 00 00 00 A1
02 28 0E 12 F6 03 00 0E 00 00 00 00 00 CD
02 28 0E 12 05 03 00 4E 00 00 00 00 00 80
02 28 0E 12 13 03 00 0F 00 00 00 00 00 2F
02 28 0E 12 21 03 00 4F 00 00 00 00 00 5D
02 28 0E 12 2F 03 00 10 00 00 00 00 00 10
02 28 0E 12 3D 03 00 50 00 00 00 00 00 62
02 28 0E 12 4F 03 00 11 00 00 00 00 00 71
02 28 0E 12 62 03 00 51 00 00 00 00 00 12
02 28 0E 12 73 03 00 12 00 00 00 00 00 5A
02 28 0E 12 84 03 00 52 00 00 00 00 00 E7
02 28 0E 12 93 03 00 13 00 00 00 00 00 BB
02 28 0E 12 A5 03 00 53 00 00 00 00 00 CD
02 28 0E 12 B6 03 00 14 00 00 00 00 00 A3
02 28 0E 12 C7 03 00 54 00 00 00 00 00 AC
02 28 0E 12 D4 03 00 15 00 00 00 00 00 FC
02 28 0E 12 E2 03 00 55 00 00 00 00 00 8E
02 28 0E 12 F0 03 00 16 00 00 00 00 00 D7
02 28 0E 12 FE 03 00 56 00 00 00 00 00 8D
02 28 0E 12 0C 03 00 17 00 00 00 00 00 32
02 28 0E 12 1C 03 00 57 00 00 00 00 00 82
02 28 0E 12 2D 03 00 18 00 00 00 00 00 0A
02 28 0E 12 3E 03 00 58 00 00 00 00 00 4F
02 28 0E 12 4F 03 00 19 00 00 00 00 00 69
02 28 0E 12 5E 03 00 59 00 00 00 00 00 2E
02 28 0E 12 70 03 00 1A 00 00 00 00 00 63
02 28 0E 12 83 03 00 5A 00 00 00 00 00 F2
02 28 0E 12 96 03 00 1B 00 00 00 00 00 B8
02 28 0E 12 A7 03 00 5B 00 00 00 00 00 CF
02 28 0E 12 B8 03 00 1C 00 00 00 00 00 99
02 28 0E 12 CB 03 00 5C 00 00 00 00 00 A8
02 28 0E 12 DE 03 00 1D 00 00 00 00 00 F2
02 28 0E 12 EF 03 00 5D 00 00 00 00 00 8D
02 28 0E 12 01 03 00 1E 00 00 00 00 00 2C
02 28 0E 12 16 03 00 5E 00 00 00 00 00 7D
02 28 0E 12 27 03 00 1F 00 00 00 00 00 13
02 28 0E 12 39 03 00 5F 00 00 00 00 00 55
02 28 0E 12 4E 03 00 20 00 00 00 00 00 A7
02 28 0E 12 5B 03 00 60 00 00 00 00 00 14
02 28 0E 12 6B 03 00 21 00 00 00 00 00 85
02 28 0E 12 7A 03 00 61 00 00 00 00 00 3A
02 28 0E 12 87 03 00 22 00 00 00 00 00 96
02 28 0E 12 98 03 00 62 00 00 00 00 00 D3
02 28 0E 12 A5 03 00 23 00 00 00 00 00 BD
02 28 0E 12 B5 03 00 63 00 00 00 00 00 ED
02 28 0E 12 C4 03 00 24 00 00 00 00 00 DD
02 28 0E 12 D3 03 00 64 00 00 00 00 00 88
02 28 0E 12 E2 03 00 25 00 00 00 00 00 FE
02 28 0E 12 F0 03 00 65 00 00 00 00 00 A8
02 28 0E 12 FE 03 00 26 00 00 00 00 00 FD
02 28 0E 12 0C 03 00 66 00 00 00 00 00 63
02 28 0E 12 19 03 00 27 00 00 00 00 00 1D
02 28 0E 12 27 03 00 67 00 00 00 00 00 7B
02 28 0E 12 3E 03 00 28 00 00 00 00 00 3F
</thermo_temp_packet>
</homenetworkserver>
4. 스크립트 구동
Flask와 함께 구동하면 굳이 루프문을 따로 작성할 필요가 없어서 편리하다
(웹서버 관련 구현은 중요한게 아니므로 포스트에서는 제외하기로 한다)
from flask import Flask
from homeDef import Home
class MyFlaskApp(Flask):
def run(self, host=None, port=None, debug=None, load_dotenv=True, **options):
with self.app_context():
pass
super(MyFlaskApp, self).run(host=host, port=port, debug=debug, load_dotenv=load_dotenv, **options)
app = MyFlaskApp(__name__)
if __name__ == '__main__':
import os
os.system('clear')
home = Home(room_info=[
{'name': 'Empty', 'light_count': 0, 'has_thermostat': False},
{'name': 'Kitchen', 'light_count': 4, 'has_thermostat': True},
{'name': 'Bedroom', 'light_count': 2, 'has_thermostat': True},
{'name': 'Computer', 'light_count': 2, 'has_thermostat': True}
], name='IPark Gwanggyo')
home.initDevices()
app.run(host='0.0.0.0', port=1234, debug=False)
home.release()
5. Homebridge 액세서리 설정
{
"accessories": [
{
"accessory": "mqttthing",
"type": "fan",
"name": "Ventilator (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"caption": "Ventilator(MQTT)",
"topics": {
"getOn": {
"topic": "home/ipark/ventilator/state",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/ventilator/command",
"apply": "return JSON.stringify({state: message});"
},
"getRotationSpeed": {
"topic": "home/ipark/ventilator/state",
"apply": "return JSON.parse(message).rotationspeed;"
},
"setRotationSpeed": {
"topic": "home/ipark/ventilator/command",
"apply": "return JSON.stringify({rotationspeed: message});"
}
},
"integerValue": true,
"manufacturer": "HDC iControls",
"serialNumber": "Bestin Air",
"model": "BIA-H100CPC",
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Kitchen Light1 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/1/0",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/1/0",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Kitchen Light2 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/1/1",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/1/1",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Kitchen Light3 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/1/2",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/1/2",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Kitchen Light4 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/1/3",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/1/3",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Bedroom Light1 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/2/0",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/2/0",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Bedroom Light2 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/2/1",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/2/1",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "PC Room Light1 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/3/0",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/3/0",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "PC Room Light2 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/light/state/3/1",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/light/command/3/1",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "thermostat",
"name": "Living room Thermostat (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getCurrentHeatingCoolingState": {
"topic": "home/ipark/thermostat/state/1",
"apply": "return JSON.parse(message).state;"
},
"setTargetHeatingCoolingState": {
"topic": "home/ipark/thermostat/command/1",
"apply": "return JSON.stringify({state: message});"
},
"getTargetHeatingCoolingState": {
"topic": "home/ipark/thermostat/state/1",
"apply": "return JSON.parse(message).state;"
},
"getCurrentTemperature": {
"topic": "home/ipark/thermostat/state/1",
"apply": "return JSON.parse(message).currentTemperature;"
},
"setTargetTemperature": {
"topic": "home/ipark/thermostat/command/1",
"apply": "return JSON.stringify({targetTemperature: message});"
},
"getTargetTemperature": {
"topic": "home/ipark/thermostat/state/1",
"apply": "return JSON.parse(message).targetTemperature;"
}
},
"minTemperature": 5,
"maxTemperature": 40,
"restrictHeatingCoolingState": [
0,
1
],
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "thermostat",
"name": "Bedroom Thermostat (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getCurrentHeatingCoolingState": {
"topic": "home/ipark/thermostat/state/2",
"apply": "return JSON.parse(message).state;"
},
"setTargetHeatingCoolingState": {
"topic": "home/ipark/thermostat/command/2",
"apply": "return JSON.stringify({state: message});"
},
"getTargetHeatingCoolingState": {
"topic": "home/ipark/thermostat/state/2",
"apply": "return JSON.parse(message).state;"
},
"getCurrentTemperature": {
"topic": "home/ipark/thermostat/state/2",
"apply": "return JSON.parse(message).currentTemperature;"
},
"setTargetTemperature": {
"topic": "home/ipark/thermostat/command/2",
"apply": "return JSON.stringify({targetTemperature: message});"
},
"getTargetTemperature": {
"topic": "home/ipark/thermostat/state/2",
"apply": "return JSON.parse(message).targetTemperature;"
}
},
"minTemperature": 5,
"maxTemperature": 40,
"restrictHeatingCoolingState": [
0,
1
],
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "thermostat",
"name": "PC Room Thermostat (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getCurrentHeatingCoolingState": {
"topic": "home/ipark/thermostat/state/3",
"apply": "return JSON.parse(message).state;"
},
"setTargetHeatingCoolingState": {
"topic": "home/ipark/thermostat/command/3",
"apply": "return JSON.stringify({state: message});"
},
"getTargetHeatingCoolingState": {
"topic": "home/ipark/thermostat/state/3",
"apply": "return JSON.parse(message).state;"
},
"getCurrentTemperature": {
"topic": "home/ipark/thermostat/state/3",
"apply": "return JSON.parse(message).currentTemperature;"
},
"setTargetTemperature": {
"topic": "home/ipark/thermostat/command/3",
"apply": "return JSON.stringify({targetTemperature: message});"
},
"getTargetTemperature": {
"topic": "home/ipark/thermostat/state/3",
"apply": "return JSON.parse(message).targetTemperature;"
}
},
"minTemperature": 5,
"maxTemperature": 40,
"restrictHeatingCoolingState": [
0,
1
],
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "valve",
"valveType": "faucet",
"name": "Gas Valve (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"setActive": {
"topic": "home/ipark/gasvalve/command",
"apply": "return JSON.stringify({state: message});"
},
"getActive": {
"topic": "home/ipark/gasvalve/state",
"apply": "return JSON.parse(message).state;"
},
"getInUse": {
"topic": "home/ipark/gasvalve/state",
"apply": "return JSON.parse(message).state;"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Elevator Down (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/elevator/state",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/elevator/command/down",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Living room Light1 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/livingroom/light/state/0",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/livingroom/light/command/0",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
},
{
"accessory": "mqttthing",
"type": "switch",
"name": "Living room Light2 (MQTT)",
"url": "MQTT Broker Address",
"username": "MQTT Broker ID",
"password": "MQTT Broker Password",
"topics": {
"getOn": {
"topic": "home/ipark/livingroom/light/state/1",
"apply": "return JSON.parse(message).state;"
},
"setOn": {
"topic": "home/ipark/livingroom/light/command/1",
"apply": "return JSON.stringify({state: message});"
}
},
"integerValue": true,
"onValue": 1,
"offValue": 0,
"history": true,
"logMqtt": true
}
],
"platforms": [
{
"name": "Config",
"port": 8581,
"platform": "config"
}
]
}
중요: Living room Light 1/2는 시리얼 통신이 아니라 별도로 자체 제작한 PCB에서 관리하는 것이라 python 코드에는 관련 내용이 포함되어 있지 않다 (자세한 내용은 거실 조명 포스트 참고)
작성하다보니 포스트가 너무 길어졌다
나머지 짜잘한 내용은 다음 글에서 이어하도록 하자...
'홈네트워크(IoT) > 광교아이파크' 카테고리의 다른 글
광교아이파크::Bestin - Apple 홈킷 연동 소스코드 GitHub 업로드 (0) | 2021.07.25 |
---|---|
광교아이파크::난방 온도값 파싱 오류 (0) | 2021.07.21 |
광교아이파크::거실 조명 Apple 홈킷 연동 (4) - Final (6) | 2021.03.11 |
광교아이파크::거실 조명 Apple 홈킷 연동 (3) (0) | 2021.03.10 |
광교아이파크::거실 조명 Apple 홈킷 연동 (2) (0) | 2021.02.18 |