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(req.encode())
self.client.write(b"\n") self.client.write(b"\n")
if DEBUG: if DEBUG:
print(">>> {}".format(req)) print("SENT: {}".format(req))
else: else:
print("Request while not connected!") print("Request while not connected!")
@ -51,7 +51,7 @@ class CUN(MAXCube):
self.client.write(req.encode()) self.client.write(req.encode())
self.client.write(b"\n") self.client.write(b"\n")
if DEBUG: if DEBUG:
print(">>> {}".format(req)) print("SENT: {}".format(req))
else: else:
print("Request while not connected!") print("Request while not connected!")
@ -59,7 +59,7 @@ class CUN(MAXCube):
if self.client is not None: if self.client is not None:
response = self.client.read_some().decode() response = self.client.read_some().decode()
if DEBUG: if DEBUG:
print("<<< {}".format(response)) print("RECV: {}".format(response))
return response return response
else: else:
print("Waiting for response while not connected!") print("Waiting for response while not connected!")
@ -80,7 +80,7 @@ class CUL(MAXCube):
if self.client is not None: if self.client is not None:
response = self.client.read(100).decode() response = self.client.read(100).decode()
if DEBUG: if DEBUG:
print("<<< {}".format(response)) print("RECV: {}".format(response))
return response return response
else: else:
print("Waiting for response while not connected!") print("Waiting for response while not connected!")

View File

@ -2,11 +2,36 @@ from MAXPacket import PushButtonState
class MAXDevice: class MAXDevice:
def __init__(self, address): def __init__(self, name, address):
self.address = 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): class MAXPushButton(MAXDevice):
def __init__(self, address): def __init__(self, name, address):
super(MAXPushButton, self).__init__(address) super(MAXPushButton, self).__init__(name, address)
self.state = PushButtonState.UNKNOWN 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 import math
from enum import Enum from enum import Enum
class MAXPacketFactory: class MAXPacketFactory:
def create_packet(rec: str): def create_packet(rec: str):
pkt_type = int(rec[7:9], 16) pkt_type = int(rec[7:9], 16)
@ -18,6 +19,8 @@ class MAXPacketFactory:
return MAXResetPacket(rec) return MAXResetPacket(rec)
elif pkt_type == 0x50: elif pkt_type == 0x50:
return MAXPushButtonPacket(rec) return MAXPushButtonPacket(rec)
elif pkt_type == 0x60:
return MAXThermostatStatePacket(rec)
else: else:
print("Unknown message type: {}".format(pkt_type)) print("Unknown message type: {}".format(pkt_type))
result = MAXPacket() result = MAXPacket()
@ -97,14 +100,52 @@ class MAXPairPongPacket(MAXPacket):
class MAXSetGroupIdPacket(MAXPacket): class MAXSetGroupIdPacket(MAXPacket):
def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str, def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str,
group_id: str): group_id: str):
self.set_values(message_counter, "01", message_flag, sender_address, dest_address, group_id) self.set_values(message_counter, "22", message_flag, sender_address, dest_address, group_id)
self.payload = "00" self.payload = "01"
def to_string(self): def to_string(self):
result = "{}\nMAXSetGroupIdPacket: payload={}".format(super().to_string(), self.payload) result = "{}\nMAXSetGroupIdPacket: payload={}".format(super().to_string(), self.payload)
return result 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): class MAXAckPacket(MAXPacket):
def __init__(self, rec): def __init__(self, rec):
super().__init__() super().__init__()
@ -118,10 +159,54 @@ class MAXAckPacket(MAXPacket):
return result 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): class MAXCubeAckPacket(MAXPacket):
def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str, def __init__(self, message_counter: str, message_flag: str, sender_address: str, dest_address: str,
group_id: str, acknowledged): 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: if acknowledged:
self.payload = "01" self.payload = "01"
else: else:

View File

