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:
Christian Loch 2021-10-05 03:51:53 +02:00
parent 94c613abe9
commit c5a7a8699c
6 changed files with 249 additions and 41 deletions

View File

@ -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!")

View File

@ -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())

View File

@ -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:

View File

@ -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
View 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
View File

@ -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")