Implement read write of dev file, add example Change debug text Implement devices class with push button and thermostat Implement pairing with thermostatsmaster
@@ -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!") |
@@ -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()) |
@@ -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,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) |
@@ -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 |
@@ -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") |