YOGYUI

광교아이파크::전원콘센트 Apple 홈킷 연동 (2) 본문

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

광교아이파크::전원콘센트 Apple 홈킷 연동 (2)

요겨 2021. 8. 23. 19:05
반응형

전원 콘센트 관련 Homebridge 액세서리는 다른 액세서리들과 마찬가지로 Mqttting 플러그인을 사용해 구현하기로 했다

https://github.com/arachnetech/homebridge-mqttthing#readme

 

GitHub - arachnetech/homebridge-mqttthing: A plugin for Homebridge allowing the integration of many different accessory types us

A plugin for Homebridge allowing the integration of many different accessory types using MQTT. - GitHub - arachnetech/homebridge-mqttthing: A plugin for Homebridge allowing the integration of many ...

github.com

 

깃허브 페이지를 보면 'Outlet' 항목이 있다

※ 막간 영어 상식

한국에서 흔히 사용하는 '콘센트'는 일본식 엉터리 영어! 미국가서 consent 이러면 아예 의사소통이 안된다

올바르게 번역하면 'electrical outlet' 혹은 'power outlet'이라고 해야 한다 (옷사러 갈때 그 '아울렛'이다)

 

기존에 구현해뒀던 조명 관련 구현에 추가하는 방향으로 빠르게 구현을 해보자

(전체 구현 코드는 깃허브 링크 참고)

 

GitHub - YOGYUI/HomeNetwork: HomeNetwork(Homebridge) Repo

HomeNetwork(Homebridge) Repo. Contribute to YOGYUI/HomeNetwork development by creating an account on GitHub.

github.com

 

1. 서버 코드 (python) 수정

우선 Outlet 클래스를 추가해주자

mqtt publish 시 필요한 정보는 'On/Off' 정보 및 '현재 소모전력값'이다

class Outlet(Device):
    measurement: float = 0.
    measurement_prev: float = 0.

    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,
            "watts": self.measurement
        }
        self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1)

 

각 방별로 콘센트가 있으므로 기존 Room 클래스에 Outlet 리스트를 추가해주자

class Room:
    name: str = 'Room'
    # 각 방에는 조명 모듈 여러개와 난방 모듈 1개 존재
    index: int = 0
    lights: List[Light]
    outlets: List[Outlet]
    thermostat: Thermostat = None

    def __init__(
        self,
        name: str = 'Room',
        index: int = 0,
        light_count: int = 0,
        has_thermostat: bool = True,
        outlet_count: int = 0,
        **kwargs
    ):
        self.name = name
        self.index = index
        self.lights = list()
        self.outlets = list()

        for i in range(light_count):
            self.lights.append(Light(name=f'Light {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'))
        for i in range(outlet_count):
            self.outlets.append(Outlet(name=f'Outlet {i + 1}', index=i, room_index=self.index, mqtt_client=kwargs.get('mqtt_client')))

    @property
    def light_count(self):
        return len(self.lights)
    
    @property
    def outlet_count(self):
        return len(self.outlets)

 

Home 객체 초기화 시 outlet 객체들에 대한 정보도 다룰 수 있도록 하자

class Home:
    rooms: List[Room]
    # 중략
    def __init__(self, room_info: List, name: str = 'Home'):
        # 중략
        self.rooms = list()
        for i, info in enumerate(room_info):
            name = info['name']
            light_count = info['light_count']
            has_thermostat = info['has_thermostat']
            outlet_count = info['outlet_count']
            self.rooms.append(Room(name=name, index=i, light_count=light_count, has_thermostat=has_thermostat, outlet_count=outlet_count, mqtt_client=self.mqtt_client))
        # 중략
        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.extend(room.outlets)
        self.device_list.append(self.gas_valve)
        self.device_list.append(self.ventilator)
        self.device_list.append(self.elevator)
        
    # 중략
    def load_config(self, filepath: str):
        # 중략
        node = root.find('rooms')
        for i, room in enumerate(self.rooms):
            room_node = node.find('room{}'.format(i))
            if room_node is not None:
                # 중략
                for j in range(room.outlet_count):
                    outlet_node = room_node.find('outlet{}'.format(j))
                    if outlet_node is not None:
                        room.outlets[j].packet_set_state_on = outlet_node.find('on').text
                        room.outlets[j].packet_set_state_off = outlet_node.find('off').text
                        room.outlets[j].packet_get_state = outlet_node.find('get').text
                        mqtt_node = outlet_node.find('mqtt')
                        room.outlets[j].mqtt_publish_topic = mqtt_node.find('publish').text
                        room.outlets[j].mqtt_subscribe_topics.append(mqtt_node.find('subscribe').text)
        # 중략
# 후략

 

