now works with fb 7360

This commit is contained in:
Timon Horlboge 2022-12-04 13:58:53 +01:00
parent ce59adccf0
commit 924eb57b34
4 changed files with 159 additions and 45 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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())