├── .gitignore ├── Hillstate-Gwanggyosan ├── Dockerfile ├── Include │ ├── Common.py │ ├── Define │ │ ├── AirConditioner.py │ │ ├── AirqualitySensor.py │ │ ├── BatchOffSwitch.py │ │ ├── Device.py │ │ ├── DimmingLight.py │ │ ├── DoorLock.py │ │ ├── Elevator.py │ │ ├── EmotionLight.py │ │ ├── GasValve.py │ │ ├── HEMS.py │ │ ├── Light.py │ │ ├── Outlet.py │ │ ├── SubPhone.py │ │ ├── Thermostat.py │ │ ├── Ventilator.py │ │ └── __init__.py │ ├── Home.py │ ├── Multiprocess │ │ ├── __init__.py │ │ ├── procFFMpeg.py │ │ └── procFFServer.py │ ├── RS485 │ │ ├── PacketParser.py │ │ ├── RS485Comm.py │ │ ├── Serial │ │ │ ├── SerialComm.py │ │ │ ├── SerialThreads.py │ │ │ └── __init__.py │ │ ├── Socket │ │ │ ├── SocketTCPClient.py │ │ │ ├── SocketThreads.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── ThinQ │ │ ├── ThinQAPI.py │ │ └── __init__.py │ ├── Threads │ │ ├── ThreadCommandQueue.py │ │ ├── ThreadDiscovery.py │ │ ├── ThreadEnergyMonitor.py │ │ ├── ThreadParseResultQueue.py │ │ ├── ThreadQueryState.py │ │ ├── ThreadTimer.py │ │ └── __init__.py │ ├── __init__.py │ └── __oldcodes__ │ │ ├── ParserLight.py │ │ ├── ParserSubPhone.py │ │ ├── ParserVarious.py │ │ └── Room.py ├── README.md ├── Template │ ├── homeassistant │ │ └── ha_configuration.yaml │ └── homebridge │ │ └── homebridge_config.json ├── activate.sh ├── app.py ├── app_info.json ├── bootstrap.sh ├── clean.py ├── config_default.xml ├── requirements.txt ├── run.sh ├── summary.png ├── uwsgi.ini.template └── web │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── elevator_ctrl.py │ ├── hems.py │ ├── light_ctrl.py │ ├── outlet_ctrl.py │ ├── packet_logger.py │ ├── system.py │ └── timer.py │ ├── config.py │ ├── main │ ├── __init__.py │ ├── errors.py │ └── views.py │ ├── static │ ├── arrow_down.png │ ├── arrow_up.png │ ├── destination.png │ ├── seven_seg_0.png │ ├── seven_seg_1.png │ ├── seven_seg_2.png │ ├── seven_seg_3.png │ ├── seven_seg_4.png │ ├── seven_seg_5.png │ ├── seven_seg_6.png │ ├── seven_seg_7.png │ ├── seven_seg_8.png │ ├── seven_seg_9.png │ ├── seven_seg_a.png │ ├── seven_seg_b.png │ ├── seven_seg_c.png │ ├── seven_seg_d.png │ ├── seven_seg_e.png │ ├── seven_seg_f.png │ ├── seven_seg_null.png │ └── styles.css │ └── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── elevator_ctrl.html │ ├── hems.html │ ├── index.html │ ├── light_ctrl.html │ ├── outlet_ctrl.html │ ├── packet_logger.html │ ├── render.html │ ├── system.html │ └── timer.html ├── IPark-Gwanggyo ├── Arduino │ ├── BOM.xlsx │ ├── README.md │ ├── assemblydrawing.png │ ├── schematic.png │ └── wallpad_livingroom_ctrl.ino ├── Include │ ├── AirqualitySensor.py │ ├── Common.py │ ├── Device.py │ ├── Doorlock.py │ ├── Elevator.py │ ├── GasValve.py │ ├── Home.py │ ├── Light.py │ ├── Outlet.py │ ├── Room.py │ ├── Thermostat.py │ ├── ThreadCommand.py │ ├── ThreadMonitoring.py │ ├── Ventilator.py │ └── __init__.py ├── README.md ├── RS485 │ ├── ControlParser.py │ ├── Define.py │ ├── DoorphoneParser.py │ ├── EnergyParser.py │ ├── PacketParser.py │ ├── RS485Comm.py │ ├── Serial │ │ ├── SerialComm.py │ │ ├── SerialThreads.py │ │ ├── Util.py │ │ └── __init__.py │ ├── SmartRecvParser.py │ ├── SmartSendParser.py │ ├── Socket │ │ ├── SocketTCPClient.py │ │ ├── SocketThreads.py │ │ └── __init__.py │ ├── __init__.py │ ├── smart_elevator_down_packets.pkl │ └── smart_elevator_up_packets.pkl ├── __init__.py ├── app.py ├── config.xml ├── ha_configuration.yaml ├── homebridge_config.json ├── requirements.txt ├── run.sh ├── summary.png ├── test │ ├── CRC8.py │ └── parse_packet_last_byte.py ├── uwsgi.ini └── web │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── elevator.py │ ├── outlet_info.py │ └── packet_logger.py │ ├── auth │ └── __init__.py │ ├── config.py │ ├── main │ ├── __init__.py │ ├── errors.py │ └── views.py │ ├── static │ └── styles.css │ └── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── elevator.html │ ├── index.html │ ├── outlet.html │ ├── packet_log.html │ ├── packet_sender.html │ └── render.html └── RS485PacketCapture ├── Capture.py ├── Checksum.py ├── Define.py ├── SerialComm.py └── SerialThreads.py /.gitignore: -------------------------------------------------------------------------------- 1 | /IPark-Gwanggyo/test/crc8list.pkl 2 | /.idea 3 | *.pyc 4 | /IPark-Gwanggyo/test/airqualitysensor.py 5 | /CameraTest 6 | /MosquittoTest 7 | /Hillstate-Gwanggyosan/Include/ThinQ/*.pem 8 | *.sock 9 | *.pid 10 | /Hillstate-Gwanggyosan/venv 11 | /Hillstate-Gwanggyosan/test.sh 12 | /Hillstate-Gwanggyosan/config.xml 13 | /Hillstate-Gwanggyosan/uwsgi.ini 14 | /Test -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM alpine:latest 3 | LABEL maintainer="YOGYUI lee2002w@gmail.com" 4 | 5 | # install required packages 6 | RUN apk add --update --no-cache \ 7 | bash curl nano vim wget git openrc \ 8 | gcc openssl-dev libffi-dev musl-dev linux-headers cargo pkgconfig \ 9 | python3 python3-dev py3-pip 10 | 11 | # create directory & copy source code & set working directory 12 | RUN mkdir -p /repos/yogyui/homenetwork/hillstate-gwanggyosan 13 | COPY . /repos/yogyui/homenetwork/hillstate-gwanggyosan 14 | WORKDIR /repos/yogyui/homenetwork/hillstate-gwanggyosan 15 | 16 | # create & activate python virtual environment, install python requirements 17 | RUN /bin/bash -c "source ./bootstrap.sh" 18 | RUN python3 clean.py 19 | 20 | # expose default web server port (todo: dynamic expose?) 21 | EXPOSE 7929 22 | 23 | # activate python venv and launch application 24 | CMD /bin/bash -c "source ./activate.sh; python3 app.py; /bin/bash" 25 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Common.py: -------------------------------------------------------------------------------- 1 | import _io 2 | import datetime 3 | import threading 4 | from functools import partial 5 | from enum import IntEnum, auto, unique 6 | import xml.etree.ElementTree as ElementTree 7 | 8 | 9 | def checkAgrumentType(obj, arg): 10 | if type(obj) == arg: 11 | return True 12 | if arg == object: 13 | return True 14 | if arg in obj.__class__.__bases__: 15 | return True 16 | return False 17 | 18 | 19 | class Callback(object): 20 | _args = None 21 | 22 | def __init__(self, *args): 23 | self._args = args 24 | self._callbacks = list() 25 | 26 | def connect(self, callback): 27 | if callback not in self._callbacks: 28 | self._callbacks.append(callback) 29 | 30 | def disconnect(self, callback=None): 31 | if callback is None: 32 | self._callbacks.clear() 33 | else: 34 | if callback in self._callbacks: 35 | self._callbacks.remove(callback) 36 | 37 | def emit(self, *args): 38 | if len(self._callbacks) == 0: 39 | return 40 | if len(args) != len(self._args): 41 | raise Exception('Callback::Argument Length Mismatch') 42 | arglen = len(args) 43 | if arglen > 0: 44 | validTypes = [checkAgrumentType(args[i], self._args[i]) for i in range(arglen)] 45 | if sum(validTypes) != arglen: 46 | raise Exception('Callback::Argument Type Mismatch (Definition: {}, Call: {})'.format(self._args, args)) 47 | for callback in self._callbacks: 48 | callback(*args) 49 | 50 | 51 | def getCurTimeStr(): 52 | now = datetime.datetime.now() 53 | return '<%04d-%02d-%02d %02d:%02d:%02d.%03d>' % (now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond // 1000) 54 | 55 | 56 | def writeLog(strMsg: str, obj: object = None): 57 | strTime = getCurTimeStr() 58 | if obj is not None: 59 | if isinstance(obj, threading.Thread): 60 | if obj.ident is not None: 61 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, obj.ident) 62 | else: 63 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 64 | else: 65 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 66 | else: 67 | strObj = '' 68 | 69 | msg = strTime + strObj + ' ' + strMsg 70 | print(msg) 71 | 72 | 73 | def prettifyPacket(packet: bytearray) -> str: 74 | return ' '.join(['%02X' % x for x in packet]) 75 | 76 | 77 | class bind(partial): 78 | # https://stackoverflow.com/questions/7811247/how-to-fill-specific-positional-arguments-with-partial-in-python 79 | """ 80 | An improved version of partial which accepts Ellipsis (...) as a placeholder 81 | """ 82 | def __call__(self, *args, **keywords): 83 | keywords = {**self.keywords, **keywords} 84 | iargs = iter(args) 85 | args = (next(iargs) if arg is ... else arg for arg in self.args) 86 | return self.func(*args, *iargs, **keywords) 87 | 88 | 89 | @unique 90 | class DeviceType(IntEnum): 91 | UNKNOWN = 0 92 | LIGHT = auto() 93 | OUTLET = auto() 94 | THERMOSTAT = auto() 95 | AIRCONDITIONER = auto() 96 | GASVALVE = auto() 97 | VENTILATOR = auto() 98 | ELEVATOR = auto() 99 | SUBPHONE = auto() 100 | HEMS = auto() 101 | BATCHOFFSWITCH = auto() 102 | DOORLOCK = auto() 103 | EMOTIONLIGHT = auto() 104 | DIMMINGLIGHT = auto() 105 | 106 | 107 | @unique 108 | class HEMSDevType(IntEnum): 109 | Unknown = 0 110 | Electricity = 1 # 전기 111 | Water = 2 # 수도 112 | Gas = 3 # 가스 113 | HotWater = 4 # 온수 114 | Heating = 5 # 난방 115 | Reserved = 10 # ? 116 | 117 | 118 | @unique 119 | class HEMSCategory(IntEnum): 120 | Unknown = 0 121 | History = 1 # 우리집 사용량 이력 (3달간, 단위: kWh/L/MWh) 122 | OtherAverage = 2 # 동일평수 평균 사용량 이력 (3달간, 단위: kWh/L/MWh) 123 | Fee = 3 # 요금 이력 (3달간, 단위: 천원) 124 | CO2 = 4 # CO2 배출량 이력 (3달간, 단위: kg) 125 | Target = 5 # 목표량 126 | Current = 7 # 현재 실시간 사용량 127 | 128 | 129 | def writeXmlFile(elem: ElementTree.Element, path: str = '', fp: _io.TextIOWrapper = None, level: int = 0): 130 | if fp is None: 131 | _fp = open(path, 'w', encoding='utf-8') 132 | _fp.write('' + '\n') 133 | else: 134 | _fp = fp 135 | _fp.write('\t' * level) 136 | _fp.write('<' + elem.tag) 137 | for key in elem.keys(): 138 | _fp.write(' ' + key + '="' + elem.attrib[key] + '"') 139 | if len(list(elem)) > 0: 140 | _fp.write('>\n') 141 | for child in list(elem): 142 | writeXmlFile(child, fp=_fp, level=level+1) 143 | _fp.write('\t' * level) 144 | _fp.write('\n') 145 | else: 146 | if elem.text is not None: 147 | txt = elem.text 148 | txt = txt.replace('\r', '') 149 | txt = txt.replace('\n', '') 150 | txt = txt.replace('\t', '') 151 | if len(txt) > 0: 152 | _fp.write('>' + txt + '\n') 153 | else: 154 | _fp.write('/>\n') 155 | else: 156 | _fp.write('/>\n') 157 | if level == 0: 158 | _fp.close() 159 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/BatchOffSwitch.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class BatchOffSwitch(Device): 6 | def __init__(self, name: str = 'BatchOffSW', index: int = 0, room_index: int = 0): 7 | super().__init__(name, index, room_index) 8 | self.dev_type = DeviceType.BATCHOFFSWITCH 9 | self.unique_id = f'batchoffswitch_{self.room_index}_{self.index}' 10 | self.mqtt_publish_topic = f'home/state/batchoffsw/{self.room_index}/{self.index}' 11 | self.mqtt_subscribe_topic = f'home/command/batchoffsw/{self.room_index}/{self.index}' 12 | 13 | def setDefaultName(self): 14 | self.name = 'BatchOffSW' 15 | 16 | def publishMQTT(self): 17 | obj = {"state": self.state} 18 | if self.mqtt_client is not None: 19 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 20 | 21 | def configMQTT(self, retain: bool = False): 22 | if self.mqtt_client is None: 23 | return 24 | 25 | topic = f'{self.ha_discovery_prefix}/switch/{self.unique_id}/config' 26 | obj = { 27 | "name": self.name, 28 | "object_id": self.unique_id, 29 | "unique_id": self.unique_id, 30 | "state_topic": self.mqtt_publish_topic, 31 | "command_topic": self.mqtt_subscribe_topic, 32 | "value_template": '{ "state": {{ value_json.state }} }', 33 | "payload_on": '{ "state": 1 }', 34 | "payload_off": '{ "state": 0 }', 35 | "icon": "mdi:home-lightbulb-outline" 36 | } 37 | self.mqtt_client.publish(topic, json.dumps(obj), 1, retain) 38 | 39 | def makePacketQueryState(self) -> bytearray: 40 | # F7 0E 01 2A 01 40 10 00 19 00 1B 03 82 EE 41 | return bytearray([0xF7, 0x0E, 0x01, 0x2A, 0x01, 0x40, 0x10, 0x00, 0x19, 0x00, 0x1B, 0x03, 0x82, 0xEE]) 42 | 43 | def makePacketSetState(self, state: bool) -> bytearray: 44 | # F7 0C 01 2A 02 40 11 XX 19 00 YY EE 45 | # XX: 02 = OFF 01 = ON 46 | # YY: Checksum (XOR SUM) 47 | packet = bytearray([0xF7, 0x0C, 0x01, 0x2A, 0x02, 0x40, 0x11]) 48 | if state: 49 | packet.extend([0x01, 0x19, 0x00]) 50 | else: 51 | packet.extend([0x02, 0x19, 0x00]) 52 | packet.append(self.calcXORChecksum(packet)) 53 | packet.append(0xEE) 54 | return packet 55 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/DoorLock.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | import threading 4 | import RPi.GPIO as GPIO 5 | from Common import Callback, writeLog 6 | 7 | 8 | class ThreadDoorLockOpen(threading.Thread): 9 | def __init__(self, gpio_port: int): 10 | threading.Thread.__init__(self) 11 | self.gpio_port = gpio_port 12 | self.sig_terminated = Callback() 13 | 14 | def run(self): 15 | writeLog('Started', self) 16 | GPIO.output(self.gpio_port, GPIO.LOW) 17 | for _ in range(2): 18 | writeLog(f"Set GPIO PIN{self.gpio_port} as LOW", self) 19 | time.sleep(0.1) 20 | GPIO.output(self.gpio_port, GPIO.HIGH) 21 | writeLog(f"Set GPIO PIN{self.gpio_port} as HIGH", self) 22 | time.sleep(0.1) 23 | time.sleep(5) # 5초간 Unsecured state를 유지해준다 24 | writeLog('Terminated', self) 25 | self.sig_terminated.emit() 26 | 27 | 28 | class DoorLock(Device): 29 | enable: bool = False 30 | gpio_port: int = 0 31 | thread_open: Union[ThreadDoorLockOpen, None] = None 32 | 33 | def __init__(self, name: str = 'Doorlock', index: int = 0, room_index: int = 0): 34 | super().__init__(name, index, room_index) 35 | self.dev_type = DeviceType.DOORLOCK 36 | self.unique_id = f'doorlock_{self.room_index}_{self.index}' 37 | self.mqtt_publish_topic = f'home/state/doorlock/{self.room_index}/{self.index}' 38 | self.mqtt_subscribe_topic = f'home/command/doorlock/{self.room_index}/{self.index}' 39 | self.state = 1 40 | self.setParams(True, 23) 41 | 42 | def setDefaultName(self): 43 | self.name = 'Doorlock' 44 | 45 | def publishMQTT(self): 46 | # 'Unsecured', 'Secured', 'Jammed', 'Unknown' 47 | state_str = 'Unknown' 48 | if self.state == 0: 49 | state_str = 'Unsecured' 50 | elif self.state == 1: 51 | state_str = 'Secured' 52 | obj = {"state": state_str} 53 | if self.mqtt_client is not None: 54 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 55 | 56 | def configMQTT(self, retain: bool = False): 57 | pass 58 | 59 | def setParams(self, enable: bool, gpio_port: int): 60 | self.enable = enable 61 | self.gpio_port = gpio_port 62 | GPIO.setmode(GPIO.BCM) 63 | GPIO.setup(self.gpio_port, GPIO.IN, GPIO.PUD_UP) # GPIO IN, Pull Down 설정 64 | 65 | def updateState(self, state: int, **kwargs): 66 | self.state = state 67 | if not self.init: 68 | self.publishMQTT() 69 | self.init = True 70 | if self.state != self.state_prev: 71 | self.publishMQTT() 72 | self.state_prev = self.state 73 | 74 | def startThreadOpen(self): 75 | if self.thread_open is None: 76 | self.updateState(0) 77 | GPIO.setup(self.gpio_port, GPIO.OUT) 78 | GPIO.output(self.gpio_port, GPIO.HIGH) 79 | self.thread_open = ThreadDoorLockOpen(self.gpio_port) 80 | self.thread_open.sig_terminated.connect(self.onThreadOpenTerminated) 81 | self.thread_open.start() 82 | else: 83 | writeLog('Thread is still working', self) 84 | 85 | def onThreadOpenTerminated(self): 86 | del self.thread_open 87 | self.thread_open = None 88 | self.updateState(1) 89 | GPIO.setup(self.gpio_port, GPIO.IN, GPIO.PUD_UP) # GPIO IN, Pull Down 설정 90 | 91 | def open(self): 92 | if self.enable: 93 | self.startThreadOpen() 94 | else: 95 | writeLog('Disabled!', self) 96 | 97 | def makePacketOpen(self) -> bytearray: 98 | # F7 0E 01 1E 02 43 11 04 00 04 FF FF B6 EE 99 | return bytearray([0xF7, 0x0E, 0x01, 0x1E, 0x02, 0x43, 0x11, 0x04, 0x00, 0x04, 0xFF, 0xFF, 0xB6, 0xEE]) 100 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/EmotionLight.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class EmotionLight(Device): 6 | def __init__(self, name: str = 'EmotionLight', index: int = 0, room_index: int = 0): 7 | super().__init__(name, index, room_index) 8 | self.dev_type = DeviceType.EMOTIONLIGHT 9 | self.unique_id = f'emotionlight_{self.room_index}_{self.index}' 10 | self.mqtt_publish_topic = f'home/state/emotionlight/{self.room_index}/{self.index}' 11 | self.mqtt_subscribe_topic = f'home/command/emotionlight/{self.room_index}/{self.index}' 12 | 13 | def setDefaultName(self): 14 | self.name = 'EmotionLight' 15 | 16 | def publishMQTT(self): 17 | obj = {"state": self.state} 18 | if self.mqtt_client is not None: 19 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 20 | 21 | def configMQTT(self, retain: bool = False): 22 | if self.mqtt_client is None: 23 | return 24 | 25 | topic = f'{self.ha_discovery_prefix}/light/{self.unique_id}/config' 26 | obj = { 27 | "name": self.name, 28 | "object_id": self.unique_id, 29 | "unique_id": self.unique_id, 30 | "state_topic": self.mqtt_publish_topic, 31 | "command_topic": self.mqtt_subscribe_topic, 32 | "schema": "template", 33 | "state_template": "{% if value_json.state %} on {% else %} off {% endif %}", 34 | "command_on_template": '{"state": 1}', 35 | "command_off_template": '{"state": 0 }' 36 | } 37 | self.mqtt_client.publish(topic, json.dumps(obj), 1, retain) 38 | 39 | def makePacketQueryState(self) -> bytearray: 40 | # F7 0B 01 15 01 40 XX 00 00 YY EE 41 | # XX: 상위 4비트 = Room Index, 하위 4비트 = Device Index 42 | # YY: Checksum (XOR SUM) 43 | packet = bytearray([0xF7, 0x0B, 0x01, 0x15, 0x01, 0x40]) 44 | packet.append((self.room_index << 4) + (self.index + 1)) 45 | packet.extend([0x00, 0x00]) 46 | packet.append(self.calcXORChecksum(packet)) 47 | packet.append(0xEE) 48 | return packet 49 | 50 | def makePacketSetState(self, state: bool) -> bytearray: 51 | # F7 0B 01 15 02 40 XX YY 00 ZZ EE 52 | # XX: 상위 4비트 = Room Index, 하위 4비트 = Device Index (1-based) 53 | # YY: 02 = OFF, 01 = ON 54 | # ZZ: Checksum (XOR SUM) 55 | packet = bytearray([0xF7, 0x0B, 0x01, 0x15, 0x02, 0x40]) 56 | packet.append((self.room_index << 4) + (self.index + 1)) 57 | if state: 58 | packet.extend([0x01, 0x00]) 59 | else: 60 | packet.extend([0x02, 0x00]) 61 | packet.append(self.calcXORChecksum(packet)) 62 | packet.append(0xEE) 63 | return packet 64 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/GasValve.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class GasValve(Device): 6 | def __init__(self, name: str = 'GasValve', index: int = 0, room_index: int = 0): 7 | super().__init__(name, index, room_index) 8 | self.dev_type = DeviceType.GASVALVE 9 | self.unique_id = f'gasvalve_{self.room_index}_{self.index}' 10 | self.mqtt_publish_topic = f'home/state/gasvalve/{self.room_index}/{self.index}' 11 | self.mqtt_subscribe_topic = f'home/command/gasvalve/{self.room_index}/{self.index}' 12 | 13 | def setDefaultName(self): 14 | self.name = 'GasValve' 15 | 16 | def publishMQTT(self): 17 | obj = {"state": self.state} 18 | if self.mqtt_client is not None: 19 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 20 | 21 | def configMQTT(self, retain: bool = False): 22 | if self.mqtt_client is None: 23 | return 24 | 25 | topic = f'{self.ha_discovery_prefix}/switch/{self.unique_id}/config' 26 | obj = { 27 | "name": self.name, 28 | "object_id": self.unique_id, 29 | "unique_id": self.unique_id, 30 | "state_topic": self.mqtt_publish_topic, 31 | "command_topic": self.mqtt_subscribe_topic, 32 | "value_template": '{ "state": {{ value_json.state }} }', 33 | "payload_on": '{ "state": 1 }', 34 | "payload_off": '{ "state": 0 }', 35 | "icon": "mdi:pipe-valve" 36 | } 37 | self.mqtt_client.publish(topic, json.dumps(obj), 1, retain) 38 | 39 | def makePacketQueryState(self) -> bytearray: 40 | # F7 0B 01 1B 01 43 11 00 00 B5 EE 41 | packet = bytearray([0xF7, 0x0B, 0x01, 0x1B, 0x01, 0x43]) 42 | packet.append(0x11) 43 | packet.extend([0x00, 0x00]) 44 | packet.append(self.calcXORChecksum(packet)) 45 | packet.append(0xEE) 46 | return packet 47 | 48 | def makePacketSetState(self, state: bool) -> bytearray: 49 | # F7 0B 01 1B 02 43 11 XX 00 YY EE 50 | # XX: 03 = OFF, 04 = ON (지원되지 않음) 51 | # YY: Checksum (XOR SUM) 52 | packet = bytearray([0xF7, 0x0B, 0x01, 0x1B, 0x02, 0x43, 0x11]) 53 | if state: 54 | packet.extend([0x04, 0x00]) 55 | else: 56 | packet.extend([0x03, 0x00]) 57 | packet.append(self.calcXORChecksum(packet)) 58 | packet.append(0xEE) 59 | return packet 60 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/HEMS.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | from Device import * 4 | from Common import HEMSDevType, HEMSCategory 5 | 6 | 7 | class HEMS(Device): 8 | data: dict 9 | 10 | def __init__(self, name: str = 'HEMS', index: int = 0, room_index: int = 0): 11 | super().__init__(name, index, room_index) 12 | self.dev_type = DeviceType.HEMS 13 | self.unique_id = f'hems_{self.room_index}_{self.index}' 14 | self.mqtt_publish_topic = f'home/state/hems/{self.room_index}/{self.index}' 15 | self.mqtt_subscribe_topic = f'home/command/hems/{self.room_index}/{self.index}' 16 | self.data = dict() 17 | 18 | def setDefaultName(self): 19 | self.name = 'HEMS' 20 | 21 | def publishMQTT(self): 22 | pass 23 | 24 | def configMQTT(self, retain: bool = False): 25 | if self.mqtt_client is None: 26 | return 27 | 28 | topic = f'{self.ha_discovery_prefix}/sensor/{self.unique_id}/config' 29 | obj = { 30 | "name": self.name + "_electricity_current", 31 | "object_id": self.unique_id + "_electricity_current", 32 | "unique_id": self.unique_id + "_electricity_current", 33 | "state_topic": self.mqtt_publish_topic + '/electricity_current', 34 | "unit_of_measurement": "W", 35 | "value_template": "{{ value_json.value }}", 36 | "device_class": "power", 37 | "state_class": "measurement" 38 | } 39 | self.mqtt_client.publish(topic, json.dumps(obj), 1, retain) 40 | 41 | def updateState(self, _: int, **kwargs): 42 | if 'monitor_data' in kwargs.keys(): 43 | monitor_data = kwargs.get('monitor_data') 44 | self.data['last_recv_time'] = datetime.datetime.now() 45 | for key in list(monitor_data.keys()): 46 | self.data[key] = monitor_data.get(key) 47 | if key in ['electricity_current']: 48 | topic = self.mqtt_publish_topic + f'/{key}' 49 | value = monitor_data.get(key) 50 | """ 51 | if value == 0: 52 | writeLog(f"zero power consumption? >> {prettifyPacket(monitor_data.get('packet'))}", self) 53 | """ 54 | obj = {"value": value} 55 | if self.mqtt_client is not None: 56 | self.mqtt_client.publish(topic, json.dumps(obj), 1) 57 | 58 | def makePacketQuery(self, devtype: HEMSDevType, category: HEMSCategory) -> bytearray: 59 | command = ((devtype.value & 0x0F) << 4) | (category.value & 0x0F) 60 | return bytearray([0x7F, 0xE0, command, 0x00, 0xEE]) 61 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/Light.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class Light(Device): 6 | def __init__(self, name: str = 'Light', index: int = 0, room_index: int = 0): 7 | super().__init__(name, index, room_index) 8 | self.dev_type = DeviceType.LIGHT 9 | self.unique_id = f'light_{self.room_index}_{self.index}' 10 | self.mqtt_publish_topic = f'home/state/light/{self.room_index}/{self.index}' 11 | self.mqtt_subscribe_topic = f'home/command/light/{self.room_index}/{self.index}' 12 | 13 | def setDefaultName(self): 14 | self.name = 'Light' 15 | 16 | def publishMQTT(self): 17 | obj = {"state": self.state} 18 | if self.mqtt_client is not None: 19 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 20 | 21 | def configMQTT(self, retain: bool = False): 22 | if self.mqtt_client is None: 23 | return 24 | 25 | topic = f'{self.ha_discovery_prefix}/light/{self.unique_id}/config' 26 | obj = { 27 | "name": self.name, 28 | "object_id": self.unique_id, 29 | "unique_id": self.unique_id, 30 | "state_topic": self.mqtt_publish_topic, 31 | "command_topic": self.mqtt_subscribe_topic, 32 | "schema": "template", 33 | "state_template": "{% if value_json.state %} on {% else %} off {% endif %}", 34 | "command_on_template": '{"state": 1}', 35 | "command_off_template": '{"state": 0}' 36 | } 37 | self.mqtt_client.publish(topic, json.dumps(obj), 1, retain) 38 | 39 | def makePacketQueryState(self) -> bytearray: 40 | # F7 0B 01 19 01 40 XX 00 00 YY EE 41 | # XX: 상위 4비트 = Room Index, 하위 4비트 = Device Index (1-based) 42 | # YY: Checksum (XOR SUM) 43 | packet = bytearray([0xF7, 0x0B, 0x01, 0x19, 0x01, 0x40]) 44 | # packet.append(self.room_index << 4) 45 | packet.append((self.room_index << 4) + (self.index + 1)) 46 | packet.extend([0x00, 0x00]) 47 | packet.append(self.calcXORChecksum(packet)) 48 | packet.append(0xEE) 49 | return packet 50 | 51 | def makePacketSetState(self, state: bool) -> bytearray: 52 | # F7 0B 01 19 02 40 XX YY 00 ZZ EE 53 | # XX: 상위 4비트 = Room Index, 하위 4비트 = Device Index (1-based) 54 | # YY: 02 = OFF, 01 = ON 55 | # ZZ: Checksum (XOR SUM) 56 | packet = bytearray([0xF7, 0x0B, 0x01, 0x19, 0x02, 0x40]) 57 | packet.append((self.room_index << 4) + (self.index + 1)) 58 | if state: 59 | packet.extend([0x01, 0x00]) 60 | else: 61 | packet.extend([0x02, 0x00]) 62 | packet.append(self.calcXORChecksum(packet)) 63 | packet.append(0xEE) 64 | return packet 65 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/Outlet.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class Outlet(Device): 6 | enable_off_command: bool = False 7 | 8 | def __init__(self, name: str = 'Outlet', index: int = 0, room_index: int = 0): 9 | super().__init__(name, index, room_index) 10 | self.dev_type = DeviceType.OUTLET 11 | self.unique_id = f'outlet_{self.room_index}_{self.index}' 12 | self.mqtt_publish_topic = f'home/state/outlet/{self.room_index}/{self.index}' 13 | self.mqtt_subscribe_topic = f'home/command/outlet/{self.room_index}/{self.index}' 14 | 15 | def setDefaultName(self): 16 | self.name = 'Outlet' 17 | 18 | def __repr__(self): 19 | # repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))}) ' 20 | repr_txt = f'<{self.__class__.__name__}, {self.name}, ' 21 | repr_txt += f'Dev Idx: {self.index}, ' 22 | repr_txt += f'Room Idx: {self.room_index}, ' 23 | repr_txt += f'Enable Off Cmd: {self.enable_off_command}' 24 | repr_txt += '>' 25 | return repr_txt 26 | 27 | def setEnableOffCommand(self, value: bool): 28 | self.enable_off_command = value 29 | 30 | def publishMQTT(self): 31 | obj = {"state": self.state} 32 | if self.mqtt_client is not None: 33 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 34 | 35 | def configMQTT(self, retain: bool = False): 36 | if self.mqtt_client is None: 37 | return 38 | 39 | topic = f'{self.ha_discovery_prefix}/switch/{self.unique_id}/config' 40 | obj = { 41 | "name": self.name, 42 | "object_id": self.unique_id, 43 | "unique_id": self.unique_id, 44 | "state_topic": self.mqtt_publish_topic, 45 | "command_topic": self.mqtt_subscribe_topic, 46 | "value_template": '{ "state": {{ value_json.state }} }', 47 | "payload_on": '{ "state": 1 }', 48 | "payload_off": '{ "state": 0 }', 49 | "icon": "mdi:power-socket-de" 50 | } 51 | self.mqtt_client.publish(topic, json.dumps(obj), 1, retain) 52 | 53 | def makePacketQueryState(self) -> bytearray: 54 | # F7 0B 01 1F 01 40 XX 00 00 YY EE 55 | # XX: 상위 4비트 = Room Index, 하위 4비트 = 0 56 | # YY: Checksum (XOR SUM) 57 | packet = bytearray([0xF7, 0x0B, 0x01, 0x1F, 0x01, 0x40]) 58 | # packet.append(self.room_index << 4) 59 | packet.append((self.room_index << 4) + (self.index + 1)) 60 | packet.extend([0x00, 0x00]) 61 | packet.append(self.calcXORChecksum(packet)) 62 | packet.append(0xEE) 63 | return packet 64 | 65 | def makePacketSetState(self, state: bool) -> bytearray: 66 | # F7 0B 01 1F 02 40 XX YY 00 ZZ EE 67 | # XX: 상위 4비트 = Room Index, 하위 4비트 = Device Index (1-based) 68 | # YY: 02 = OFF, 01 = ON 69 | # ZZ: Checksum (XOR SUM) 70 | packet = bytearray([0xF7, 0x0B, 0x01, 0x1F, 0x02, 0x40]) 71 | packet.append((self.room_index << 4) + (self.index + 1)) 72 | if state: 73 | packet.extend([0x01, 0x00]) 74 | else: 75 | packet.extend([0x02, 0x00]) 76 | packet.append(self.calcXORChecksum(packet)) 77 | packet.append(0xEE) 78 | return packet 79 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/Ventilator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class Ventilator(Device): 6 | def __init__(self, name: str = 'Ventilator', index: int = 0, room_index: int = 0): 7 | super().__init__(name, index, room_index) 8 | self.dev_type = DeviceType.VENTILATOR 9 | self.unique_id = f'ventilator_{self.room_index}_{self.index}' 10 | self.mqtt_publish_topic = f'home/state/ventilator/{self.room_index}/{self.index}' 11 | self.mqtt_subscribe_topic = f'home/command/ventilator/{self.room_index}/{self.index}' 12 | self.rotation_speed: int = -1 13 | self.rotation_speed_prev: int = -1 14 | 15 | def setDefaultName(self): 16 | self.name = 'Ventilator' 17 | 18 | def publishMQTT(self): 19 | obj = { 20 | "state": self.state, 21 | "rotationspeed": 0 22 | } 23 | if self.state: 24 | if self.rotation_speed == 0x01: 25 | obj['rotationspeed'] = 30 26 | elif self.rotation_speed == 0x03: 27 | obj['rotationspeed'] = 60 28 | elif self.rotation_speed == 0x07: 29 | obj['rotationspeed'] = 100 30 | if self.mqtt_client is not None: 31 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 32 | 33 | def configMQTT(self, retain: bool = False): 34 | if self.mqtt_client is None: 35 | return 36 | 37 | topic = f'{self.ha_discovery_prefix}/fan/{self.unique_id}/config' 38 | obj = { 39 | "name": self.name, 40 | "object_id": self.unique_id, 41 | "unique_id": self.unique_id, 42 | "state_topic": self.mqtt_publish_topic, 43 | "state_value_template": "{% if value_json.state %} ON {% else %} OFF {% endif %}", 44 | "command_topic": self.mqtt_subscribe_topic, 45 | "command_template": "{% set values = {'OFF': 0, 'ON': 1} %} \ 46 | { \"state\": {{ values[value] if value in values.keys() else 0 }} }", 47 | "percentage_state_topic": self.mqtt_publish_topic, 48 | "percentage_value_template": "{{ value_json.rotationspeed }}", 49 | "percentage_command_topic": self.mqtt_subscribe_topic, 50 | "percentage_command_template": '{ "rotationspeed": {{ value }} }', 51 | "speed_range_min": 1, 52 | "speed_range_max": 100 53 | } 54 | self.mqtt_client.publish(topic, json.dumps(obj), 1, retain) 55 | 56 | def updateState(self, state: int, **kwargs): 57 | self.state = state 58 | if not self.init: 59 | self.publishMQTT() 60 | self.init = True 61 | if self.state != self.state_prev: 62 | self.publishMQTT() 63 | self.state_prev = self.state 64 | # 풍량 인자 65 | rotation_speed = kwargs.get('rotation_speed') 66 | if rotation_speed is not None: 67 | self.rotation_speed = rotation_speed 68 | if self.rotation_speed != self.rotation_speed_prev: 69 | self.publishMQTT() 70 | self.rotation_speed_prev = self.rotation_speed 71 | 72 | def makePacketQueryState(self) -> bytearray: 73 | # F7 0B 01 2B 01 40 11 00 00 XX EE 74 | # XX: Checksum (XOR SUM) 75 | packet = bytearray([0xF7, 0x0B, 0x01, 0x2B, 0x01, 0x40]) 76 | packet.append(0x11) 77 | packet.extend([0x00, 0x00]) 78 | packet.append(self.calcXORChecksum(packet)) 79 | packet.append(0xEE) 80 | return packet 81 | 82 | def makePacketSetState(self, state: bool) -> bytearray: 83 | # F7 0B 01 2B 02 40 11 XX 00 YY EE 84 | # XX: 0x01=On, 0x02=Off 85 | # YY: Checksum (XOR SUM) 86 | packet = bytearray([0xF7, 0x0B, 0x01, 0x2B, 0x02, 0x40]) 87 | # packet.append(0x10 + (self.room_index & 0x0F)) 88 | packet.append(0x11) # 환기는 거실(공간인덱스 1)에 설치된걸로 설정되어 있다 89 | if state: 90 | packet.extend([0x01, 0x00]) 91 | else: 92 | packet.extend([0x02, 0x00]) 93 | packet.append(self.calcXORChecksum(packet)) 94 | packet.append(0xEE) 95 | return packet 96 | 97 | def makePacketSetRotationSpeed(self, rotation_speed: int) -> bytearray: 98 | # F7 0B 01 2B 02 42 11 XX 00 YY EE 99 | # XX: 풍량 (0x01=약, 0x03=중, 0x07=강) 100 | # YY: Checksum (XOR SUM) 101 | packet = bytearray([0xF7, 0x0B, 0x01, 0x2B, 0x02, 0x42]) 102 | # packet.append(0x10 + (self.room_index & 0x0F)) 103 | packet.append(0x11) # 환기는 거실(공간인덱스 1)에 설치된걸로 설정되어 있다 104 | packet.extend([rotation_speed & 0xFF, 0x00]) 105 | packet.append(self.calcXORChecksum(packet)) 106 | packet.append(0xEE) 107 | return packet 108 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Define/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from Device import Device 9 | from Light import Light 10 | from Outlet import Outlet 11 | from GasValve import GasValve 12 | from Thermostat import Thermostat 13 | from Ventilator import Ventilator 14 | from AirConditioner import AirConditioner 15 | from Elevator import Elevator 16 | from EmotionLight import EmotionLight 17 | from DimmingLight import DimmingLight 18 | # from DoorLock import DoorLock 19 | from SubPhone import SubPhone 20 | from AirqualitySensor import AirqualitySensor 21 | from BatchOffSwitch import BatchOffSwitch 22 | from HEMS import HEMS 23 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Multiprocess/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | 9 | from procFFServer import procFFServer 10 | from procFFMpeg import procFFMpeg -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Multiprocess/procFFMpeg.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import multiprocessing as mp 5 | from multiprocessing import connection 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 7 | INCPATH = os.path.dirname(CURPATH) 8 | sys.path.extend([CURPATH, INCPATH]) 9 | sys.path = list(set(sys.path)) 10 | del CURPATH, INCPATH 11 | from Common import writeLog 12 | 13 | 14 | def procFFMpeg(cfg: dict, pipe: connection.Connection): 15 | pid = os.getpid() 16 | name = mp.current_process().name 17 | prefix = f'[MultiProcess][{name}({pid})] ' 18 | writeLog(prefix + 'Started') 19 | 20 | idev = cfg.get('input_device') 21 | fps = cfg.get('frame_rate') 22 | width = cfg.get('width') 23 | height = cfg.get('height') 24 | feed = cfg.get('feed_path') 25 | cmd = f"exec ~/ffmpeg/ffmpeg -i {idev} -r {fps} -s {width}x{height} -threads 1 {feed}" 26 | 27 | with subprocess.Popen(cmd, 28 | shell=True, 29 | stdin=subprocess.PIPE, 30 | stdout=subprocess.PIPE, 31 | stderr=subprocess.STDOUT 32 | ) as subproc: 33 | pid = subproc.pid 34 | writeLog(prefix + f'Subprocess {pid} opened') 35 | msg = f'{pid}' 36 | pipe.send_bytes(msg.encode(encoding='utf-8', errors='ignore')) 37 | writeLog(prefix + f'Send Subprocess PID Info to pipe') 38 | buff = subproc.stdout.read() 39 | print(buff.decode(encoding='UTF-8')) 40 | 41 | writeLog(prefix + 'Terminated') 42 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Multiprocess/procFFServer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import multiprocessing as mp 5 | from multiprocessing import connection 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 7 | INCPATH = os.path.dirname(CURPATH) 8 | sys.path.extend([CURPATH, INCPATH]) 9 | sys.path = list(set(sys.path)) 10 | del CURPATH, INCPATH 11 | from Common import writeLog 12 | 13 | 14 | def procFFServer(cfg: dict, pipe: connection.Connection): 15 | pid = os.getpid() 16 | name = mp.current_process().name 17 | prefix = f'[MultiProcess][{name}({pid})] ' 18 | writeLog(prefix + 'Started') 19 | 20 | conf_path = cfg.get('conf_file_path') 21 | cmd = f'exec ~/ffmpeg/ffserver -f {conf_path}' 22 | 23 | with subprocess.Popen(cmd, 24 | shell=True, 25 | stdin=subprocess.PIPE, 26 | stdout=subprocess.PIPE, 27 | stderr=subprocess.STDOUT 28 | ) as subproc: 29 | pid = subproc.pid 30 | writeLog(prefix + f'Subprocess {pid} opened') 31 | msg = f'{pid}' 32 | pipe.send_bytes(msg.encode(encoding='utf-8', errors='ignore')) 33 | writeLog(prefix + f'Send Subprocess PID Info to pipe') 34 | buff = subproc.stdout.read() 35 | print(buff.decode(encoding='UTF-8')) 36 | 37 | writeLog(prefix + 'Terminated') 38 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/RS485/RS485Comm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | from enum import IntEnum 5 | from typing import Union 6 | from Serial import * 7 | from Socket import * 8 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/Include/RS485 9 | INCPATH = os.path.dirname(CURPATH) # {$PROJECT}/Include/ 10 | sys.path.extend([CURPATH, INCPATH]) 11 | sys.path = list(set(sys.path)) 12 | del CURPATH, INCPATH 13 | from Common import writeLog, Callback 14 | 15 | 16 | class RS485HwType(IntEnum): 17 | Serial = 0 18 | Socket = 1 19 | Unknown = 2 20 | 21 | 22 | class RS485Config: 23 | enable: bool = True 24 | comm_type: RS485HwType 25 | serial_port: str = '/dev/ttyUSB0' 26 | serial_baud: int = 9600 27 | serial_databit: int = 8 28 | serial_parity: str = 'N' 29 | serial_stopbits: float = 1. 30 | socket_ipaddr: str = '127.0.0.1' 31 | socket_port: int = 80 32 | check_connection: bool = True 33 | 34 | 35 | class RS485Comm: 36 | _comm_obj: Union[SerialComm, TCPClient, None] = None 37 | _hw_type: RS485HwType = RS485HwType.Unknown 38 | _last_conn_addr: str = '' 39 | _last_conn_port: int = 0 40 | 41 | def __init__(self, name: str = 'RS485Comm'): 42 | self._name = name 43 | self.sig_connected = Callback() 44 | self.sig_disconnected = Callback() 45 | self.sig_send_data = Callback(bytes) 46 | self.sig_recv_data = Callback(bytes) 47 | self.sig_exception = Callback(str) 48 | 49 | def setType(self, comm_type: RS485HwType): 50 | if self._comm_obj is not None: 51 | self.release() 52 | if comm_type == RS485HwType.Serial: 53 | self._comm_obj = SerialComm(self._name) 54 | elif comm_type == RS485HwType.Socket: 55 | self._comm_obj = TCPClient(self._name) 56 | self._hw_type = comm_type 57 | if self._comm_obj is not None: 58 | self._comm_obj.sig_connected.connect(self.onConnect) 59 | self._comm_obj.sig_disconnected.connect(self.onDisconnect) 60 | self._comm_obj.sig_send_data.connect(self.onSendData) 61 | self._comm_obj.sig_recv_data.connect(self.onRecvData) 62 | self._comm_obj.sig_exception.connect(self.onException) 63 | writeLog(f"Set HW Type as '{comm_type.name}'", self) 64 | 65 | def getType(self) -> RS485HwType: 66 | return self._hw_type 67 | 68 | def release(self): 69 | if self._comm_obj is not None: 70 | self._comm_obj.release() 71 | del self._comm_obj 72 | self._comm_obj = None 73 | 74 | def connect(self, addr: str, port: int, **kwargs) -> bool: 75 | # serial - (devport, baud) 76 | # socket - (ipaddr, port) 77 | self._last_conn_addr = addr 78 | self._last_conn_port = port 79 | return self._comm_obj.connect(addr, port, **kwargs) 80 | 81 | def disconnect(self): 82 | self._comm_obj.disconnect() 83 | 84 | def reconnect(self, count: int = 1): 85 | self.disconnect() 86 | for _ in range(count): 87 | if self.isConnected(): 88 | break 89 | writeLog(f'Debug Issue >> {self._last_conn_addr}, {self._last_conn_port}', self) 90 | self.connect(self._last_conn_addr, self._last_conn_port) 91 | time.sleep(1) 92 | 93 | def isConnected(self) -> bool: 94 | if self._comm_obj is None: 95 | return False 96 | return self._comm_obj.isConnected() 97 | 98 | def sendData(self, data: Union[bytes, bytearray, str]): 99 | if self._comm_obj is not None: 100 | if len(data) > 0: 101 | self._comm_obj.sendData(data) 102 | 103 | def time_after_last_recv(self) -> float: 104 | if self._comm_obj is None: 105 | return 0. 106 | return self._comm_obj.time_after_last_recv() 107 | 108 | # Callbacks 109 | def onConnect(self, success: bool): 110 | if success: 111 | self.sig_connected.emit() 112 | else: 113 | self.sig_disconnected.emit() 114 | 115 | def onDisconnect(self): 116 | self.sig_disconnected.emit() 117 | 118 | def onSendData(self, data: bytes): 119 | self.sig_send_data.emit(data) 120 | 121 | def onRecvData(self, data: bytes): 122 | self.sig_recv_data.emit(data) 123 | 124 | def onException(self, msg: str): 125 | self.sig_exception.emit(msg) 126 | 127 | @property 128 | def name(self) -> str: 129 | return self._name 130 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/RS485/Serial/SerialThreads.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import queue 5 | import serial 6 | import threading 7 | import traceback 8 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/Include/RS485/Serial 9 | INCPATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/Include/ 10 | sys.path.extend([CURPATH, INCPATH]) 11 | sys.path = list(set(sys.path)) 12 | del CURPATH, INCPATH 13 | from Common import writeLog, Callback 14 | 15 | 16 | class ThreadSend(threading.Thread): 17 | _keepAlive: bool = True 18 | 19 | def __init__(self, serial_: serial.Serial, queue_: queue.Queue): 20 | threading.Thread.__init__(self, name='Serial Send Thread') 21 | self.sig_send_data = Callback(bytes) 22 | self.sig_terminated = Callback() 23 | self.sig_exception = Callback(str) 24 | self._serial = serial_ 25 | self._queue = queue_ 26 | 27 | def run(self): 28 | writeLog(f'Started ({self._serial.port})', self) 29 | while self._keepAlive: 30 | try: 31 | if not self._queue.empty(): 32 | data = self._queue.get() 33 | sendLen = len(data) 34 | # self._serial.setRTS(True) 35 | while sendLen > 0: 36 | nLen = self._serial.write(data[(len(data) - sendLen):]) 37 | sData = data[(len(data) - sendLen):(len(data) - sendLen + nLen)] 38 | self.sig_send_data.emit(sData) 39 | sendLen -= nLen 40 | # self._serial.setRTS(False) 41 | else: 42 | time.sleep(1e-3) 43 | except Exception as e: 44 | writeLog('Exception::{}'.format(e), self) 45 | traceback.print_exc() 46 | self.sig_exception.emit(str(e)) 47 | writeLog(f'Terminated ({self._serial.port})', self) 48 | self.sig_terminated.emit() 49 | 50 | def stop(self): 51 | self._keepAlive = False 52 | 53 | 54 | class ThreadReceive(threading.Thread): 55 | _keepAlive: bool = True 56 | 57 | def __init__(self, serial_: serial.Serial, queue_: queue.Queue): 58 | threading.Thread.__init__(self, name='Serial Recv Thread') 59 | self.sig_terminated = Callback() 60 | self.sig_recv_data = Callback() 61 | self.sig_exception = Callback(str) 62 | self._serial = serial_ 63 | self._queue = queue_ 64 | 65 | def run(self): 66 | writeLog(f'Started ({self._serial.port})', self) 67 | while self._keepAlive: 68 | try: 69 | if self._serial.isOpen(): 70 | if self._serial.in_waiting > 0: 71 | rcv = self._serial.read(self._serial.in_waiting) 72 | self.sig_recv_data.emit() 73 | self._queue.put(rcv) 74 | else: 75 | time.sleep(1e-3) 76 | else: 77 | time.sleep(1e-3) 78 | except Exception as e: 79 | writeLog(f'Exception::{self._serial.port}::{e}', self) 80 | traceback.print_exc() 81 | self.sig_exception.emit(str(e)) 82 | # break 83 | writeLog(f'Terminated ({self._serial.port})', self) 84 | self.sig_terminated.emit() 85 | 86 | def stop(self): 87 | self._keepAlive = False 88 | 89 | 90 | class ThreadCheckRecvQueue(threading.Thread): 91 | _keepAlive: bool = True 92 | 93 | def __init__(self, serial_: serial.Serial, queue_: queue.Queue): 94 | threading.Thread.__init__(self, name='Serial Check Thread') 95 | self.sig_get = Callback(bytes) 96 | self.sig_terminated = Callback() 97 | self.sig_exception = Callback(str) 98 | self._serial = serial_ 99 | self._queue = queue_ 100 | 101 | def run(self): 102 | writeLog(f'Started ({self._serial.port})', self) 103 | while self._keepAlive: 104 | try: 105 | if not self._queue.empty(): 106 | chunk = self._queue.get() 107 | self.sig_get.emit(chunk) 108 | else: 109 | time.sleep(1e-3) 110 | except Exception as e: 111 | writeLog('Exception::{}'.format(e), self) 112 | traceback.print_exc() 113 | self.sig_exception.emit(str(e)) 114 | writeLog(f'Terminated ({self._serial.port})', self) 115 | self.sig_terminated.emit() 116 | 117 | def stop(self): 118 | self._keepAlive = False 119 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/RS485/Serial/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from SerialComm import SerialComm 9 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/RS485/Socket/SocketThreads.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import queue 5 | import socket 6 | import threading 7 | from typing import Union 8 | from abc import abstractmethod, ABCMeta 9 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/Include/RS485/Socket 10 | INCPATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/Include/ 11 | sys.path.extend([CURPATH, INCPATH]) 12 | sys.path = list(set(sys.path)) 13 | del CURPATH, INCPATH 14 | from Common import writeLog, Callback 15 | 16 | 17 | class ThreadCommon(threading.Thread): 18 | __metaclass__ = ABCMeta 19 | 20 | _keepAlive: bool = True 21 | _sock: Union[socket.socket, None] = None 22 | _loop_sleep_time: float = 1e-3 23 | 24 | def __init__(self, name: str = 'Socket Thread Common'): 25 | threading.Thread.__init__(self, name=name) 26 | self.sig_terminated = Callback() 27 | self.sig_send = Callback(bytes) 28 | self.sig_recv = Callback(bytes) 29 | self.sig_exception = Callback(str, bool) # message, terminate socket flag 30 | self.setDaemon(True) 31 | 32 | def run(self): 33 | writeLog('Started', self) 34 | while self._keepAlive: 35 | self.loop() 36 | if self._loop_sleep_time > 0: 37 | time.sleep(self._loop_sleep_time) 38 | writeLog('Terminated', self) 39 | self.sig_terminated.emit() 40 | 41 | @abstractmethod 42 | def loop(self): 43 | pass 44 | 45 | def stop(self): 46 | self._keepAlive = False 47 | 48 | def setSocket(self, sock: socket.socket): 49 | self._sock = sock 50 | 51 | 52 | class ThreadSend(ThreadCommon): 53 | def __init__(self, sock: socket.socket, queue_send: queue.Queue): 54 | super().__init__(name='Socket Thread Send') 55 | self._sock = sock 56 | self._queue_send = queue_send 57 | 58 | def loop(self): 59 | try: 60 | if not isinstance(self._sock, socket.socket): 61 | return 62 | if not self._queue_send.empty(): 63 | data = self._queue_send.get() 64 | datalen = len(data) 65 | while datalen > 0: 66 | sendlen = self._sock.send(data) 67 | self.sig_send.emit(data[:sendlen]) 68 | data = data[sendlen:] 69 | datalen = len(data) 70 | except OSError as e: 71 | if e.args[0] == 10038: 72 | self.sig_exception.emit(f'OSError 10038 ({e})', True) 73 | except Exception as e: 74 | self.sig_exception.emit(f'Exception ({e})', True) 75 | 76 | 77 | class ThreadRecv(ThreadCommon): 78 | def __init__(self, sock: socket.socket, queue_recv: queue.Queue, bufsize: int = 4096): 79 | super().__init__(name='Socket Thread Recv') 80 | self._sock = sock 81 | self._queue_recv = queue_recv 82 | self._bufsize = bufsize 83 | self._loop_sleep_time = 0. 84 | 85 | def loop(self): 86 | try: 87 | if isinstance(self._sock, socket.socket): 88 | data = self._sock.recv(self._bufsize) 89 | if data is None or len(data) == 0: 90 | self.sig_exception.emit('Lost connection', True) 91 | self.stop() 92 | else: 93 | self.sig_recv.emit(data) 94 | self._queue_recv.put(data) 95 | except OSError as e: 96 | if e.args[0] == 10038: 97 | self.sig_exception.emit(str(e), False) 98 | self.stop() 99 | elif e.args[0] == 10022: 100 | self.sig_exception.emit(str(e), False) 101 | except Exception as e: 102 | self.sig_exception.emit(f'Exception ({e})', True) 103 | self.stop() 104 | 105 | 106 | class ThreadCheckRecvQueue(ThreadCommon): 107 | def __init__(self, queue_recv: queue.Queue): 108 | super().__init__(name='Socket Thread Check Recv Queue') 109 | self._queue_recv = queue_recv 110 | 111 | def loop(self): 112 | if not self._queue_recv.empty(): 113 | data = self._queue_recv.get() 114 | self.sig_recv.emit(data) 115 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/RS485/Socket/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from SocketTCPClient import TCPClient 9 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/RS485/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from RS485Comm import RS485Comm, RS485Config, RS485HwType 9 | from PacketParser import PacketParser, ParserType 10 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/ThinQ/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from ThinQAPI import ThinQ 9 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Threads/ThreadDiscovery.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import threading 5 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # Project/Include/Threads 6 | INCPATH = os.path.dirname(CURPATH) # Project/Include/ 7 | PROJPATH = os.path.dirname(INCPATH) # Proejct/ 8 | sys.path.extend([CURPATH, INCPATH, PROJPATH]) 9 | sys.path = list(set(sys.path)) 10 | del CURPATH, INCPATH, PROJPATH 11 | from Common import Callback, writeLog 12 | 13 | 14 | class ThreadDiscovery(threading.Thread): 15 | _keepAlive: bool = True 16 | 17 | def __init__(self, timeout: int): 18 | threading.Thread.__init__(self, name='Discovery Thread') 19 | self.timeout = timeout 20 | self.sig_terminated = Callback() 21 | 22 | def run(self): 23 | writeLog(f'Started (for {self.timeout} seconds)', self) 24 | tm_start = time.perf_counter() 25 | check_value = -1 26 | while self._keepAlive: 27 | elapsed = time.perf_counter() - tm_start 28 | remain = self.timeout - elapsed 29 | if remain <= 0: 30 | break 31 | if int(remain) != check_value: 32 | check_value = int(remain) 33 | if check_value <= 10: 34 | writeLog(f"Discovery will be terminated in {check_value} seconds!") 35 | time.sleep(0.1) 36 | writeLog('Terminated', self) 37 | self.sig_terminated.emit() 38 | 39 | def stop(self): 40 | self._keepAlive = False 41 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Threads/ThreadParseResultQueue.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import queue 5 | import threading 6 | from typing import Union 7 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 8 | INCPATH = os.path.dirname(CURPATH) 9 | sys.path.extend([CURPATH, INCPATH]) 10 | sys.path = list(set(sys.path)) 11 | del CURPATH, INCPATH 12 | from Define import * 13 | from Common import Callback, writeLog 14 | 15 | 16 | class ThreadParseResultQueue(threading.Thread): 17 | _keepAlive: bool = True 18 | 19 | def __init__(self, queue_: queue.Queue): 20 | threading.Thread.__init__(self, name='Parse Result Queue Thread') 21 | self._queue = queue_ 22 | self.sig_get = Callback(dict) 23 | self.sig_terminated = Callback() 24 | 25 | def run(self): 26 | writeLog('Started', self) 27 | while self._keepAlive: 28 | if not self._queue.empty(): 29 | result = self._queue.get() 30 | self.sig_get.emit(result) 31 | else: 32 | time.sleep(1e-3) 33 | writeLog('Terminated', self) 34 | self.sig_terminated.emit() 35 | 36 | def stop(self): 37 | self._keepAlive = False 38 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Threads/ThreadQueryState.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import threading 5 | from typing import List 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # Project/Include/Threads 7 | INCPATH = os.path.dirname(CURPATH) # Project/Include/ 8 | DEFPATH = os.path.join(INCPATH, 'Define') # Project/Include/Define 9 | sys.path.extend([CURPATH, INCPATH, DEFPATH]) 10 | sys.path = list(set(sys.path)) 11 | del CURPATH, INCPATH, DEFPATH 12 | from Common import Callback, writeLog 13 | 14 | 15 | class ThreadQueryState(threading.Thread): 16 | _keepAlive: bool = True 17 | 18 | def __init__( 19 | self, 20 | device_list: list, 21 | parser_mapping: dict, 22 | rs485_info_list: list, 23 | period: int, 24 | verbose: bool 25 | ): 26 | threading.Thread.__init__(self, name='Query State Thread') 27 | self.device_list = device_list 28 | self.parser_mapping = parser_mapping 29 | self.rs485_info_list = rs485_info_list 30 | self.period = period 31 | self.verbose = verbose 32 | self.sig_terminated = Callback() 33 | self.available = True 34 | 35 | def run(self): 36 | writeLog('Started', self) 37 | while self._keepAlive: 38 | for dev in self.device_list: 39 | if not self._keepAlive: 40 | break 41 | 42 | dev_type = dev.getType() 43 | index = self.parser_mapping.get(dev_type) 44 | info = self.rs485_info_list[index] 45 | packet_query = dev.makePacketQueryState() 46 | while not self.available: 47 | if not self._keepAlive: 48 | break 49 | time.sleep(1e-3) 50 | while info.parser.isRS485LineBusy(): 51 | if not self._keepAlive: 52 | break 53 | time.sleep(1e-3) 54 | if self.verbose: 55 | writeLog(f'sending query packet for {dev_type.name}/idx={dev.index}/room={dev.room_index}', self) 56 | info.parser.sendPacket(packet_query, self.verbose) 57 | 58 | time.sleep(self.period * 1e-3) 59 | writeLog('Terminated', self) 60 | self.sig_terminated.emit() 61 | 62 | def stop(self): 63 | self._keepAlive = False 64 | 65 | def setAvailable(self, value: bool): 66 | self.available = value 67 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Threads/ThreadTimer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import threading 5 | import traceback 6 | from typing import List 7 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # Project/Include/Threads 8 | INCPATH = os.path.dirname(CURPATH) # Project/Include/ 9 | PROJPATH = os.path.dirname(INCPATH) # Proejct/ 10 | RS485PATH = os.path.join(PROJPATH, 'RS485') # Project/RS485 11 | sys.path.extend([CURPATH, RS485PATH, INCPATH]) 12 | sys.path = list(set(sys.path)) 13 | del CURPATH, INCPATH, PROJPATH, RS485PATH 14 | from Common import Callback, writeLog 15 | from RS485 import RS485Comm 16 | 17 | 18 | class ThreadTimer(threading.Thread): 19 | _keepAlive: bool = True 20 | _publish_count: int = 0 21 | _home_initialized: bool = False 22 | 23 | def __init__( 24 | self, 25 | rs485_list: List[RS485Comm], 26 | publish_interval: int = 60, 27 | interval_ms: int = 2000, 28 | check_idle_sec: int = 30, 29 | reconnect_limit_sec: int = 60, 30 | verbose_regular_publish: dict = None 31 | ): 32 | threading.Thread.__init__(self, name='Timer Thread') 33 | self._rs485_list = rs485_list 34 | self._publish_interval = publish_interval # 단위: 초 35 | self._interval_ms = interval_ms # 단위: 밀리초 36 | self._check_idle_sec = check_idle_sec 37 | self._reconnect_limit_sec = reconnect_limit_sec 38 | self._verbose_regular_publish = verbose_regular_publish 39 | self.sig_terminated = Callback() 40 | self.sig_publish_regular = Callback() 41 | 42 | def run(self): 43 | first_publish: bool = False 44 | time.sleep(2) # wait for initialization 45 | writeLog('Started', self) 46 | tm_publish = time.perf_counter() 47 | tm_loop = time.perf_counter_ns() / 1e6 48 | while self._keepAlive: 49 | try: 50 | if not self._home_initialized: 51 | writeLog('Waiting for Initializing Home...', self) 52 | time.sleep(1) 53 | continue 54 | 55 | if time.perf_counter_ns() / 1e6 - tm_loop > self._interval_ms: 56 | tm_loop = time.perf_counter_ns() / 1e6 57 | rs485_all_connected: bool = sum([x.isConnected() for x in self._rs485_list]) == len(self._rs485_list) 58 | if rs485_all_connected and not first_publish: 59 | first_publish = True 60 | writeLog('RS485 are all opened >> Publish', self) 61 | self.sig_publish_regular.emit() 62 | tm_publish = time.perf_counter() 63 | self.check_rs485_status() 64 | 65 | if time.perf_counter() - tm_publish >= self._publish_interval: 66 | self.sig_publish_regular.emit() 67 | self._publish_count += 1 68 | if self._verbose_regular_publish is not None: 69 | enable = self._verbose_regular_publish.get('enable') 70 | interval = self._verbose_regular_publish.get('interval') 71 | if enable and self._publish_count % interval == 0: 72 | writeLog(f'Regular Publishing Device State MQTT (#: {self._publish_count}, interval: {self._publish_interval} sec)', self) 73 | tm_publish = time.perf_counter() 74 | 75 | time.sleep(100e-3) 76 | except Exception as e: 77 | writeLog(f'Exception::{e}', self) 78 | traceback.print_exc() 79 | writeLog('Terminated', self) 80 | self.sig_terminated.emit() 81 | 82 | def stop(self): 83 | self._keepAlive = False 84 | 85 | def setMqttPublishInterval(self, interval: int): 86 | self._publish_interval = interval 87 | writeLog(f'Set Regular MQTT Publish Interval as {self._publish_interval} sec', self) 88 | 89 | def set_home_initialized(self): 90 | self._home_initialized = True 91 | 92 | def check_rs485_status(self) -> bool: 93 | result = True 94 | for obj in self._rs485_list: 95 | if obj.isConnected(): 96 | delta = obj.time_after_last_recv() 97 | if delta > self._check_idle_sec: 98 | msg = 'Warning!! RS485 <{}> is not receiving for {:.1f} seconds'.format(obj.name, delta) 99 | writeLog(msg, self) 100 | if delta > self._reconnect_limit_sec: 101 | result = False 102 | # 일정 시간 이상 패킷을 받지 못하면 재접속 시도 103 | obj.reconnect() 104 | else: 105 | result = False 106 | writeLog('Warning!! RS485 <{}> is not connected'.format(obj.name), self) 107 | delta = obj.time_after_last_recv() 108 | if delta > self._reconnect_limit_sec: 109 | # 일정 시간 이상 패킷을 받지 못하면 재접속 시도 110 | writeLog('Try to reconnect RS485 <{}>'.format(obj.name), self) 111 | obj.reconnect() 112 | return result 113 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/Threads/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from ThreadCommandQueue import ThreadCommandQueue 9 | from ThreadTimer import ThreadTimer 10 | from ThreadParseResultQueue import ThreadParseResultQueue 11 | from ThreadEnergyMonitor import ThreadEnergyMonitor 12 | from ThreadDiscovery import ThreadDiscovery 13 | from ThreadQueryState import ThreadQueryState 14 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from Home import get_home, Home 9 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/Include/__oldcodes__/ParserLight.py: -------------------------------------------------------------------------------- 1 | from PacketParser import * 2 | import datetime 3 | 4 | 5 | class ParserLight(PacketParser): 6 | enable_store_packet_header_19: bool = False 7 | enable_store_packet_header_1E: bool = False 8 | enable_store_packet_header_1F: bool = False 9 | enable_store_packet_header_43: bool = False 10 | enable_store_packet_unknown: bool = True 11 | 12 | def interpretPacket(self, packet: bytearray): 13 | try: 14 | store: bool = True 15 | packet_info = {'packet': packet, 'timestamp': datetime.datetime.now()} 16 | if packet[3] == 0x19: # 조명 17 | self.handleLight(packet) 18 | packet_info['device'] = 'light' 19 | store = self.enable_store_packet_header_19 20 | elif packet[3] == 0x1E: # 현관 도어락 (?) 21 | writeLog(f'Doorlock Packet: {self.prettifyPacket(packet)}', self) 22 | packet_info['device'] = 'doorlock' 23 | store = self.enable_store_packet_header_1E 24 | elif packet[3] == 0x1F: # 아울렛 (콘센트) 25 | self.handleOutlet(packet) 26 | packet_info['device'] = 'outlet' 27 | store = self.enable_store_packet_header_1F 28 | elif packet[3] == 0x43: # 에너지 사용량 쿼리인듯? 29 | self.handleHEMS(packet) 30 | packet_info['device'] = 'hems' 31 | store = self.enable_store_packet_header_43 32 | else: 33 | writeLog(f'Unknown packet: {self.prettifyPacket(packet)}', self) 34 | packet_info['device'] = 'unknown' 35 | store = self.enable_store_packet_unknown 36 | if store: 37 | if len(self.packet_storage) > self.max_packet_store_cnt: 38 | self.packet_storage.pop(0) 39 | self.packet_storage.append(packet_info) 40 | except Exception as e: 41 | writeLog('interpretPacket::Exception::{} ({})'.format(e, packet), self) 42 | 43 | def handleLight(self, packet: bytearray): 44 | room_idx = packet[6] >> 4 45 | if packet[4] == 0x01: # 상태 쿼리 46 | pass 47 | elif packet[4] == 0x02: # 상태 변경 명령 48 | pass 49 | elif packet[4] == 0x04: # 각 방별 On/Off 50 | dev_idx = packet[6] & 0x0F 51 | if dev_idx == 0: # 일반 쿼리 (존재하는 모든 디바이스) 52 | light_count = len(packet) - 10 53 | for idx in range(light_count): 54 | state = 0 if packet[8 + idx] == 0x02 else 1 55 | result = { 56 | 'device': DeviceType.LIGHT, 57 | 'index': idx, 58 | 'room_index': room_idx, 59 | 'state': state 60 | } 61 | self.sig_parse_result.emit(result) 62 | else: # 상태 변경 명령 직후 응답 63 | state = 0 if packet[8] == 0x02 else 1 64 | result = { 65 | 'device': DeviceType.LIGHT, 66 | 'index': dev_idx - 1, 67 | 'room_index': room_idx, 68 | 'state': state 69 | } 70 | self.sig_parse_result.emit(result) 71 | 72 | def handleOutlet(self, packet: bytearray): 73 | room_idx = packet[6] >> 4 74 | if packet[4] == 0x01: # 상태 쿼리 75 | pass 76 | elif packet[4] == 0x02: # 상태 변경 명령 77 | pass 78 | elif packet[4] == 0x04: # 각 방별 상태 (On/Off) 79 | dev_idx = packet[6] & 0x0F 80 | if dev_idx == 0: # 일반 쿼리 (모든 디바이스) 81 | outlet_count = (len(packet) - 10) // 9 82 | for idx in range(outlet_count): 83 | # XX YY -- -- -- -- -- -- ZZ 84 | # XX: 상위 4비트 = 공간 인덱스, 하위 4비트는 디바이스 인덱스 85 | # YY: 02 = OFF, 01 = ON 86 | # ZZ: 02 = 대기전력 차단 수동, 01 = 대기전력 차단 자동 87 | # 중간에 있는 패킷들은 전력량계 데이터같은데, 파싱 위한 레퍼런스가 없음 88 | dev_packet = packet[8 + idx * 9: 8 + (idx + 1) * 9] 89 | state = 0 if dev_packet[1] == 0x02 else 1 90 | result = { 91 | 'device': DeviceType.OUTLET, 92 | 'index': idx, 93 | 'room_index': room_idx, 94 | 'state': state 95 | } 96 | self.sig_parse_result.emit(result) 97 | else: # 상태 변경 명령 직후 응답 98 | state = 0 if packet[8] == 0x02 else 1 99 | result = { 100 | 'device': DeviceType.OUTLET, 101 | 'index': dev_idx - 1, 102 | 'room_index': room_idx, 103 | 'state': state 104 | } 105 | self.sig_parse_result.emit(result) 106 | 107 | def handleHEMS(self, packet: bytearray): 108 | if packet[4] == 0x01: # 상태 쿼리 109 | pass 110 | else: 111 | writeLog(f'Unknown packet (HEMS): {self.prettifyPacket(packet)}', self) 112 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/activate.sh: -------------------------------------------------------------------------------- 1 | if [ -n "${BASH_SOURCE-}" ]; then 2 | script_path="${BASH_SOURCE}" 3 | elif [ -n "${ZSH_VERSION-}" ]; then 4 | script_path="${(%):-%x}" 5 | else 6 | script_path=$0 7 | fi 8 | script_dir=$(dirname $(realpath $script_path)) 9 | 10 | PY_VENV_PATH=${script_dir}/venv 11 | if [[ ! -d "$PY_VENV_PATH" ]]; then 12 | source ${script_dir}/bootstrap.sh 13 | fi 14 | source ${PY_VENV_PATH}/bin/activate 15 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/app.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | from Include import * 3 | from web import create_webapp, get_app_config 4 | from werkzeug.debug import DebuggedApplication 5 | 6 | import json 7 | import argparse 8 | parser = argparse.ArgumentParser(description='Home Network Application Arguments') 9 | parser.add_argument( 10 | '--config_file_path', 11 | help='absolute path of configuration file path') 12 | parser.add_argument( 13 | '--mqtt_broker', 14 | help='MQTT broker configuration (json formatted string)') 15 | parser.add_argument( 16 | '--rs485', 17 | help='RS485 port(s) configuration (json formatted string)') 18 | parser.add_argument( 19 | '--discovery', 20 | help='Device discovery configuration (json formatted string)') 21 | parser.add_argument( 22 | '--parser_mapping', 23 | help='RS485 parser mapping configuration (json formatted string)') 24 | parser.add_argument( 25 | '--periodic_query_state', 26 | help='Periodic query state configuration (json formatted string)') 27 | parser.add_argument( 28 | '--subphone', 29 | help="Kitchen subphone configuration (json formatted string)") 30 | parser.add_argument( 31 | '--etc', 32 | help='Other optional configuration (json formatted string)') 33 | args = parser.parse_args() 34 | 35 | app_config = get_app_config(args.config_file_path) 36 | app = create_webapp() 37 | app.app_context().push() 38 | app.wsgi_app = DebuggedApplication(app.wsgi_app, app_config.LOG) 39 | app.debug = app_config.LOG 40 | 41 | try: 42 | app_config.set_config_mqtt_broker(json.loads(args.mqtt_broker)) 43 | except Exception as e: 44 | print(f'no argument ({e})') 45 | try: 46 | app_config.set_config_rs485(json.loads(args.rs485)) 47 | except Exception as e: 48 | print(f'no argument ({e})') 49 | try: 50 | app_config.set_config_discovery(json.loads(args.discovery)) 51 | except Exception as e: 52 | print(f'no argument ({e})') 53 | try: 54 | app_config.set_config_parser_mapping(json.loads(args.parser_mapping)) 55 | except Exception as e: 56 | print(f'no argument ({e})') 57 | try: 58 | app_config.set_config_periodic_query_state(json.loads(args.periodic_query_state)) 59 | except Exception as e: 60 | print(f'no argument ({e})') 61 | try: 62 | app_config.set_config_subphone(json.loads(args.subphone)) 63 | except Exception as e: 64 | print(f'no argument ({e})') 65 | try: 66 | app_config.set_config_etc(json.loads(args.etc)) 67 | except Exception as e: 68 | print(f'no argument ({e})') 69 | 70 | home: Home = get_home('Hillstate-Gwanggyosan', args.config_file_path) 71 | home.initRS485Connection() 72 | 73 | 74 | def onExitApp(): 75 | print("Web server is closing...") 76 | home.release() 77 | 78 | 79 | atexit.register(onExitApp) 80 | 81 | if __name__ == '__main__': 82 | app.run(host=app_config.HOST, port=app_config.PORT, debug=app_config.LOG) 83 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/app_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.7", 3 | "date": "2025.05.16", 4 | "author": { 5 | "name": "yogyui", 6 | "email": "lee2002w@gmail.com", 7 | "url": "https://yogyui.tistory.com" 8 | } 9 | } -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/bootstrap.sh: -------------------------------------------------------------------------------- 1 | if [ -n "${BASH_SOURCE-}" ]; then 2 | script_path="${BASH_SOURCE}" 3 | elif [ -n "${ZSH_VERSION-}" ]; then 4 | script_path="${(%):-%x}" 5 | else 6 | script_path=$0 7 | fi 8 | script_dir=$(dirname $(realpath $script_path)) 9 | 10 | # create python virtual environment 11 | PY_VENV_PATH=${script_dir}/venv 12 | python3 -m venv ${PY_VENV_PATH} 13 | 14 | # activate virtual environment 15 | source ${PY_VENV_PATH}/bin/activate 16 | 17 | # install python packages 18 | pip install --upgrade pip 19 | pip install --no-cache-dir -r ${script_dir}/requirements.txt 20 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/clean.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 5 | for (p, d, files) in os.walk(CURPATH): 6 | if '__pycache__' in p: 7 | shutil.rmtree(p, ignore_errors=False) -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/config_default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.0.0.0 5 | 7929 6 | 0 7 | 8 | 9 | 30 10 | 11 | 12 | 13 | port1 14 | 0 15 | 0 16 | 0 17 | 0 18 | 19 | /dev/ttyUSB0 20 | 9600 21 | 8 22 | N 23 | 1 24 | 25 | 26 | 127.0.0.1 27 | 8899 28 | 29 | 1 30 | 64 31 | 3 32 | 33 | 100 34 | 50 35 | 36 | 37 | 38 | 39 | 127.0.0.1 40 | 1883 41 | username 42 | password 43 | yogyui_hyundai_ht 44 | 45 | 0 46 | 47 | /config/ssl/fullchain.pem 48 | /config/ssl/privkey.pem 49 | 50 | 0 51 | 52 | 1 53 | 100 54 | 55 | 56 | 57 | 1 58 | homeassistant 59 | 60 | 61 | 62 | 63 | 64 | 0 65 | 0 66 | 0 67 | 0 68 | 0 69 | 0 70 | 0 71 | 0 72 | 0 73 | 0 74 | 0 75 | 0 76 | 77 | 1 78 | 79 | 0 80 | 60 81 | 1 82 | 83 | 84 | 0 85 | 100 86 | 0 87 | 88 | 0 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask_wtf 3 | flask_moment 4 | flask_httpauth 5 | flask_bootstrap 6 | werkzeug 7 | paho-mqtt==1.6.1 8 | pyserial 9 | requests 10 | beautifulsoup4 11 | regex 12 | pyOpenSSL 13 | psutil 14 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/run.sh: -------------------------------------------------------------------------------- 1 | #! /usr/sh 2 | curpath=$(dirname $(realpath $BASH_SOURCE)) 3 | /usr/local/bin/uwsgi ${curpath}/uwsgi.ini -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/summary.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/uwsgi.ini.template: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | chdir = /home/pi/Project/HomeNetwork/Hillstate-Gwanggyosan 3 | 4 | http = 0.0.0.0:7929 5 | http = 127.0.0.1:7930 6 | socket = ./uwsgi.sock 7 | chmod-socket = 666 8 | 9 | wsgi-file = ./app.py 10 | callable = app 11 | ; daemonize = ./uwsgi.log 12 | uid = hillstate_gwanggyosan_server 13 | pidfile = ./uwsgi.pid 14 | 15 | ; master = true 16 | processes = 1 17 | threads = 1 18 | enable-threads = true 19 | vacuum = true 20 | disable-logging = false 21 | die-on-term = true 22 | 23 | reload-mercy = 5 24 | worker-reload-mercy = 5 25 | single-interpreter = false 26 | lazy-apps = true 27 | harakiri-verbose = false 28 | 29 | stats = 127.0.0.1:7931 30 | ignore-write-errors = true 31 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | from flask import Flask 8 | from flask_bootstrap import Bootstrap 9 | from flask_moment import Moment 10 | from config import Config, get_app_config 11 | 12 | bootstrap = Bootstrap() 13 | moment = Moment() 14 | 15 | 16 | def create_webapp(): 17 | app = Flask(__name__) 18 | 19 | app_config: Config = get_app_config() 20 | app.config.from_object(app_config) 21 | 22 | app_config.init_app(app) 23 | bootstrap.init_app(app) 24 | moment.init_app(app) 25 | 26 | from .main import main as blueprint_main 27 | app.register_blueprint(blueprint_main) 28 | 29 | from .api import api as blueprint_api 30 | app.register_blueprint(blueprint_api, url_prefix='/api') 31 | 32 | print(f"Flask App is created ({app})") 33 | 34 | return app 35 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | api = Blueprint('api', __name__) 4 | 5 | from . import light_ctrl 6 | from . import outlet_ctrl 7 | from . import elevator_ctrl 8 | from . import packet_logger 9 | from . import hems 10 | from . import timer 11 | from . import system 12 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/api/elevator_ctrl.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from flask import render_template, jsonify, request 3 | from datetime import datetime 4 | import os 5 | import sys 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/web/api/ 7 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/ 8 | INCPATH = os.path.join(PROJPATH, 'Include') # {$PROJECT}/Include/ 9 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 10 | sys.path = list(set(sys.path)) 11 | del CURPATH, PROJPATH, INCPATH 12 | from Include import get_home 13 | from Common import DeviceType 14 | 15 | 16 | def get_state_string() -> str: 17 | home = get_home() 18 | elevator = home.findDevice(DeviceType.ELEVATOR, 0, 0) 19 | if elevator is None: 20 | return 'NOT DEFINED' 21 | 22 | dev_info_list = elevator.dev_info_list 23 | if len(dev_info_list) == 0: 24 | state_str = 'IDLE' 25 | else: 26 | state_str = '' 27 | for info in dev_info_list: 28 | state_str += f'{info.index}호기: ' 29 | img_path_list = [] 30 | try: 31 | if info.floor[0] == 'B': 32 | img_path_list.append(f'/static/seven_seg_b.png') 33 | else: 34 | img_path_list.append(f'/static/seven_seg_{int(info.floor[0])}.png') 35 | except Exception: 36 | img_path_list.append('/static/seven_seg_null.png') 37 | try: 38 | img_path_list.append(f'/static/seven_seg_{int(info.floor[1])}.png') 39 | except Exception: 40 | img_path_list.append('/static/seven_seg_null.png') 41 | 42 | img_width, img_height = 60, 80 43 | for img_path in img_path_list: 44 | state_str += f'' 45 | 46 | state_str += ' ' 47 | 48 | if elevator.state == 0: # idle 49 | pass 50 | elif elevator.state == 1: # arrived 51 | state_str += f'' 52 | else: 53 | if info.direction.value == 5: # moving up 54 | state_str += f'' 55 | elif info.direction.value == 6: # moving down 56 | state_str += f'' 57 | state_str += "
" 58 | return state_str 59 | 60 | 61 | @api.route('/elevator_ctrl', methods=['GET', 'POST']) 62 | def elevator_ctrl(): 63 | home = get_home() 64 | now = datetime.now() 65 | 66 | req = request.get_data().decode(encoding='utf-8') 67 | if 'command_call_down' in req: 68 | home.onMqttCommandElevator('', {'state': 6}) 69 | elif 'command_call_up' in req: 70 | home.onMqttCommandElevator('', {'state': 5}) 71 | 72 | return render_template( 73 | "elevator_ctrl.html", 74 | current_time=now.strftime('%Y-%m-%d %H:%M:%S'), 75 | state=get_state_string() 76 | ) 77 | 78 | 79 | @api.route('/elevator_ctrl/update', methods=['GET', 'POST']) 80 | def elevator_update(): 81 | home = get_home() 82 | now = datetime.now() 83 | 84 | return jsonify({ 85 | 'current_time': now.strftime('%Y-%m-%d %H:%M:%S'), 86 | 'state': get_state_string() 87 | }) 88 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/api/light_ctrl.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from flask import render_template, jsonify, request 3 | from datetime import datetime 4 | import os 5 | import sys 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/web/api/ 7 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/ 8 | INCPATH = os.path.join(PROJPATH, 'Include') # {$PROJECT}/Include/ 9 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 10 | sys.path = list(set(sys.path)) 11 | del CURPATH, PROJPATH, INCPATH 12 | from Include import get_home 13 | 14 | @api.route('/light_ctrl', methods=['GET', 'POST']) 15 | def light_ctrl(): 16 | home = get_home() 17 | 18 | return render_template('light_ctrl.html') -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/api/outlet_ctrl.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from flask import render_template, jsonify, request 3 | from datetime import datetime 4 | import os 5 | import sys 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/web/api/ 7 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/ 8 | INCPATH = os.path.join(PROJPATH, 'Include') # {$PROJECT}/Include/ 9 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 10 | sys.path = list(set(sys.path)) 11 | del CURPATH, PROJPATH, INCPATH 12 | from Include import get_home 13 | 14 | @api.route('/outlet_ctrl', methods=['GET', 'POST']) 15 | def outlet_ctrl(): 16 | home = get_home() 17 | 18 | return render_template('outlet_ctrl.html') -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/api/system.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from flask import render_template 3 | import os 4 | import sys 5 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/web/api/ 6 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/ 7 | INCPATH = os.path.join(PROJPATH, 'Include') # {$PROJECT}/Include/ 8 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 9 | sys.path = list(set(sys.path)) 10 | del CURPATH, PROJPATH, INCPATH 11 | from Include import get_home 12 | 13 | 14 | @api.route('/restart', methods=['GET', 'POST']) 15 | def restart(): 16 | home = get_home() 17 | home.restart() 18 | return render_template('index.html') -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/api/timer.py: -------------------------------------------------------------------------------- 1 | # TODO: rs485 및 파서를 리스트로 객체화하게 바꾸었다 2 | # 코드 구조를 전면적으로 개편해야 한다 3 | from . import api 4 | from flask import render_template, jsonify, request 5 | from wtforms import Form, IntegerField, SelectField, validators 6 | from datetime import datetime 7 | import os 8 | import sys 9 | import json 10 | import http 11 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/web/api/ 12 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/ 13 | INCPATH = os.path.join(PROJPATH, 'Include') # {$PROJECT}/Include/ 14 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 15 | sys.path = list(set(sys.path)) 16 | del CURPATH, PROJPATH, INCPATH 17 | from Include import get_home 18 | from Common import DeviceType 19 | 20 | 21 | class TesfForm(Form): 22 | ontime = IntegerField('on_time', [validators.Length(min=0)]) 23 | offtime = IntegerField('off_time', [validators.Length(min=0)]) 24 | repeat = SelectField('repeat') 25 | 26 | 27 | def get_timer_info() -> list: 28 | home = get_home() 29 | info = [] 30 | for i in range(4): 31 | d = dict() 32 | thermostat = home.findDevice(DeviceType.THERMOSTAT, 0, i + 1) 33 | if thermostat is not None: 34 | d['has_thermostat'] = True 35 | d['thermostat_timer_running'] = int(thermostat.isTimerOnOffRunning()) 36 | params = thermostat.timer_onoff_params 37 | d['thermostat_on_time'] = params.get('on_time') 38 | d['thermostat_off_time'] = params.get('off_time') 39 | d['thermostat_repeat'] = int(params.get('repeat')) 40 | d['thermostat_off_when_terminate'] = int(params.get('off_when_terminate')) 41 | else: 42 | d['has_thermostat'] = False 43 | d['thermostat_timer_running'] = 0 44 | d['thermostat_on_time'] = 0 45 | d['thermostat_off_time'] = 0 46 | d['thermostat_repeat'] = 0 47 | d['thermostat_off_when_terminate'] = 0 48 | 49 | airconditioner = home.findDevice(DeviceType.AIRCONDITIONER, 0, i + 1) 50 | if airconditioner is not None: 51 | d['has_airconditioner'] = True 52 | d['airconditioner_timer_running'] = int(airconditioner.isTimerOnOffRunning()) 53 | params = airconditioner.timer_onoff_params 54 | d['airconditioner_on_time'] = params.get('on_time') 55 | d['airconditioner_off_time'] = params.get('off_time') 56 | d['airconditioner_repeat'] = int(params.get('repeat')) 57 | d['airconditioner_off_when_terminate'] = int(params.get('off_when_terminate')) 58 | else: 59 | d['has_airconditioner'] = False 60 | d['airconditioner_timer_running'] = 0 61 | d['airconditioner_on_time'] = 0 62 | d['airconditioner_off_time'] = 0 63 | d['airconditioner_repeat'] = 0 64 | d['airconditioner_off_when_terminate'] = 0 65 | 66 | info.append(d) 67 | return info 68 | 69 | 70 | @api.route('/timer', methods=['GET', 'POST']) 71 | def timer(): 72 | timer_info = get_timer_info() 73 | return render_template( 74 | 'timer.html', 75 | room1=timer_info[0], 76 | room2=timer_info[1], 77 | room3=timer_info[2], 78 | room4=timer_info[3] 79 | ) 80 | 81 | 82 | @api.route('/timer/update', methods=['GET', 'POST']) 83 | def timer_update(): 84 | timer_info = get_timer_info() 85 | return render_template( 86 | 'timer.html', 87 | room1=timer_info[0], 88 | room2=timer_info[1], 89 | room3=timer_info[2], 90 | room4=timer_info[3] 91 | ) 92 | 93 | 94 | @api.route('/timer/set//', methods=['POST']) 95 | def timer_activate(room_idx: str, dev_type: str): 96 | home = get_home() 97 | try: 98 | data = json.loads(request.get_data()) 99 | room_idx = int(room_idx) - 1 100 | 101 | if dev_type == 'cool': 102 | dev = home.findDevice(DeviceType.AIRCONDITIONER, 0, room_idx) 103 | elif dev_type == 'heat': 104 | dev = home.findDevice(DeviceType.THERMOSTAT, 0, room_idx) 105 | else: 106 | return '', http.HTTPStatus.NO_CONTENT 107 | 108 | if dev is not None: 109 | if 'activate' in data.keys(): 110 | value = int(data.get('activate')) 111 | dev.startTimerOnOff() if value else dev.stopTimerOnOff() 112 | elif 'on_time' in data.keys() and 'off_time' in data.keys() and 'repeat' in data.keys(): 113 | on_time = int(data.get('on_time')) 114 | off_time = int(data.get('off_time')) 115 | repeat = bool(int(data.get('repeat'))) 116 | dev.setTimerOnOffParams(on_time, off_time, repeat) 117 | else: 118 | return '', http.HTTPStatus.NO_CONTENT 119 | except Exception as e: 120 | print(e) 121 | return '', http.HTTPStatus.NO_CONTENT 122 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | from . import views, errors 6 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/main/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | from . import main 3 | 4 | 5 | @main.app_errorhandler(403) 6 | def forbidden(_): 7 | if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: 8 | response = jsonify({'error': 'forbidden'}) 9 | response.status_code = 403 10 | return response 11 | return render_template('403.html'), 403 12 | 13 | 14 | @main.app_errorhandler(404) 15 | def page_not_found(_): 16 | if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: 17 | response = jsonify({'error': 'not found'}) 18 | response.status_code = 404 19 | return response 20 | return render_template('404.html'), 404 21 | 22 | 23 | @main.app_errorhandler(500) 24 | def internal_server_error(_): 25 | if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: 26 | response = jsonify({'error': 'internal server error'}) 27 | response.status_code = 500 28 | return response 29 | return render_template('500.html'), 500 30 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/main/views.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | from flask import render_template 3 | 4 | 5 | @main.route('/', methods=['GET', 'POST']) 6 | def index(): 7 | return render_template('index.html') 8 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/arrow_down.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/arrow_up.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/destination.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_0.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_1.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_2.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_3.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_4.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_5.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_6.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_7.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_8.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_9.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_a.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_b.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_c.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_d.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_e.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_f.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/seven_seg_null.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/static/seven_seg_null.png -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/static/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | margin-bottom: 40px; 8 | } 9 | 10 | .footer { 11 | position: absolute; 12 | bottom: 0; 13 | width: 100%; 14 | height: 40px; 15 | background-color: #f5f5f5; 16 | text-align: center; 17 | padding: 10px; 18 | } 19 | 20 | .navbar-content { 21 | background-color: #68B950; 22 | } 23 | 24 | .navbar-brand { 25 | flex: 0 0 auto; 26 | margin: 0; 27 | padding: 0; 28 | height: 100%; 29 | } 30 | 31 | .navbar-inverse { 32 | background-color: #000000; 33 | } 34 | 35 | .navbar-inverse .navbar-brand { 36 | color: #ffffff; 37 | } 38 | 39 | .toggle { 40 | --width: 40px; 41 | --height: calc(var(--width) / 2); 42 | --border-radius: calc(var(--height) / 2); 43 | 44 | display: inline-block; 45 | cursor: pointer; 46 | } 47 | 48 | .toggle__input { 49 | display: none; 50 | } 51 | 52 | .toggle__fill { 53 | position: relative; 54 | width: var(--width); 55 | height: var(--height); 56 | border-radius: var(--border-radius); 57 | background: #dddddd; 58 | transition: background 0.2s; 59 | } 60 | 61 | .toggle__fill::after { 62 | content: ""; 63 | position: absolute; 64 | top: 0; 65 | left: 0; 66 | height: var(--height); 67 | width: var(--height); 68 | background: #ffffff; 69 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.25); 70 | border-radius: var(--border-radius); 71 | transition: transform 0.2s; 72 | } 73 | 74 | .toggle__input:checked ~ .toggle__fill { 75 | background: #009578; 76 | } 77 | 78 | .toggle__input:checked ~ .toggle__fill::after { 79 | transform: translateX(var(--height)); 80 | } 81 | 82 | input[type="number"]::-webkit-inner-spin-button, 83 | input[type="number"]::-webkit-outer-spin-button { 84 | -webkit-appearance: inner-spin-button !important; 85 | opacity: 1 !important; 86 | width: 80px; 87 | position: absolute; 88 | top: 0; 89 | right: 0; 90 | height: 100%; 91 | background-color: blue !important; 92 | border: 2.5px solid red !important; 93 | } 94 | 95 | input[type=number] { 96 | position: relative; 97 | padding: 5px; 98 | width: 80px; 99 | padding-right: 25px; 100 | height: 30px; 101 | text-align: center; 102 | font-family: sans-serif; 103 | font-size: 2rem; 104 | border: 1px solid #9c9c9c !important; 105 | border-radius: 0.2em; 106 | } 107 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | 3 | {% block title %}YOGYUI 힐스테이트 광교산{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block navbar %} 14 | 51 | {% endblock %} 52 | 53 | {% block content %} 54 |
55 | {% for message in get_flashed_messages() %} 56 |
57 | 58 | {{ message }} 59 |
60 | {% endfor %} 61 | 62 | {% block page_content %} 63 | {% endblock %} 64 |
65 | 66 |
67 |
68 |

Designed by YOGYUI

69 |
70 |
71 | {% endblock %} 72 | 73 | {% block scripts %} 74 | {{ super() }} 75 | {{ moment.include_moment() }} 76 | {% endblock %} -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/elevator_ctrl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block page_content %} 5 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 |

[ Current Time ]

16 |

17 | {% if current_time %} 18 | {{ current_time }} 19 | {% endif %} 20 |

21 | 22 |

[ State ]

23 |

24 | {% if state %} 25 | {{ state }} 26 | {% endif %} 27 |

28 |
29 | 30 | {% endblock %} 31 | 32 | {% block scripts %} 33 | {{ super() }} 34 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block page_content %} 5 | 13 |
14 | 25 |

조명

26 |

콘센트

27 |

엘리베이터

28 |

패킷

29 |

HEMS

30 |

타이머

31 |
32 | {% endblock %} 33 | 34 | {% block scripts %} 35 | {{ super() }} 36 | {% endblock %} -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/light_ctrl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block page_content %} 5 | 8 |
9 | 10 |
11 | 12 | {% endblock %} 13 | 14 | {% block scripts %} 15 | {{ super() }} 16 | 19 | {% endblock %} -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/outlet_ctrl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block page_content %} 5 | 8 |
9 | 10 |
11 | 12 | {% endblock %} 13 | 14 | {% block scripts %} 15 | {{ super() }} 16 | 19 | {% endblock %} -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/render.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |

3 | {{ field.label }} 4 | {{ field(**kwargs)|safe }} 5 | {% if field.errors %} 6 |

    7 | {% for error in field.errors %} 8 |
  • {{ error }}
  • 9 | {% endfor %} 10 |
11 | {% endif %} 12 |

13 | {% endmacro %} 14 | -------------------------------------------------------------------------------- /Hillstate-Gwanggyosan/web/templates/system.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/Hillstate-Gwanggyosan/web/templates/system.html -------------------------------------------------------------------------------- /IPark-Gwanggyo/Arduino/BOM.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/IPark-Gwanggyo/Arduino/BOM.xlsx -------------------------------------------------------------------------------- /IPark-Gwanggyo/Arduino/README.md: -------------------------------------------------------------------------------- 1 | Summary 2 | ------------- 3 | Wi-Fi + MQTT to control Bestin livingroom wallpad illuminations.
4 | PCB is designed using Altium PCB Designer.
5 | You can see details in 6 | [this article](https://yogyui.tistory.com/entry/%EA%B4%91%EA%B5%90%EC%95%84%EC%9D%B4%ED%8C%8C%ED%81%AC%EA%B1%B0%EC%8B%A4-%EC%A1%B0%EB%AA%85-Apple-%ED%99%88%ED%82%B7-%EC%97%B0%EB%8F%99-1).
7 | 8 | Firmware 9 | ------------- 10 | You can download flash to ESP8266 using Arduino IDE.
11 | Source code is available in this repository ([link](https://github.com/YOGYUI/HomeNetwork/blob/main/IPark-Gwanggyo/Arduino/wallpad_livingroom_ctrl.ino)).
12 | 13 | 14 | Core IC 15 | ------------- 16 | * ESP8266-12E/F: WiFi Module 17 | * CP2102: USB-USART Convert (for flash download, debugging) 18 | * MCP4725: DAC, voltage output to wallpad control signal 19 | * TMUX1237: Multiplexer, switch output signal between wallpad touch signal and DAC output 20 | 21 | You can see other ICs, passive elements in 22 | [BOM.xlsx](https://github.com/YOGYUI/HomeNetwork/blob/main/IPark-Gwanggyo/Arduino/BOM.xlsx). 23 | 24 | Schematic Drawing 25 | ------------- 26 | ![schematic](./schematic.png) 27 | 28 | Assembly Drawing 29 | ------------- 30 | ![assembly](./assemblydrawing.png) -------------------------------------------------------------------------------- /IPark-Gwanggyo/Arduino/assemblydrawing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/IPark-Gwanggyo/Arduino/assemblydrawing.png -------------------------------------------------------------------------------- /IPark-Gwanggyo/Arduino/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/IPark-Gwanggyo/Arduino/schematic.png -------------------------------------------------------------------------------- /IPark-Gwanggyo/Arduino/wallpad_livingroom_ctrl.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | char publish_msg[256]; 7 | StaticJsonDocument<256> json_doc; 8 | 9 | const char* WIFI_SSID = "Your WiFi SSID"; 10 | const char* WIFI_PW = "Your WiFi Password"; 11 | WiFiClient wifi_client; 12 | 13 | const char* MQTT_BROKER_ADDR = "Your MQTT Broker(Mosquitto) IP Address"; 14 | const int MQTT_BROKER_PORT = 1234; // Your MQTT Broker(Mosquitto) Port 15 | const char* MQTT_ID = "Your MQTT Broker(Mosquitto) Auth ID"; 16 | const char* MQTT_PW = "Your MQTT Broker(Mosquitto) Auth Password"; 17 | PubSubClient mqtt_client(wifi_client); 18 | 19 | const int DAC_RESOLUTION = 12; 20 | const double DAC_VREG = 5.0; 21 | Adafruit_MCP4725 dac; 22 | 23 | const int MUX_SEL_PIN = 13; 24 | const int LIGHT1_STATE_PIN = 14; 25 | const int LIGHT2_STATE_PIN = 12; 26 | const int LED1_PIN = 9; 27 | const int LED2_PIN = 10; 28 | int last_state_light1 = -1; 29 | int last_state_light2 = -1; 30 | 31 | const int MONITOR_INTERVAL_MS = 250; 32 | long last_monitor_time = 0; 33 | 34 | enum MUXOUT { 35 | WALLPAD = 0, 36 | DACOUT = 1 37 | }; 38 | 39 | uint16_t convert_dac_value(double voltage) { 40 | return uint16_t( (pow(2, DAC_RESOLUTION) - 1) / DAC_VREG * voltage); 41 | } 42 | 43 | void setDacOutVoltage(double voltage) { 44 | uint16_t conv_val = convert_dac_value(voltage); 45 | Serial.printf("Set DAC Output Voltage: %f V\n", voltage); 46 | dac.setVoltage(conv_val, false); 47 | } 48 | 49 | void setMuxOut(MUXOUT value) { 50 | if (value == WALLPAD) { 51 | digitalWrite(MUX_SEL_PIN, LOW); 52 | Serial.println("MUX OUT >> WALLPAD"); 53 | } else if (value == DACOUT) { 54 | digitalWrite(MUX_SEL_PIN, HIGH); 55 | Serial.println("MUX OUT >> DAC OUT"); 56 | } 57 | } 58 | 59 | void changeLightState(int index) { 60 | if (index == 1) { 61 | setDacOutVoltage(4.0); 62 | } else if (index == 2) { 63 | setDacOutVoltage(3.0); 64 | } 65 | setMuxOut(DACOUT); 66 | delay(100); 67 | setMuxOut(WALLPAD); 68 | setDacOutVoltage(5.0); 69 | } 70 | 71 | void mqtt_callback(char* topic, byte* payload, unsigned int length) { 72 | Serial.print("Message arrived ["); 73 | Serial.print(topic); 74 | Serial.print("] "); 75 | for (int i = 0; i < length; i++) { 76 | Serial.print((char)payload[i]); 77 | } 78 | Serial.println(); 79 | 80 | if (!strcmp(topic, "home/ipark/livingroom/light/command/0")) { 81 | changeLightState(1); 82 | } else if(!strcmp(topic, "home/ipark/livingroom/light/command/1")) { 83 | changeLightState(2); 84 | } 85 | } 86 | 87 | void setup() { 88 | Serial.begin(115200); 89 | 90 | WiFi.begin(WIFI_SSID, WIFI_PW); 91 | Serial.print("\nWiFi Connecting"); 92 | while (WiFi.status() != WL_CONNECTED) { 93 | delay(500); 94 | Serial.print("."); 95 | } 96 | Serial.println(); 97 | 98 | mqtt_client.setServer(MQTT_BROKER_ADDR, MQTT_BROKER_PORT); 99 | mqtt_client.setCallback(mqtt_callback); 100 | 101 | Serial.print("Connected, IP address: "); 102 | Serial.println(WiFi.localIP()); 103 | Serial.printf("MAC address = %s\n", WiFi.softAPmacAddress().c_str()); 104 | 105 | pinMode(MUX_SEL_PIN, OUTPUT); 106 | pinMode(LIGHT1_STATE_PIN, INPUT); 107 | pinMode(LIGHT2_STATE_PIN, INPUT); 108 | 109 | dac.begin(0x60); 110 | setDacOutVoltage(5.0); 111 | setMuxOut(WALLPAD); 112 | 113 | readLightStateAll(); 114 | last_monitor_time = millis(); 115 | } 116 | 117 | void establish_mqtt_connection() { 118 | if (mqtt_client.connected()) 119 | return; 120 | while (!mqtt_client.connected()) { 121 | Serial.println("Try to connect MQTT Broker"); 122 | if (mqtt_client.connect("ESP8266_WALLPAD_LIVINGROOM", MQTT_ID, MQTT_PW)) { 123 | Serial.println("Connected"); 124 | mqtt_client.subscribe("home/ipark/livingroom/light/command/0"); 125 | mqtt_client.subscribe("home/ipark/livingroom/light/command/1"); 126 | } else { 127 | Serial.print("failed, rc="); 128 | Serial.print(mqtt_client.state()); 129 | delay(2000); 130 | } 131 | } 132 | } 133 | 134 | void readLightState(int index) { 135 | int state = -1; 136 | if (index == 1) { 137 | state = digitalRead(LIGHT1_STATE_PIN); 138 | if (state != last_state_light1) { 139 | last_state_light1 = state; 140 | publishLightState(1); 141 | } 142 | } else if (index == 2) { 143 | state = digitalRead(LIGHT2_STATE_PIN); 144 | if (state != last_state_light2) { 145 | last_state_light2 = state; 146 | publishLightState(2); 147 | } 148 | } 149 | } 150 | 151 | void readLightStateAll() { 152 | readLightState(1); 153 | readLightState(2); 154 | } 155 | 156 | void publishLightState(int index) { 157 | size_t n = 0; 158 | 159 | if (index == 1) { 160 | json_doc["state"] = last_state_light1; 161 | n = serializeJson(json_doc, publish_msg); 162 | mqtt_client.publish( 163 | "home/ipark/livingroom/light/state/0", 164 | publish_msg, 165 | n); 166 | Serial.print("Published (home/ipark/livingroom/light/state/0): "); 167 | Serial.println(publish_msg); 168 | } else if (index == 2) { 169 | json_doc["state"] = last_state_light2; 170 | n = serializeJson(json_doc, publish_msg); 171 | mqtt_client.publish( 172 | "home/ipark/livingroom/light/state/1", 173 | publish_msg, 174 | n); 175 | Serial.print("Published (home/ipark/livingroom/light/state/1): "); 176 | Serial.println(publish_msg); 177 | } 178 | } 179 | 180 | void loop() { 181 | establish_mqtt_connection(); 182 | mqtt_client.loop(); 183 | 184 | long current = millis(); 185 | if (current - last_monitor_time >= MONITOR_INTERVAL_MS) { 186 | last_monitor_time = current; 187 | readLightStateAll(); 188 | } 189 | } -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Common.py: -------------------------------------------------------------------------------- 1 | 2 | import datetime 3 | import threading 4 | from functools import reduce 5 | 6 | 7 | def checkAgrumentType(obj, arg): 8 | if type(obj) == arg: 9 | return True 10 | if arg == object: 11 | return True 12 | if arg in obj.__class__.__bases__: 13 | return True 14 | return False 15 | 16 | 17 | class Callback(object): 18 | _args = None 19 | 20 | def __init__(self, *args): 21 | self._args = args 22 | self._callbacks = list() 23 | 24 | def connect(self, callback): 25 | if callback not in self._callbacks: 26 | self._callbacks.append(callback) 27 | 28 | def emit(self, *args): 29 | if len(args) != len(self._args): 30 | raise Exception('Callback::Argument Length Mismatch') 31 | arglen = len(args) 32 | if arglen > 0: 33 | validTypes = [checkAgrumentType(args[i], self._args[i]) for i in range(arglen)] 34 | if sum(validTypes) != arglen: 35 | raise Exception('Callback::Argument Type Mismatch (Definition: {}, Call: {})'.format(self._args, args)) 36 | for callback in self._callbacks: 37 | callback(*args) 38 | 39 | 40 | def timestampToString(timestamp: datetime.datetime): 41 | h = timestamp.hour 42 | m = timestamp.minute 43 | s = timestamp.second 44 | us = timestamp.microsecond 45 | return '%02d:%02d:%02d.%06d' % (h, m, s, us) 46 | 47 | 48 | def getCurTimeStr(): 49 | return '<%s>' % timestampToString(datetime.datetime.now()) 50 | 51 | 52 | def writeLog(strMsg: str, obj: object = None): 53 | strTime = getCurTimeStr() 54 | if obj is not None: 55 | if isinstance(obj, threading.Thread): 56 | if obj.ident is not None: 57 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, obj.ident) 58 | else: 59 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 60 | else: 61 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 62 | else: 63 | strObj = '' 64 | 65 | msg = strTime + strObj + ' ' + strMsg 66 | print(msg) 67 | 68 | 69 | def calculate_bestin_checksum(packet: bytearray) -> int: 70 | # special thanks to laz- 71 | # https://gist.github.com/laz-/a507af756e13e64ed3aaceb236b5ad49 72 | try: 73 | return reduce(lambda x, y: ((x ^ y) + 1) & 0xFF, packet, 3) 74 | except Exception as e: 75 | writeLog(f'Calc Bestin Checksum Error ({e})') 76 | return 0 77 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Device.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import List 3 | import paho.mqtt.client as mqtt 4 | from abc import ABCMeta, abstractmethod 5 | from Common import writeLog, calculate_bestin_checksum 6 | 7 | 8 | class Device: 9 | __metaclass__ = ABCMeta 10 | 11 | name: str = 'Device' 12 | room_index: int = 0 13 | init: bool = False 14 | state: int = 0 # mostly, 0 is OFF and 1 is ON 15 | state_prev: int = 0 16 | """ 17 | packet_set_state_on: str = '' 18 | packet_set_state_off: str = '' 19 | packet_get_state: str = '' 20 | """ 21 | mqtt_client: mqtt.Client = None 22 | mqtt_publish_topic: str = '' 23 | mqtt_subscribe_topics: List[str] 24 | 25 | last_published_time: float = time.perf_counter() 26 | publish_interval_sec: float = 10. 27 | 28 | def __init__(self, name: str = 'Device', **kwargs): 29 | self.name = name 30 | if 'room_index' in kwargs.keys(): 31 | self.room_index = kwargs['room_index'] 32 | self.mqtt_client = kwargs.get('mqtt_client') 33 | self.mqtt_subscribe_topics = list() 34 | # writeLog('Device Created >> Name: {}, Room Index: {}'.format(self.name, self.room_index), self) 35 | writeLog('Device Created >> {}'.format(str(self)), self) 36 | 37 | @abstractmethod 38 | def publish_mqtt(self): 39 | pass 40 | 41 | def __repr__(self): 42 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 43 | repr_txt += f' Room Idx: {self.room_index}' 44 | repr_txt += '>' 45 | return repr_txt 46 | 47 | def make_packet_common(self, header: int, length: int, packet_type: int, timestamp: int = 0) -> bytearray: 48 | packet = bytearray([ 49 | 0x02, 50 | header & 0xFF, 51 | length & 0xFF, 52 | packet_type & 0xFF, 53 | timestamp & 0xFF 54 | ]) 55 | packet.extend(bytearray([0] * (length - 5))) 56 | return packet 57 | 58 | @abstractmethod 59 | def make_packet_set_state(self, target: int, timestamp: int = 0) -> bytearray: 60 | pass 61 | 62 | @abstractmethod 63 | def make_packet_query_state(self, timestamp: int = 0) -> bytearray: 64 | pass 65 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Doorlock.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import threading 4 | from typing import Union 5 | from Device import Device 6 | import RPi.GPIO as GPIO 7 | from Common import Callback, writeLog 8 | 9 | 10 | class ThreadOpen(threading.Thread): 11 | def __init__(self, gpio_port: int, repeat: int, interval_ms: int): 12 | threading.Thread.__init__(self) 13 | self.gpio_port = gpio_port 14 | self.repeat = repeat 15 | self.interval_ms = interval_ms 16 | self.sig_terminated = Callback() 17 | 18 | def run(self): 19 | writeLog('Started', self) 20 | for i in range(self.repeat): 21 | GPIO.output(self.gpio_port, GPIO.HIGH) 22 | time.sleep(self.interval_ms / 1000) 23 | GPIO.output(self.gpio_port, GPIO.LOW) 24 | time.sleep(self.interval_ms / 1000) 25 | writeLog('Terminated', self) 26 | self.sig_terminated.emit() 27 | 28 | 29 | class Doorlock(Device): 30 | enable: bool = False 31 | gpio_port: int = 0 32 | repeat: int = 0 33 | interval_ms: int = 0 34 | thread_open: Union[ThreadOpen, None] = None 35 | 36 | def __init__(self, name: str = 'Doorlock', **kwargs): 37 | super().__init__(name, **kwargs) 38 | 39 | def setParams(self, enable: bool = False, gpio_port: int = 23, repeat: int = 2, interval_ms: int = 200): 40 | self.enable = enable 41 | self.gpio_port = gpio_port 42 | self.repeat = repeat 43 | self.interval_ms = interval_ms 44 | 45 | GPIO.setmode(GPIO.BCM) 46 | GPIO.setup(self.gpio_port, GPIO.IN, GPIO.PUD_DOWN) # GPIO IN, Pull Down 설정 47 | 48 | def startThreadOpen(self): 49 | if self.thread_open is None: 50 | GPIO.setup(self.gpio_port, GPIO.OUT) 51 | GPIO.output(self.gpio_port, GPIO.LOW) 52 | self.thread_open = ThreadOpen(self.gpio_port, self.repeat, self.interval_ms) 53 | self.thread_open.sig_terminated.connect(self.onThreadOpenTerminated) 54 | self.thread_open.start() 55 | else: 56 | writeLog('Thread is still working', self) 57 | 58 | def onThreadOpenTerminated(self): 59 | del self.thread_open 60 | self.thread_open = None 61 | self.publish_mqtt() 62 | GPIO.setup(self.gpio_port, GPIO.IN, GPIO.PUD_DOWN) # GPIO IN, Pull Down 설정 63 | 64 | def open(self): 65 | if self.enable: 66 | self.startThreadOpen() 67 | else: 68 | writeLog('Disabled!', self) 69 | 70 | def publish_mqtt(self): 71 | obj = {"state": int(self.state == 1)} 72 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 73 | 74 | def __repr__(self): 75 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 76 | repr_txt += '>' 77 | return repr_txt 78 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Elevator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Common import Callback 3 | from Device import Device 4 | 5 | 6 | class Elevator(Device): 7 | my_floor: int = -1 8 | current_floor: str = 'unknown' 9 | current_floor_prev: str = 'unknown' 10 | notify_floor: bool = False 11 | 12 | def __init__(self, name: str = 'Elevator', **kwargs): 13 | super().__init__(name, **kwargs) 14 | self.sig_call_up = Callback() 15 | self.sig_call_down = Callback() 16 | 17 | def call_up(self): 18 | self.sig_call_up.emit() 19 | 20 | def call_down(self): 21 | self.sig_call_down.emit() 22 | 23 | def publish_mqtt(self): 24 | """ 25 | state value 26 | 1: moving 27 | 4: arrived 28 | """ 29 | obj = {"state": int(self.state == 4)} 30 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 31 | self.mqtt_client.publish("home/ipark/elevator/state/occupancy", json.dumps(obj), 1) 32 | 33 | def publish_mqtt_floor(self): 34 | obj = {"floor": self.current_floor} 35 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 36 | 37 | def __repr__(self): 38 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 39 | repr_txt += '>' 40 | return repr_txt 41 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/GasValve.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class GasValve(Device): 6 | def __init__(self, name: str = 'GasValve', **kwargs): 7 | super().__init__(name, **kwargs) 8 | 9 | def publish_mqtt(self): 10 | # 0 = closed, 1 = opened, 2 = opening/closing 11 | obj = {"state": int(self.state == 1)} 12 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 13 | 14 | def __repr__(self): 15 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 16 | repr_txt += '>' 17 | return repr_txt 18 | 19 | def make_packet_set_state(self, target: int, timestamp: int = 0) -> bytearray: 20 | if target: 21 | return bytearray([]) # not allowed open valve 22 | packet = bytearray([0x02, 0x31, 0x02, timestamp & 0xFF]) 23 | packet.extend(bytearray([0x00] * 5)) 24 | packet.append(calculate_bestin_checksum(packet)) 25 | return packet 26 | 27 | def make_packet_query_state(self, timestamp: int = 0) -> bytearray: 28 | packet = bytearray([0x02, 0x31, 0x00, timestamp & 0xFF]) 29 | packet.extend(bytearray([0x00] * 5)) 30 | packet.append(calculate_bestin_checksum(packet)) 31 | return packet 32 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Light.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class Light(Device): 6 | def __init__(self, name: str = 'Light', index: int = 0, **kwargs): 7 | self.index = index 8 | super().__init__(name, **kwargs) 9 | 10 | def publish_mqtt(self): 11 | obj = {"state": self.state} 12 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 13 | 14 | def __repr__(self): 15 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 16 | repr_txt += f' Room Idx: {self.room_index}, Dev Idx: {self.index}' 17 | repr_txt += '>' 18 | return repr_txt 19 | 20 | def make_packet_set_state(self, target: int, timestamp: int = 0) -> bytearray: 21 | packet = self.make_packet_common(0x31, 13, 0x01, timestamp) 22 | packet[5] = self.room_index & 0x0F 23 | packet[6] = 0x01 << self.index 24 | if target: 25 | packet[6] += 0x80 26 | packet[11] = 0x04 27 | else: 28 | packet[11] = 0x00 29 | packet[12] = calculate_bestin_checksum(packet[:-1]) 30 | return packet 31 | 32 | def make_packet_query_state(self, timestamp: int = 0) -> bytearray: 33 | packet = self.make_packet_common(0x31, 7, 0x11, timestamp) 34 | packet[5] = self.room_index & 0x0F 35 | packet[6] = calculate_bestin_checksum(packet[:-1]) 36 | return packet 37 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Outlet.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Device import * 3 | 4 | 5 | class Outlet(Device): 6 | measurement: float = 0. 7 | measurement_prev: float = 0. 8 | 9 | def __init__(self, name: str = 'Outlet', index: int = 0, **kwargs): 10 | self.index = index 11 | super().__init__(name, **kwargs) 12 | 13 | def publish_mqtt(self): 14 | """ 15 | curts = time.perf_counter() 16 | if curts - self.last_published_time > self.publish_interval_sec: 17 | obj = { 18 | "watts": self.measurement 19 | } 20 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 21 | self.last_published_time = curts 22 | """ 23 | obj = { 24 | "state": self.state, 25 | "watts": self.measurement 26 | } 27 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 28 | 29 | def __repr__(self): 30 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 31 | repr_txt += f' Room Idx: {self.room_index}, Dev Idx: {self.index}' 32 | repr_txt += '>' 33 | return repr_txt 34 | 35 | def make_packet_set_state(self, target: int, timestamp: int = 0) -> bytearray: 36 | packet = self.make_packet_common(0x31, 13, 0x01, timestamp) 37 | packet[5] = self.room_index & 0x0F 38 | packet[7] = 0x01 << self.index 39 | if target: 40 | packet[7] += 0x80 41 | packet[11] = 0x09 << self.index # 확실하지 않음... 42 | else: 43 | packet[11] = 0x00 44 | packet[12] = calculate_bestin_checksum(packet[:-1]) 45 | return packet 46 | 47 | def make_packet_query_state(self, timestamp: int = 0) -> bytearray: 48 | # 확실하지 않음, 조명 쿼리 패킷과 동일한 것으로 판단됨 (어차피 응답 패킷에 조명/아울렛 정보가 같이 담겨있음) 49 | packet = self.make_packet_common(0x31, 7, 0x11, timestamp) 50 | packet[5] = self.room_index & 0x0F 51 | packet[6] = calculate_bestin_checksum(packet[:-1]) 52 | return packet 53 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Room.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from Light import Light 3 | from Outlet import Outlet 4 | from Thermostat import Thermostat 5 | from Device import Device 6 | from Common import writeLog 7 | 8 | 9 | class Room: 10 | name: str = 'Room' 11 | # 각 방에는 조명 모듈 여러개와 난방 모듈 1개 존재 12 | index: int = 0 13 | lights: List[Light] 14 | outlets: List[Outlet] 15 | thermostat: Thermostat = None 16 | 17 | def __init__( 18 | self, 19 | name: str = 'Room', 20 | index: int = 0, 21 | light_count: int = 0, 22 | has_thermostat: bool = True, 23 | outlet_count: int = 0, 24 | **kwargs 25 | ): 26 | self.name = name 27 | self.index = index 28 | self.lights = list() 29 | self.outlets = list() 30 | 31 | for i in range(light_count): 32 | self.lights.append(Light( 33 | name=f'Light {i + 1}', 34 | index=i, 35 | room_index=self.index, 36 | mqtt_client=kwargs.get('mqtt_client') 37 | )) 38 | if has_thermostat: 39 | self.thermostat = Thermostat( 40 | name='Thermostat', 41 | room_index=self.index, 42 | mqtt_client=kwargs.get('mqtt_client') 43 | ) 44 | for i in range(outlet_count): 45 | self.outlets.append(Outlet( 46 | name=f'Outlet {i + 1}', 47 | index=i, 48 | room_index=self.index, 49 | mqtt_client=kwargs.get('mqtt_client') 50 | )) 51 | 52 | writeLog(f'Room Created >> {self}', self) 53 | 54 | def __repr__(self): 55 | return f"Room <{self.name}>: Index={self.index}," \ 56 | f" Light#={len(self.lights)}," \ 57 | f" Outlet#={len(self.outlets)}," \ 58 | f" Thermostat:{self.thermostat is not None}" 59 | 60 | def getDevices(self) -> List[Device]: 61 | devices = list() 62 | devices.extend(self.lights) 63 | if self.thermostat is not None: 64 | devices.append(self.thermostat) 65 | devices.extend(self.outlets) 66 | return devices 67 | 68 | @property 69 | def light_count(self): 70 | return len(self.lights) 71 | 72 | @property 73 | def outlet_count(self): 74 | return len(self.outlets) 75 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Thermostat.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List 3 | from Device import * 4 | 5 | 6 | class Thermostat(Device): 7 | temperature_current: float = 0. 8 | temperature_current_prev: float = 0. 9 | temperature_setting: float = 0. 10 | temperature_setting_prev: float = 0. 11 | packet_set_temperature: List[str] 12 | 13 | def __init__(self, name: str = 'Thermostat', **kwargs): 14 | super().__init__(name, **kwargs) 15 | self.packet_set_temperature = [''] * 71 # 5.0 ~ 40.0, step=0.5 16 | 17 | def publish_mqtt(self): 18 | obj = { 19 | "state": 'HEAT' if self.state == 1 else 'OFF', 20 | "currentTemperature": self.temperature_current, 21 | "targetTemperature": self.temperature_setting 22 | } 23 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 24 | 25 | def __repr__(self): 26 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 27 | repr_txt += f' Room Idx: {self.room_index}' 28 | repr_txt += '>' 29 | return repr_txt 30 | 31 | def make_packet_set_state(self, target: int, timestamp: int = 0) -> bytearray: 32 | packet = self.make_packet_common(0x28, 14, 0x12, timestamp) 33 | packet[5] = self.room_index & 0x0F 34 | if target: 35 | packet[6] = 0x01 36 | else: 37 | packet[6] = 0x02 38 | packet[13] = calculate_bestin_checksum(packet[:-1]) 39 | return packet 40 | 41 | def make_packet_query_state(self, timestamp: int = 0) -> bytearray: 42 | packet = self.make_packet_common(0x28, 7, 0x11, timestamp) 43 | packet[5] = self.room_index & 0x0F 44 | packet[6] = calculate_bestin_checksum(packet[:-1]) 45 | return packet 46 | 47 | def make_packet_set_temperature(self, target: float, timestamp: int = 0) -> bytearray: 48 | packet = self.make_packet_common(0x28, 14, 0x12, timestamp) 49 | packet[5] = self.room_index & 0x0F 50 | value_int = int(target) 51 | value_float = target - value_int 52 | packet[7] = value_int & 0xFF 53 | if value_float != 0: 54 | packet[7] += 0x40 55 | packet[13] = calculate_bestin_checksum(packet[:-1]) 56 | return packet 57 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/ThreadMonitoring.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import threading 5 | from typing import List 6 | from Common import Callback, writeLog 7 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # Project/Include 8 | PROJPATH = os.path.dirname(CURPATH) # Proejct/ 9 | RS485PATH = os.path.join(PROJPATH, 'RS485') # Project/RS485 10 | sys.path.extend([CURPATH, PROJPATH, RS485PATH]) 11 | sys.path = list(set(sys.path)) 12 | del CURPATH, PROJPATH, RS485PATH 13 | from RS485 import RS485Comm 14 | 15 | 16 | class ThreadMonitoring(threading.Thread): 17 | _keepAlive: bool = True 18 | _home_initialized: bool = False 19 | 20 | def __init__( 21 | self, 22 | rs485_list: List[RS485Comm], 23 | publish_interval: int = 60, 24 | interval_ms: int = 2000 25 | ): 26 | threading.Thread.__init__(self, name='Monitoring Thread') 27 | self._rs485_list = rs485_list 28 | self._publish_interval = publish_interval 29 | self._interval_ms = interval_ms 30 | self.sig_terminated = Callback() 31 | self.sig_publish_regular = Callback() 32 | 33 | def run(self): 34 | first_publish: bool = False 35 | writeLog('Started', self) 36 | tm = time.perf_counter() 37 | while self._keepAlive: 38 | if not self._home_initialized: 39 | writeLog('Home is not initialized!', self) 40 | time.sleep(0.1) 41 | continue 42 | rs485_all_connected: bool = sum([x.isConnected() for x in self._rs485_list]) == len(self._rs485_list) 43 | if rs485_all_connected and not first_publish: 44 | first_publish = True 45 | writeLog('RS485 are all opened >> Publish', self) 46 | self.sig_publish_regular.emit() 47 | 48 | for obj in self._rs485_list: 49 | if obj.isConnected(): 50 | delta = obj.time_after_last_recv() 51 | if delta > 10: 52 | msg = 'Warning!! RS485 <{}> is not receiving for {:.1f} seconds'.format(obj.name, delta) 53 | writeLog(msg, self) 54 | if delta > 120: 55 | # 2분이상이면 재접속 시도 56 | obj.reconnect() 57 | else: 58 | writeLog('Warning!! RS485 <{}> is not connected'.format(obj.name), self) 59 | delta = obj.time_after_last_recv() 60 | if delta > 120: 61 | # 2분이상이면 재접속 시도 62 | writeLog('Try to reconnect RS485 <{}>'.format(obj.name), self) 63 | obj.reconnect() 64 | 65 | if time.perf_counter() - tm > self._publish_interval: 66 | writeLog('Regular Publishing Device State MQTT (interval: {} sec)'.format(self._publish_interval), self) 67 | self.sig_publish_regular.emit() 68 | tm = time.perf_counter() 69 | time.sleep(self._interval_ms / 1000) 70 | writeLog('Terminated', self) 71 | self.sig_terminated.emit() 72 | 73 | def stop(self): 74 | self._keepAlive = False 75 | 76 | def set_home_initialized(self): 77 | self._home_initialized = True 78 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/Ventilator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List 3 | from Device import * 4 | 5 | 6 | class Ventilator(Device): 7 | state_natural: int = 0 8 | rotation_speed: int = 0 9 | rotation_speed_prev: int = 0 10 | timer_remain: int = 0 11 | packet_set_rotation_speed: List[str] 12 | 13 | def __init__(self, name: str = 'Ventilator', **kwargs): 14 | super().__init__(name, **kwargs) 15 | self.packet_set_rotation_speed = [''] * 3 16 | 17 | def publish_mqtt(self): 18 | obj = { 19 | "state": self.state, 20 | "rotationspeed": int(self.rotation_speed / 3 * 100) 21 | } 22 | self.mqtt_client.publish(self.mqtt_publish_topic, json.dumps(obj), 1) 23 | 24 | def __repr__(self): 25 | repr_txt = f'<{self.name}({self.__class__.__name__} at {hex(id(self))})' 26 | repr_txt += '>' 27 | return repr_txt 28 | 29 | def make_packet_set_state(self, target: int, timestamp: int = 0) -> bytearray: 30 | packet = bytearray([0x02, 0x61, 0x01, timestamp & 0xFF, 0x00]) 31 | packet.append(0x01) if target else packet.append(0x00) 32 | packet.extend([0x01, 0x00, 0x00]) 33 | packet.append(calculate_bestin_checksum(packet)) 34 | return packet 35 | 36 | def make_packet_query_state(self, timestamp: int = 0) -> bytearray: 37 | packet = bytearray([0x02, 0x61, 0x00, timestamp & 0xFF, 0x00]) 38 | packet.extend([0x00, 0x00, 0x00, 0x00]) 39 | packet.append(calculate_bestin_checksum(packet)) 40 | return packet 41 | 42 | def make_packet_set_rotation_speed(self, target: int, timestamp: int = 0) -> bytearray: 43 | """ 44 | 풍량 설정 (3단계) 45 | :param target: 1=미풍, 2=약풍, 3=강풍 46 | """ 47 | target = max(1, min(3, target)) 48 | packet = bytearray([0x02, 0x61, 0x03, timestamp & 0xFF, 0x00, 0x00, target, 0x00, 0x00]) 49 | packet.append(calculate_bestin_checksum(packet)) 50 | return packet 51 | 52 | def make_packet_set_natural(self, target: int, timestamp: int = 0) -> bytearray: 53 | """ 54 | 자연환기 On/Off 55 | :param target: 0=Off, 1=On 56 | """ 57 | packet = bytearray([0x02, 0x61, 0x07, timestamp & 0xFF, 0x00]) 58 | packet.append(0x10) if target else packet.append(0x00) 59 | packet.extend([0x00, 0x00, 0x00]) 60 | packet.append(calculate_bestin_checksum(packet)) 61 | return packet 62 | 63 | def make_packet_set_timer(self, value: int, timestamp: int = 0) -> bytearray: 64 | """ 65 | 타이머 설정 66 | :param value: 0=Off, others=timer value (unit=minute) 67 | """ 68 | packet = bytearray([0x02, 0x61, 0x04, timestamp & 0xFF, 0x00]) 69 | packet.append(value & 0xFF) 70 | packet.extend([0x00, 0x00, 0x00]) 71 | packet.append(calculate_bestin_checksum(packet)) 72 | return packet 73 | 74 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/Include/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | 7 | from Common import writeLog, Callback 8 | from Device import Device 9 | from Light import Light 10 | from GasValve import GasValve 11 | from Thermostat import Thermostat 12 | from Ventilator import Ventilator 13 | from Elevator import Elevator 14 | from Outlet import Outlet 15 | from AirqualitySensor import AirqualitySensor 16 | from Room import Room 17 | from Doorlock import Doorlock 18 | from ThreadCommand import ThreadCommand 19 | from ThreadMonitoring import ThreadMonitoring 20 | from Home import Home, get_home 21 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/ControlParser.py: -------------------------------------------------------------------------------- 1 | from PacketParser import * 2 | 3 | 4 | class ControlParser(PacketParser): 5 | enable_log_header_28: bool = True 6 | enable_log_header_31: bool = True 7 | enable_log_header_61: bool = True 8 | 9 | def handlePacket(self): 10 | try: 11 | idx = self.buffer.find(0x2) 12 | if idx > 0: 13 | self.buffer = self.buffer[idx:] 14 | 15 | if len(self.buffer) >= 3: 16 | packetLen = self.buffer[2] if self.buffer[1] not in [0x31, 0x61] else 10 17 | if len(self.buffer) >= packetLen: 18 | chunk = self.buffer[:packetLen] 19 | if self.chunk_cnt >= self.max_chunk_cnt: 20 | self.chunk_cnt = 0 21 | self.chunk_cnt += 1 22 | if self.enable_console_log: 23 | msg = ' '.join(['%02X' % x for x in chunk]) 24 | # print(msg) 25 | try: 26 | if chunk[1] == 0x28: 27 | # 난방 28 | if chunk[3] in [0x21, 0xA1]: 29 | pass 30 | elif chunk[3] in [0x11, 0x91]: 31 | pass 32 | elif chunk[3] == 0x12: 33 | pass 34 | elif chunk[1] == 0x31: 35 | # 가스 36 | # print(msg) 37 | """ 38 | self.chunk_cnt += 1 39 | if chunk[2] == 0x80: 40 | # print(msg) 41 | # self.chunk_cnt += 1 42 | pass 43 | """ 44 | pass 45 | elif chunk[1] == 0x61: 46 | # 환기 47 | # print(msg) 48 | # self.chunk_cnt += 1 49 | pass 50 | else: 51 | print(msg) 52 | pass 53 | except Exception: 54 | pass 55 | self.interpretPacket(chunk) 56 | self.buffer = self.buffer[packetLen:] 57 | except Exception as e: 58 | writeLog('handlePacket Exception::{}'.format(e), self) 59 | 60 | def interpretPacket(self, packet: bytearray): 61 | try: 62 | if len(packet) < 10: 63 | return 64 | header = packet[1] # [0x28, 0x31, 0x61] 65 | command = packet[3] 66 | self.timestamp = packet[4] 67 | if header == 0x28 and command in [0x91, 0x92]: 68 | # 난방 관련 패킷 69 | self.handleThermostat(packet) 70 | elif header == 0x31 and packet[2] in [0x80, 0x82]: 71 | # 가스밸브 관련 패킷 (길이 정보 없음, 무조건 10 고정) 72 | self.handleGasValve(packet) 73 | elif header == 0x61 and packet[2] in [0x80, 0x81, 0x83, 0x84, 0x87]: 74 | # 환기 관련 패킷 75 | self.handleVentilator(packet) 76 | else: 77 | pass 78 | 79 | # packet log 80 | enable = True 81 | if header == 0x28 and not self.enable_log_header_28: 82 | enable = False 83 | if header == 0x31 and not self.enable_log_header_31: 84 | enable = False 85 | if header == 0x61 and not self.enable_log_header_61: 86 | enable = False 87 | if enable: 88 | self.sig_raw_packet.emit(packet) 89 | except Exception as e: 90 | writeLog('interpretPacket Exception::{}'.format(e), self) 91 | 92 | def handleThermostat(self, packet: bytearray): 93 | # packet[3] == 0x91: 쿼리 응답 / 0x92: 명령 응답 94 | room_idx = packet[5] & 0x0F 95 | state = packet[6] & 0x01 96 | temperature_setting = (packet[7] & 0x3F) + (packet[7] & 0x40 > 0) * 0.5 97 | temperature_current = int.from_bytes(packet[8:10], byteorder='big') / 10.0 98 | result = { 99 | 'device': 'thermostat', 100 | 'room_index': room_idx, 101 | 'state': state, 102 | 'temperature_setting': temperature_setting, 103 | 'temperature_current': temperature_current 104 | } 105 | self.sig_parse_result.emit(result) 106 | 107 | def handleGasValve(self, packet: bytearray): 108 | # packet[2] == 0x80: 쿼리 응답 109 | # packet[2] == 0x82: 명령 응답 110 | state = packet[5] 111 | result = { 112 | 'device': 'gasvalve', 113 | 'state': state 114 | } 115 | self.sig_parse_result.emit(result) 116 | 117 | def handleVentilator(self, packet: bytearray): 118 | state = packet[5] & 0x01 119 | state_natural = (packet[5] & 0x10) >> 4 120 | rotation_speed = packet[6] 121 | timer_remain = packet[7] 122 | result = { 123 | 'device': 'ventilator', 124 | 'state': state, 125 | 'state_natural': state_natural, 126 | 'rotation_speed': rotation_speed, 127 | 'timer_remain': timer_remain 128 | } 129 | self.sig_parse_result.emit(result) 130 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/Define.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import threading 3 | 4 | 5 | def checkAgrumentType(obj, arg): 6 | if type(obj) == arg: 7 | return True 8 | if arg == object: 9 | return True 10 | if arg in obj.__class__.__bases__: 11 | return True 12 | return False 13 | 14 | 15 | class Callback(object): 16 | _args = None 17 | 18 | def __init__(self, *args): 19 | self._args = args 20 | self._callbacks = list() 21 | 22 | def connect(self, callback): 23 | if callback not in self._callbacks: 24 | self._callbacks.append(callback) 25 | 26 | def emit(self, *args): 27 | if len(args) != len(self._args): 28 | raise Exception('Callback::Argument Length Mismatch') 29 | arglen = len(args) 30 | if arglen > 0: 31 | validTypes = [checkAgrumentType(args[i], self._args[i]) for i in range(arglen)] 32 | if sum(validTypes) != arglen: 33 | raise Exception('Callback::Argument Type Mismatch (Definition: {}, Call: {})'.format(self._args, args)) 34 | for callback in self._callbacks: 35 | callback(*args) 36 | 37 | 38 | def timestampToString(timestamp: datetime.datetime): 39 | h = timestamp.hour 40 | m = timestamp.minute 41 | s = timestamp.second 42 | us = timestamp.microsecond 43 | return '%02d:%02d:%02d.%06d' % (h, m, s, us) 44 | 45 | 46 | def getCurTimeStr(): 47 | return '<%s>' % timestampToString(datetime.datetime.now()) 48 | 49 | 50 | def writeLog(strMsg: str, obj: object = None): 51 | strTime = getCurTimeStr() 52 | if obj is not None: 53 | if isinstance(obj, threading.Thread): 54 | if obj.ident is not None: 55 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, obj.ident) 56 | else: 57 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 58 | else: 59 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 60 | else: 61 | strObj = '' 62 | 63 | msg = strTime + strObj + ' ' + strMsg 64 | print(msg) 65 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/DoorphoneParser.py: -------------------------------------------------------------------------------- 1 | from PacketParser import PacketParser 2 | 3 | 4 | class DoorphoneParser(PacketParser): 5 | def handlePacket(self): 6 | if len(self.buffer) > 16: 7 | chunk = self.buffer[:16] 8 | if self.chunk_cnt >= self.max_chunk_cnt: 9 | self.chunk_cnt = 0 10 | self.chunk_cnt += 1 11 | if self.enable_console_log: 12 | msg = ' '.join(['%02X' % x for x in chunk]) 13 | print(msg) 14 | 15 | self.buffer = self.buffer[16:] 16 | """ 17 | idx = self.buffer.find(0x2) 18 | if idx > 0: 19 | self.buffer = self.buffer[idx:] 20 | 21 | if len(self.buffer) >= 3: 22 | packetLen = self.buffer[2] 23 | 24 | if len(self.buffer) >= packetLen: 25 | chunk = self.buffer[:packetLen] 26 | if self.chunk_cnt >= self.max_chunk_cnt: 27 | self.chunk_cnt = 0 28 | self.chunk_cnt += 1 29 | if self.enable_console_log: 30 | msg = ' '.join(['%02X' % x for x in chunk]) 31 | print(msg) 32 | self.sig_parse.emit(chunk) 33 | self.buffer = self.buffer[packetLen:] 34 | """ 35 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/PacketParser.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABCMeta 2 | from Define import Callback, writeLog 3 | from RS485Comm import * 4 | 5 | 6 | class PacketParser: 7 | __metaclass__ = ABCMeta 8 | 9 | buffer: bytearray 10 | enable_console_log: bool = False 11 | chunk_cnt: int = 0 12 | max_chunk_cnt: int = 1e6 13 | max_buffer_size: int = 200 14 | line_busy: bool = False 15 | timestamp: int = 0 16 | 17 | def __init__(self, rs485: RS485Comm): 18 | self.buffer = bytearray() 19 | self.sig_parse_result = Callback(dict) 20 | self.sig_raw_packet = Callback(bytearray) 21 | rs485.sig_send_data.connect(self.onSendData) 22 | rs485.sig_recv_data.connect(self.onRecvData) 23 | self.rs485 = rs485 24 | 25 | def release(self): 26 | self.buffer.clear() 27 | 28 | def sendPacket(self, packet: bytearray): 29 | self.rs485.sendData(packet) 30 | 31 | def sendPacketString(self, packet_str: str): 32 | self.rs485.sendData(bytearray([int(x, 16) for x in packet_str.split(' ')])) 33 | 34 | def onSendData(self, data: bytes): 35 | msg = ' '.join(['%02X' % x for x in data]) 36 | writeLog("Send >> {}".format(msg), self) 37 | 38 | def onRecvData(self, data: bytes): 39 | self.line_busy = True 40 | if len(self.buffer) > self.max_buffer_size: 41 | self.buffer.clear() 42 | self.buffer.extend(data) 43 | self.handlePacket() 44 | self.line_busy = False 45 | 46 | @abstractmethod 47 | def handlePacket(self): 48 | pass 49 | 50 | @abstractmethod 51 | def interpretPacket(self, packet: bytearray): 52 | pass 53 | 54 | def startRecv(self, count: int = 64): 55 | self.buffer.clear() 56 | self.chunk_cnt = 0 57 | self.enable_console_log = True 58 | while self.chunk_cnt < count: 59 | pass 60 | self.enable_console_log = False 61 | 62 | def getRS485HwType(self) -> RS485HwType: 63 | return self.rs485.getType() 64 | 65 | def isRS485LineBusy(self) -> bool: 66 | if self.rs485.getType() == RS485HwType.Socket: 67 | return False # 무선 송신 레이턴시때문에 언제 라인이 IDLE인지 정확히 파악할 수 없다 68 | return self.line_busy 69 | 70 | def get_packet_timestamp(self) -> int: 71 | return self.timestamp 72 | 73 | @staticmethod 74 | def prettifyPacket(packet: bytearray) -> str: 75 | return ' '.join(['%02X' % x for x in packet]) 76 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/RS485Comm.py: -------------------------------------------------------------------------------- 1 | import time 2 | from enum import IntEnum 3 | from typing import Union 4 | from Serial import * 5 | from Socket import * 6 | from Define import writeLog, Callback 7 | 8 | 9 | class RS485HwType(IntEnum): 10 | Serial = 0 11 | Socket = 1 12 | Unknown = 2 13 | 14 | 15 | class RS485Config: 16 | enable: bool = True 17 | comm_type: RS485HwType 18 | serial_port: str = '/dev/ttyUSB0' 19 | serial_baud: int = 9600 20 | socket_ipaddr: str = '127.0.0.1' 21 | socket_port: int = 80 22 | 23 | 24 | class RS485Comm: 25 | _comm_obj: Union[SerialComm, TCPClient, None] = None 26 | _hw_type: RS485HwType = RS485HwType.Unknown 27 | _last_conn_addr: str = '' 28 | _last_conn_port: int = 0 29 | 30 | def __init__(self, name: str = 'RS485Comm'): 31 | self._name = name 32 | self.sig_connected = Callback() 33 | self.sig_disconnected = Callback() 34 | self.sig_send_data = Callback(bytes) 35 | self.sig_recv_data = Callback(bytes) 36 | self.sig_exception = Callback(str) 37 | 38 | def setType(self, comm_type: RS485HwType): 39 | if self._comm_obj is not None: 40 | self.release() 41 | if comm_type == RS485HwType.Serial: 42 | self._comm_obj = SerialComm(self._name) 43 | elif comm_type == RS485HwType.Socket: 44 | self._comm_obj = TCPClient(self._name) 45 | self._hw_type = comm_type 46 | if self._comm_obj is not None: 47 | self._comm_obj.sig_connected.connect(self.onConnect) 48 | self._comm_obj.sig_disconnected.connect(self.onDisconnect) 49 | self._comm_obj.sig_send_data.connect(self.onSendData) 50 | self._comm_obj.sig_recv_data.connect(self.onRecvData) 51 | self._comm_obj.sig_exception.connect(self.onException) 52 | writeLog(f"Set HW Type as '{comm_type.name}'", self) 53 | 54 | def getType(self) -> RS485HwType: 55 | return self._hw_type 56 | 57 | def release(self): 58 | if self._comm_obj is not None: 59 | self._comm_obj.release() 60 | del self._comm_obj 61 | self._comm_obj = None 62 | 63 | def connect(self, addr: str, port: int) -> bool: 64 | # serial - (devport, baud) 65 | # socket - (ipaddr, port) 66 | self._last_conn_addr = addr 67 | self._last_conn_port = port 68 | return self._comm_obj.connect(addr, port) 69 | 70 | def disconnect(self): 71 | self._comm_obj.disconnect() 72 | 73 | def reconnect(self, count: int = 1): 74 | self.disconnect() 75 | for _ in range(count): 76 | if self.isConnected(): 77 | break 78 | self.connect(self._last_conn_addr, self._last_conn_port) 79 | time.sleep(1) 80 | 81 | def isConnected(self) -> bool: 82 | if self._comm_obj is None: 83 | return False 84 | return self._comm_obj.isConnected() 85 | 86 | def sendData(self, data: Union[bytes, bytearray, str]): 87 | if self._comm_obj is not None: 88 | self._comm_obj.sendData(data) 89 | 90 | def time_after_last_recv(self) -> float: 91 | if self._comm_obj is None: 92 | return 0. 93 | return self._comm_obj.time_after_last_recv() 94 | 95 | # Callbacks 96 | def onConnect(self, success: bool): 97 | if success: 98 | self.sig_connected.emit() 99 | else: 100 | self.sig_disconnected.emit() 101 | 102 | def onDisconnect(self): 103 | self.sig_disconnected.emit() 104 | 105 | def onSendData(self, data: bytes): 106 | self.sig_send_data.emit(data) 107 | 108 | def onRecvData(self, data: bytes): 109 | self.sig_recv_data.emit(data) 110 | 111 | def onException(self, msg: str): 112 | self.sig_exception.emit(msg) 113 | self.reconnect() 114 | 115 | @property 116 | def name(self) -> str: 117 | return self._name 118 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/Serial/SerialThreads.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import queue 5 | import serial 6 | import threading 7 | import traceback 8 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/RS485/Serial 9 | RS485PATH = os.path.dirname(CURPATH) # {$PROJECT}/RS485/ 10 | sys.path.extend([CURPATH, RS485PATH]) 11 | sys.path = list(set(sys.path)) 12 | del CURPATH, RS485PATH 13 | from Define import Callback, writeLog 14 | 15 | 16 | class ThreadSend(threading.Thread): 17 | _keepAlive: bool = True 18 | 19 | def __init__(self, serial_: serial.Serial, queue_: queue.Queue): 20 | threading.Thread.__init__(self, name='Serial Send Thread') 21 | self.sig_send_data = Callback(bytes) 22 | self.sig_terminated = Callback() 23 | self.sig_exception = Callback(str) 24 | self._serial = serial_ 25 | self._queue = queue_ 26 | 27 | def run(self): 28 | writeLog('Started', self) 29 | while self._keepAlive: 30 | try: 31 | if not self._queue.empty(): 32 | data = self._queue.get() 33 | sendLen = len(data) 34 | while sendLen > 0: 35 | nLen = self._serial.write(data[(len(data) - sendLen):]) 36 | sData = data[(len(data) - sendLen):(len(data) - sendLen + nLen)] 37 | self.sig_send_data.emit(sData) 38 | sendLen -= nLen 39 | else: 40 | time.sleep(1e-3) 41 | except Exception as e: 42 | writeLog('Exception::{}'.format(e), self) 43 | traceback.print_exc() 44 | self.sig_exception.emit(str(e)) 45 | writeLog('Terminated', self) 46 | self.sig_terminated.emit() 47 | 48 | def stop(self): 49 | self._keepAlive = False 50 | 51 | 52 | class ThreadReceive(threading.Thread): 53 | _keepAlive: bool = True 54 | 55 | def __init__(self, serial_: serial.Serial, queue_: queue.Queue): 56 | threading.Thread.__init__(self, name='Serial Recv Thread') 57 | self.sig_terminated = Callback() 58 | # self.sig_recv_data = Callback(bytes) 59 | self.sig_recv_data = Callback() 60 | self.sig_exception = Callback(str) 61 | self._serial = serial_ 62 | self._queue = queue_ 63 | 64 | def run(self): 65 | writeLog('Started', self) 66 | while self._keepAlive: 67 | try: 68 | if self._serial.isOpen(): 69 | if self._serial.in_waiting > 0: 70 | rcv = self._serial.read(self._serial.in_waiting) 71 | # self.sig_recv_data.emit(rcv) 72 | self.sig_recv_data.emit() 73 | self._queue.put(rcv) 74 | else: 75 | time.sleep(1e-3) 76 | else: 77 | time.sleep(1e-3) 78 | except Exception as e: 79 | writeLog(f'Exception::{self._serial.port}::{e}', self) 80 | traceback.print_exc() 81 | self.sig_exception.emit(str(e)) 82 | # break 83 | writeLog('Terminated', self) 84 | self.sig_terminated.emit() 85 | 86 | def stop(self): 87 | self._keepAlive = False 88 | 89 | 90 | class ThreadCheck(threading.Thread): 91 | _keepAlive: bool = True 92 | 93 | def __init__(self, queue_: queue.Queue): 94 | threading.Thread.__init__(self, name='Serial Check Thread') 95 | self.sig_get = Callback(bytes) 96 | self.sig_terminated = Callback() 97 | self.sig_exception = Callback(str) 98 | self._queue = queue_ 99 | 100 | def run(self): 101 | writeLog('Started', self) 102 | while self._keepAlive: 103 | try: 104 | if not self._queue.empty(): 105 | self.sig_get.emit(self._queue.get()) 106 | else: 107 | time.sleep(1e-3) 108 | except Exception as e: 109 | writeLog('Exception::{}'.format(e), self) 110 | traceback.print_exc() 111 | self.sig_exception.emit(str(e)) 112 | writeLog('Terminated', self) 113 | self.sig_terminated.emit() 114 | 115 | def stop(self): 116 | self._keepAlive = False 117 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/Serial/Util.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Union 2 | from PyQt5 import QtSerialPort 3 | 4 | 5 | def list_serial() -> List[Dict[str, Union[str, int, bool]]]: 6 | available = QtSerialPort.QSerialPortInfo().availablePorts() 7 | lst = [{ 8 | 'port': x.portName(), 9 | 'manufacturer': x.manufacturer(), 10 | 'description': x.description(), 11 | 'serialnumber': x.serialNumber(), 12 | 'systemlocation': x.systemLocation(), 13 | 'productidentifier': x.productIdentifier(), 14 | 'vendoridentifier': x.vendorIdentifier(), 15 | 'isbusy': x.isBusy(), 16 | 'isvalid': x.isValid() 17 | } for x in available] 18 | return lst 19 | 20 | 21 | if __name__ == '__main__': 22 | serial_list = list_serial() 23 | for elem in serial_list: 24 | print('{') 25 | for i, (key, value) in enumerate(elem.items()): 26 | print(" '{}': '{}'".format(key, value), end='') 27 | if i == len(elem.items()) - 1: 28 | print('') 29 | else: 30 | print(',') 31 | print('}') 32 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/Serial/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from SerialComm import SerialComm 9 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/SmartRecvParser.py: -------------------------------------------------------------------------------- 1 | from PacketParser import * 2 | from Define import Callback 3 | from RS485Comm import * 4 | 5 | 6 | class SmartRecvParser(PacketParser): 7 | year: int = 0 8 | month: int = 0 9 | day: int = 0 10 | hour: int = 0 11 | minute: int = 0 12 | second: int = 0 13 | 14 | flag_moving: bool = False 15 | flag_send_up_packet: bool = False 16 | flag_send_down_packet: bool = False 17 | 18 | def __init__(self, rs485: RS485Comm): 19 | super().__init__(rs485) 20 | self.sig_call_elevator = Callback(int, int) # up(1)/down(0) flag, timestamp 21 | 22 | def handlePacket(self): 23 | try: 24 | idx = self.buffer.find(0x2) 25 | if idx > 0: 26 | self.buffer = self.buffer[idx:] 27 | 28 | if len(self.buffer) >= 3: 29 | packetLen = self.buffer[2] 30 | if len(self.buffer) >= 5: 31 | self.timestamp = self.buffer[4] 32 | 33 | if len(self.buffer) >= 12: 34 | self.flag_moving = bool(self.buffer[11]) 35 | if self.flag_moving: # 0 = stopped, 1 = moving, 4 = arrived 36 | self.flag_send_up_packet = False 37 | self.flag_send_down_packet = False 38 | 39 | if len(self.buffer) >= packetLen: 40 | chunk = self.buffer[:packetLen] 41 | if chunk[3] == 0x11: 42 | if self.flag_send_up_packet: 43 | self.sig_call_elevator.emit(1, self.timestamp) 44 | if self.flag_send_down_packet: 45 | self.sig_call_elevator.emit(0, self.timestamp) 46 | 47 | if len(chunk) >= 11: 48 | if chunk[1] == 0xC1 and chunk[3] == 0x13: 49 | self.year = chunk[5] 50 | self.month = chunk[6] 51 | self.day = chunk[7] 52 | self.hour = chunk[8] 53 | self.minute = chunk[9] 54 | self.second = chunk[10] 55 | 56 | if self.enable_console_log: 57 | msg = ' '.join(['%02X' % x for x in chunk]) 58 | print('[SER 1] ' + msg) 59 | 60 | self.interpretPacket(chunk) 61 | self.buffer = self.buffer[packetLen:] 62 | except Exception as e: 63 | writeLog('handlePacket Exception::{}'.format(e), self) 64 | 65 | def setFlagCallUp(self): 66 | self.flag_send_up_packet = True 67 | 68 | def setFlagCallDown(self): 69 | self.flag_send_down_packet = True 70 | 71 | def interpretPacket(self, packet: bytearray): 72 | try: 73 | if len(packet) < 4: 74 | return 75 | header = packet[1] # [0xC1] 76 | packetLen = packet[2] 77 | cmd = packet[3] 78 | if header == 0xC1 and packetLen == 0x13 and cmd == 0x13: 79 | if len(packet) >= 13: 80 | state = packet[11] 81 | # 0xFF : unknown, 최상위 비트가 1이면 지하 82 | if packet[12] == 0xFF: 83 | current_floor = 'unknown' 84 | elif packet[12] & 0x80: 85 | current_floor = f'B{packet[12] & 0x7F}' 86 | else: 87 | current_floor = f'{packet[12] & 0xFF}' 88 | result = { 89 | 'device': 'elevator', 90 | 'state': state, 91 | 'current_floor': current_floor 92 | } 93 | self.sig_parse_result.emit(result) 94 | 95 | # packet log 96 | self.sig_raw_packet.emit(packet) 97 | except Exception as e: 98 | writeLog('interpretPacket Exception::{}'.format(e), self) -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/SmartSendParser.py: -------------------------------------------------------------------------------- 1 | # import os 2 | # import pickle 3 | # from typing import List 4 | import time 5 | from PacketParser import * 6 | from RS485Comm import * 7 | from functools import reduce 8 | 9 | 10 | class SmartSendParser(PacketParser): 11 | """ 12 | elevator_up_packets: List[str] 13 | elevator_down_packets: List[str] 14 | """ 15 | elevator_call_count: int = 1 16 | elevator_call_interval: int = 0 # 엘리베이터 호출 반복 호출 사이 딜레이 (단위: ms) 17 | is_calling_elevator: bool = False 18 | 19 | def __init__(self, rs485: RS485Comm, elevator_call_count: int = 1): 20 | super().__init__(rs485) 21 | # packets in here 22 | """ 23 | curpath = os.path.dirname(os.path.abspath(__file__)) 24 | picklepath = os.path.join(curpath, 'smart_elevator_up_packets.pkl') 25 | if os.path.isfile(picklepath): 26 | with open(picklepath, 'rb') as fp: 27 | temp = pickle.load(fp) 28 | temp.sort(key=lambda x: x[4]) 29 | self.elevator_up_packets = [' '.join(['%02X' % x for x in e]) for e in temp] 30 | else: 31 | self.elevator_up_packets = [''] * 256 32 | picklepath = os.path.join(curpath, 'smart_elevator_down_packets.pkl') 33 | if os.path.isfile(picklepath): 34 | with open(picklepath, 'rb') as fp: 35 | temp = pickle.load(fp) 36 | temp.sort(key=lambda x: x[4]) 37 | self.elevator_down_packets = [' '.join(['%02X' % x for x in e]) for e in temp] 38 | else: 39 | self.elevator_down_packets = [''] * 256 40 | """ 41 | self.elevator_call_count = max(1, elevator_call_count) 42 | 43 | def handlePacket(self): 44 | try: 45 | idx = self.buffer.find(0x2) 46 | if idx > 0: 47 | self.buffer = self.buffer[idx:] 48 | 49 | if len(self.buffer) >= 3: 50 | packetLen = self.buffer[2] 51 | if len(self.buffer) >= 5: 52 | self.timestamp = self.buffer[4] 53 | if len(self.buffer) >= packetLen: 54 | chunk = self.buffer[:packetLen] 55 | 56 | if self.enable_console_log: 57 | msg = ' '.join(['%02X' % x for x in chunk]) 58 | print('[SER 2] ' + msg) 59 | 60 | self.interpretPacket(chunk) 61 | self.buffer = self.buffer[packetLen:] 62 | # TODO: bypass here 63 | except Exception as e: 64 | writeLog('handlePacket Exception::{}'.format(e), self) 65 | 66 | def setElevatorCallCount(self, count: int): 67 | self.elevator_call_count = max(1, count) 68 | 69 | def setElevatorCallInterval(self, interval: int): 70 | self.elevator_call_interval = interval 71 | 72 | def sendCallElevatorPacket(self, updown: int, timestamp: int): 73 | # updown 0 = down, 1 = up 74 | if self.is_calling_elevator: 75 | return 76 | self.is_calling_elevator = True 77 | for i in range(self.elevator_call_count): 78 | """ 79 | temp = (timestamp + i) % 256 80 | if updown: 81 | packet = self.elevator_up_packets[temp] 82 | else: 83 | packet = self.elevator_down_packets[temp] 84 | self.sendPacketString(packet) 85 | """ 86 | if updown: 87 | packet = self.make_packet_call_up((timestamp + i) % 256) 88 | else: 89 | packet = self.make_packet_call_down((timestamp + i) % 256) 90 | self.sendPacket(packet) 91 | if self.elevator_call_interval > 0: 92 | time.sleep(self.elevator_call_interval / 1000) 93 | 94 | self.is_calling_elevator = False 95 | 96 | def interpretPacket(self, packet: bytearray): 97 | # packet log 98 | # self.sig_raw_packet.emit(packet) 99 | pass 100 | 101 | def make_packet_call_down(self, timestamp: int) -> bytearray: 102 | packet = bytearray([0x02, 0xC1, 0x0C, 0x91, timestamp, 0x10, 0x01, 0x00, 0x02, 0x01, 0x02]) 103 | packet.append(self.calculate_bestin_checksum(packet)) 104 | return packet 105 | 106 | def make_packet_call_up(self, timestamp: int) -> bytearray: 107 | packet = bytearray([0x02, 0xC1, 0x0C, 0x91, timestamp, 0x20, 0x01, 0x00, 0x02, 0x01, 0x02]) 108 | packet.append(self.calculate_bestin_checksum(packet)) 109 | return packet 110 | 111 | @staticmethod 112 | def calculate_bestin_checksum(packet: bytearray) -> int: 113 | try: 114 | return reduce(lambda x, y: ((x ^ y) + 1) & 0xFF, packet, 3) 115 | except Exception as e: 116 | writeLog(f'Calc Bestin Checksum Error ({e})') 117 | return 0 -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/Socket/SocketThreads.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import queue 5 | import socket 6 | import threading 7 | from typing import Union 8 | from abc import abstractmethod, ABCMeta 9 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # {$PROJECT}/RS485/Socket 10 | RS485PATH = os.path.dirname(os.path.dirname(CURPATH)) # {$PROJECT}/RS485/ 11 | sys.path.extend([CURPATH, RS485PATH]) 12 | sys.path = list(set(sys.path)) 13 | del CURPATH, RS485PATH 14 | from Define import writeLog, Callback 15 | 16 | 17 | class ThreadCommon(threading.Thread): 18 | __metaclass__ = ABCMeta 19 | 20 | _keepAlive: bool = True 21 | _sock: Union[socket.socket, None] = None 22 | _loop_sleep_time: float = 1e-3 23 | 24 | def __init__(self, name: str = 'Socket Thread Common'): 25 | threading.Thread.__init__(self, name=name) 26 | self.sig_terminated = Callback() 27 | self.sig_send = Callback(bytes) 28 | self.sig_recv = Callback(bytes) 29 | self.sig_exception = Callback(str, bool) # message, terminate socket flag 30 | self.setDaemon(True) 31 | 32 | def run(self): 33 | writeLog('Started', self) 34 | while self._keepAlive: 35 | self.loop() 36 | if self._loop_sleep_time > 0: 37 | time.sleep(self._loop_sleep_time) 38 | writeLog('Terminated', self) 39 | self.sig_terminated.emit() 40 | 41 | @abstractmethod 42 | def loop(self): 43 | pass 44 | 45 | def stop(self): 46 | self._keepAlive = False 47 | 48 | def setSocket(self, sock: socket.socket): 49 | self._sock = sock 50 | 51 | 52 | class ThreadSend(ThreadCommon): 53 | def __init__(self, sock: socket.socket, queue_send: queue.Queue): 54 | super().__init__(name='Socket Thread Send') 55 | self._sock = sock 56 | self._queue_send = queue_send 57 | 58 | def loop(self): 59 | try: 60 | if not isinstance(self._sock, socket.socket): 61 | return 62 | if not self._queue_send.empty(): 63 | data = self._queue_send.get() 64 | datalen = len(data) 65 | while datalen > 0: 66 | sendlen = self._sock.send(data) 67 | self.sig_send.emit(data[:sendlen]) 68 | data = data[sendlen:] 69 | datalen = len(data) 70 | except OSError as e: 71 | if e.args[0] == 10038: 72 | self.sig_exception.emit(f'OSError 10038 ({e})', True) 73 | except Exception as e: 74 | self.sig_exception.emit(f'Exception ({e})', True) 75 | 76 | 77 | class ThreadRecv(ThreadCommon): 78 | def __init__(self, sock: socket.socket, queue_recv: queue.Queue, bufsize: int = 4096): 79 | super().__init__(name='Socket Thread Recv') 80 | self._sock = sock 81 | self._queue_recv = queue_recv 82 | self._bufsize = bufsize 83 | self._loop_sleep_time = 0. 84 | 85 | def loop(self): 86 | try: 87 | if isinstance(self._sock, socket.socket): 88 | data = self._sock.recv(self._bufsize) 89 | if data is None: 90 | self.sig_exception.emit('Lost connection (null data)', True) 91 | self.stop() 92 | elif len(data) == 0: 93 | self.sig_exception.emit('Lost connection (data len=0)', True) 94 | self.stop() 95 | else: 96 | self.sig_recv.emit(data) 97 | self._queue_recv.put(data) 98 | except OSError as e: 99 | if e.args[0] == 10038: 100 | self.sig_exception.emit(str(e), False) 101 | self.stop() 102 | elif e.args[0] == 10022: 103 | self.sig_exception.emit(str(e), False) 104 | except Exception as e: 105 | self.sig_exception.emit(f'Exception ({e})', True) 106 | self.stop() 107 | 108 | 109 | class ThreadCheckRecvQueue(ThreadCommon): 110 | def __init__(self, queue_recv: queue.Queue): 111 | super().__init__(name='Socket Thread Check Recv Queue') 112 | self._queue_recv = queue_recv 113 | 114 | def loop(self): 115 | if not self._queue_recv.empty(): 116 | data = self._queue_recv.get() 117 | self.sig_recv.emit(data) 118 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/Socket/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from SocketTCPClient import TCPClient 9 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | del CURPATH 7 | 8 | from RS485Comm import RS485Comm, RS485Config, RS485HwType 9 | from PacketParser import PacketParser 10 | from EnergyParser import EnergyParser 11 | from ControlParser import ControlParser 12 | from SmartRecvParser import SmartRecvParser 13 | from SmartSendParser import SmartSendParser 14 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/smart_elevator_down_packets.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/IPark-Gwanggyo/RS485/smart_elevator_down_packets.pkl -------------------------------------------------------------------------------- /IPark-Gwanggyo/RS485/smart_elevator_up_packets.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/IPark-Gwanggyo/RS485/smart_elevator_up_packets.pkl -------------------------------------------------------------------------------- /IPark-Gwanggyo/__init__.py: -------------------------------------------------------------------------------- 1 | from app import home 2 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/app.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | from Include import get_home 3 | from web import create_webapp, config 4 | from werkzeug.debug import DebuggedApplication 5 | 6 | app_debug: bool = True 7 | 8 | app = create_webapp() 9 | app.app_context().push() 10 | app.wsgi_app = DebuggedApplication(app.wsgi_app, app_debug) 11 | app.debug = app_debug 12 | home = get_home('IPark-Gwanggyo (Bestin)') 13 | home.initRS485Connection() 14 | 15 | 16 | def onExitApp(): 17 | print("Web server is closing...") 18 | home.release() 19 | 20 | 21 | atexit.register(onExitApp) 22 | 23 | if __name__ == '__main__': 24 | app.run(host=config.HOST, port=config.PORT, debug=app_debug) 25 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask_wtf 3 | flask_moment 4 | flask_httpauth 5 | flask_bootstrap 6 | werkzeug 7 | paho-mqtt==1.6.1 8 | pyserial 9 | requests 10 | beautifulsoup4 11 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | /bin/python3 /home/pi/Project/HomeNetwork/IPark-Gwanggyo/app.py 4 | read -p "Press enter to continue..." 5 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/IPark-Gwanggyo/summary.png -------------------------------------------------------------------------------- /IPark-Gwanggyo/test/CRC8.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | REFLECT_BIT_ORDER_TABLE = [ 4 | 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 5 | 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, 6 | 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 7 | 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, 8 | 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 9 | 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 10 | 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 11 | 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 12 | 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 13 | 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 14 | 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 15 | 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, 16 | 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 17 | 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, 18 | 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 19 | 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, 20 | 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 21 | 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, 22 | 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 23 | 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, 24 | 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 25 | 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, 26 | 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 27 | 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, 28 | 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 29 | 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 30 | 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 31 | 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, 32 | 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 33 | 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 34 | 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 35 | 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF 36 | ] 37 | 38 | 39 | class CRC8: 40 | def __init__(self, polynomial: int, init_value: int, reflect_input: bool, reflect_output: bool, xor_output: int): 41 | self.polynomial = max(0, min(255, polynomial)) 42 | self.init_value = max(0, min(255, init_value)) 43 | self.reflect_input = reflect_input 44 | self.reflect_output = reflect_output 45 | self.xor_output = max(0, min(255, xor_output)) 46 | 47 | def __repr__(self): 48 | txt = 'CRC8 - Poly(0x{:02X}), Init(0x{:02X}), Ref_In({}), Ref_Out({}), XorOut(0x{:02X})'.format( 49 | self.polynomial, self.init_value, self.reflect_input, self.reflect_output, self.xor_output) 50 | return txt 51 | 52 | def calculate(self, data: Union[bytes, bytearray, str]): 53 | crc = self.init_value 54 | for i in range(len(data)): 55 | if isinstance(data, str): 56 | index = ord(data[i]) 57 | else: 58 | index = data[i] 59 | temp = REFLECT_BIT_ORDER_TABLE[index] if self.reflect_input else index 60 | crc ^= temp 61 | for j in range(8): 62 | crc = (crc << 1) ^ self.polynomial if (crc & 0x80) else (crc << 1) 63 | # crc = max(0, min(255, crc)) 64 | if self.reflect_output: 65 | crc = self.reflect(crc) 66 | crc = (crc ^ self.xor_output) & 0xFF 67 | return crc 68 | 69 | @staticmethod 70 | def reflect(value: int) -> int: 71 | reflected = 0 72 | for i in range(8): 73 | if value & 0x01: 74 | reflected |= (1 << ((8 - 1) - i)) 75 | value = (value >> 1) 76 | return reflected 77 | 78 | 79 | if __name__ == '__main__': 80 | data_ = '123456789' 81 | crc_8 = CRC8(0x07, 0x00, False, False, 0x00) 82 | print(crc_8) 83 | print('CRC-8: {:02X}'.format(crc_8.calculate(data_))) 84 | crc_8_cdma2000 = CRC8(0x9B, 0xFF, False, False, 0x00) 85 | print('CRC-8/CDMA2000: {:02X}'.format(crc_8_cdma2000.calculate(data_))) 86 | crc_8_darc = CRC8(0x39, 0x00, True, True, 0x00) 87 | print('CRC-8/DARC: {:02X}'.format(crc_8_darc.calculate(data_))) 88 | crc_8_dvb_s2 = CRC8(0xD5, 0x00, False, False, 0x00) 89 | print('CRC-8/DVB-S2: {:02X}'.format(crc_8_dvb_s2.calculate(data_))) 90 | crc_8_ebu = CRC8(0x1D, 0xFF, True, True, 0x00) 91 | print('CRC-8/EBU: {:02X}'.format(crc_8_ebu.calculate(data_))) 92 | crc_8_icode = CRC8(0x1D, 0xFD, False, False, 0x00) 93 | print('CRC-8/I-CODE: {:02X}'.format(crc_8_icode.calculate(data_))) 94 | crc_8_itu = CRC8(0x07, 0x00, False, False, 0x55) 95 | print('CRC-8/ITU: {:02X}'.format(crc_8_itu.calculate(data_))) 96 | crc_8_maxim = CRC8(0x31, 0x00, True, True, 0x00) 97 | print('CRC-8/MAXIM: {:02X}'.format(crc_8_maxim.calculate(data_))) 98 | crc_8_rohc = CRC8(0x07, 0xFF, True, True, 0x00) 99 | print('CRC-8/ROHC: {:02X}'.format(crc_8_rohc.calculate(data_))) 100 | crc_8_wcdma = CRC8(0x9B, 0x00, True, True, 0x00) 101 | print('CRC-8/WCDMA: {:02X}'.format(crc_8_wcdma.calculate(data_))) 102 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/test/parse_packet_last_byte.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pickle 4 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 5 | INCPATH = os.path.dirname(CURPATH) 6 | sys.path.extend([INCPATH]) 7 | sys.path = list(set(sys.path)) 8 | from homeDef import Home 9 | from CRC8 import CRC8 10 | 11 | home = Home(init_service=False) 12 | packets = [] 13 | packets.extend(home.parser_smart.elevator_down_packets) 14 | packets.extend(home.parser_smart.elevator_up_packets) 15 | packets = [bytearray([int(y, 16) for y in x.split(' ')]) for x in packets] 16 | 17 | polynomials = [x for x in range(256)] 18 | init_values = [x for x in range(256)] 19 | ref_ins = [False, True] 20 | ref_outs = [False, True] 21 | xor_outpus = [x for x in range(256)] 22 | """ 23 | pkl_path = os.path.abspath('./crc8list.pkl') 24 | if os.path.isfile(pkl_path): 25 | with open(pkl_path, 'rb') as fp: 26 | crc_list = pickle.load(fp) 27 | else: 28 | crc_list = [] 29 | for poly in polynomials: 30 | for init_val in init_values: 31 | for refin in ref_ins: 32 | for refout in ref_outs: 33 | for xorout in xor_outpus: 34 | crc = CRC8(poly, init_val, refin, refout, xorout) 35 | print(crc) 36 | crc_list.append(crc) 37 | with open(pkl_path, 'wb') as fp: 38 | pickle.dump(crc_list, fp) 39 | """ 40 | keep_going = True 41 | for poly in polynomials: 42 | if not keep_going: 43 | break 44 | for init_val in init_values: 45 | if not keep_going: 46 | break 47 | for refin in ref_ins: 48 | if not keep_going: 49 | break 50 | for refout in ref_outs: 51 | if not keep_going: 52 | break 53 | for xorout in xor_outpus: 54 | crc = CRC8(poly, init_val, refin, refout, xorout) 55 | print(crc) 56 | result = [crc.calculate(packet[:-1]) == packet[-1] for packet in packets] 57 | correct_cnt = sum(result) 58 | wrong_cnt = len(packets) - correct_cnt 59 | print(correct_cnt, wrong_cnt) 60 | if wrong_cnt == 0: 61 | keep_going = False 62 | if not keep_going: 63 | break 64 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | chdir = /home/pi/Project/HomeNetwork/IPark-Gwanggyo 3 | 4 | http = 0.0.0.0:1235 5 | http = 127.0.0.1:1236 6 | socket = ./uwsgi.sock 7 | chmod-socket = 666 8 | 9 | wsgi-file = ./app.py 10 | callable = app 11 | ; daemonize = ./uwsgi.log 12 | uid = bestin_server 13 | pidfile = ./uwsgi.pid 14 | 15 | ; master = true 16 | processes = 1 17 | threads = 1 18 | enable-threads = true 19 | vacuum = true 20 | disable-logging = false 21 | die-on-term = true 22 | 23 | reload-mercy = 5 24 | worker-reload-mercy = 5 25 | single-interpreter = false 26 | lazy-apps = true 27 | harakiri-verbose = false 28 | 29 | stats = 127.0.0.1:1237 30 | ignore-write-errors = true 31 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | CURPATH = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.extend([CURPATH]) 5 | sys.path = list(set(sys.path)) 6 | 7 | from flask import Flask 8 | from flask_bootstrap import Bootstrap 9 | from flask_moment import Moment 10 | from config import config 11 | 12 | bootstrap = Bootstrap() 13 | moment = Moment() 14 | 15 | 16 | def create_webapp(): 17 | app = Flask(__name__) 18 | app.config.from_object(config) 19 | 20 | config.init_app(app) 21 | bootstrap.init_app(app) 22 | moment.init_app(app) 23 | 24 | from .main import main as blueprint_main 25 | app.register_blueprint(blueprint_main) 26 | 27 | from .api import api as blueprint_api 28 | app.register_blueprint(blueprint_api, url_prefix='/api') 29 | 30 | print(f"Flask App is created ({app})") 31 | 32 | return app 33 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | api = Blueprint('api', __name__) 4 | 5 | from . import packet_logger 6 | from . import outlet_info 7 | from . import elevator 8 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/api/elevator.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from flask import render_template, jsonify, request 3 | from datetime import datetime 4 | import os 5 | import sys 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # /project/web/api/ 7 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # /project/ 8 | INCPATH = os.path.join(PROJPATH, 'Include') # /project/Include 9 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 10 | sys.path = list(set(sys.path)) 11 | from Include import get_home 12 | 13 | 14 | @api.route('/elevator', methods=['GET', 'POST']) 15 | def elevator(): 16 | home = get_home() 17 | req = request.get_data().decode(encoding='utf-8') 18 | if 'command' in req: 19 | home.elevator.call_down() 20 | 21 | return render_template( 22 | "elevator.html", 23 | time=datetime.now(), 24 | state=home.elevator.state, 25 | current_floor=home.elevator.current_floor 26 | ) 27 | 28 | 29 | @api.route('/elevator/update', methods=['GET', 'POST']) 30 | def elevator_update(): 31 | home = get_home() 32 | now = datetime.now() 33 | dev = home.elevator 34 | 35 | return jsonify({ 36 | 'time': now.strftime('%Y-%m-%d %H:%M:%S'), 37 | 'state': dev.state, 38 | 'current_floor': dev.current_floor 39 | }) 40 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/api/outlet_info.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from flask import render_template, jsonify 3 | from flask_wtf import FlaskForm 4 | from wtforms import FloatField, TextAreaField, BooleanField 5 | import os 6 | import sys 7 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # /project/web/api/ 8 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # /project/ 9 | INCPATH = os.path.join(PROJPATH, 'Include') # /project/Include 10 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 11 | sys.path = list(set(sys.path)) 12 | from Include import get_home 13 | 14 | 15 | class OutletStatusForm(FlaskForm): 16 | about_me = TextAreaField('About me') 17 | confirmed = BooleanField('Confirmed') 18 | field_1_1 = FloatField(f"Outlet {1}", validators=[], render_kw={"disabled": "disabled"}, 19 | id=f"outlet_{1}_{1}") 20 | field_1_2 = FloatField(f"Outlet {2}", validators=[], render_kw={"disabled": "disabled"}, 21 | id=f"outlet_{1}_{2}") 22 | field_1_3 = FloatField(f"Outlet {3}", validators=[], render_kw={"disabled": "disabled"}, 23 | id=f"outlet_{1}_{3}") 24 | 25 | field_2_1 = FloatField(f"Outlet {1}", validators=[], render_kw={"disabled": "disabled"}, 26 | id=f"outlet_{2}_{1}") 27 | field_2_2 = FloatField(f"Outlet {2}", validators=[], render_kw={"disabled": "disabled"}, 28 | id=f"outlet_{2}_{2}") 29 | 30 | field_3_1 = FloatField(f"Outlet {1}", validators=[], render_kw={"disabled": "disabled"}, 31 | id=f"outlet_{3}_{1}") 32 | field_3_2 = FloatField(f"Outlet {2}", validators=[], render_kw={"disabled": "disabled"}, 33 | id=f"outlet_{3}_{2}") 34 | 35 | 36 | @api.route('/outlet_info', methods=['GET', 'POST']) 37 | def outlet_info(): 38 | form = OutletStatusForm() 39 | return render_template('outlet.html', form=form) 40 | 41 | 42 | @api.route('/outlet_info/update', methods=['POST']) 43 | def outlet_update(): 44 | home = get_home() 45 | data = dict() 46 | for room in home.rooms: 47 | idx = room.index 48 | for i, o in enumerate(room.outlets): 49 | data[f'room{idx}_outlet{i + 1}'] = o.measurement 50 | return jsonify(data) 51 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/api/packet_logger.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from flask import render_template, jsonify, request 3 | import http 4 | import os 5 | import sys 6 | CURPATH = os.path.dirname(os.path.abspath(__file__)) # /project/web/api/ 7 | PROJPATH = os.path.dirname(os.path.dirname(CURPATH)) # /project/ 8 | INCPATH = os.path.join(PROJPATH, 'Include') # /project/Include 9 | sys.path.extend([CURPATH, PROJPATH, INCPATH]) 10 | sys.path = list(set(sys.path)) 11 | from Include import get_home 12 | 13 | 14 | @api.route('/packet/logger', methods=['GET', 'POST']) 15 | def packet_logger(): 16 | return render_template('packet_log.html') 17 | 18 | 19 | @api.route('/packet/logger/update', methods=['POST']) 20 | def packet_logger_update(): 21 | home = get_home() 22 | p1 = '
'.join([' '.join(['%02X' % y for y in x]) for x in home.packets_energy[::-1]]) 23 | p2 = '
'.join([' '.join(['%02X' % y for y in x]) for x in home.packets_control[::-1]]) 24 | p3 = '
'.join([' '.join(['%02X' % y for y in x]) for x in home.packets_smart_recv[::-1]]) 25 | return jsonify({'energy': p1, 'control': p2, 'smart_recv': p3}) 26 | 27 | 28 | @api.route('/packet/logger/clear/', methods=['POST']) 29 | def packet_log_clear(target): 30 | home = get_home() 31 | if target == 'energy': 32 | home.packets_energy.clear() 33 | elif target == 'control': 34 | home.packets_control.clear() 35 | elif target == 'smart_recv': 36 | home.packets_smart_recv.clear() 37 | return '', http.HTTPStatus.NO_CONTENT 38 | 39 | 40 | @api.route('/packet/logger//enable/', methods=['POST']) 41 | def packet_log_enable(device, target): 42 | home = get_home() 43 | req = request.get_data().decode(encoding='utf-8') 44 | value = int(req[6:].strip()) if 'value=' in req else 1 45 | if device == 'energy': 46 | if target == '31': 47 | home.enable_log_energy_31 = bool(value) 48 | elif target == '41': 49 | home.enable_log_energy_41 = bool(value) 50 | elif target == '42': 51 | home.enable_log_energy_42 = bool(value) 52 | elif target == 'D1': 53 | home.enable_log_energy_d1 = bool(value) 54 | elif target == 'room1': 55 | home.enable_log_energy_room_1 = bool(value) 56 | elif target == 'room2': 57 | home.enable_log_energy_room_2 = bool(value) 58 | elif target == 'room3': 59 | home.enable_log_energy_room_3 = bool(value) 60 | home.packets_energy.clear() 61 | elif device == 'control': 62 | if target == '28': 63 | home.enable_log_control_28 = bool(value) 64 | elif target == '31': 65 | home.enable_log_control_31 = bool(value) 66 | elif target == '61': 67 | home.enable_log_control_61 = bool(value) 68 | home.packets_control.clear() 69 | return '', http.HTTPStatus.NO_CONTENT 70 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOGYUI/HomeNetwork/4e0db2f32ab26aedd19087062f2fdcd4811d98e3/IPark-Gwanggyo/web/auth/__init__.py -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | import xml.etree.ElementTree as ET 4 | 5 | 6 | class Config: 7 | HOST: str = '0.0.0.0' 8 | PORT: int = 9999 9 | 10 | SECRET_KEY = 'Yogyui Secret Key' # for CSRF 11 | 12 | def init_app(self, app: Flask): 13 | curpath = os.path.dirname(os.path.abspath(__file__)) # /project/web 14 | projpath = os.path.dirname(curpath) # /project 15 | xml_path = os.path.join(projpath, 'config.xml') 16 | 17 | try: 18 | if os.path.isfile(xml_path): 19 | root = ET.parse(xml_path).getroot() 20 | node = root.find('webserver') 21 | node_host = node.find('host') 22 | self.HOST = node_host.text 23 | node_port = node.find('port') 24 | self.PORT = int(node_port.text) 25 | except Exception as e: 26 | print(f'Config - Exception {e}') 27 | 28 | 29 | config = Config() 30 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | from . import views, errors 6 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/main/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | from . import main 3 | 4 | 5 | @main.app_errorhandler(403) 6 | def forbidden(_): 7 | if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: 8 | response = jsonify({'error': 'forbidden'}) 9 | response.status_code = 403 10 | return response 11 | return render_template('403.html'), 403 12 | 13 | 14 | @main.app_errorhandler(404) 15 | def page_not_found(_): 16 | if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: 17 | response = jsonify({'error': 'not found'}) 18 | response.status_code = 404 19 | return response 20 | return render_template('404.html'), 404 21 | 22 | 23 | @main.app_errorhandler(500) 24 | def internal_server_error(_): 25 | if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: 26 | response = jsonify({'error': 'internal server error'}) 27 | response.status_code = 500 28 | return response 29 | return render_template('500.html'), 500 30 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/main/views.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | from flask import render_template 3 | 4 | 5 | @main.route('/', methods=['GET', 'POST']) 6 | def index(): 7 | return render_template('index.html') 8 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/static/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | margin-bottom: 40px; 8 | } 9 | 10 | .footer { 11 | position: absolute; 12 | bottom: 0; 13 | width: 100%; 14 | height: 40px; 15 | background-color: #f5f5f5; 16 | text-align: center; 17 | padding: 10px; 18 | } 19 | 20 | .navbar-content { 21 | background-color: #68B950; 22 | } 23 | 24 | .navbar-brand { 25 | flex: 0 0 auto; 26 | margin: 0; 27 | padding: 0; 28 | height: 100%; 29 | } 30 | 31 | .navbar-inverse { 32 | background-color: #000000; 33 | } 34 | 35 | .navbar-inverse .navbar-brand { 36 | color: #ffffff; 37 | } -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | 3 | {% block title %}YOGYUI 광교 아이파크{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block navbar %} 14 | 52 | {% endblock %} 53 | 54 | {% block content %} 55 |
56 | {% for message in get_flashed_messages() %} 57 |
58 | 59 | {{ message }} 60 |
61 | {% endfor %} 62 | 63 | {% block page_content %} 64 | {% endblock %} 65 |
66 | 67 |
68 |
69 |

Designed by YOGYUI

70 |
71 |
72 | {% endblock %} 73 | 74 | {% block scripts %} 75 | {{ super() }} 76 | {{ moment.include_moment() }} 77 | {% endblock %} -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/elevator.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block page_content %} 5 | 8 |
9 |

[ Current Time]

10 |

11 | {% if time %} 12 | {{ time }} 13 | {% endif %} 14 |

15 |

[ State ]

16 |

17 | {% if state == 1 %} 18 | MOVING 19 | {% elif state == 4 %} 20 | ARRIVED 21 | {% else %} 22 | STOPPED 23 | {% endif %} 24 |

25 | 26 |

[ Current Floor ]

27 |

28 | {% if current_floor %} 29 | {{ current_floor }} 30 | {% endif %} 31 |

32 | 33 |
34 | 35 |
36 |
37 | {% endblock %} 38 | 39 | {% block scripts %} 40 | {{ super() }} 41 | 61 | {% endblock %} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block page_content %} 5 | 13 |
14 | 25 |

Packet Logger

26 |

Outlet Status

27 |

Elevator

28 |
29 | {% endblock %} 30 | 31 | {% block scripts %} 32 | {{ super() }} 33 | {% endblock %} -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/outlet.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% from "render.html" import render_field %} 4 | 5 | {% block page_content %} 6 | 9 | 10 |
11 | {{ wtf.quick_form(form, id="form") }} 12 |
13 | {% endblock %} 14 | 15 | {% block scripts %} 16 | {{ super() }} 17 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/packet_sender.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Packet Sender 5 | 6 | 7 | Enerygy 8 |
9 | {% if packet %} 10 | 11 | {% else %} 12 | 13 | {% endif %} 14 | 15 |
16 | {% if result %} 17 | {{result}} 18 | {% endif %} 19 |
20 | Control 21 | 22 | 23 | -------------------------------------------------------------------------------- /IPark-Gwanggyo/web/templates/render.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |

3 | {{ field.label }} 4 | {{ field(**kwargs)|safe }} 5 | {% if field.errors %} 6 |

    7 | {% for error in field.errors %} 8 |
  • {{ error }}
  • 9 | {% endfor %} 10 |
11 | {% endif %} 12 |

13 | {% endmacro %} 14 | -------------------------------------------------------------------------------- /RS485PacketCapture/Capture.py: -------------------------------------------------------------------------------- 1 | from SerialComm import * 2 | 3 | 4 | class ParserLight: 5 | buffer: bytearray 6 | max_buffer_size: int = 200 7 | 8 | def __init__(self, ser: SerialComm): 9 | self.buffer = bytearray() 10 | self.serial = ser 11 | self.serial.sig_recv_data.connect(self.onRecvData) 12 | 13 | def onRecvData(self, data: bytes): 14 | if len(self.buffer) > self.max_buffer_size: 15 | self.buffer.clear() 16 | self.buffer.extend(data) 17 | self.parseBuffer() 18 | 19 | def parseBuffer(self): 20 | idx = self.buffer.find(0xF7) 21 | if idx > 0: 22 | self.buffer = self.buffer[idx:] 23 | if len(self.buffer) >= 3: 24 | packet_length = self.buffer[1] 25 | if len(self.buffer) >= packet_length: 26 | if self.buffer[packet_length - 1] == 0xEE: 27 | packet = self.buffer[:packet_length] 28 | packet_str = ' '.join(['%02X' % x for x in packet]) 29 | if packet[2] == 0x01: 30 | if packet[3] == 0x19: # 조명 상태 31 | header = packet[4] 32 | room_idx = packet[6] >> 4 33 | 34 | if header == 0x01: # 쿼리/명령 35 | if room_idx == 1: # 거실 36 | # print(packet_str) 37 | pass 38 | elif room_idx == 2: # 침실(방1) 39 | # print(packet_str) 40 | pass 41 | elif room_idx == 3: # 서재(방2) 42 | # print(packet_str) 43 | pass 44 | elif room_idx == 4: # 컴퓨터방 (방3) 45 | # print(packet_str) 46 | pass 47 | elif room_idx == 6: # 주방 48 | print(packet_str) 49 | pass 50 | elif header == 0x04: # 응답 51 | light_count = packet_length - 10 52 | # print(f'room {room_idx} light count: {light_count}') 53 | else: 54 | print(packet_str) 55 | elif packet[3] == 0x1F: 56 | # print(f'????: {packet_str}') 57 | pass 58 | else: 59 | print(packet_str) 60 | else: 61 | print(packet_str) 62 | self.buffer = self.buffer[packet_length:] 63 | 64 | 65 | 66 | if __name__ == '__main__': 67 | ser = SerialComm() 68 | parser = ParserLight(ser) 69 | 70 | def printMenu(): 71 | if ser.isConnected(): 72 | print('Connected ({}, {})'.format(ser.port, ser.baudrate)) 73 | print('0: Disconnect, 1: Test-1, 2: Test-2, 3: Terminate') 74 | else: 75 | print('0: Connect, 1: Terminate') 76 | 77 | def loop(): 78 | os.system('clear') 79 | printMenu() 80 | sysin = sys.stdin.readline() 81 | try: 82 | head = int(sysin.split('\n')[0]) 83 | except Exception: 84 | loop() 85 | return 86 | 87 | if ser.isConnected(): 88 | if head == 0: 89 | ser.disconnect() 90 | loop() 91 | elif head == 1: 92 | ser.sendData(bytearray([0xF7, 0x0B, 0x01, 0x19, 0x02, 0x40, 0x41, 0x01, 0x00, 0xE6, 0xEE])) 93 | loop() 94 | elif head == 2: 95 | ser.sendData(bytearray([0xF7, 0x0B, 0x01, 0x19, 0x02, 0x40, 0x41, 0x02, 0x00, 0xE5, 0xEE])) 96 | loop() 97 | elif head == 3: 98 | ser.release() 99 | else: 100 | loop() 101 | else: 102 | if head == 0: 103 | """ 104 | print('Port: ') 105 | sysin = sys.stdin.readline() 106 | try: 107 | port = sysin.split('\n')[0] 108 | except Exception: 109 | port = '/dev/ttyUSB0' 110 | 111 | print('Baud Rate: ') 112 | sysin = sys.stdin.readline() 113 | try: 114 | baud = int(sysin.split('\n')[0]) 115 | except Exception: 116 | baud = 9600 117 | 118 | ser.connect(port, baud) 119 | """ 120 | ser.connect('/dev/ttyUSB0', 9600) 121 | loop() 122 | elif head == 1: 123 | ser.release() 124 | else: 125 | loop() 126 | 127 | loop() -------------------------------------------------------------------------------- /RS485PacketCapture/Checksum.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | packet_string_list = [ 4 | 'F7 0B 01 19 01 40 10 00 00 B5 EE', 5 | 'F7 0B 01 19 02 40 11 01 00 B6 EE', 6 | 'F7 0B 01 19 02 40 12 01 00 B5 EE', 7 | 'F7 0B 01 19 01 40 20 00 00 85 EE', 8 | 'F7 0B 01 19 01 40 30 00 00 95 EE', 9 | 'F7 0B 01 19 01 40 40 00 00 E5 EE', 10 | 'F7 0B 01 19 01 40 60 00 00 C5 EE', 11 | 'F7 0C 01 19 04 40 60 00 02 02 C7 EE', 12 | 'F7 0D 01 19 04 40 10 00 01 01 01 B7 EE', 13 | 'F7 0B 01 19 04 40 40 00 02 E2 EE', 14 | 'F7 0B 01 1F 01 40 60 00 00 C3 EE', 15 | 'F7 1C 01 1F 04 40 60 00 61 01 00 00 00 00 00 00 02 62 01 00 00 00 00 00 00 02 D2 EE' 16 | ] 17 | 18 | def convert(byte_str: str): 19 | return bytearray([int(x, 16) for x in byte_str.split(' ')]) 20 | 21 | packets = [convert(x)for x in packet_string_list] 22 | 23 | def calc(packet: bytearray): 24 | checksum_in_packet = packet[-2] 25 | """ 26 | checksum_calc = 0 27 | for i in range(0, len(packet) - 2): 28 | checksum_calc = checksum_calc ^ packet[i] 29 | """ 30 | checksum_calc = reduce(lambda x, y: x ^ y, packet[:-2], 0) 31 | print('checksum_in_packet: %02X, checksum_calc: %02X' % (checksum_in_packet, checksum_calc)) 32 | 33 | for p in packets: 34 | calc(p) 35 | -------------------------------------------------------------------------------- /RS485PacketCapture/Define.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import threading 3 | 4 | 5 | def checkAgrumentType(obj, arg): 6 | if type(obj) == arg: 7 | return True 8 | if arg == object: 9 | return True 10 | if arg in obj.__class__.__bases__: 11 | return True 12 | return False 13 | 14 | 15 | class Callback(object): 16 | _args = None 17 | _callback = None 18 | 19 | def __init__(self, *args): 20 | self._args = args 21 | 22 | def connect(self, callback): 23 | self._callback = callback 24 | 25 | def disconnect(self): 26 | self._callback = None 27 | 28 | def emit(self, *args): 29 | if len(args) != len(self._args): 30 | raise Exception('Callback::Argument Length Mismatch') 31 | arglen = len(args) 32 | if arglen > 0: 33 | validTypes = [checkAgrumentType(args[i], self._args[i]) for i in range(arglen)] 34 | if sum(validTypes) != arglen: 35 | raise Exception('Callback::Argument Type Mismatch (Definition: {}, Call: {})'.format(self._args, args)) 36 | if self._callback is not None: 37 | self._callback(*args) 38 | 39 | 40 | def timestampToString(timestamp: datetime.datetime): 41 | h = timestamp.hour 42 | m = timestamp.minute 43 | s = timestamp.second 44 | us = timestamp.microsecond 45 | return '%02d:%02d:%02d.%06d' % (h, m, s, us) 46 | 47 | 48 | def getCurTimeStr(): 49 | return '<%s>' % timestampToString(datetime.datetime.now()) 50 | 51 | 52 | def writeLog(strMsg: str, obj: object = None): 53 | strTime = getCurTimeStr() 54 | if obj is not None: 55 | if isinstance(obj, threading.Thread): 56 | if obj.ident is not None: 57 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, obj.ident) 58 | else: 59 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 60 | else: 61 | strObj = ' [%s (0x%X)]' % (type(obj).__name__, id(obj)) 62 | else: 63 | strObj = '' 64 | 65 | msg = strTime + strObj + ' ' + strMsg 66 | print(msg) 67 | -------------------------------------------------------------------------------- /RS485PacketCapture/SerialThreads.py: -------------------------------------------------------------------------------- 1 | import time 2 | import queue 3 | import serial 4 | import threading 5 | import traceback 6 | from Define import Callback, writeLog 7 | 8 | 9 | class ThreadSend(threading.Thread): 10 | _keepAlive: bool = True 11 | 12 | def __init__(self, serial_: serial.Serial, queue_: queue.Queue): 13 | threading.Thread.__init__(self) 14 | self.sig_send_data = Callback(bytes) 15 | self.sig_terminated = Callback() 16 | self.sig_exception = Callback(str) 17 | self._serial = serial_ 18 | self._queue = queue_ 19 | 20 | def run(self): 21 | writeLog('Started', self) 22 | while self._keepAlive: 23 | try: 24 | if not self._queue.empty(): 25 | data = self._queue.get() 26 | sendLen = len(data) 27 | while sendLen > 0: 28 | nLen = self._serial.write(data[(len(data) - sendLen):]) 29 | sData = data[(len(data) - sendLen):(len(data) - sendLen + nLen)] 30 | self.sig_send_data.emit(sData) 31 | sendLen -= nLen 32 | else: 33 | time.sleep(1e-3) 34 | except Exception as e: 35 | writeLog('Exception::{}'.format(e), self) 36 | traceback.print_exc() 37 | self.sig_exception.emit(str(e)) 38 | writeLog('Terminated', self) 39 | self.sig_terminated.emit() 40 | 41 | def stop(self): 42 | self._keepAlive = False 43 | 44 | 45 | class ThreadReceive(threading.Thread): 46 | _keepAlive: bool = True 47 | 48 | def __init__(self, serial_: serial.Serial, queue_: queue.Queue): 49 | threading.Thread.__init__(self) 50 | self.sig_terminated = Callback() 51 | # self.sig_recv_data = Callback(bytes) 52 | self.sig_recv_data = Callback() 53 | self.sig_exception = Callback(str) 54 | self._serial = serial_ 55 | self._queue = queue_ 56 | 57 | def run(self): 58 | writeLog('Started', self) 59 | while self._keepAlive: 60 | try: 61 | if self._serial.isOpen(): 62 | if self._serial.in_waiting > 0: 63 | rcv = self._serial.read(self._serial.in_waiting) 64 | # self.sig_recv_data.emit(rcv) 65 | self.sig_recv_data.emit() 66 | self._queue.put(rcv) 67 | else: 68 | time.sleep(1e-3) 69 | else: 70 | time.sleep(1e-3) 71 | except Exception as e: 72 | writeLog(f'Exception::{self._serial.port}::{e}', self) 73 | traceback.print_exc() 74 | self.sig_exception.emit(str(e)) 75 | # break 76 | writeLog('Terminated', self) 77 | self.sig_terminated.emit() 78 | 79 | def stop(self): 80 | self._keepAlive = False 81 | 82 | 83 | class ThreadCheck(threading.Thread): 84 | _keepAlive: bool = True 85 | 86 | def __init__(self, queue_: queue.Queue): 87 | threading.Thread.__init__(self) 88 | self.sig_get = Callback(bytes) 89 | self.sig_terminated = Callback() 90 | self.sig_exception = Callback(str) 91 | self._queue = queue_ 92 | 93 | def run(self): 94 | writeLog('Started', self) 95 | while self._keepAlive: 96 | try: 97 | if not self._queue.empty(): 98 | self.sig_get.emit(self._queue.get()) 99 | else: 100 | time.sleep(1e-3) 101 | except Exception as e: 102 | writeLog('Exception::{}'.format(e), self) 103 | traceback.print_exc() 104 | self.sig_exception.emit(str(e)) 105 | writeLog('Terminated', self) 106 | self.sig_terminated.emit() 107 | 108 | def stop(self): 109 | self._keepAlive = False 110 | --------------------------------------------------------------------------------