YOGYUI

광교아이파크::Bestin ↔ Apple HomeKit 연동 Summary (1) 본문

홈네트워크(IoT)/광교아이파크

광교아이파크::Bestin ↔ Apple HomeKit 연동 Summary (1)

요겨 2021. 3. 20. 13:00
반응형

Bentin 홈네트워크 월패드에서 제어 가능한 디바이스 대부분을 Apple Homekit에 액세서리로 연동하는데 성공했다

2020년 12월 초부터 시작해서 2021년 3월까지 대충 4개월정도 걸린 것 같다

(재택근무하는 와중에 짬짬이 시간을 내서 하다보니 생각보다 길어졌다)

Raspberry Pi 4도 새걸로 한개 마련하고, USB-Serial 컨버터도 여러개 구매하고 PCB도 제작하다보니 돈이 꽤 많이 들었다 (대략 30만원? ㅠㅠ 다음번 포스팅 때 정확한 금액을 산출해 볼 예정)

돈이 들어서 아깝다기보다는 홈네트워크 관련 경험을 쌓았다는 데 의의를 두자... 정신승리

내년에 입주하는 힐스테이트(내 집!!!)는 과연 어떤 네트워크를 쓸지 벌써부터 기대된다

 

대장정의 막을 내릴 겸, 코드 정리도 할 겸 마무리 포스팅을 작성해보기로 했다

 

Apple Homekit 연동을 위해 Raspberry Pi 4에 Homebridge를 구동하고, 디바이스간 통신 프로토콜을 MQTT로 결정한 뒤 broker인 Moqsuitto도 함께 구동하고 있다

Raspberry Pi - Homebridge, Flask Server, Mosquitto 구동 중

 

거실 조명 제어를 제외하면 모두 신발장에 있는 홈네트워크 게이트웨이의 RS-485 포트에 USB-RS485 컨버터를 통해 통신라인을 후킹해서 시리얼패킷 파싱 및 송신까지 Raspberry Pi에서 담당하게 하였다

 

거실 조명 제어는 외부에서 접근할 수 있는 통신 라인을 찾을 수가 없어서, 거실 월패드의 터치패드 부분의 PCB를 역공학해서 하드웨어단에서 접근할 수 있게 WiFi Module (ESP-12F)을 기반으로 한 PCB를 직접 설계 및 제작해서 제어가 가능하도록 구현했다

 

Raspberry Pi 내에서 RS-485 패킷 핸들링 및 Homebridge와의 연동은 Python 언어로 작성된 스크립트도 구동하며, 웹 연동을 위해 Flask 라이브러리를 사용해 웹서버를 개설해 동작하고 있다

 

전체 시스템 다이어그램을 그려보면 다음과 같다

Bestin - Apple HomeKit 연동 System Diagram

정리해보니 뭔가 한 일에 비해 시스템 자체는 간단해보여서 조금은 허무하다 ㅋㅋ

 

코드 유지보수 편의성 향상을 위해 코드를 객체 지향적으로 깔끔하게 정리하고 가능하면 깃허브까지 연동해보도록 하자

 

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 코드에는 관련 내용이 포함되어 있지 않다 (자세한 내용은 거실 조명 포스트 참고)

 

 

작성하다보니 포스트가 너무 길어졌다

나머지 짜잘한 내용은 다음 글에서 이어하도록 하자...

반응형
Comments