Browse Source

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
master
Christian Loch 2 years ago
parent
commit
c5a7a8699c
6 changed files with 249 additions and 41 deletions
  1. +4
    -4
      MAXCube.py
  2. +29
    -4
      MAXDevice.py
  3. +88
    -3
      MAXPacket.py
  4. +69
    -27
      MAXPacketHandler.py
  5. +6
    -0
      devices.conf
  6. +53
    -3
      main.py

+ 4
- 4
MAXCube.py 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!")

+ 29
- 4
MAXDevice.py 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())

+ 88
- 3
MAXPacket.py 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:


+ 69
- 27
MAXPacketHandler.py 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,44 +54,82 @@ 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)
dev.state = pkt.button_state
print("Button {} state={}".format(dev.address, dev.state))
if not pkt.retransmit:
act_pkt = MAXCubeAckPacket(pkt.counter, "00", self.cube.addr,
pkt.sender_address, pkt.group_id, True)
act_str = act_pkt.serialize()
#print(act_str)
self.cube.request(act_str)
if dev is not None:
dev.state = pkt.button_state
print("Button {} state={}".format(dev.address, dev.state))
if not pkt.retransmit:
act_pkt = MAXCubeAckPacket(pkt.counter, "00", self.cube.addr,
pkt.sender_address, pkt.group_id, True)
act_str = act_pkt.serialize()
#print(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
- 0
devices.conf 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

+ 53
- 3
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")

Loading…
Cancel
Save