@ -1,9 +1,9 @@
from typing import List
from MAXCube import MAXCube from MAXCube import MAXCube
from MAXPacket import MAXPacket, MAXPacketFactory, MAXPairPingPacket, MAXPairPongPacket, MAXAckPacket, \ from MAXPacket import MAXPacket, MAXPacketFactory, MAXPairPingPacket, MAXPairPongPacket, MAXAckPacket, \
MAXResetPacket, MAXWakeUpPacket, MAXSetGroupIdPacket, MAXPushButtonPacket, MAXCubeAckPacket MAXSetGroupIdPacket, MAXPushButtonPacket, MAXCubeAckPacket, MAXSetTempPacket, \
from MAXDevice import MAXDevice, MAXPushButton MAXRadiatorControlMode, MAXAddLinkPartnerPacket
from MAXDevice import MAXDevice, MAXPushButton, MAXThermostat
from time import sleep
from enum import Enum from enum import Enum
@ -13,6 +13,10 @@ class HandshakeState(Enum):
PONG_ACK = 3 PONG_ACK = 3
GROUP_ID_SENT = 4 GROUP_ID_SENT = 4
GROUP_ID_ACK = 5 GROUP_ID_ACK = 5
CONFIG_TEMP_SENT = 6
CONFIG_TEMP_ACK = 7
ADD_LINK_PARTNER_SENT = 8
ADD_LINK_PARTNER_ACK = 9
class Handshake: class Handshake:
@ -23,11 +27,11 @@ class Handshake:
class MAXPacketHandler: class MAXPacketHandler:
def __init__(self, cube: MAXCube): def __init__(self, cube, devices: MAXCube):
self.cube = cube self.cube = cube
self.quit_flag = False self.quit_flag = False
self.handshakes = [] self.handshakes = []
self.devices: list[MAXDevice] = [] self.devices: List[MAXDevice] = devices
def handle_msg(self, pkt: MAXPacket): def handle_msg(self, pkt: MAXPacket):
print(pkt.to_string()) print(pkt.to_string())
@ -38,7 +42,7 @@ class MAXPacketHandler:
pong = MAXPairPongPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr, pong = MAXPairPongPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr,
dest_address=pkt.sender_address, group_id="00") dest_address=pkt.sender_address, group_id="00")
pong_str = pong.serialize() pong_str = pong.serialize()
#print(pong.to_string()) print(pong.to_string())
self.cube.request(pong_str) self.cube.request(pong_str)
handshake.state = HandshakeState.PONG_SENT handshake.state = HandshakeState.PONG_SENT
elif isinstance(pkt, MAXAckPacket): elif isinstance(pkt, MAXAckPacket):
@ -50,29 +54,74 @@ class MAXPacketHandler:
if cur_handshake is not None: if cur_handshake is not None:
if cur_handshake.state == HandshakeState.PONG_SENT: if cur_handshake.state == HandshakeState.PONG_SENT:
cur_handshake.state = HandshakeState.PONG_ACK cur_handshake.state = HandshakeState.PONG_ACK
print(pkt.to_string()) #print(pkt.to_string())
if cur_handshake.dev_type == "05": if cur_handshake.dev_type == "05":
# Handshake is finished # Handshake is finished
print("Paired device with addr={} and type={}".format(cur_handshake.partner_addr, print("Paired device with addr={} and type={}".format(cur_handshake.partner_addr,
cur_handshake.dev_type)) 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) self.handshakes.remove(cur_handshake)
else: else:
set_group_id = MAXSetGroupIdPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr, set_group_id = MAXSetGroupIdPacket(message_counter="00", message_flag="00", sender_address=self.cube.addr,
dest_address=pkt.sender_address, group_id="00") dest_address=pkt.sender_address, group_id="00")
set_group_id_str = set_group_id.serialize() 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) self.cube.request(set_group_id_str)
cur_handshake.state = HandshakeState.GROUP_ID_SENT cur_handshake.state = HandshakeState.GROUP_ID_SENT
return
if cur_handshake.state == HandshakeState.GROUP_ID_SENT: if cur_handshake.state == HandshakeState.GROUP_ID_SENT:
cur_handshake.state = HandshakeState.GROUP_ID_ACK 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): elif isinstance(pkt, MAXPushButtonPacket):
dev = None dev = None
for known_dev in self.devices: for known_dev in self.devices:
if known_dev.address == pkt.sender_address: if known_dev.address == pkt.sender_address:
dev = known_dev dev = known_dev
break break
if dev is None: if dev is not None:
dev = MAXPushButton(pkt.sender_address)
dev.state = pkt.button_state dev.state = pkt.button_state
print("Button {} state={}".format(dev.address, dev.state)) print("Button {} state={}".format(dev.address, dev.state))
if not pkt.retransmit: if not pkt.retransmit:
@ -80,14 +129,7 @@ class MAXPacketHandler:
pkt.sender_address, pkt.group_id, True) pkt.sender_address, pkt.group_id, True)
act_str = act_pkt.serialize() act_str = act_pkt.serialize()
#print(act_str) #print(act_str)
self.cube.request(act_str) #self.cube.request(act_str)
#ToDo: Does not work properly
else: else:
print("Paket is not ours!") 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 MAXCube import CUL
from MAXPacketHandler import MAXPacketHandler from MAXPacketHandler import MAXPacketHandler
from MAXPacket import MAXPacketFactory
from time import sleep
import sys
import signal
DEBUG = True 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__': if __name__ == '__main__':
read_devices("devices.conf")
cube = CUL("123456") cube = CUL("123456")
cube.connect("COM5") cube.connect("COM10")
print(cube.version_string()) print(cube.version_string())
handler = MAXPacketHandler(cube) handler = MAXPacketHandler(cube, devices)
handler.receive_loop() signal.signal(signal.SIGINT, signal.default_int_handler)
receive_loop()
cube.disconnect() cube.disconnect()
write_devices("devices.conf")