├── .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('' + elem.tag + '>\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 + '' + elem.tag + '>\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 |
15 |
16 |
27 |
28 | {% if session['o365_user_info'] %}
29 | {% endif %}
30 |
38 |
39 |
48 |
49 |
50 |
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 |
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 |
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 |
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 | 
27 |
28 | Assembly Drawing
29 | -------------
30 | 
--------------------------------------------------------------------------------
/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 |
15 |
16 |
27 |
28 | {% if session['o365_user_info'] %}
29 | {% endif %}
30 |
33 |
36 |
39 |
40 |
49 |
50 |
51 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------