그리고 Home 객체 초기화할 때 outlet 정보도 딕셔너리 안에 추가하도록 한다

(거실은 3개, 침실과 컴퓨터방은 각각 2개)

home = Home(room_info=[
        {'name': 'Empty', 'light_count': 0, 'has_thermostat': False, 'outlet_count': 0},
        {'name': 'Kitchen', 'light_count': 4, 'has_thermostat': True, 'outlet_count': 3},
        {'name': 'Bedroom', 'light_count': 2, 'has_thermostat': True, 'outlet_count': 2},
        {'name': 'Computer', 'light_count': 2, 'has_thermostat': True, 'outlet_count': 2}
    ], name='IPark-Gwanggyo')

home.initDevices()

 

마지막으로 Energy RS-485 패킷 파싱 구문에 Outlet 관련 코드를 추가해주자

def onParserEnergyResult(self, chunk: bytearray):
    if len(chunk) < 8:
        return
    header = chunk[1]  # [0x31, 0x41, 0x42, 0xD1]
    command = chunk[3]
    room_idx = 0
    if header == 0x31:
        if 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
             
            # 콘센트 소비전력 패킷
            for i in range(room.outlet_count):
                dev = room.outlets[i]
                dev.state = (chunk[7] & (0x01 << i)) >> i
                if room_idx == 1 and i == 2:
                    dev.state = 1
                if len(chunk) >= 14 + 2 * i + 2 + 1:
                    dev.measurement = int.from_bytes(chunk[14 + 2 * i: 14 + 2 * i + 2], byteorder='big') / 10.
                else:
                    dev.measurement = 0
                if int(dev.measurement) != int(dev.measurement_prev) or not dev.init:
                    dev.publish_mqtt()
                    dev.init = True
                dev.measurement_prev = dev.measurement
        elif command in [0x11]:
            room_idx = chunk[5] & 0x0F

거실 (room index = 1)의 세 번째 소켓은 On/Off 제어가 불가능하므로 항상 state=1로 두었다

그리고 소비 전력이 바뀔 때마다 publish하도록 구현했는데, 소수점 단위로 바뀌면 너무 자주 notify해서 서버에 부하가 갈 것 같아 정수값이 바뀔 때마다 publish하도록 구현했다

 

2. Config XML 파일 수정

앞서 추출한 소켓 관련 On/Off 패킷들을 추가해주자

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<homenetworkserver>
    <rooms>
        <room1>
             <outlet0>
                <on>02 31 0D 01 FC 01 00 81 00 00 00 09 4F</on>
                <off>02 31 0D 01 D8 01 00 01 00 00 00 00 EC</off>
                <get>02 31 07 11 9B 01 C0</get>
                <mqtt>
                    <publish>home/ipark/outlet/state/1/0</publish>
                    <subscribe>home/ipark/outlet/command/1/0</subscribe>
                </mqtt>
            </outlet0>
<!-- 후략 -->

 

3. Homebridge Config 파일 수정

Mqtthing 포맷에 맞춰 액세서리를 추가하면 된다

(거실/주방 첫번째 outlet 구현 예시)

{
    "accessory": "mqttthing",
    "type": "outlet",
    "name": "Living room Outlet1 (MQTT)",
    "url": "mqtt server url",
    "username": "mqtt auth id",
    "password": "mqtt auth password",
    "topics": {
        "getOn": {
            "topic": "home/ipark/outlet/state/1/0",
            "apply": "return JSON.parse(message).state;"
        },
        "setOn": {
            "topic": "home/ipark/outlet/command/1/0",
            "apply": "return JSON.stringify({state: message});"
        },
        "getWatts": {
            "topic": "home/ipark/outlet/state/1/0",
            "apply": "return JSON.parse(message).watts;"
        }
    },
    "onValue": 1,
    "offValue": 0,
    "integerValue": false,
    "history": true
}

getOn, setOn, getWatts 토픽만 제대로 적어주면 정상적으로 동작한다

소비전력은 floating 형태로 전송할 것이므로 'integetValue' 속성은 false로 하고, 소비전력 이력을 남기기 위해 'history' 속성을 true로 해준다

 

4. 액세서리 확인

홈브릿지를 재시작하고 액세서리가 제대로 추가되었는지 확인해보자

홈브릿지 액세서리 리스트

아이폰으로도 확인해보자

모두 켜져있는 것으로, 현재 상태가 제대로 표시되었다

 

이제 동작이 제대로 되는지 확인해보자!

 

[시리즈 링크]

광교아이파크::전원콘센트 Apple 홈킷 연동 (1)

광교아이파크::전원콘센트 Apple 홈킷 연동 (2)

광교아이파크::전원콘센트 Apple 홈킷 연동 (3) - Final

 

반응형
Comments