diff --git a/.gitignore b/.gitignore index 7587344..c0c8839 100644 --- a/.gitignore +++ b/.gitignore @@ -243,4 +243,4 @@ fabric.properties # local config test options.json - +.DS_Store diff --git a/fritz_temp_sync/Dockerfile b/fritz_temp_sync/Dockerfile index 225290b..ee10167 100755 --- a/fritz_temp_sync/Dockerfile +++ b/fritz_temp_sync/Dockerfile @@ -11,6 +11,7 @@ WORKDIR /data COPY sync_ha_fb.py /srv COPY fritzbox.py /srv COPY homeassistant.py /srv +COPY device.py /srv COPY run.sh / RUN chmod a+x /run.sh diff --git a/fritz_temp_sync/config.yaml b/fritz_temp_sync/config.yaml index d2bbbaa..f564228 100755 --- a/fritz_temp_sync/config.yaml +++ b/fritz_temp_sync/config.yaml @@ -1,10 +1,11 @@ -name: "Fritz!Box Temperature Sync" +name: "Fritz!Box Temperature Sync Dev" description: "Sync Fritz!DECT thermostat temperatures with other sensors in Home Assistant" -version: "0.3.1" +version: "0.4.3" startup: "application" -stage: "experimental" -slug: "fritz_temp_sync" +stage: "stable" +slug: "fritz_temp_sync_dev" homeassistant_api: true +init: false arch: - aarch64 - amd64 @@ -14,7 +15,6 @@ arch: options: fritzbox: url: "http://fritz.box" - username: null password: null mappings: - sensor: null @@ -28,4 +28,4 @@ schema: mappings: - sensor: str thermostate: str - update_timeout: int \ No newline at end of file + update_timeout: int diff --git a/fritz_temp_sync/device.py b/fritz_temp_sync/device.py new file mode 100755 index 0000000..a192e2d --- /dev/null +++ b/fritz_temp_sync/device.py @@ -0,0 +1,714 @@ + +from __future__ import annotations + +from abc import ABC, abstractmethod +from enum import Enum, IntFlag, auto +from typing import Dict, List, Optional, Union +import typing + + +class WeekDay(IntFlag): + MON = 0b1 + TUE = 0b10 + WED = 0b100 + THU = 0b1000 + FRI = 0b10000 + SAT = 0b100000 + SUN = 0b1000000 + + +class Manufacturer: + def __init__(self, name: str): + self.name: str = name + + def __repr__(self): + return f"name: {self.name}" + + def to_json(self): + return {"name": self.name} + + @staticmethod + def parse_dict(manufacturer: Dict): + return Manufacturer(manufacturer["name"]) + + +class FirmwareVersion: + def __init__(self, search: bool, current: str, update: bool, running: bool): + self.search: bool = search + self.current: str = current + self.update: bool = update + self.running: bool = running + + def __repr__(self): + return f"search: {self.search}; current: {self.current}; update: {self.update}; running: {self.running}" + + def to_json(self): + return {"search": self.search, "current": self.current, "update": self.update, "running": self.running} + + @staticmethod + def parse_dict(firmware_version: dict): + return FirmwareVersion(firmware_version["search"], + firmware_version["current"], + firmware_version["update"], + firmware_version["running"]) + + +class PushService: + def __init__(self, mail_address: str, unit_settings: List, is_enabled: bool): + self.mail_address: str = mail_address + self.unit_settings: List = unit_settings + self.is_enabled: bool = is_enabled + + def __repr__(self): + return f"" + + def to_json(self): + return {"mailAddress": self.mail_address, "unitSettings": self.unit_settings, "isEnabled": self.is_enabled} + + @staticmethod + def parse_dict(push_service: Dict): + return PushService(push_service["mailAddress"], push_service["unitSettings"], push_service["isEnabled"]) + + +class SkillType(Enum): + SmartHomeTemperatureSensor = auto() + SmartHomeThermostat = auto() + SmartHomeBattery = auto() + + +class Skill(ABC): + def __init__(self, skill_type: SkillType): + self.type: SkillType = skill_type + + @abstractmethod + def to_json(self): + pass + + @abstractmethod + def to_web_data(self, device_id: int): + pass + + @staticmethod + def parse_dict(skill: Dict): + skill_type = SkillType[skill["type"]] + if skill_type == SkillType.SmartHomeTemperatureSensor: + return TemperatureSkill.parse_dict(skill) + elif skill_type == SkillType.SmartHomeThermostat: + return ThermostatSkill.parse_dict(skill) + elif skill_type == SkillType.SmartHomeBattery: + return BatterySkill.parse_dict(skill) + else: + raise NotImplementedError(skill_type) + + +class PresetName(Enum): + LOWER_TEMPERATURE = auto() + UPPER_TEMPERATURE = auto() + HOLIDAY_TEMPERATURE = auto() + + +class Preset: + def __init__(self, name: PresetName, temperature: Optional[int]): + self.name: PresetName = name + self.temperature: Optional[int] = temperature + + def __repr__(self): + return f"" + + def to_json(self): + return {"name": self.name.name, "temperature": self.temperature} + + +class Description: + def __init__(self, action: str, preset_temperature: Optional[Preset]): + self.action: str = action + self.preset_temperature: Optional[Preset] = preset_temperature + + def __repr__(self): + desc = f"" + + def to_json(self): + return {"description": self.description.to_json(), "timeSetting": self.time_setting.to_json()} + + @staticmethod + def parse_dict(change: Dict): + return Change(Description.parse_dict(change["description"]), TimeSetting.parse_dict(change["timeSetting"])) + + +class TemperatureDropDetection: + def __init__(self, do_not_heat_offset_in_minutes: int, sensitivity: int): + self.do_not_heat_offset_in_minutes: int = do_not_heat_offset_in_minutes + self.sensitivity: int = sensitivity + + def __repr__(self): + # ToDo + pass + + def to_json(self): + return { + "doNotHeatOffsetInMinutes": self.do_not_heat_offset_in_minutes, + "sensitivity": self.sensitivity + } + + def to_web_data(self, device_id: int): + return { + "WindowOpenTrigger": self.sensitivity + 3, + "WindowOpenTimer": self.do_not_heat_offset_in_minutes + } + + @staticmethod + def parse_dict(drop_detection: Dict): + return TemperatureDropDetection(drop_detection["doNotHeatOffsetInMinutes"], + drop_detection["sensitivity"]) + + +class ScheduleKind(Enum): + REPETITIVE = auto() + WEEKLY_TIMETABLE = auto() + + +class ThermostatSkillMode(Enum): + TARGET_TEMPERATURE = auto() + + +class Action: + def __init__(self, is_enabled: bool, time_setting: TimeSetting, description: Description): + self.is_enabled: bool = is_enabled + self.time_setting: TimeSetting = time_setting + self.description: Description = description + + def __repr__(self): + return f"" + + def to_json(self): + return {"isEnabled": self.is_enabled, + "timeSetting": self.time_setting.to_json(), + "description": self.description.to_json()} + + @staticmethod + def parse_dict(action: Dict): + return Action(action["isEnabled"], TimeSetting.parse_dict(action["timeSetting"]), + Description.parse_dict(action["description"])) + + +class Schedule: + def __init__(self, is_enabled: bool, kind: ScheduleKind, name: str, actions: List[Action]): + self.is_enabled: bool = is_enabled + self.kind: ScheduleKind = kind + self.name: str = name + self.actions: List[Action] = actions + + def __repr__(self): + return f"" + + def to_json(self): + return { + "isEnabled": self.is_enabled, + "kind": self.kind.name, + "name": self.name, + "actions": [action.to_json() for action in self.actions] + } + + def to_web_data(self, device_id: int): + data = {} + if self.kind == ScheduleKind.REPETITIVE and self.name == "HOLIDAYS": # ToDo: Enum for names? + enabled_count = 0 + for num, holiday in enumerate(self.actions): + num += 1 + _, start_month, start_day = holiday.time_setting.start_date.split("-") + _, end_month, end_day = holiday.time_setting.end_date.split("-") + data.update({ + f"Holiday{num}StartDay": start_day, + f"Holiday{num}StartMonth": start_month, + f"Holiday{num}StartHour": holiday.time_setting.start_time.split(":")[0], + f"Holiday{num}EndDay": end_day, + f"Holiday{num}EndMonth": end_month, + f"Holiday{num}EndHour": holiday.time_setting.end_time.split(":")[0], + f"Holiday{num}Enabled": 1 if holiday.is_enabled else 0, + f"Holiday{num}ID": num + }) + if holiday.is_enabled: + enabled_count += 1 + data["HolidayEnabledCount"] = enabled_count + elif self.kind == ScheduleKind.REPETITIVE and self.name == "SUMMER_TIME": + _, start_month, start_day = self.actions[0].time_setting.start_date.split("-") + _, end_month, end_day = self.actions[0].time_setting.end_date.split("-") + data = { + "SummerStartDay": start_day, + "SummerStartMonth": start_month, + "SummerEndDay": end_day, + "SummerEndMonth": end_month, + "SummerEnabled": 1 if self.is_enabled else 0 + } + elif self.kind == ScheduleKind.WEEKLY_TIMETABLE and self.name == "TEMPERATURE": + timer_items = {} + for action in self.actions: + if not action.is_enabled: + continue + time = "".join(action.time_setting.start_time.split(":")[:-1]) + if time not in timer_items.keys(): + timer_items[time] = [0, 0] + heat = 1 if action.description.preset_temperature.name == PresetName.UPPER_TEMPERATURE else 0 + days = timer_items[time][heat] + days |= action.time_setting.day_of_week + timer_items[time][heat] = days + num = 0 + for key in timer_items.keys(): + if timer_items[key][0] != 0: + data.update({f"timer_item_{num}": f"{key};0;{timer_items[key][0].as_integer_ratio()[0]}"}) + num += 1 + if timer_items[key][1] != 0: + data.update({f"timer_item_{num}": f"{key};1;{timer_items[key][1].as_integer_ratio()[0]}"}) + num += 1 + else: + raise NotImplementedError(self.name) + + return data + + @staticmethod + def parse_dict(schedule: Dict): # ToDo: Make TypedDicts for all those dicts + return Schedule(schedule["isEnabled"], + ScheduleKind[schedule["kind"]], + schedule["name"], + [Action.parse_dict(action) for action in schedule["actions"]]) + + +class TimeControl: + def __init__(self, is_enabled: bool, time_schedules: List[Schedule]): + self.is_enabled: bool = is_enabled + self.time_schedules: List[Schedule] = time_schedules + + def __repr__(self): + return f"" - - def to_json(self): - return {"mailAddress": self.mail_address, "unitSettings": self.unit_settings, "isEnabled": self.is_enabled} - - @staticmethod - def parse_dict(push_service: Dict): - return PushService(push_service["mailAddress"], push_service["unitSettings"], push_service["isEnabled"]) - - -class SkillType(Enum): - SmartHomeTemperatureSensor = auto() - SmartHomeThermostat = auto() - SmartHomeBattery = auto() - - -class Skill(ABC): - def __init__(self, skill_type: SkillType): - self.type: SkillType = skill_type - - @abstractmethod - def to_json(self): - pass - - @abstractmethod - def to_web_data(self, device_id: int): - pass - - @staticmethod - def parse_dict(skill: Dict): - skill_type = SkillType[skill["type"]] - if skill_type == SkillType.SmartHomeTemperatureSensor: - return TemperatureSkill.parse_dict(skill) - elif skill_type == SkillType.SmartHomeThermostat: - return ThermostatSkill.parse_dict(skill) - elif skill_type == SkillType.SmartHomeBattery: - return BatterySkill.parse_dict(skill) - else: - raise NotImplementedError(skill_type) - - -class PresetName(Enum): - LOWER_TEMPERATURE = auto() - UPPER_TEMPERATURE = auto() - HOLIDAY_TEMPERATURE = auto() - - -class Preset: - def __init__(self, name: PresetName, temperature: int): - self.name: PresetName = name - self.temperature: int = temperature - - def __repr__(self): - return f"" - - def to_json(self): - return {"name": self.name.name, "temperature": self.temperature} - - -class Description: - def __init__(self, action: str, preset_temperature: Optional[Preset]): - self.action: str = action - self.preset_temperature: Optional[Preset] = preset_temperature - - def __repr__(self): - desc = f"" - - def to_json(self): - return {"description": self.description.to_json(), "timeSetting": self.time_setting.to_json()} - - @staticmethod - def parse_dict(change: Dict): - return Change(Description.parse_dict(change["description"]), TimeSetting.parse_dict(change["timeSetting"])) - - -class TemperatureDropDetection: - def __init__(self, do_not_heat_offset_in_minutes: int, sensitivity: int, is_window_open: bool): - self.do_not_heat_offset_in_minutes: int = do_not_heat_offset_in_minutes - self.sensitivity: int = sensitivity - self.is_window_open: bool = is_window_open - - def __repr__(self): - # ToDo - pass - - def to_json(self): - return { - "doNotHeatOffsetInMinutes": self.do_not_heat_offset_in_minutes, - "sensitivity": self.sensitivity, - "isWindowOpen": self.is_window_open - } - - def to_web_data(self, device_id: int): - return { - "WindowOpenTrigger": self.sensitivity + 3, - "WindowOpenTimer": self.do_not_heat_offset_in_minutes - } - - @staticmethod - def parse_dict(drop_detection: Dict): - return TemperatureDropDetection(drop_detection["doNotHeatOffsetInMinutes"], - drop_detection["sensitivity"], - drop_detection["isWindowOpen"]) - - -class ScheduleKind(Enum): - REPETITIVE = auto() - WEEKLY_TIMETABLE = auto() - - -class ThermostatSkillMode(Enum): - TARGET_TEMPERATURE = auto() - - -class Action: - def __init__(self, is_enabled: bool, time_setting: TimeSetting, description: Description): - self.is_enabled: bool = is_enabled - self.time_setting: TimeSetting = time_setting - self.description: Description = description - - def __repr__(self): - return f"" - - def to_json(self): - return {"isEnabled": self.is_enabled, - "timeSetting": self.time_setting.to_json(), - "description": self.description.to_json()} - - @staticmethod - def parse_dict(action: Dict): - return Action(action["isEnabled"], TimeSetting.parse_dict(action["timeSetting"]), - Description.parse_dict(action["description"])) - - -class Schedule: - def __init__(self, is_enabled: bool, kind: ScheduleKind, name: str, actions: List[Action]): - self.is_enabled: bool = is_enabled - self.kind: ScheduleKind = kind - self.name: str = name - self.actions: List[Action] = actions - - def __repr__(self): - return f"" - - def to_json(self): - return { - "isEnabled": self.is_enabled, - "kind": self.kind.name, - "name": self.name, - "actions": [action.to_json() for action in self.actions] - } - - def to_web_data(self, device_id: int): - data = {} - if self.kind == ScheduleKind.REPETITIVE and self.name == "HOLIDAYS": # ToDo: Enum for names? - enabled_count = 0 - for num, holiday in enumerate(self.actions): - num += 1 - _, start_month, start_day = holiday.time_setting.start_date.split("-") - _, end_month, end_day = holiday.time_setting.end_date.split("-") - data.update({ - f"Holiday{num}StartDay": start_day, - f"Holiday{num}StartMonth": start_month, - f"Holiday{num}StartHour": holiday.time_setting.start_time.split(":")[0], - f"Holiday{num}EndDay": end_day, - f"Holiday{num}EndMonth": end_month, - f"Holiday{num}EndHour": holiday.time_setting.end_time.split(":")[0], - f"Holiday{num}Enabled": 1 if holiday.is_enabled else 0, - f"Holiday{num}ID": num - }) - if holiday.is_enabled: - enabled_count += 1 - data["HolidayEnabledCount"] = enabled_count - elif self.kind == ScheduleKind.REPETITIVE and self.name == "SUMMER_TIME": - _, start_month, start_day = self.actions[0].time_setting.start_date.split("-") - _, end_month, end_day = self.actions[0].time_setting.end_date.split("-") - data = { - "SummerStartDay": start_day, - "SummerStartMonth": start_month, - "SummerEndDay": end_day, - "SummerEndMonth": end_month, - "SummerEnabled": 1 if self.is_enabled else 0 - } - elif self.kind == ScheduleKind.WEEKLY_TIMETABLE and self.name == "TEMPERATURE": - timer_items = {} - for action in self.actions: - if not action.is_enabled: - continue - time = "".join(action.time_setting.start_time.split(":")[:-1]) - if time not in timer_items.keys(): - timer_items[time] = [0, 0] - heat = 1 if action.description.preset_temperature.name == PresetName.UPPER_TEMPERATURE else 0 - days = timer_items[time][heat] - days |= action.time_setting.day_of_week - timer_items[time][heat] = days - num = 0 - for key in timer_items.keys(): - if timer_items[key][0] != 0: - data.update({f"timer_item_{num}": f"{key};0;{timer_items[key][0].as_integer_ratio()[0]}"}) - num += 1 - if timer_items[key][1] != 0: - data.update({f"timer_item_{num}": f"{key};1;{timer_items[key][1].as_integer_ratio()[0]}"}) - num += 1 - else: - raise NotImplementedError(self.name) - - return data - - @staticmethod - def parse_dict(schedule: Dict): # ToDo: Make TypedDicts for all those dicts - return Schedule(schedule["isEnabled"], - ScheduleKind[schedule["kind"]], - schedule["name"], - [Action.parse_dict(action) for action in schedule["actions"]]) - - -class TimeControl: - def __init__(self, is_enabled: bool, time_schedules: List[Schedule]): - self.is_enabled: bool = is_enabled - self.time_schedules: List[Schedule] = time_schedules - - def __repr__(self): - return f" None: @@ -757,6 +34,7 @@ class FritzBox: async def hold_connection_alive(self) -> None: while True: # Session automatically destroyed after 20m of inactivity + # according to the manual await asyncio.sleep(19 * 60) self.check_session() @@ -799,7 +77,7 @@ class FritzBox: if not self.login(): logging.error("Failed to login to Fritz!Box") else: - logging.info("Already logged in") + logging.debug("Already logged in") def login(self) -> bool: logging.info(f"Login user {self.user} to Fritz!Box") @@ -814,7 +92,7 @@ class FritzBox: elif self.user is None and elem.tag == "Users": for user_elem in elem: if "fritz" in user_elem.text: - user = user_elem.text + self.user = user_elem.text assert challenge is not None and self.user is not None diff --git a/fritz_temp_sync/homeassistant.py b/fritz_temp_sync/homeassistant.py index 47bb9cd..19f5f72 100755 --- a/fritz_temp_sync/homeassistant.py +++ b/fritz_temp_sync/homeassistant.py @@ -27,22 +27,24 @@ class HomeAssistantAPI: async def connect(self): retries = 5 + logging.info("Connect to home assistant...") while True: try: self.ws = await websockets.connect(self.url) self.sender = asyncio.create_task(self.sending()) await self.auth() self.receiver = asyncio.create_task(self.receiving()) - return - except InvalidStatusCode: + return True + except (InvalidStatusCode, TimeoutError): if retries > 0: retries -= 1 - await asyncio.sleep(10) - logging.info("Retry home assistant connection...") + await asyncio.sleep(30) + logging.info(f"Retry home assistant connection... ({retries})") continue else: logging.error("Invalid status code while connecting to Home Assistant") await self.exit_loop() + return False async def wait_for_close(self): await self.ws.wait_closed() @@ -85,7 +87,6 @@ class HomeAssistantAPI: 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()) diff --git a/fritz_temp_sync/run.sh b/fritz_temp_sync/run.sh index 2e60ee2..c40f1aa 100755 --- a/fritz_temp_sync/run.sh +++ b/fritz_temp_sync/run.sh @@ -1,3 +1,3 @@ -#!/usr/bin/env bashio +#!/usr/bin/with-contenv bashio python3 /srv/sync_ha_fb.py /data/options.json \ No newline at end of file diff --git a/fritz_temp_sync/sync_ha_fb.py b/fritz_temp_sync/sync_ha_fb.py index a2023d2..8cf57ec 100755 --- a/fritz_temp_sync/sync_ha_fb.py +++ b/fritz_temp_sync/sync_ha_fb.py @@ -3,9 +3,8 @@ import asyncio import os import sys -from typing import Dict -from fritzbox import FritzBox, Device +from fritzbox import FritzBox from homeassistant import HomeAssistantAPI import logging import json @@ -18,62 +17,40 @@ async def handle_event(idx: int): logging.debug(f"Wait for events for {idx}") while event := await ha.events[idx].get(): - entity_id = event["data"]["entity_id"] - if entity_id in sensor_mappings.keys() or entity_id in thermostate_mappings.keys(): - state = await ha.get_device_state(entity_id) - new_state = event["data"]["new_state"] - logging.info(f"received changed state from {entity_id}") - 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() - logged = False - """ - 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"] + try: + entity_id = event["data"]["entity_id"] + if entity_id in sensor_mappings.keys() or entity_id in thermostate_mappings.keys(): + state = await ha.get_device_state(entity_id) + new_state = event["data"]["new_state"] + logging.debug(f"received changed state from {entity_id}") + 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"{entity_id}: {new_state['attributes']['temperature']} ({sensor_temp})") + logging.info(f"{therm_name}: {therm_temp}\n{sensor}: {sensor_state['attributes']['temperature']} ({sensor_temp})") fb.correct_offset(therm_name, sensor_temp) - """ - current_temp, current_offset, id, name = fb.get_device_data(name=thermostate) - if not logged: - logging.info( - f"Current measurement from {entity_id}: {new_state['attributes']['temperature']} ({rounded})") - logged = True - logging.info(f"Current measurement from {thermostate}: {current_temp}") - new_offset = current_offset + rounded - current_temp - if new_offset != current_offset: - old_offset = current_offset - logging.debug(f"Set offset for {thermostate} from {current_offset} to {new_offset}") - fb.set_offset(current_temp, new_offset, id, name) - current_temp, current_offset, id, name = fb.get_device_data(name=thermostate) - logging.debug(f"Target: {new_offset} ; Set: {current_offset}") - if new_offset == current_offset: - logging.info(f"Adjustet offset from {old_offset} to {new_offset}") - else: - logging.warning(f"Failed to adjust offset from {old_offset} to {new_offset}") - fb.logout() - """ + + elif entity_id in sensor_mappings.keys(): + sensor_temp = round(float(new_state["attributes"]["temperature"]) * 2) / 2 + 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}\n{entity_id}: {new_state['attributes']['temperature']} ({sensor_temp})") + fb.correct_offset(therm_name, sensor_temp) + except KeyError: + pass async def init(ha: HomeAssistantAPI, fb: FritzBox): - await ha.connect() + if not await ha.connect(): + return logging.debug("Subscribe") state_changed_id = await ha.subscribe_event("state_changed") logging.debug(state_changed_id) @@ -81,31 +58,37 @@ async def init(ha: HomeAssistantAPI, fb: FritzBox): fb.login() await ha.wait_for_close() logging.info("Websocket closed, shutting down..") - asyncio.get_running_loop().stop() +async def main(): + config_path = sys.argv[1] + config = json.load(open(config_path)) + level = logging.INFO + if "log_level" in config: + if config["log_level"] == "DEBUG": + level = logging.DEBUG + logging.basicConfig(level=level, format="[%(asctime)s] [%(levelname)s] %(message)s") + logging.debug(config) -logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] %(message)s") -config_path = sys.argv[1] -config = json.load(open(config_path)) -logging.debug(config) + 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"] + global ha + ha = HomeAssistantAPI(supervisor_token, supervisor_url) -loop = asyncio.get_event_loop() -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" -ha = HomeAssistantAPI(os.environ["SUPERVISOR_TOKEN"], supervisor_url) + 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"] -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"] + try: + await init(ha, fb) + except KeyboardInterrupt: + pass -loop.create_task(init(ha, fb)) -try: - loop.run_forever() -except KeyboardInterrupt: - pass +asyncio.run(main())