Implement control flow in main
Implement read write of dev file, add example Change debug text Implement devices class with push button and thermostat Implement pairing with thermostats
This commit is contained in:
parent
94c613abe9
commit
c5a7a8699c
@ -18,7 +18,7 @@ class MAXCube:
|
||||
self.client.write(req.encode())
|
||||
self.client.write(b"\n")
|
||||
if DEBUG:
|
||||
print(">>> {}".format(req))
|
||||
print("SENT: {}".format(req))
|
||||
else:
|
||||
print("Request while not connected!")
|
||||
|
||||
@ -51,7 +51,7 @@ class CUN(MAXCube):
|
||||
self.client.write(req.encode())
|
||||
self.client.write(b"\n")
|
||||
if DEBUG:
|
||||
print(">>> {}".format(req))
|
||||
print("SENT: {}".format(req))
|
||||
else:
|
||||
print("Request while not connected!")
|
||||
|
||||
@ -59,7 +59,7 @@ class CUN(MAXCube):
|
||||
if self.client is not None:
|
||||
response = self.client.read_some().decode()
|
||||
if DEBUG:
|
||||
print("<<< {}".format(response))
|
||||
print("RECV: {}".format(response))
|
||||
return response
|
||||
else:
|
||||
print("Waiting for response while not connected!")
|
||||
@ -80,7 +80,7 @@ class CUL(MAXCube):
|
||||
if self.client is not None:
|
||||
response = self.client.read(100).decode()
|
||||
if DEBUG:
|
||||
print("<<< {}".format(response))
|
||||
print("RECV: {}".format(response))
|
||||
return response
|
||||
else:
|
||||
print("Waiting for response while not connected!")
|
||||
|
33
MAXDevice.py
33
MAXDevice.py
@ -2,11 +2,36 @@ from MAXPacket import PushButtonState
|
||||
|
||||
|
||||
class MAXDevice:
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
def __init__(self, name, address):
|
||||
self.address: str = address
|
||||
self.name: str = name
|
||||
|
||||
def to_string(self):
|
||||
return "{};{}".format(self.name, self.address.lower())
|
||||
|
||||
def from_string(line):
|
||||
splitted = line.split(';')
|
||||
if len(splitted) >= 3:
|
||||
if splitted[0] == "1":
|
||||
new_dev = MAXThermostat(splitted[1], splitted[2].lower())
|
||||
return new_dev
|
||||
elif splitted[0] == "5":
|
||||
new_dev = MAXPushButton(splitted[1], splitted[2].lower())
|
||||
return new_dev
|
||||
|
||||
|
||||
class MAXPushButton(MAXDevice):
|
||||
def __init__(self, address):
|
||||
super(MAXPushButton, self).__init__(address)
|
||||
def __init__(self, name, address):
|
||||
super(MAXPushButton, self).__init__(name, address)
|
||||
self.state = PushButtonState.UNKNOWN
|
||||
|
||||
def serialize(self):
|
||||
return "5;{}".format(self.to_string())
|
||||
|
||||
|
||||
class MAXThermostat(MAXDevice):
|
||||
def __init__(self, name, address):
|
||||
super(MAXThermostat, self).__init__(name, address)
|
||||
|
||||
def serialize(self):
|
||||
return "1;{}".format(self.to_string())
|
||||
|
91
MAXPacket.py
91
MAXPacket.py
@ -1,6 +1,7 @@
|
||||
import math
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MAXPacketFactory:
|
||||
def create_packet(rec: str):
|
||||
pkt_type = int(rec[7:9], 16)
|
||||
@ -18,6 +19,8 @@ class MAXPacketFactory:
|
||||
return MAXResetPacket(rec)
|
||||
elif pkt_type == 0x50:
|
||||
return MAXPushButtonPacket(rec)
|
||||
elif pkt_type == 0x60:
|
||||
return MAXThermostatStatePacket(rec)
|
||||
else:
|
||||
print("Unknown message type: {}".format(pkt_type))
|
||||
result = MAXPacket()
|
||||
@ -97,14 +100,52 @@ class MAXPairPongPacket(MAXPacket):
|
||||
class MAXSetGroupIdPacket(MAXPacket):
|
||||
def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str,
|
||||
group_id: str):
|
||||
self.set_values(message_counter, "01", message_flag, sender_address, dest_address, group_id)
|
||||
self.payload = "00"
|
||||
self.set_values(message_counter, "22", message_flag, sender_address, dest_address, group_id)
|
||||
self.payload = "01"
|
||||
|
||||
def to_string(self):
|
||||
result = "{}\nMAXSetGroupIdPacket: payload={}".format(super().to_string(), self.payload)
|
||||
return result
|
||||
|
||||
|
||||
class MAXRadiatorControlMode(Enum):
|
||||
AUTO = 0,
|
||||
MANUAL = 1,
|
||||
TEMPORARY = 2,
|
||||
BOOST = 3
|
||||
|
||||
|
||||
class MAXSetTempPacket(MAXPacket):
|
||||
def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str,
|
||||
group_id: str, temp: float, mode: MAXRadiatorControlMode):
|
||||
self.set_values(message_counter, "40", message_flag, sender_address, dest_address, group_id)
|
||||
|
||||
if temp > 30.5:
|
||||
temp = 30.5
|
||||
elif temp < 4.5:
|
||||
temp = 4.5
|
||||
|
||||
payload_int = round(temp*2).to_bytes(1, 'big')[0]
|
||||
payload_int = payload_int | ((mode.value[0] & 3) << 6)
|
||||
self.payload = format(payload_int, 'x')
|
||||
|
||||
def to_string(self):
|
||||
result = "{}\nMAXSetTempPacket: payload={}".format(super().to_string(), self.payload)
|
||||
return result
|
||||
|
||||
|
||||
class MAXAddLinkPartnerPacket(MAXPacket):
|
||||
def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str,
|
||||
group_id: str, partner_addr: str, partner_type: int):
|
||||
self.set_values(message_counter, "40", message_flag, sender_address, dest_address, group_id)
|
||||
type_str = format(partner_type, 'x').zfill(2)
|
||||
self.payload = "{}{}".format(partner_addr, type_str)
|
||||
|
||||
def to_string(self):
|
||||
result = "{}\nMAXAddLinkPartnerPacket: payload={}".format(super().to_string(), self.payload)
|
||||
return result
|
||||
|
||||
|
||||
class MAXAckPacket(MAXPacket):
|
||||
def __init__(self, rec):
|
||||
super().__init__()
|
||||
@ -118,10 +159,54 @@ class MAXAckPacket(MAXPacket):
|
||||
return result
|
||||
|
||||
|
||||
class MAXThermostatStatePacket(MAXPacket):
|
||||
def __init__(self, rec):
|
||||
super().__init__()
|
||||
self.from_received(rec)
|
||||
payload_1 = int(rec[23:25], 16)
|
||||
payload_2 = int(rec[25:27], 16)
|
||||
payload_3 = int(rec[27:29], 16)
|
||||
payload_4 = int(rec[29:31], 16)
|
||||
payload_5 = int(rec[31:33], 16)
|
||||
|
||||
ctrl_mode_bits = payload_1 & 3
|
||||
if ctrl_mode_bits == 0:
|
||||
self.ctrl_mode = MAXRadiatorControlMode.AUTO
|
||||
elif ctrl_mode_bits == 1:
|
||||
self.ctrl_mode = MAXRadiatorControlMode.MANUAL
|
||||
elif ctrl_mode_bits == 2:
|
||||
self.ctrl_mode = MAXRadiatorControlMode.TEMPORARY
|
||||
elif ctrl_mode_bits == 3:
|
||||
self.ctrl_mode = MAXRadiatorControlMode.BOOST
|
||||
else:
|
||||
print("Invalid control mode: {}".format(ctrl_mode_bits))
|
||||
|
||||
self.dst_active = (payload_1 & 0b100) >> 2
|
||||
self.lan_gateway = (payload_1 & 0b1000) >> 3
|
||||
self.locked = (payload_1 & 0b10000) >> 4
|
||||
self.rf_error = (payload_1 & 0b100000) >> 5
|
||||
self.bat_low = (payload_1 & 0b1000000) >> 6
|
||||
self.valve_pos = payload_2
|
||||
self.target_temp = (payload_3 & 0x7f) / 2
|
||||
self.current_temp = payload_4 & 1
|
||||
self.current_temp = self.current_temp << 8
|
||||
self.current_temp = self.current_temp | (payload_5 & 0xff)
|
||||
self.current_temp = self.current_temp / 10.0
|
||||
|
||||
|
||||
def to_string(self):
|
||||
result = "{}\nMAXThermostatStatePacket: dst_active={} lan_gateway={} locked={} rf_error={} bat_low={} " \
|
||||
"valve_pos={} target_temp={} " \
|
||||
"current_temp={}".format(super().to_string(), self.dst_active, self.lan_gateway,
|
||||
self.locked, self.rf_error, self.bat_low, self.valve_pos,
|
||||
self.target_temp, self.current_temp)
|
||||
return result
|
||||
|
||||
|
||||
class MAXCubeAckPacket(MAXPacket):
|
||||
def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str,
|
||||
group_id: str, acknowledged):
|
||||
self.set_values(message_counter, "01", message_flag, sender_address, dest_address, group_id)
|
||||
self.set_values(message_counter, "02", message_flag, sender_address, dest_address, group_id)
|
||||
if acknowledged:
|
||||
self.payload = "01"
|
||||
else:
|
||||
|
@ -1,9 +1,9 @@
|
||||
from typing import List
|
||||
from MAXCube import MAXCube
|
||||
from MAXPacket import MAXPacket, MAXPacketFactory, MAXPairPingPacket, MAXPairPongPacket, MAXAckPacket, \
|
||||
MAXResetPacket, MAXWakeUpPacket, MAXSetGroupIdPacket, MAXPushButtonPacket, MAXCubeAckPacket
|
||||
from MAXDevice import MAXDevice, MAXPushButton
|
||||
|
||||
from time import sleep
|
||||
MAXSetGroupIdPacket, MAXPushButtonPacket, MAXCubeAckPacket, MAXSetTempPacket, \
|
||||
MAXRadiatorControlMode, MAXAddLinkPartnerPacket
|
||||
from MAXDevice import MAXDevice, MAXPushButton, MAXThermostat
|
||||
from enum import Enum
|
||||
|
||||
|
||||
@ -13,6 +13,10 @@ class HandshakeState(Enum):
|
||||
PONG_ACK = 3
|
||||
GROUP_ID_SENT = 4
|
||||
GROUP_ID_ACK = 5
|
||||
CONFIG_TEMP_SENT = 6
|
||||
CONFIG_TEMP_ACK = 7
|
||||
ADD_LINK_PARTNER_SENT = 8
|
||||
ADD_LINK_PARTNER_ACK = 9
|
||||
|
||||
|
||||
class Handshake:
|
||||
@ -23,11 +27,11 @@ class Handshake:
|
||||
|
||||
|
||||
class MAXPacketHandler:
|
||||
def __init__(self, cube: MAXCube):
|
||||
def __init__(self, cube, devices: MAXCube):
|
||||
self.cube = cube
|
||||
self.quit_flag = False
|
||||
self.handshakes = []
|
||||
self.devices: list[MAXDevice] = []
|
||||
self.devices: List[MAXDevice] = devices
|
||||
|
||||
def handle_msg(self, pkt: MAXPacket):
|
||||
print(pkt.to_string())
|
||||
@ -38,7 +42,7 @@ class MAXPacketHandler:
|
||||
pong = MAXPairPongPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr,
|
||||
dest_address=pkt.sender_address, group_id="00")
|
||||
pong_str = pong.serialize()
|
||||
#print(pong.to_string())
|
||||
print(pong.to_string())
|
||||
self.cube.request(pong_str)
|
||||
handshake.state = HandshakeState.PONG_SENT
|
||||
elif isinstance(pkt, MAXAckPacket):
|
||||
@ -50,29 +54,74 @@ class MAXPacketHandler:
|
||||
if cur_handshake is not None:
|
||||
if cur_handshake.state == HandshakeState.PONG_SENT:
|
||||
cur_handshake.state = HandshakeState.PONG_ACK
|
||||
print(pkt.to_string())
|
||||
#print(pkt.to_string())
|
||||
if cur_handshake.dev_type == "05":
|
||||
# Handshake is finished
|
||||
print("Paired device with addr={} and type={}".format(cur_handshake.partner_addr,
|
||||
cur_handshake.dev_type))
|
||||
found = False
|
||||
for dev in self.devices:
|
||||
if dev.address == cur_handshake.partner_addr:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
new_dev = MAXPushButton("PushButton_{}".format(cur_handshake.partner_addr),
|
||||
cur_handshake.partner_addr)
|
||||
self.devices.append(new_dev)
|
||||
self.handshakes.remove(cur_handshake)
|
||||
|
||||
else:
|
||||
set_group_id = MAXSetGroupIdPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr,
|
||||
dest_address=pkt.sender_address, group_id="00")
|
||||
set_group_id_str = set_group_id.serialize()
|
||||
print(set_group_id_str)
|
||||
print(set_group_id.to_string())
|
||||
self.cube.request(set_group_id_str)
|
||||
cur_handshake.state = HandshakeState.GROUP_ID_SENT
|
||||
return
|
||||
if cur_handshake.state == HandshakeState.GROUP_ID_SENT:
|
||||
cur_handshake.state = HandshakeState.GROUP_ID_ACK
|
||||
|
||||
set_temp = MAXSetTempPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr,
|
||||
dest_address=pkt.sender_address, group_id="00", temp=21.5,
|
||||
mode=MAXRadiatorControlMode.MANUAL)
|
||||
set_temp_str = set_temp.serialize()
|
||||
print(set_temp.to_string())
|
||||
self.cube.request(set_temp_str)
|
||||
|
||||
cur_handshake.state = HandshakeState.CONFIG_TEMP_SENT
|
||||
return
|
||||
if cur_handshake.state == HandshakeState.CONFIG_TEMP_SENT:
|
||||
cur_handshake.state = HandshakeState.CONFIG_TEMP_ACK
|
||||
|
||||
add_link = MAXAddLinkPartnerPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr,
|
||||
dest_address=pkt.sender_address, group_id="00", partner_addr=self.cube.addr, partner_type=0)
|
||||
add_link_str = add_link.serialize()
|
||||
print(add_link.to_string())
|
||||
self.cube.request(add_link_str)
|
||||
cur_handshake.state = HandshakeState.ADD_LINK_PARTNER_SENT
|
||||
return
|
||||
if cur_handshake.state == HandshakeState.ADD_LINK_PARTNER_SENT:
|
||||
cur_handshake.state = HandshakeState.ADD_LINK_PARTNER_ACK
|
||||
found = False
|
||||
for dev in self.devices:
|
||||
if dev.address == cur_handshake.partner_addr:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
new_dev = MAXThermostat("Thermostat_{}".format(cur_handshake.partner_addr),
|
||||
cur_handshake.partner_addr)
|
||||
self.devices.append(new_dev)
|
||||
print("Paired device with addr={} and type={}".format(cur_handshake.partner_addr,
|
||||
cur_handshake.dev_type))
|
||||
self.handshakes.remove(cur_handshake)
|
||||
return
|
||||
elif isinstance(pkt, MAXPushButtonPacket):
|
||||
dev = None
|
||||
for known_dev in self.devices:
|
||||
if known_dev.address == pkt.sender_address:
|
||||
dev = known_dev
|
||||
break
|
||||
if dev is None:
|
||||
dev = MAXPushButton(pkt.sender_address)
|
||||
if dev is not None:
|
||||
dev.state = pkt.button_state
|
||||
print("Button {} state={}".format(dev.address, dev.state))
|
||||
if not pkt.retransmit:
|
||||
@ -80,14 +129,7 @@ class MAXPacketHandler:
|
||||
pkt.sender_address, pkt.group_id, True)
|
||||
act_str = act_pkt.serialize()
|
||||
#print(act_str)
|
||||
self.cube.request(act_str)
|
||||
#self.cube.request(act_str)
|
||||
#ToDo: Does not work properly
|
||||
else:
|
||||
print("Paket is not ours!")
|
||||
|
||||
def receive_loop(self):
|
||||
while not self.quit_flag and self.cube.is_connected():
|
||||
resp = self.cube.response()
|
||||
if resp is not None and resp[0:1] == "Z":
|
||||
pkt = MAXPacketFactory.create_packet(resp)
|
||||
self.handle_msg(pkt)
|
||||
sleep(0.1)
|
6
devices.conf
Normal file
6
devices.conf
Normal file
@ -0,0 +1,6 @@
|
||||
5;PushButton_085757;085757
|
||||
1;ku_tuer;1aaefe
|
||||
1;wz_links;1babec
|
||||
1;wz_rechts;1ae849
|
||||
1;ku_fenster;1babb6
|
||||
1;bz;1babdc
|
56
main.py
56
main.py
@ -1,12 +1,62 @@
|
||||
from typing import List
|
||||
from MAXDevice import MAXDevice, MAXThermostat, MAXPushButton
|
||||
from MAXCube import CUL
|
||||
from MAXPacketHandler import MAXPacketHandler
|
||||
from MAXPacket import MAXPacketFactory
|
||||
from time import sleep
|
||||
import sys
|
||||
import signal
|
||||
|
||||
DEBUG = True
|
||||
|
||||
devices: List[MAXDevice] = []
|
||||
quit_flag = False
|
||||
cube: CUL = None
|
||||
handler: MAXPacketHandler = None
|
||||
|
||||
|
||||
def read_devices(file_name):
|
||||
file = open(file_name, "r")
|
||||
lines = file.readlines()
|
||||
file.close()
|
||||
|
||||
for line in lines:
|
||||
if not line.startswith("#") and line.strip() != "\n":
|
||||
new_dev = MAXDevice.from_string(line)
|
||||
if new_dev is not None:
|
||||
devices.append(new_dev)
|
||||
|
||||
|
||||
def write_devices(file_name):
|
||||
file = open(file_name, "w")
|
||||
for device in devices:
|
||||
dev_str = device.serialize().replace("\n", "")
|
||||
file.write(dev_str)
|
||||
file.write("\n")
|
||||
file.close()
|
||||
|
||||
|
||||
def receive_loop():
|
||||
global quit_flag
|
||||
while not quit_flag and cube.is_connected():
|
||||
try:
|
||||
resp = cube.response()
|
||||
if resp is not None and resp[0:1] == "Z":
|
||||
pkt = MAXPacketFactory.create_packet(resp)
|
||||
handler.handle_msg(pkt)
|
||||
sleep(0.1)
|
||||
except KeyboardInterrupt:
|
||||
quit_flag = True
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
read_devices("devices.conf")
|
||||
cube = CUL("123456")
|
||||
cube.connect("COM5")
|
||||
cube.connect("COM10")
|
||||
print(cube.version_string())
|
||||
handler = MAXPacketHandler(cube)
|
||||
handler.receive_loop()
|
||||
handler = MAXPacketHandler(cube, devices)
|
||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||
receive_loop()
|
||||
cube.disconnect()
|
||||
write_devices("devices.conf")
|
||||
|
Loading…
Reference in New Issue
Block a user