Change to asyncio, keep connection to fb alive, use data from ha for comparing
This commit is contained in:
parent
1f0a829831
commit
9bf21d04e3
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,6 +15,7 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
|
.venv/
|
||||||
lib/
|
lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
from typing import Optional, Tuple
|
import asyncio
|
||||||
|
from asyncio import Task
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Tuple, Dict
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
@ -6,23 +9,34 @@ import hashlib
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class FritzBox:
|
class FritzBox:
|
||||||
def __init__(self, url:str, password:str, user:str = None) -> None:
|
def __init__(self, url: str, password: str, user: str = None) -> None:
|
||||||
self._endpoints = {
|
self._endpoints = {
|
||||||
"login": "login_sid.lua?version=2",
|
"login": "login_sid.lua?version=2",
|
||||||
"logout": "index.lua",
|
"logout": "index.lua",
|
||||||
"data": "data.lua"
|
"data": "data.lua"
|
||||||
}
|
}
|
||||||
self.url = url
|
self.url: str = url
|
||||||
self.session = requests.Session()
|
self.user: Optional[str] = user
|
||||||
self.password = password
|
self.session: requests.Session = requests.Session()
|
||||||
self.sid = None
|
self.password: str = password
|
||||||
|
self.sid: Optional[str] = None
|
||||||
|
self.update_time: Dict[str, datetime] = {}
|
||||||
|
self.hold_connection: Optional[Task] = None
|
||||||
|
|
||||||
|
async def hold_connection_alive(self) -> None:
|
||||||
|
while True:
|
||||||
|
# Session automatically destroyed after 20m of inactivity
|
||||||
|
await asyncio.sleep(19*60)
|
||||||
|
self.check_session()
|
||||||
|
|
||||||
def _calc_challenge_v2(self, challenge: str) -> str:
|
def _calc_challenge_v2(self, challenge: str) -> str:
|
||||||
|
|
||||||
logging.debug(f"Calculate v2 challenge: {challenge}")
|
logging.debug(f"Calculate v2 challenge: {challenge}")
|
||||||
chall_regex = re.compile("2\$(?P<iter1>[0-9a-zA-Z]+)\$(?P<salt1>[0-9a-zA-Z]+)\$(?P<iter2>[0-9a-zA-Z]+)\$(?P<salt2>[0-9a-zA-Z]+)")
|
chall_regex = re.compile(
|
||||||
|
"2\$(?P<iter1>[0-9a-zA-Z]+)\$(?P<salt1>[0-9a-zA-Z]+)\$(?P<iter2>[0-9a-zA-Z]+)\$(?P<salt2>[0-9a-zA-Z]+)")
|
||||||
|
|
||||||
chall_parts = chall_regex.match(challenge).groupdict()
|
chall_parts = chall_regex.match(challenge).groupdict()
|
||||||
salt1: bytes = bytes.fromhex(chall_parts["salt1"])
|
salt1: bytes = bytes.fromhex(chall_parts["salt1"])
|
||||||
iter1: int = int(chall_parts["iter1"])
|
iter1: int = int(chall_parts["iter1"])
|
||||||
@ -42,8 +56,24 @@ class FritzBox:
|
|||||||
response = challenge + "-" + hashlib.md5(response).hexdigest()
|
response = challenge + "-" + hashlib.md5(response).hexdigest()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def login(self, user:str = None) -> bool:
|
def check_session(self) -> None:
|
||||||
logging.debug(f"login user {user}")
|
data = {
|
||||||
|
"xhr": 1,
|
||||||
|
"sid": self.sid,
|
||||||
|
"lang": "de",
|
||||||
|
"page": "overview",
|
||||||
|
"xhrId": "first",
|
||||||
|
"noMenuRef": 1
|
||||||
|
}
|
||||||
|
r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data)
|
||||||
|
if len(r.history) > 0:
|
||||||
|
if not self.login():
|
||||||
|
logging.error("Failed to login to Fritz!Box")
|
||||||
|
else:
|
||||||
|
logging.info("Already logged in")
|
||||||
|
|
||||||
|
def login(self, user: str = None) -> bool:
|
||||||
|
logging.info(f"Login user {user} to Fritz!Box")
|
||||||
challenge = None
|
challenge = None
|
||||||
r = self.session.get(f"{self.url}/{self._endpoints['login']}")
|
r = self.session.get(f"{self.url}/{self._endpoints['login']}")
|
||||||
xml = ET.fromstring(r.text)
|
xml = ET.fromstring(r.text)
|
||||||
@ -65,7 +95,7 @@ class FritzBox:
|
|||||||
response = self._calc_challenge_v1(challenge)
|
response = self._calc_challenge_v1(challenge)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"username": user,
|
"username": user,
|
||||||
"response": response
|
"response": response
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,27 +105,31 @@ class FritzBox:
|
|||||||
for elem in xml:
|
for elem in xml:
|
||||||
if elem.tag == "SID":
|
if elem.tag == "SID":
|
||||||
self.sid = elem.text
|
self.sid = elem.text
|
||||||
|
|
||||||
logging.debug(f"Authenticated fritzbox: {len(self.sid) != self.sid.count('0')}")
|
logging.info(f"Authenticated Fritz!Box: {len(self.sid) != self.sid.count('0')}")
|
||||||
|
if len(self.sid) != self.sid.count("0"):
|
||||||
|
self.hold_connection = asyncio.create_task(self.hold_connection_alive())
|
||||||
return len(self.sid) != self.sid.count("0")
|
return len(self.sid) != self.sid.count("0")
|
||||||
|
|
||||||
def logout(self) -> bool:
|
def logout(self) -> bool:
|
||||||
logging.debug("logout")
|
logging.info("logout")
|
||||||
data = {
|
data = {
|
||||||
"xhr":1,
|
"xhr": 1,
|
||||||
"sid": self.sid,
|
"sid": self.sid,
|
||||||
"logout": 1,
|
"logout": 1,
|
||||||
"no_sidrenew":""}
|
"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)
|
||||||
|
if self.hold_connection is not None:
|
||||||
|
self.hold_connection.cancel()
|
||||||
|
|
||||||
return r.status_code == 200
|
return r.status_code == 200
|
||||||
|
|
||||||
def list_devices(self):
|
def list_devices(self) -> Optional[Dict]:
|
||||||
data = {
|
data = {
|
||||||
"xhr": 1,
|
"xhr": 1,
|
||||||
"sid": self.sid,
|
"sid": self.sid,
|
||||||
"lang": "de",
|
"lang": "de",
|
||||||
"page":"sh_dev",
|
"page": "sh_dev",
|
||||||
"xhrId": "all"
|
"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)
|
||||||
@ -106,7 +140,7 @@ class FritzBox:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
devices = json.loads(r.text)["data"]["devices"]
|
devices = json.loads(r.text)["data"]["devices"]
|
||||||
|
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
def get_device_data(self, id: int = None, name: str = None) -> Optional[Tuple[float, float, int, str]]:
|
def get_device_data(self, id: int = None, name: str = None) -> Optional[Tuple[float, float, int, str]]:
|
||||||
@ -115,11 +149,12 @@ class FritzBox:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
devices = self.list_devices()
|
devices = self.list_devices()
|
||||||
|
device = None
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if device["id"] == id or device["displayName"] == name:
|
if device["id"] == id or device["displayName"] == name:
|
||||||
break
|
break
|
||||||
device = None
|
device = None
|
||||||
|
|
||||||
if device is None:
|
if device is None:
|
||||||
logging.debug(f"Device {id} {name} not found")
|
logging.debug(f"Device {id} {name} not found")
|
||||||
return None
|
return None
|
||||||
@ -135,32 +170,42 @@ class FritzBox:
|
|||||||
|
|
||||||
return current_temp, current_offset, device["id"], device["displayName"]
|
return current_temp, current_offset, device["id"], device["displayName"]
|
||||||
|
|
||||||
def set_offset(self, current_temp: str, offset: float, device_id: int, device_name: str):
|
def set_offset(self, current_temp: str, offset: float, device_id: int, device_name: str) -> None:
|
||||||
data = {
|
data = {
|
||||||
"xhr": 1,
|
"xhr": 1,
|
||||||
"sid": self.sid,
|
"sid": self.sid,
|
||||||
"lang": "de",
|
"lang": "de",
|
||||||
"device": device_id,
|
"device": device_id,
|
||||||
"page": "home_auto_hkr_edit"
|
"page": "home_auto_hkr_edit"
|
||||||
}
|
}
|
||||||
r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data)
|
r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"xhr":1,
|
"xhr": 1,
|
||||||
"sid": self.sid,
|
"sid": self.sid,
|
||||||
"lang": "de",
|
"lang": "de",
|
||||||
"device": device_id,
|
"device": device_id,
|
||||||
"view": "",
|
"view": "",
|
||||||
"back_to_page": "sh_dev",
|
"back_to_page": "sh_dev",
|
||||||
"ule_device_name": device_name,
|
"ule_device_name": device_name,
|
||||||
"WindowOpenTrigger":8,
|
"WindowOpenTrigger": 8,
|
||||||
"WindowOpenTimer":10,
|
"WindowOpenTimer": 10,
|
||||||
"tempsensor": "own",
|
"tempsensor": "own",
|
||||||
"Roomtemp": f"{current_temp}",
|
"Roomtemp": f"{current_temp}",
|
||||||
"ExtTempsensorID":"tochoose",
|
"ExtTempsensorID": "tochoose",
|
||||||
"Offset": f"{offset}",
|
"Offset": f"{offset}",
|
||||||
"apply":"",
|
"apply": "",
|
||||||
"oldpage":"/net/home_auto_hkr_edit.lua"
|
"oldpage": "/net/home_auto_hkr_edit.lua"
|
||||||
}
|
}
|
||||||
|
|
||||||
r = self.session.post(f"{self.url}/{self._endpoints['data']}", data=data)
|
self.session.post(f"{self.url}/{self._endpoints['data']}", data=data)
|
||||||
|
|
||||||
|
def correct_offset(self, device_name: str, real_temp: float):
|
||||||
|
if device_name in self.update_time.keys():
|
||||||
|
logging.info(f"Last update for {device_name} {datetime.now() - self.update_time[device_name]} ago")
|
||||||
|
delta = timedelta(minutes=5)
|
||||||
|
if device_name not in self.update_time.keys() or (datetime.now() - self.update_time[device_name]) > delta:
|
||||||
|
current_temp, current_offset, idx, name = self.get_device_data(name=device_name)
|
||||||
|
new_offset = current_offset + real_temp - current_temp
|
||||||
|
logging.info(f"Should update offset from {current_offset} to {new_offset}")
|
||||||
|
self.update_time[device_name] = datetime.now()
|
||||||
|
@ -1,66 +1,146 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
from asyncio import Queue, Task, Event, Lock
|
||||||
import websocket
|
from typing import Callable, Dict, Optional
|
||||||
import time
|
import websockets
|
||||||
|
|
||||||
|
"""
|
||||||
|
- sender fun, bekommt packete per queue
|
||||||
|
- receiver fun, schreibt packete in map id -> msg bzw events in queue(?)
|
||||||
|
- blockieren beim auf antwort warten per "pseudo-queue", in die sich alle wartenden eintragen und warten, dass sie leer ist, receiver leert queue wenn irgendeine nachricht rein kommt
|
||||||
|
- andere (auch außerhalb) können dann auf neue daten warten und ggf verarbeiten
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantAPI:
|
class HomeAssistantAPI:
|
||||||
def __init__(self, token:str, initialize: Callable[[HomeAssistantAPI], None]) -> None:
|
def __init__(self, token: str, url: str) -> None:
|
||||||
self.token = token
|
self.token = token
|
||||||
self.msg_id = 1
|
self.msg_id = 1
|
||||||
self.ws = None
|
self.msg_id_lock = Lock()
|
||||||
self.subscriptions = {}
|
self.ws: websockets.WebSocketClientProtocol = None
|
||||||
self.init_callback = initialize
|
self.url = url
|
||||||
|
self.receiver: Optional[Task] = None
|
||||||
|
self.sender: Optional[Task] = None
|
||||||
|
self.sending_queue: Queue = Queue()
|
||||||
|
self.authenticated: Event = Event()
|
||||||
|
self.events: Dict[int, Queue] = {}
|
||||||
|
self.responses: Dict[int, Dict] = {}
|
||||||
|
self.response_events: Dict[int, Event] = {}
|
||||||
|
self.response_lock: Lock = Lock()
|
||||||
|
|
||||||
def handle_message(self, ws: websocket.WebSocket, msg: str) -> None:
|
async def connect(self):
|
||||||
if self.ws is None:
|
self.ws = await websockets.connect(self.url)
|
||||||
self.ws = ws
|
self.sender = asyncio.create_task(self.sending())
|
||||||
|
await self.auth()
|
||||||
|
self.receiver = asyncio.create_task(self.receiving())
|
||||||
|
|
||||||
message: object = json.loads(msg)
|
async def wait_for_close(self):
|
||||||
|
await self.ws.wait_closed()
|
||||||
if message["type"] == "auth_required":
|
|
||||||
response = {
|
async def receiving(self):
|
||||||
|
logging.debug("Start receiving")
|
||||||
|
async for message in self.ws:
|
||||||
|
msg: Dict = json.loads(message)
|
||||||
|
if msg["type"] == "event":
|
||||||
|
if msg["id"] not in self.events.keys():
|
||||||
|
logging.error(f"Received event for not subscribted id: {msg['id']} {msg['event_type']}")
|
||||||
|
continue
|
||||||
|
await self.events[msg["id"]].put(msg["event"])
|
||||||
|
else:
|
||||||
|
async with self.response_lock:
|
||||||
|
self.responses[msg["id"]] = msg
|
||||||
|
if msg["id"] in self.response_events.keys():
|
||||||
|
self.response_events[msg["id"]].set()
|
||||||
|
|
||||||
|
async def wait_for(self, idx):
|
||||||
|
async with self.response_lock:
|
||||||
|
if idx in self.responses.keys():
|
||||||
|
msg = self.responses[idx]
|
||||||
|
del self.responses[idx]
|
||||||
|
return msg
|
||||||
|
self.response_events[idx] = Event()
|
||||||
|
|
||||||
|
await self.response_events[idx].wait()
|
||||||
|
async with self.response_lock:
|
||||||
|
del self.response_events[idx]
|
||||||
|
if idx not in self.responses.keys():
|
||||||
|
logging.error("Response ID not found")
|
||||||
|
return None
|
||||||
|
msg = self.responses[idx]
|
||||||
|
del self.responses[idx]
|
||||||
|
return msg
|
||||||
|
|
||||||
|
async def exit_loop(self):
|
||||||
|
if self.sender is not None:
|
||||||
|
self.sender.cancel()
|
||||||
|
if self.receiver is not None:
|
||||||
|
self.receiver.cancel()
|
||||||
|
asyncio.get_running_loop().stop()
|
||||||
|
|
||||||
|
async def auth(self):
|
||||||
|
msg = json.loads(await self.ws.recv())
|
||||||
|
if msg["type"] != "auth_required":
|
||||||
|
logging.error("Authentication error: Not required")
|
||||||
|
await self.exit_loop()
|
||||||
|
response = {
|
||||||
"type": "auth",
|
"type": "auth",
|
||||||
"access_token": self.token
|
"access_token": self.token
|
||||||
}
|
}
|
||||||
logging.debug(response)
|
await self.sending_queue.put(response)
|
||||||
ws.send(json.dumps(response))
|
msg = json.loads(await self.ws.recv())
|
||||||
return
|
if msg["type"] == "auth_invalid":
|
||||||
elif message["type"] == "auth_invalid":
|
|
||||||
logging.info("Auth failed")
|
logging.info("Auth failed")
|
||||||
ws.close()
|
await self.exit_loop()
|
||||||
return None
|
elif msg["type"] == "auth_ok":
|
||||||
elif message["type"] == "auth_ok":
|
|
||||||
logging.debug("Authenticated")
|
logging.debug("Authenticated")
|
||||||
self.init_callback(self)
|
self.authenticated.set()
|
||||||
self.init_callback = None
|
|
||||||
return
|
|
||||||
elif message["type"] == "event":
|
|
||||||
event = message["event"]
|
|
||||||
if event["event_type"] in self.subscriptions.keys():
|
|
||||||
self.subscriptions[event["event_type"]](event)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Received", message)
|
logging.error(f"Unknown answer for auth: {msg}")
|
||||||
|
await self.exit_loop()
|
||||||
def subscribe_event(self, event_type: str, callback: Callable[[object], None]):
|
|
||||||
|
|
||||||
if self.ws is None:
|
async def sending(self):
|
||||||
logging.debug("Websocket not set")
|
while msg := await self.sending_queue.get():
|
||||||
return
|
await self.ws.send(json.dumps(msg))
|
||||||
|
|
||||||
|
async def subscribe_event(self, event_type: str):
|
||||||
|
await self.authenticated.wait()
|
||||||
|
|
||||||
if event_type in self.subscriptions.keys():
|
|
||||||
logging.warning(f"Already subscribed to {event_type}")
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.info(f"Subscribe to {event_type}")
|
logging.info(f"Subscribe to {event_type}")
|
||||||
self.subscriptions[event_type] = callback
|
async with self.msg_id_lock:
|
||||||
response = {
|
msg_id = self.msg_id
|
||||||
"id": self.msg_id,
|
response = {
|
||||||
"type": "subscribe_events",
|
"id": msg_id,
|
||||||
"event_type": event_type
|
"type": "subscribe_events",
|
||||||
}
|
"event_type": event_type
|
||||||
self.msg_id += 1
|
}
|
||||||
self.ws.send(json.dumps(response))
|
self.events[msg_id] = Queue()
|
||||||
|
self.msg_id += 1
|
||||||
|
await self.sending_queue.put(response)
|
||||||
|
return msg_id
|
||||||
|
|
||||||
|
async def get_states(self):
|
||||||
|
await self.authenticated.wait()
|
||||||
|
async with self.msg_id_lock:
|
||||||
|
message = {
|
||||||
|
"id": self.msg_id,
|
||||||
|
"type": "get_states"
|
||||||
|
}
|
||||||
|
self.msg_id += 1
|
||||||
|
await self.sending_queue.put(message)
|
||||||
|
|
||||||
|
response = await self.wait_for(message["id"])
|
||||||
|
# ToDo: Error handling
|
||||||
|
return response["result"]
|
||||||
|
|
||||||
|
async def get_device_state(self, entity_id: str):
|
||||||
|
device_states = await self.get_states()
|
||||||
|
for device_state in device_states:
|
||||||
|
if device_state["entity_id"] == entity_id:
|
||||||
|
return device_state
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@ -3,31 +3,59 @@
|
|||||||
hier die verbindungen zu HA aufbauen etc
|
hier die verbindungen zu HA aufbauen etc
|
||||||
außerdem das vergleichen der werte und dass anstoßen der updates
|
außerdem das vergleichen der werte und dass anstoßen der updates
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from fritzbox import FritzBox
|
from fritzbox import FritzBox
|
||||||
from homeassistant import HomeAssistantAPI
|
from homeassistant import HomeAssistantAPI
|
||||||
import logging
|
import logging
|
||||||
import websocket
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
mappings = {}
|
sensor_mappings = {}
|
||||||
|
thermostate_mappings = {}
|
||||||
|
|
||||||
def handle_event(event):
|
async def handle_event(idx: int):
|
||||||
entity_id = event["data"]["entity_id"]
|
logging.debug(f"Wait for events for {idx}")
|
||||||
if entity_id in mappings.keys():
|
|
||||||
new_state = event["data"]["new_state"]
|
while event := await ha.events[idx].get():
|
||||||
logging.debug(entity_id)
|
entity_id = event["data"]["entity_id"]
|
||||||
logging.debug(new_state["attributes"]["temperature"])
|
if entity_id in sensor_mappings.keys() or entity_id in thermostate_mappings.keys():
|
||||||
rounded = round(float(new_state["attributes"]["temperature"])*2)/2
|
state = await ha.get_device_state(entity_id)
|
||||||
logging.debug(rounded)
|
new_state = event["data"]["new_state"]
|
||||||
if new_state["attributes"]["device_class"] == "temperature":
|
logging.info(f"received changed state from {entity_id}")
|
||||||
if entity_id in mappings.keys():
|
if entity_id in thermostate_mappings.keys() and state["state"] != "unavailable":
|
||||||
|
therm_temp = new_state["attributes"]["current_temperature"]
|
||||||
|
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
|
||||||
|
if therm_temp != sensor_temp:
|
||||||
|
logging.info(f"{therm_name}: {therm_temp}")
|
||||||
|
logging.info(f"{sensor}: {sensor_state['attributes']['temperature']} ({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
|
||||||
|
"""
|
||||||
fb.login()
|
fb.login()
|
||||||
logged = False
|
logged = False
|
||||||
for thermostate in mappings[entity_id]:
|
"""
|
||||||
|
for thermostate in sensor_mappings[entity_id]:
|
||||||
|
therm_state = await ha.get_device_state(thermostate)
|
||||||
|
if therm_state["state"] == "unavailable":
|
||||||
|
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}")
|
||||||
|
logging.info(f"{entity_id}: {new_state['attributes']['temperature']} ({sensor_temp})")
|
||||||
|
fb.correct_offset(therm_name, sensor_temp)
|
||||||
|
"""
|
||||||
current_temp, current_offset, id, name = fb.get_device_data(name=thermostate)
|
current_temp, current_offset, id, name = fb.get_device_data(name=thermostate)
|
||||||
if not logged:
|
if not logged:
|
||||||
logging.info(f"Current measurement from {entity_id}: {new_state['attributes']['temperature']} ({rounded})")
|
logging.info(
|
||||||
|
f"Current measurement from {entity_id}: {new_state['attributes']['temperature']} ({rounded})")
|
||||||
logged = True
|
logged = True
|
||||||
logging.info(f"Current measurement from {thermostate}: {current_temp}")
|
logging.info(f"Current measurement from {thermostate}: {current_temp}")
|
||||||
new_offset = current_offset + rounded - current_temp
|
new_offset = current_offset + rounded - current_temp
|
||||||
@ -42,37 +70,65 @@ def handle_event(event):
|
|||||||
else:
|
else:
|
||||||
logging.warning(f"Failed to adjust offset from {old_offset} to {new_offset}")
|
logging.warning(f"Failed to adjust offset from {old_offset} to {new_offset}")
|
||||||
fb.logout()
|
fb.logout()
|
||||||
|
"""
|
||||||
|
|
||||||
def on_error(ws, error):
|
|
||||||
print(error)
|
|
||||||
|
|
||||||
def on_close(ws, close_status_code, close_msg):
|
async def init(ha: HomeAssistantAPI):
|
||||||
pass
|
await ha.connect()
|
||||||
|
|
||||||
def on_open(ws):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def init(ha: HomeAssistantAPI):
|
|
||||||
logging.debug("Subscribe")
|
logging.debug("Subscribe")
|
||||||
ha.subscribe_event("state_changed", handle_event)
|
state_changed_id = await ha.subscribe_event("state_changed")
|
||||||
|
logging.debug(state_changed_id)
|
||||||
|
asyncio.create_task(handle_event(state_changed_id))
|
||||||
|
fb.login()
|
||||||
|
await ha.wait_for_close()
|
||||||
|
logging.info("Websocket closed, shutting down..")
|
||||||
|
asyncio.get_running_loop().stop()
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_config(config_path: str, ha: HomeAssistantAPI):
|
||||||
|
config = json.load(open(config_path))
|
||||||
|
therm_ids = {}
|
||||||
|
for state in await ha.get_states():
|
||||||
|
if state["entity_id"].startswith("climate.") and "friendly_name" in state["attributes"].keys():
|
||||||
|
therm_ids[state["attributes"]["friendly_name"]] = state["entity_id"]
|
||||||
|
|
||||||
|
mappings = []
|
||||||
|
for mapping in config["mappings"]:
|
||||||
|
if not mapping["thermostate"].startswith("climate."):
|
||||||
|
mapping["thermostate"] = therm_ids[mapping["thermostate"]]
|
||||||
|
mappings.append(mapping)
|
||||||
|
config["mappings"] = mappings
|
||||||
|
json.dump(open(config_path), config)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] %(message)s")
|
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] %(message)s")
|
||||||
config = json.load(open("/data/options.json"))
|
config_path = "/data/options.json"
|
||||||
|
config_path = "options.json"
|
||||||
|
config = json.load(open(config_path))
|
||||||
logging.debug(config)
|
logging.debug(config)
|
||||||
for mapping in config["mappings"]:
|
|
||||||
if mapping["sensor"] not in mappings.keys():
|
|
||||||
mappings[mapping["sensor"]] = []
|
|
||||||
mappings[mapping["sensor"]].append(mapping["thermostate"])
|
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
fb = FritzBox(config["fritzbox"]["url"], config["fritzbox"]["password"])
|
fb = FritzBox(config["fritzbox"]["url"], config["fritzbox"]["password"])
|
||||||
ha = HomeAssistantAPI(os.environ["SUPERVISOR_TOKEN"], init)
|
supervisor_url = "ws://supervisor/core/websocket"
|
||||||
|
supervisor_url = "ws://192.168.124.187:8123/api/websocket"
|
||||||
|
ha = HomeAssistantAPI(os.environ["SUPERVISOR_TOKEN"], supervisor_url)
|
||||||
|
|
||||||
websocket.enableTrace(False)
|
if '"thermostate": "climate.' not in open(config_path).read():
|
||||||
ws = websocket.WebSocketApp("ws://supervisor/core/websocket",
|
config = loop.run_until_complete(migrate_config(config_path, ha))
|
||||||
on_open=on_open,
|
logging.info(config)
|
||||||
on_message=ha.handle_message,
|
exit()
|
||||||
on_error=on_error,
|
|
||||||
on_close=on_close)
|
|
||||||
|
|
||||||
ws.run_forever()
|
for mapping in config["mappings"]:
|
||||||
|
if mapping["sensor"] not in sensor_mappings.keys():
|
||||||
|
sensor_mappings[mapping["sensor"]] = []
|
||||||
|
sensor_mappings[mapping["sensor"]].append(mapping["thermostate"])
|
||||||
|
thermostate_mappings[mapping["thermostate"]] = mapping["sensor"]
|
||||||
|
|
||||||
|
loop.create_task(init(ha))
|
||||||
|
try:
|
||||||
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
Loading…
x
Reference in New Issue
Block a user