From b664df1c4de87b494dc48d1213e8550b005e810d Mon Sep 17 00:00:00 2001 From: connorroy Date: Sun, 2 Nov 2025 15:02:27 +0000 Subject: [PATCH] Refactoring continues --- src/main.py | 31 ++--- src/mqtt_manager.py | 190 ++++++++++++++++++++------ src/pytevolve.py | 320 ++++++++++++++++++++------------------------ 3 files changed, 314 insertions(+), 227 deletions(-) diff --git a/src/main.py b/src/main.py index 19e7269..266643d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,7 @@ from pytevolve import Tevolve from mqtt_manager import MqttManager import threading +import time host = "192.168.0.100" @@ -10,32 +11,28 @@ if __name__ == "__main__": tevolve = Tevolve() - tevolve.get_token() + + token_thread_event = threading.Event() + + t = threading.Thread(target=tevolve.token_manager, args=(token_thread_event,), daemon=True).start() + + while token_thread_event.is_set() is False: + time.sleep(1) + + tevolve.get_dev() tevolve.get_devices() status = tevolve.get_status() - mqtt_manager = MqttManager() - - # threading.Thread(target=mqtt_manager.start_mqtt, args=(host,)).start() - - + mqtt_manager = MqttManager(tevolve) while 1: if mqtt_manager.is_connected == True: - mqtt_manager.publish_discovery(tevolve.devices) - - mqtt_manager.publish_heaters(status) + mqtt_manager.update_homeassistant_entity(status) break - # Update Device States - - - - - tevolve.get_sid() - # test.post_websocket() - # test.create_websocket() \ No newline at end of file + tevolve.post_websocket() + tevolve.create_websocket(mqtt_manager) \ No newline at end of file diff --git a/src/mqtt_manager.py b/src/mqtt_manager.py index 0066b80..c694f2f 100644 --- a/src/mqtt_manager.py +++ b/src/mqtt_manager.py @@ -1,10 +1,12 @@ -import json from pathlib import Path import paho.mqtt.client as mqtt -import threading +from pytevolve import * class MqttManager: - def __init__(self): + def __init__(self, tevolve): + + self.tevolve = tevolve + self.host = "192.168.0.100" self.username = "connorroy" self.password = "arkreactor7" @@ -20,65 +22,161 @@ class MqttManager: self.client.connect(self.host, 1883, 60) threading.Thread(target=self.client.loop_forever).start() - # self.client.loop_forever() - def publish_heaters(self, rad_data): - # self.object_ids = heaters + # def publish_heaters(self, rad_data): + # # self.object_ids = heaters + # + # discovery_topic = "homeassistant/climate/" + # + # json_payload = self._load_json() + # + # + # for device_index, device in enumerate(self.object_ids["nodes"]): + # + # + # + # if device["lost"] is True: + # + # + # self.client.publish(topic=json_payload[device_index]["availability_topic"], qos=0, retain=False, + # payload="offline") + # else: + # self.client.publish(topic=json_payload[device_index]["availability_topic"], qos=0, retain=False, + # payload="online") + # # self.client.subscribe(topic=json_payload[device_index]["mode_state_topic"]) + # + # + # if rad_data[device_index][device["name"]]["mode"] == "manual": + # mode = "heat" + # else: + # mode = "off" + # + # self.client.publish(topic=json_payload[device_index]["mode_state_topic"], qos=0, retain=False, + # payload=mode) + # + # + # # Current Temperature + # current_temp = rad_data[device_index][device["name"]]["mtemp"] + # self.client.publish(topic=json_payload[device_index]["current_temperature_topic"], qos=0, retain=False, + # payload=str(current_temp)) + # + # # Current Setpoint + # current_setpoint = rad_data[device_index][device["name"]]["stemp"] + # self.client.publish(topic=json_payload[device_index]["temperature_state_topic"], qos=0, retain=False, + # payload=str(current_setpoint)) + # + # # Report Correct Heating State + # if float(current_temp) < float(current_setpoint) and mode == "heat": + # self.client.publish(topic=json_payload[device_index]["action_topic"], qos=0, retain=False, + # payload="heating") + # elif float(current_temp) >= float(current_setpoint) and mode == "heat": + # self.client.publish(topic=json_payload[device_index]["action_topic"], qos=0, retain=False, + # payload="idle") + # else: + # self.client.publish(topic=json_payload[device_index]["action_topic"], qos=0, retain=False, + # payload="off") - discovery_topic = "homeassistant/climate/" + def update_homeassistant_entity(self, data): json_payload = self._load_json() - - for device_index, device in enumerate(self.object_ids["nodes"]): - - self.client.subscribe(topic=json_payload[device_index]["state_topic"]) - - if device["lost"] is True: + for j in json_payload: - self.client.publish(topic=json_payload[device_index]["availability_topic"], qos=0, retain=False, + target = "" + position = None + for index, i in enumerate(self.object_ids["nodes"]): + if "path" in data.keys(): + target = str(data["path"].split("/")[2]) + if target == str(i["addr"]): + position = index + break + else: + for k_index, k in enumerate(self.object_ids["nodes"]): + if str(i["name"]) == list(data.keys())[k_index]: + target = i["addr"] + position = index + break + + + if self.object_ids["nodes"][position]["lost"] is True: + + self.client.publish(topic=json_payload[position]["availability_topic"], qos=0, retain=False, payload="offline") else: - self.client.publish(topic=json_payload[device_index]["availability_topic"], qos=0, retain=False, + self.client.publish(topic=json_payload[position]["availability_topic"], qos=0, retain=False, payload="online") - self.client.subscribe(topic=json_payload[device_index]["mode_state_topic"]) - if rad_data[device_index][device["name"]]["mode"] == "manual": + + + + if "path" in data.keys(): + current_temp = data["body"]["mtemp"] + else: + current_temp = data[self.object_ids["nodes"][position]["name"]]["mtemp"] + self.client.publish(topic=json_payload[position]["current_temperature_topic"], qos=0, retain=False, + payload=str(current_temp)) + + if "path" in data.keys(): + current_setpoint = data["body"]["stemp"] + else: + current_setpoint = data[self.object_ids["nodes"][position]["name"]]["stemp"] + self.client.publish(topic=json_payload[position]["temperature_state_topic"], qos=0, retain=False, + payload=str(current_setpoint)) + + if "path" in data.keys(): + requested_mode = data["body"]["mode"] + else: + requested_mode = data[self.object_ids["nodes"][position]["name"]]["mode"] + + + if requested_mode == "manual": mode = "heat" else: mode = "off" - - - - self.client.publish(topic=json_payload[device_index]["mode_state_topic"], qos=0, retain=False, + self.client.publish(topic=json_payload[position]["mode_state_topic"], qos=0, retain=False, payload=mode) - self.client.publish(topic=json_payload[device_index]["action_topic"], qos=0, retain=False, - payload="heating") - # self.client.subscribe(topic=json_payload[device_index]["current_temperature_topic"]) - self.client.publish(topic=json_payload[device_index]["current_temperature_topic"], qos=0, retain=False, - payload="17") + # Report Correct Heating State + if float(current_temp) < float(current_setpoint) and mode == "heat": + self.client.publish(topic=json_payload[position]["action_topic"], qos=0, retain=False, + payload="heating") + elif float(current_temp) >= float(current_setpoint) and mode == "heat": + self.client.publish(topic=json_payload[position]["action_topic"], qos=0, retain=False, + payload="idle") + else: + self.client.publish(topic=json_payload[position]["action_topic"], qos=0, retain=False, + payload="off") + list(data).pop(position) + + + + def subscribe_heating(self): + + json_payload = self._load_json() + + for device_index, device in enumerate(self.object_ids["nodes"]): + + self.client.subscribe(topic=json_payload[device_index]["state_topic"]) + self.client.subscribe(topic=json_payload[device_index]["temperature_command_topic"]) + self.client.subscribe(topic=json_payload[device_index]["mode_command_topic"]) + def on_connect(self, client, userdata, flags, reason_code, properties): print("Connected") self.is_connected = True + self.subscribe_heating() - - # # Subscribing in on_connect() means that if we lose the connection and - # # reconnect then subscriptions will be renewed. - # client.subscribe("$SYS/#") - def on_disconnect(self, client, userdata, flags, reason): print("Disconnected") self.is_connected = False @@ -87,6 +185,27 @@ class MqttManager: def on_message(self, client, userdata, msg): print(msg.topic + " " + str(msg.payload)) + target_id = "" + topic = msg.topic.split("/")[2] + payload = msg.payload.decode("utf-8") + + + uid = msg.topic.split("/")[0] + + for i in self.object_ids["nodes"]: + if i["uid"] == uid: + target_id = i["addr"] + + if target_id != "": + match topic: + case "currentmode": + self.tevolve.set_mode(target_id, payload) + case "settemp": + self.tevolve.set_temperature(target_id, payload) + case "setmode": + self.tevolve.set_mode(target_id, payload) + + # self.update_homeassistant_entity(self.tevolve.get_status()) "Adds devices via mqtt discovery" @@ -102,7 +221,8 @@ class MqttManager: for index, i in enumerate(self.object_ids["nodes"]): climate_json[index]["uniq_id"] = i["uid"] - discovery_topic = "homeassistant/climate/" + i["uid"] + "/config" + name = i["name"].replace(" ", "_") + discovery_topic = "homeassistant/climate/" + name + "/config" self.client.publish(topic=discovery_topic, payload=json.dumps(climate_json[index]), qos=0, retain=False) @@ -123,10 +243,4 @@ class MqttManager: json_list.append(new_data) return json_list -# test = MqttManager() - -# Blocking call that processes network traffic, dispatches callbacks and -# handles reconnecting. -# Other loop*() functions are available that give a threaded interface and a -# manual interface. diff --git a/src/pytevolve.py b/src/pytevolve.py index 03c49f1..3d23cf6 100644 --- a/src/pytevolve.py +++ b/src/pytevolve.py @@ -12,38 +12,52 @@ headers = {'content-type': 'application/x-www-form-urlencoded', dev_id = "082e858131ff012c51" - +API_URL = "https://api-tevolve.termoweb.net/api/v2" +DEV_URL = "/grouped_devs" class Tevolve: + + token = "" def __init__(self): - self.api_url = "https://api-tevolve.termoweb.net/api/v2" - self.dev_url = "/grouped_devs" + + self.need_token = True self.dev_id = "" - self.token_primary = "" + self.token_primary = None self.token_refresh = "" self.sid = "" self.devices = [] + self.latest_message = "" + self.time_token_start = 0 setmode = "" setTemperature = "" - - token = None sid = "" websocket_url = "" mode = "" set_point = "" websocket_message = "" - hallway_mode = "" - hallway_current_temp = "" - hallway_set_temp = "" - hallway_active = "" - livingroom_mode = "" - livingroom_current_temp = "" - livingroom_set_temp = "" - livingroom_active = "" + + def token_manager(self, event): + + while 1: + + current_time = time.time() + time_since_refresh = current_time - self.time_token_start + + if self.need_token or time_since_refresh > 3600: + + x = requests.post(url, headers=headers, data=data) + + if x.status_code == 200: + self.token_primary = x.json()["access_token"] + self.time_token_start = time.time() + event.set() + else: + time.sleep(5) + else: time.sleep(60) def get_dev(self): @@ -54,7 +68,7 @@ class Tevolve: 'Authorization': "Bearer " + self.token_primary} - dev_request = requests.get(str(self.api_url+self.dev_url), headers=headers).json() + dev_request = requests.get(str(API_URL+DEV_URL), headers=headers).json() self.dev_id = dev_request[0]["devs"][0]["dev_id"] @@ -78,25 +92,6 @@ class Tevolve: self.token_refresh = "" print("Could not get token") - - - - - - - @staticmethod - def token_manager(): - - while Tevolve.token is None: - - x = requests.post(url, headers=headers, data=data) - - if x.status_code == 200: - Tevolve.token = x.json()["access_token"] - else: - time.sleep(30) - - def get_sid(self): url = "https://api-tevolve.termoweb.net/socket.io/?token=" + self.token_primary + "&dev_id=" + str(self.dev_id) + "&EIO" \ @@ -117,8 +112,8 @@ class Tevolve: header = {'content-type': 'application/json', "Authorization": "Bearer " + self.token_primary} - url = str(self.api_url + endpoint) - devices_request = requests.get(str(self.api_url + endpoint), headers=header).json() + url = str(API_URL + endpoint) + devices_request = requests.get(str(API_URL + endpoint), headers=header).json() self.devices = devices_request @@ -126,10 +121,6 @@ class Tevolve: raise - - - - def post_websocket(self): # Tevolve.token_manager() @@ -152,15 +143,124 @@ class Tevolve: requests.request("POST", url, headers=headers, data=payload) - def create_websocket(self): + def set_mode(self, heater_id, mode): + + heater_id = heater_id + # mode = Tevolve.setmode + headers = { + 'host': 'api-tevolve.termoweb.net', + 'origin': 'https://tevolve.termoweb.net', + 'content-type': 'application/json', + 'accept': 'application/json, text/plain, */*', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' + 'Version/15.4 Safari/605.1.15', + 'authorization': 'Bearer ' + self.token_primary, + 'referer': 'https://tevolve.termoweb.net/' + + } + url = "https://api-tevolve.termoweb.net/api/v2/devs/082e858131ff012c51/htr/" + str(heater_id) + "/status" + + if mode == "heat": + mode = "manual" + + payload = json.dumps({ + "mode": mode + }) + + response = requests.post(url, headers=headers, data=payload) + if response.status_code == 201 or response.status_code == 200: + pass + # def get_mode(self): + # headers = { + # 'host': 'api-tevolve.termoweb.net', + # 'origin': 'https://tevolve.termoweb.net', + # 'content-type': 'application/json', + # 'accept': 'application/json, text/plain, */*', + # 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' + # 'Version/15.4 Safari/605.1.15', + # 'authorization': 'Bearer ' + self.token_primary, + # 'referer': 'https://tevolve.termoweb.net/', + # + # } + # url = "https://api-tevolve.termoweb.net/api/v2/devs/082e858131ff012c51/htr/" + self.heater_id + "/status" + # + # response = requests.get(url, headers=headers) + # if response.status_code == 201 or response.status_code == 200: + # mode = response.json()["mode"] + # if mode == "manual": + # return "heat" - websocket_url = "wss://api-tevolve.termoweb.net/socket.io/?token=" + self.token_primary + "&dev_id=" +str(self.dev_id)+ "&EIO=3&transport=websocket&sid=" + self.sid + "" + def get_status(self): + result = {} + try: + for index ,i in enumerate(self.devices["nodes"]): + headers = { + 'host': 'api-tevolve.termoweb.net', + 'origin': 'https://tevolve.termoweb.net', + 'content-type': 'application/json', + 'accept': 'application/json, text/plain, */*', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' + 'Version/15.4 Safari/605.1.15', + 'authorization': 'Bearer ' + self.token_primary, + 'referer': 'https://tevolve.termoweb.net/', + + } + url = "https://api-tevolve.termoweb.net/api/v2/devs/"+self.dev_id+"/htr/"+str(i["addr"])+"/status" + + response = requests.get(url, headers=headers) + if response.status_code == 201 or response.status_code == 200: + result.update({i["name"]: response.json()}) + except Exception as e: + raise e + return result + + + def set_temperature(self, heater_id, set_temperature): + + headers = { + 'host': 'api-tevolve.termoweb.net', + 'origin': 'https://tevolve.termoweb.net', + 'content-type': 'application/json', + 'accept': 'application/json, text/plain, */*', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' + 'Version/15.4 Safari/605.1.15', + 'authorization': 'Bearer ' + self.token_primary, + 'referer': 'https://tevolve.termoweb.net/' + + } + + url = "https://api-tevolve.termoweb.net/api/v2/devs/082e858131ff012c51/htr/" + str(heater_id) + "/status" + + payload = json.dumps({ + "stemp": str(float(set_temperature)), + "units": "C", + "mode": "manual" + }) + + response = requests.post(url, headers=headers, data=payload) + if response.status_code == 201 or response.status_code == 200: + pass + + + def create_websocket(self, mqtt_connection): + + + websocket_url = "wss://api-tevolve.termoweb.net/socket.io/?token=" + self.token_primary + \ + "&dev_id=" +str(self.dev_id)+ "&EIO=3&transport=websocket&sid=" + self.sid + "" def on_message(ws, message): self.websocket_message = message print(message) + if len(message) > 20: + + a = json.loads(message[20:]) + if a[0] == "update": + self.last_update = a[1] + mqtt_connection.update_homeassistant_entity(self.last_update) + + def on_open(ws): def run(): @@ -181,14 +281,15 @@ class Tevolve: def on_close(ws, close_status_code, close_msg): print("WEB SOCKET ClOSED") + + mqtt_connection.publish_heater() + time.sleep(2) self.post_websocket() - self.create_websocket() - - + self.create_websocket(mqtt_connection) def on_error(ws, err): print("WEB SOCKET ERROR") - self.post_websocket() - self.create_websocket() + # self.post_websocket() + # self.create_websocket(mqtt_connection) ws = websocket.WebSocketApp(websocket_url) websocket.enableTrace(False) @@ -199,128 +300,3 @@ class Tevolve: ws.run_forever(ping_interval=10, ping_payload='42/api/v2/socket_io,["message","ping"]') - @staticmethod - def set_mode(heater_id): - - heater_id = heater_id - mode = Tevolve.setmode - headers = { - 'host': 'api-tevolve.termoweb.net', - 'origin': 'https://tevolve.termoweb.net', - 'content-type': 'application/json', - 'accept': 'application/json, text/plain, */*', - 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' - 'Version/15.4 Safari/605.1.15', - 'authorization': 'Bearer ' + Tevolve.token, - 'referer': 'https://tevolve.termoweb.net/' - - } - url = "https://api-tevolve.termoweb.net/api/v2/devs/082e858131ff012c51/htr/" + heater_id + "/status" - - if mode == "heat": - mode = "manual" - - payload = json.dumps({ - "mode": mode - }) - - response = requests.post(url, headers=headers, data=payload) - if response.status_code == 201 or response.status_code == 200: - pass - - - def get_mode(self): - headers = { - 'host': 'api-tevolve.termoweb.net', - 'origin': 'https://tevolve.termoweb.net', - 'content-type': 'application/json', - 'accept': 'application/json, text/plain, */*', - 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' - 'Version/15.4 Safari/605.1.15', - 'authorization': 'Bearer ' + self.token_primary, - 'referer': 'https://tevolve.termoweb.net/', - - } - url = "https://api-tevolve.termoweb.net/api/v2/devs/082e858131ff012c51/htr/" + self.heater_id + "/status" - - response = requests.get(url, headers=headers) - if response.status_code == 201 or response.status_code == 200: - mode = response.json()["mode"] - if mode == "manual": - return "heat" - - - def get_status(self): - result = [] - try: - for index ,i in enumerate(self.devices["nodes"]): - headers = { - 'host': 'api-tevolve.termoweb.net', - 'origin': 'https://tevolve.termoweb.net', - 'content-type': 'application/json', - 'accept': 'application/json, text/plain, */*', - 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' - 'Version/15.4 Safari/605.1.15', - 'authorization': 'Bearer ' + self.token_primary, - 'referer': 'https://tevolve.termoweb.net/', - - } - url = "https://api-tevolve.termoweb.net/api/v2/devs/"+self.dev_id+"/htr/"+str(i["addr"])+"/status" - - response = requests.get(url, headers=headers) - if response.status_code == 201 or response.status_code == 200: - result.append({i["name"]: response.json()}) - except Exception as e: - raise e - return result - - @staticmethod - def set_temperature(heater_id, set_temperature): - - headers = { - 'host': 'api-tevolve.termoweb.net', - 'origin': 'https://tevolve.termoweb.net', - 'content-type': 'application/json', - 'accept': 'application/json, text/plain, */*', - 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) ' - 'Version/15.4 Safari/605.1.15', - 'authorization': 'Bearer ' + Tevolve.token, - 'referer': 'https://tevolve.termoweb.net/' - - } - - url = "https://api-tevolve.termoweb.net/api/v2/devs/082e858131ff012c51/htr/" + heater_id + "/status" - - payload = json.dumps({ - "stemp": str(float(set_temperature)), - "units": "C", - "mode": "manual" - }) - - response = requests.post(url, headers=headers, data=payload) - if response.status_code == 201 or response.status_code == 200: - pass - -if __name__ == '__main__': - test = Tevolve() - test.get_token() - test.get_dev() - - test.get_devices() - - - for device in test.devices["nodes"]: - if device["name"] == "Hallway": - test.get_status(device["addr"]) - - test.get_sid() - - - test.post_websocket() - test.create_websocket() - - - while 1: - if test.websocket_message != "": - print(test.websocket_message) - test.websocket_message = "" \ No newline at end of file