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(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!") |
@@ -2,11 +2,36 @@ from MAXPacket import PushButtonState | |||||
class MAXDevice: | 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): | 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 | 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 | 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.payload = "00" | |||||
self.set_values(message_counter, "22", message_flag, sender_address, dest_address, group_id) | |||||
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: | ||||
@@ -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 | |||||
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 | 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,44 +54,82 @@ 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: | |||||
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: | 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) |
@@ -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 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.receive_loop() | |||||
handler = MAXPacketHandler(cube, devices) | |||||
signal.signal(signal.SIGINT, signal.default_int_handler) | |||||
receive_loop() | |||||
cube.disconnect() | cube.disconnect() | ||||
write_devices("devices.conf") |