From 924eb57b3432befaece1d1ef03ea8b7ed2cf8b0d Mon Sep 17 00:00:00 2001 From: SecretMineDE Date: Sun, 4 Dec 2022 13:58:53 +0100 Subject: [PATCH 1/2] now works with fb 7360 --- fritz_temp_sync/Dockerfile | 2 +- fritz_temp_sync/config.yaml | 6 +- fritz_temp_sync/fritzbox.py | 172 +++++++++++++++++++++++++++------- fritz_temp_sync/sync_ha_fb.py | 24 +++-- 4 files changed, 159 insertions(+), 45 deletions(-) diff --git a/fritz_temp_sync/Dockerfile b/fritz_temp_sync/Dockerfile index ee10167..9d58d8c 100755 --- a/fritz_temp_sync/Dockerfile +++ b/fritz_temp_sync/Dockerfile @@ -3,7 +3,7 @@ FROM $BUILD_FROM # Install requirements for add-on RUN apk update && apk add --no-cache python3 py-pip -RUN python3 -m pip install websockets requests +RUN python3 -m pip install websockets requests beautifulsoup4 WORKDIR /data diff --git a/fritz_temp_sync/config.yaml b/fritz_temp_sync/config.yaml index f564228..76c7d75 100755 --- a/fritz_temp_sync/config.yaml +++ b/fritz_temp_sync/config.yaml @@ -1,6 +1,6 @@ name: "Fritz!Box Temperature Sync Dev" description: "Sync Fritz!DECT thermostat temperatures with other sensors in Home Assistant" -version: "0.4.3" +version: "0.5.0" startup: "application" stage: "stable" slug: "fritz_temp_sync_dev" @@ -16,6 +16,8 @@ options: fritzbox: url: "http://fritz.box" password: null + verify_ssl: true + old_fb: false mappings: - sensor: null thermostate: null @@ -25,6 +27,8 @@ schema: url: url username: "str?" password: str + verify_ssl: bool + old_fb: bool mappings: - sensor: str thermostate: str diff --git a/fritz_temp_sync/fritzbox.py b/fritz_temp_sync/fritzbox.py index 49262e9..617d87c 100755 --- a/fritz_temp_sync/fritzbox.py +++ b/fritz_temp_sync/fritzbox.py @@ -11,15 +11,19 @@ from datetime import datetime, timedelta from typing import Optional, Dict, List import requests +from bs4 import BeautifulSoup from device import Device + class FritzBox: - def __init__(self, url: str, password: str, update_timeout: int, user: str = None, dry_run: bool = False) -> None: + def __init__(self, url: str, password: str, update_timeout: int, user: str = None, dry_run: bool = False, + verify_ssl: bool = True, old_fb: bool = False) -> None: self._endpoints = { "login": "login_sid.lua?version=2", "logout": "index.lua", - "data": "data.lua" + "data": "data.lua", + "device_details": "net/home_auto_hkr_edit.lua" } self.url: str = url self.dry_run: bool = dry_run @@ -30,6 +34,12 @@ class FritzBox: self.update_timeout: int = update_timeout self.update_time: Dict[str, datetime] = {} self.hold_connection: Optional[Task] = None + self.verify_ssl = verify_ssl + self.old_fb = old_fb + + if not verify_ssl: + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) async def hold_connection_alive(self) -> None: while True: @@ -72,7 +82,7 @@ class FritzBox: "xhrId": "first", "noMenuRef": 1 } - r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data) + r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data, verify=self.verify_ssl) if len(r.history) > 0: if not self.login(): logging.error("Failed to login to Fritz!Box") @@ -82,7 +92,7 @@ class FritzBox: def login(self) -> bool: logging.info(f"Login user {self.user} to Fritz!Box") challenge = None - r = self.session.get(f"{self.url}/{self._endpoints['login']}") + r = self.session.get(f"{self.url}/{self._endpoints['login']}", verify=self.verify_ssl) xml = ET.fromstring(r.text) for elem in xml: if elem.tag == "SID": @@ -106,7 +116,7 @@ class FritzBox: "response": response } - r = self.session.post(f"{self.url}/{self._endpoints['login']}", data=data) + r = self.session.post(f"{self.url}/{self._endpoints['login']}", data=data, verify=self.verify_ssl) logging.debug(r.text) xml = ET.fromstring(r.text) for elem in xml: @@ -125,12 +135,69 @@ class FritzBox: "sid": self.sid, "logout": 1, "no_sidrenew": ""} - r = self.session.post(f"{self.url}/{self._endpoints['logout']}", data=data) + r = self.session.post(f"{self.url}/{self._endpoints['logout']}", data=data, verify=self.verify_ssl) if self.hold_connection is not None: self.hold_connection.cancel() return r.status_code == 200 + def list_devices_old(self) -> Optional[List[Union[Device, Dict]]]: + data = { + "xhr": 1, + "sid": self.sid, + "lang": "de", + "page": "sh", + "xhrId": "all" + } + r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data, verify=self.verify_ssl) + logging.debug(r.text[:100]) + if len(r.history) > 0: + if self.login(): + r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data, verify=self.verify_ssl) + else: + return None + + devices_overview_raw = BeautifulSoup(r.text, "html.parser") + table_rows = devices_overview_raw.find(id="uiSmarthomeTables").table + + devices_raw = [] + + for r in table_rows.find_all("tr"): + name = r.findAll("td", {'class': ['name', 'cut_overflow']}) + button = r.findAll("td", {'class': 'btncolumn'}) + temperature = r.findAll("td", {'class': 'temperature'}) + + if name is None or len(name) < 1: + continue + if button is None or len(button) < 1: + continue + if temperature is None or len(temperature) < 1: + continue + + name = name[0].string + id = int(button[0].button["value"]) + temperature = float(temperature[0].string.split(" ")[0].replace(",", ".")) + + request_data = { + "device": id, + "sid": self.sid, + "xhr": 1 + } + + r = self.session.get(f"{self.url}/{self._endpoints['device_details']}", params=request_data, + verify=self.verify_ssl) + device_content_raw = BeautifulSoup(r.text, "html.parser") + offset = float(device_content_raw.find("input", {"type": "hidden", "name": "Offset"})["value"]) + devices_raw.append({ + "name": name, + "display_name": name, + "id": id, + "temperature": temperature, + "offset": offset + }) + + return devices_raw + def list_devices(self) -> Optional[List[Device]]: data = { "xhr": 1, @@ -139,11 +206,11 @@ class FritzBox: "page": "sh_dev", "xhrId": "all" } - r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data) + r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data, verify=self.verify_ssl) logging.debug(r.text[:100]) if len(r.history) > 0: if self.login(): - r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data) + r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data, verify=self.verify_ssl) else: return None devices: List[Device] = [] @@ -156,12 +223,18 @@ class FritzBox: if idx is None and name is None: logging.debug("No id or name given") return None - - devices = self.list_devices() + if self.old_fb: + devices = self.list_devices_old() + else: + devices = self.list_devices() device = None for device in devices: - if device.id == idx or device.display_name == name: - break + if isinstance(device, dict): + if device["id"] == idx or device["display_name"] == name: + break + else: + if device.id == idx or device.display_name == name: + break device = None if device is None: @@ -174,27 +247,49 @@ class FritzBox: if self.dry_run: logging.warning("No updates in dry-run-mode") return - data = { - "xhr": 1, - "sid": self.sid, - "lang": "de", - "device": device.id, - "page": "home_auto_hkr_edit" - } - self.session.post(f"{self.url}/{self._endpoints['data']}", data=data) - data = { - "xhr": 1, - "sid": self.sid, - "lang": "de", - "view": "", - "back_to_page": "sh_dev", - "apply": "", - "oldpage": "/net/home_auto_hkr_edit.lua" - } - data.update(device.to_web_data()) + old_type = isinstance(device, dict) + if old_type: + device_id = device["id"] + else: + device_id = device.id - self.session.post(f"{self.url}/{self._endpoints['data']}", data=data) + if not old_type: + data = { + "xhr": 1, + "sid": self.sid, + "lang": "de", + "device": device_id, + "page": "home_auto_hkr_edit" + } + self.session.post(f"{self.url}/{self._endpoints['data']}", data=data, verify=self.verify_ssl) + + data = { + "xhr": 1, + "sid": self.sid, + "lang": "de", + "view": "", + "back_to_page": "sh_dev", + "apply": "", + "oldpage": "/net/home_auto_hkr_edit.lua" + } + data.update(device.to_web_data()) + + else: + data = { + "xhr": 1, + "sid": self.sid, + "lang": "de", + "no_sidrenew": "", + "device": device_id, + "view": "", + "back_to_page": "/net/home_auto_overview.lua", + "ule_device_name": device["name"], + "Offset": device["offset"], + "apply": "", + "oldpage": "/net/home_auto_hkr_edit.lua", + } + self.session.post(f"{self.url}/{self._endpoints['data']}", data=data, verify=self.verify_ssl) def correct_offset(self, device_name: str, real_temp: float): elapsed = None @@ -206,8 +301,17 @@ class FritzBox: device: Optional[Device] = self.get_device_data(name=device_name) if device is None: return - new_offset = device.get_offset() + real_temp - device.get_temperature() - logging.info(f"Update offset from {device.get_offset()} to {new_offset}") - device.set_offset(new_offset) + + if self.old_fb: + new_offset = device["offset"] + real_temp - device["temperature"] + logging.info(f"Update offset from {device['offset']} to {new_offset}") + else: + new_offset = device.get_offset() + real_temp - device.get_temperature() + logging.info(f"Update offset from {device.get_offset()} to {new_offset}") + + if self.old_fb: + device["offset"] = new_offset + else: + device.set_offset(new_offset) self.set_offset(device) self.update_time[device.display_name] = datetime.now() diff --git a/fritz_temp_sync/sync_ha_fb.py b/fritz_temp_sync/sync_ha_fb.py index 8cf57ec..1b466b5 100755 --- a/fritz_temp_sync/sync_ha_fb.py +++ b/fritz_temp_sync/sync_ha_fb.py @@ -30,7 +30,8 @@ async def handle_event(idx: int): sensor_state = await ha.get_device_state(sensor) sensor_temp = round(float(sensor_state["attributes"]["temperature"]) * 2) / 2 if therm_temp != sensor_temp: - logging.info(f"{therm_name}: {therm_temp}\n{sensor}: {sensor_state['attributes']['temperature']} ({sensor_temp})") + logging.info( + f"{therm_name}: {therm_temp}\n{sensor}: {sensor_state_temp} ({sensor_temp})") fb.correct_offset(therm_name, sensor_temp) elif entity_id in sensor_mappings.keys(): @@ -41,8 +42,9 @@ async def handle_event(idx: int): continue therm_temp = float(therm_state["attributes"]["current_temperature"]) therm_name = therm_state["attributes"]["friendly_name"] - if therm_temp != sensor_temp: - logging.info(f"{therm_name}: {therm_temp}\n{entity_id}: {new_state['attributes']['temperature']} ({sensor_temp})") + if therm_temp != sensor_temp or True: + logging.info( + f"{therm_name}: {therm_temp}\n{entity_id}: {new_state_temp} ({sensor_temp})") fb.correct_offset(therm_name, sensor_temp) except KeyError: pass @@ -59,6 +61,7 @@ async def init(ha: HomeAssistantAPI, fb: FritzBox): await ha.wait_for_close() logging.info("Websocket closed, shutting down..") + async def main(): config_path = sys.argv[1] config = json.load(open(config_path)) @@ -71,12 +74,14 @@ async def main(): global fb fb = FritzBox(url=config["fritzbox"]["url"], - user=config["fritzbox"]["username"], - password=config["fritzbox"]["password"], - update_timeout=config["update_timeout"], - dry_run=False) - supervisor_url = "ws://supervisor/core/websocket" - supervisor_token = os.environ["SUPERVISOR_TOKEN"] + user=config["fritzbox"]["username"], + password=config["fritzbox"]["password"], + update_timeout=config["update_timeout"], + dry_run=False, + verify_ssl=config["fritzbox"]["verify_ssl"], + old_fb=config["fritzbox"]["old_fb"]) + supervisor_url = "ws://192.168.176.2:8123/api/websocket" + supervisor_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZWZhZThhYjBhNTE0MDBhYmNjNzE4Yzc3YjE5MzNkNiIsImlhdCI6MTY3MDE1MTQxMCwiZXhwIjoxOTg1NTExNDEwfQ.0_PlrqiUtEjKhsKzID7xpyQGunWlbO5cr1EPtnzO5tE" global ha ha = HomeAssistantAPI(supervisor_token, supervisor_url) @@ -91,4 +96,5 @@ async def main(): except KeyboardInterrupt: pass + asyncio.run(main()) -- 2.47.2 From 962e2c84af9e9360dfee7de16f35238c13174c9f Mon Sep 17 00:00:00 2001 From: SecretMineDE Date: Sun, 4 Dec 2022 13:59:02 +0100 Subject: [PATCH 2/2] More changes i guess --- fritz_temp_sync/fritzbox.py | 9 +++++---- fritz_temp_sync/sync_ha_fb.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/fritz_temp_sync/fritzbox.py b/fritz_temp_sync/fritzbox.py index 617d87c..f43c8ee 100755 --- a/fritz_temp_sync/fritzbox.py +++ b/fritz_temp_sync/fritzbox.py @@ -8,7 +8,7 @@ import re import xml.etree.ElementTree as ET from asyncio import Task from datetime import datetime, timedelta -from typing import Optional, Dict, List +from typing import Optional, Dict, List, Union import requests from bs4 import BeautifulSoup @@ -243,7 +243,7 @@ class FritzBox: return device - def set_offset(self, device: Device) -> None: + def set_offset(self, device: Union[Device, Dict]) -> None: if self.dry_run: logging.warning("No updates in dry-run-mode") return @@ -298,7 +298,8 @@ class FritzBox: logging.info(f"Last update for {device_name} {elapsed} ago") delta = timedelta(minutes=self.update_timeout) if device_name not in self.update_time.keys() or elapsed > delta: - device: Optional[Device] = self.get_device_data(name=device_name) + device: Optional[Union[Dict, Device]] = self.get_device_data(name=device_name) + logging.info(f"device: {device}") if device is None: return @@ -314,4 +315,4 @@ class FritzBox: else: device.set_offset(new_offset) self.set_offset(device) - self.update_time[device.display_name] = datetime.now() + self.update_time[device_name] = datetime.now() diff --git a/fritz_temp_sync/sync_ha_fb.py b/fritz_temp_sync/sync_ha_fb.py index 1b466b5..50111c5 100755 --- a/fritz_temp_sync/sync_ha_fb.py +++ b/fritz_temp_sync/sync_ha_fb.py @@ -14,7 +14,7 @@ thermostate_mappings = {} async def handle_event(idx: int): - logging.debug(f"Wait for events for {idx}") + logging.info(f"Wait for events for {idx}") while event := await ha.events[idx].get(): try: @@ -28,14 +28,19 @@ async def handle_event(idx: int): therm_name = new_state["attributes"]["friendly_name"] sensor = thermostate_mappings[entity_id] sensor_state = await ha.get_device_state(sensor) - sensor_temp = round(float(sensor_state["attributes"]["temperature"]) * 2) / 2 + sensor_state_temp = sensor_state["attributes"]["temperature"] if "temperatur" in sensor_state[ + "attributes"] else sensor_state["state"] + sensor_temp = round(float(sensor_state_temp) * 2) / 2 if therm_temp != sensor_temp: logging.info( f"{therm_name}: {therm_temp}\n{sensor}: {sensor_state_temp} ({sensor_temp})") fb.correct_offset(therm_name, sensor_temp) elif entity_id in sensor_mappings.keys(): - sensor_temp = round(float(new_state["attributes"]["temperature"]) * 2) / 2 + new_state_temp = new_state["attributes"]["temperature"] if "temperatur" in new_state[ + "attributes"] else new_state["state"] + sensor_temp = round(float(new_state_temp) * 2) / 2 + logging.info(sensor_temp) for thermostate in sensor_mappings[entity_id]: therm_state = await ha.get_device_state(thermostate) if therm_state["state"] == "unavailable": @@ -46,8 +51,8 @@ async def handle_event(idx: int): logging.info( f"{therm_name}: {therm_temp}\n{entity_id}: {new_state_temp} ({sensor_temp})") fb.correct_offset(therm_name, sensor_temp) - except KeyError: - pass + except KeyError as e: + logging.error(e) async def init(ha: HomeAssistantAPI, fb: FritzBox): -- 2.47.2