From 80e96db5de46d1389a905dfd08ab40de212b900b Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sat, 7 Feb 2026 00:00:57 +0100 Subject: [PATCH 01/29] WIP Rest API 1 --- pyfritzhome/cli.py | 89 +++++++++---------- pyfritzhome/devicetypes/__init__.py | 30 ++----- .../devicetypes/fritzhomedevicebase.py | 80 ++++++++--------- .../devicetypes/fritzhomedeviceswitch.py | 81 ----------------- .../devicetypes/fritzhomeentitybase.py | 70 ++++----------- pyfritzhome/devicetypes/fritzhomeinterface.py | 27 ++++++ .../devicetypes/fritzhomeinterfacebase.py | 34 +++++++ .../devicetypes/fritzhomeonoffinterface.py | 74 +++++++++++++++ pyfritzhome/devicetypes/fritzhomeunit.py | 22 +++++ pyfritzhome/devicetypes/fritzhomeunitbase.py | 74 +++++++++++++++ pyfritzhome/fritzhome.py | 86 +++++++++++++++--- pyfritzhome/fritzhomedevice.py | 30 ++----- 12 files changed, 417 insertions(+), 280 deletions(-) delete mode 100644 pyfritzhome/devicetypes/fritzhomedeviceswitch.py create mode 100644 pyfritzhome/devicetypes/fritzhomeinterface.py create mode 100644 pyfritzhome/devicetypes/fritzhomeinterfacebase.py create mode 100644 pyfritzhome/devicetypes/fritzhomeonoffinterface.py create mode 100644 pyfritzhome/devicetypes/fritzhomeunit.py create mode 100644 pyfritzhome/devicetypes/fritzhomeunitbase.py diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 9f1aae4..6b0669a 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -20,15 +20,12 @@ def list_all(fritz, args): print("#" * 30) print("name=%s" % device.name) print(" ain=%s" % device.ain) - print(" id=%s" % device.identifier) print(" productname=%s" % device.productname) print(" manufacturer=%s" % device.manufacturer) print(" present=%s" % device.present) - print(" lock=%s" % device.lock) - print(" devicelock=%s" % device.device_lock) - print(" is_group=%s" % device.is_group) - if device.is_group: - print(" group_members=%s" % device.group_members) + #~ print(" is_group=%s" % device.is_group) + #~ if device.is_group: + #~ print(" group_members=%s" % device.group_members) if device.present is False: continue @@ -36,46 +33,46 @@ def list_all(fritz, args): if device.has_switch: print(" Switch:") print(" switch_state=%s" % device.switch_state) - if device.has_powermeter: - print(" Powermeter:") - print(" power=%s" % device.power) - print(" energy=%s" % device.energy) - print(" voltage=%s" % device.voltage) - if device.has_temperature_sensor: - print(" Temperature:") - print(" temperature=%s" % device.temperature) - print(" offset=%s" % device.offset) - if device.has_thermostat: - print(" Thermostat:") - print(" battery_low=%s" % device.battery_low) - print(" battery_level=%s" % device.battery_level) - print(" actual=%s" % device.actual_temperature) - print(" target=%s" % device.target_temperature) - print(" comfort=%s" % device.comfort_temperature) - print(" eco=%s" % device.eco_temperature) - print(" window=%s" % device.window_open) - print(" window_until=%s" % device.window_open_endtime) - print(" boost=%s" % device.boost_active) - print(" boost_until=%s" % device.boost_active_endtime) - print(" adaptive_heating_running=%s" % device.adaptive_heating_running) - print(" summer=%s" % device.summer_active) - print(" holiday=%s" % device.holiday_active) - if device.has_alarm: - print(" Alert:") - print(" alert=%s" % device.alert_state) - if device.has_lightbulb: - print(" Light bulb:") - print(" state=%s" % ("Off" if device.state == 0 else "On")) - if device.has_level: - print(" level=%s" % device.level) - if device.has_color: - print(" hue=%s" % device.hue) - print(" saturation=%s" % device.saturation) - if device.has_blind: - print(" Blind:") - print(" level=%s" % device.level) - print(" levelpercentage=%s" % device.levelpercentage) - print(" endpositionset=%s" % device.endpositionsset) + #~ if device.has_powermeter: + #~ print(" Powermeter:") + #~ print(" power=%s" % device.power) + #~ print(" energy=%s" % device.energy) + #~ print(" voltage=%s" % device.voltage) + #~ if device.has_temperature_sensor: + #~ print(" Temperature:") + #~ print(" temperature=%s" % device.temperature) + #~ print(" offset=%s" % device.offset) + #~ if device.has_thermostat: + #~ print(" Thermostat:") + #~ print(" battery_low=%s" % device.battery_low) + #~ print(" battery_level=%s" % device.battery_level) + #~ print(" actual=%s" % device.actual_temperature) + #~ print(" target=%s" % device.target_temperature) + #~ print(" comfort=%s" % device.comfort_temperature) + #~ print(" eco=%s" % device.eco_temperature) + #~ print(" window=%s" % device.window_open) + #~ print(" window_until=%s" % device.window_open_endtime) + #~ print(" boost=%s" % device.boost_active) + #~ print(" boost_until=%s" % device.boost_active_endtime) + #~ print(" adaptive_heating_running=%s" % device.adaptive_heating_running) + #~ print(" summer=%s" % device.summer_active) + #~ print(" holiday=%s" % device.holiday_active) + #~ if device.has_alarm: + #~ print(" Alert:") + #~ print(" alert=%s" % device.alert_state) + #~ if device.has_lightbulb: + #~ print(" Light bulb:") + #~ print(" state=%s" % ("Off" if device.state == 0 else "On")) + #~ if device.has_level: + #~ print(" level=%s" % device.level) + #~ if device.has_color: + #~ print(" hue=%s" % device.hue) + #~ print(" saturation=%s" % device.saturation) + #~ if device.has_blind: + #~ print(" Blind:") + #~ print(" level=%s" % device.level) + #~ print(" levelpercentage=%s" % device.levelpercentage) + #~ print(" endpositionset=%s" % device.endpositionsset) def device_name(fritz, args): diff --git a/pyfritzhome/devicetypes/__init__.py b/pyfritzhome/devicetypes/__init__.py index c84fb3b..c84c52f 100644 --- a/pyfritzhome/devicetypes/__init__.py +++ b/pyfritzhome/devicetypes/__init__.py @@ -1,32 +1,16 @@ """Init file for the device types.""" -from .fritzhomedevicealarm import FritzhomeDeviceAlarm -from .fritzhomedevicebutton import FritzhomeDeviceButton -from .fritzhomedevicehumidity import FritzhomeDeviceHumidity -from .fritzhomedevicelevel import FritzhomeDeviceLevel -from .fritzhomedevicepowermeter import FritzhomeDevicePowermeter -from .fritzhomedevicerepeater import FritzhomeDeviceRepeater -from .fritzhomedeviceswitch import FritzhomeDeviceSwitch -from .fritzhomedevicetemperature import FritzhomeDeviceTemperature -from .fritzhomedevicethermostat import FritzhomeDeviceThermostat -from .fritzhomedevicelightbulb import FritzhomeDeviceLightBulb -from .fritzhomedeviceblind import FritzhomeDeviceBlind +from .fritzhomeunit import FritzhomeUnit from .fritzhometemplate import FritzhomeTemplate from .fritzhometrigger import FritzhomeTrigger - +from .fritzhomedevicebase import FritzhomeDeviceBase +from .fritzhomeinterface import FritzhomeInterface +from .fritzhomeonoffinterface import * __all__ = ( - "FritzhomeDeviceAlarm", - "FritzhomeDeviceButton", - "FritzhomeDeviceHumidity", - "FritzhomeDeviceLevel", - "FritzhomeDevicePowermeter", - "FritzhomeDeviceRepeater", - "FritzhomeDeviceSwitch", - "FritzhomeDeviceTemperature", - "FritzhomeDeviceThermostat", - "FritzhomeDeviceLightBulb", - "FritzhomeDeviceBlind", + "FritzhomeUnit", "FritzhomeTemplate", "FritzhomeTrigger", + "FritzhomeInterface", + "FritzhomeOnOffInterface", "FritzhomeOnOffMixin", ) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 3ebabc9..1014417 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -14,22 +14,14 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): """The Fritzhome Device class.""" - battery_level = None - battery_low = None - identifier = None - is_group = None - fw_version = None - group_members = None - manufacturer = None - productname = None - present = None - tx_busy = None + def __init__(self, fritz=None, node=None): + super().__init__(fritz, node) + self._units = [] def __repr__(self): """Return a string.""" - return "{ain} {identifier} {manuf} {prod} {name}".format( + return "{ain} {manuf} {prod} {name}".format( ain=self.ain, - identifier=self.identifier, manuf=self.manufacturer, prod=self.productname, name=self.name, @@ -42,31 +34,39 @@ def update(self): def _update_from_node(self, node): _LOGGER.debug("update base device") super()._update_from_node(node) - self.ain = node.attrib["identifier"] - self.identifier = node.attrib["id"] - self.fw_version = node.attrib["fwversion"] - self.manufacturer = node.attrib["manufacturer"] - self.productname = node.attrib["productname"] - - self.present = bool(int(node.findtext("present"))) - - groupinfo = node.find("groupinfo") - self.is_group = groupinfo is not None - if self.is_group: - self.group_members = str(groupinfo.findtext("members")).split(",") - - try: - self.tx_busy = self.get_node_value_as_int_as_bool(node, "txbusy") - except Exception: - pass - - try: - self.battery_low = self.get_node_value_as_int_as_bool(node, "batterylow") - self.battery_level = int(self.get_node_value_as_int(node, "battery")) - except Exception: - pass - - # General - def get_present(self): - """Check if the device is present.""" - return self._fritz.get_device_present(self.ain) + + @property + def uid(self): + return self._node["UID"] + + @property + def manufacturer(self): + return self._node["manufacturer"] + + @property + def product_name(self): + return self._node["productName"] + + # legacy + @property + def productname(self): + return self.product_name + + # legacy + @property + def is_connected(self): + return self._node["isConnected"] + + # legacy + @property + def present(self): + return self.is_connected + + def clear_units(self): + self._units = [] + + def add_unit(self, unit): + self._units.append(unit) + + def units(self): + return self._units.values() diff --git a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py deleted file mode 100644 index 92f1de3..0000000 --- a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py +++ /dev/null @@ -1,81 +0,0 @@ -"""The switch device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDeviceSwitch(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - switch_state = None - switch_mode = None - lock = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_switch: - self._update_switch_from_node(node) - - # Switch - @property - def has_switch(self): - """Check if the device has switch function.""" - if self._has_feature(FritzhomeDeviceFeatures.SWITCH): - # for AVM plugs like FRITZ!DECT 200 and FRITZ!DECT 210 - return True - if self._has_feature( - FritzhomeDeviceFeatures.SWITCHABLE - ) and not self._has_feature(FritzhomeDeviceFeatures.LIGHTBULB): - # for HAN-FUN plugs - return True - return False - - def _update_switch_from_node(self, node): - _LOGGER.debug("update switch device") - if self._has_feature(FritzhomeDeviceFeatures.SWITCH): - val = node.find("switch") - try: - self.switch_state = self.get_node_value_as_int_as_bool(val, "state") - except Exception: - self.switch_state = None - self.switch_mode = self.get_node_value(val, "mode") - try: - self.lock = self.get_node_value_as_int_as_bool(val, "lock") - except Exception: - self.lock = None - - # optional value - try: - self.device_lock = self.get_node_value_as_int_as_bool(val, "devicelock") - except Exception: - pass - else: - val = node.find("simpleonoff") - try: - self.switch_state = self.get_node_value_as_int_as_bool(val, "state") - except Exception: - self.switch_state = None - - def get_switch_state(self): - """Get the switch state.""" - return self._fritz.get_switch_state(self.ain) - - def set_switch_state_on(self, wait=False): - """Set the switch state to on.""" - return self._fritz.set_switch_state_on(self.ain, wait) - - def set_switch_state_off(self, wait=False): - """Set the switch state to off.""" - return self._fritz.set_switch_state_off(self.ain, wait) - - def set_switch_state_toggle(self, wait=False): - """Toggle the switch state.""" - return self._fritz.set_switch_state_toggle(self.ain, wait) diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index c6d8de1..0774b51 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -7,7 +7,7 @@ import logging -from xml.etree import ElementTree +import json from .fritzhomedevicefeatures import FritzhomeDeviceFeatures _LOGGER = logging.getLogger(__name__) @@ -16,69 +16,31 @@ class FritzhomeEntityBase(ABC): """The Fritzhome Entity class.""" - _fritz = None - ain: str - _functionsbitmask: int = 0 - supported_features = None - def __init__(self, fritz=None, node=None): """Create an entity base object.""" - if fritz is not None: - self._fritz = fritz + self._fritz = fritz + self._node = node if node is not None: self._update_from_node(node) def __repr__(self): """Return a string.""" - return "{ain} {name}".format( - ain=self.ain, - name=self.name, - ) - - def _has_feature(self, feature: FritzhomeDeviceFeatures) -> bool: - return feature in FritzhomeDeviceFeatures(self._functionsbitmask) + return f"{self.ain} {self.name}" def _update_from_node(self, node): - _LOGGER.debug(ElementTree.tostring(node)) - self.ain = node.attrib["identifier"] - self._functionsbitmask = int(node.attrib["functionbitmask"]) - - self.name = node.findtext("name").strip() - - self.supported_features = [] - for feature in FritzhomeDeviceFeatures: - if self._has_feature(feature): - self.supported_features.append(feature) + _LOGGER.debug(json.dumps(node)) + if self.ain != node["ain"]: + raise ValueError("updating invalid ain") + self._node = node @property - def device_and_unit_id(self): - """Get the device and possible unit id.""" - if ( - self.ain.startswith("tmp") - or self.ain.startswith("grp") - or self.ain.startswith("trg") - ): - return (self.ain, None) - elif self.ain.startswith("Z") and len(self.ain) == 19: - return (self.ain[0:17], self.ain[17:]) - elif "-" in self.ain: - return tuple(self.ain.split("-")) - return (self.ain, None) - - # XML Helpers + def node(self): + return self._node; - def get_node_value(self, elem, node): - """Get the node value.""" - return elem.findtext(node) - - def get_node_value_as_int(self, elem, node) -> int: - """Get the node value as integer.""" - return int(self.get_node_value(elem, node)) - - def get_node_value_as_int_as_bool(self, elem, node) -> bool: - """Get the node value as boolean.""" - return bool(self.get_node_value_as_int(elem, node)) + @property + def ain(self): + return self._node["ain"]; - def get_temp_from_node(self, elem, node): - """Get the node temp value as float.""" - return float(self.get_node_value(elem, node)) / 2 + @property + def name(self): + return self._node["name"]; diff --git a/pyfritzhome/devicetypes/fritzhomeinterface.py b/pyfritzhome/devicetypes/fritzhomeinterface.py new file mode 100644 index 0000000..8d451b5 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeinterface.py @@ -0,0 +1,27 @@ +"""The entity base class.""" + +# -*- coding: utf-8 -*- + +from __future__ import print_function +from abc import ABC + + +import logging +import json + +from .fritzhomeinterfacebase import FritzhomeInterfaceBase +from .fritzhomeonoffinterface import FritzhomeOnOffInterface + +_LOGGER = logging.getLogger(__name__) + +class FritzhomeInterface(FritzhomeOnOffInterface): + """The Fritzhome Interface class.""" + + def __init__(self, type, node = None): + """Create an entity base object.""" + super().__init__(type, node) + + # interfaces are not entities, only their parent units are, therefore this is + # called with the unit REST node + def _update_from_node(self, node): + super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomeinterfacebase.py b/pyfritzhome/devicetypes/fritzhomeinterfacebase.py new file mode 100644 index 0000000..769b5ce --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeinterfacebase.py @@ -0,0 +1,34 @@ +"""The entity base class.""" + +# -*- coding: utf-8 -*- + +from __future__ import print_function +from abc import ABC + + +import logging +import json + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeInterfaceBase(): + """The Fritzhome Interface class.""" + + def __init__(self, type, node): + """Create an entity base object.""" + self.type = type + self._node = node + if node is not None: + self._update_from_node(node) + + def __repr__(self): + """Return a string.""" + return f"{self.type} of {self._unit.ain}" + + def _update_from_node(self, node): + pass + + @property + def node(self): + return self._node; diff --git a/pyfritzhome/devicetypes/fritzhomeonoffinterface.py b/pyfritzhome/devicetypes/fritzhomeonoffinterface.py new file mode 100644 index 0000000..161e1f6 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeonoffinterface.py @@ -0,0 +1,74 @@ +"""The switch device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .fritzhomeinterfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeOnOffInterface(FritzhomeInterfaceBase): + """The Fritzhome OnOff interface class.""" + + # Switch + @property + def is_switch(self): + return self.type == "onOffInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_switch: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.switch_state = self._node["active"] + + def set_switch_state_on(self, wait=False): + self._node["active"] = True + + def set_switch_state_off(self, wait=False): + self._node["active"] = False + + def set_switch_state_toggle(self, wait=False): + self._node["active"] = not self._node["active"] + + +class FritzhomeOnOffMixin(): + + def find_switch_interface(self): + #~ return next((unit for unit in self._units if unit.is_switch), None) + for unit in self.units(): + if interface := unit.interfaces.get("onOffInterface"): + return (unit, interface) + return None + + @property + def has_switch(self): + return self.find_switch_interface() != None + + @property + def switch_state(self): + """ Get the current switch state """ + if pair := self.find_switch_interface(): + return pair[1].switch_state + + def set_switch_state_on(self): + """Set the switch state to on.""" + if pair := self.find_switch_interface(): + pair[1].set_switch_state_on() + pair[0].update() + + def set_switch_state_off(self): + """Set the switch state to off.""" + if pair := self.find_switch_interface(): + pair[1].set_switch_state_off() + pair[0].update() + + def set_switch_state_toggle(self): + """Toggle the switch state.""" + if pair := self.find_switch_interface(): + pair[1].set_switch_state_toggle() + pair[0].update() + diff --git a/pyfritzhome/devicetypes/fritzhomeunit.py b/pyfritzhome/devicetypes/fritzhomeunit.py new file mode 100644 index 0000000..894c5ac --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeunit.py @@ -0,0 +1,22 @@ +"""The base device class.""" +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import logging + +from .fritzhomeunitbase import FritzhomeUnitBase +from .fritzhomeonoffinterface import * + +_LOGGER = logging.getLogger(__name__) + +class FritzhomeUnit(FritzhomeUnitBase, + FritzhomeOnOffMixin): + """The Fritzhome Device class.""" + + def __init__(self, fritz=None, node=None): + """Create a device object.""" + super().__init__(fritz, node) + + def _update_from_node(self, node): + super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py new file mode 100644 index 0000000..2a0f055 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -0,0 +1,74 @@ +"""The base device class.""" +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import logging + +from .fritzhomeentitybase import FritzhomeEntityBase +from .fritzhomeinterface import FritzhomeInterface + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeUnitBase(FritzhomeEntityBase): + """The Fritzhome Device class.""" + + def __init__(self, fritz=None, node=None): + super().__init__(fritz, node) + interfaces = {} + + def __repr__(self): + """Return a string.""" + return f"{self.ain} of device {self.parent}" + + def fetch(self): + """Update the device values.""" + # TODO: update specific unit (targeted REST endpoint) + self._fritz.update_units() + + def _update_from_node(self, node): + super()._update_from_node(node) + _LOGGER.debug("update unit base") + if self.ain != node["ain"]: + raise ValueError + + # unshare class attribute on write + self.interfaces = {} + for iface, node in node["interfaces"].items(): + self.interfaces[iface] = FritzhomeInterface(iface, node) + + def units(self): + return [self] + + def update(self): + pass + + @property + def parent(self): + return self._node["parentUid"] + + @property + def device(self): + return self._node["deviceUid"] + + @property + def is_connected(self): + return self._node["isConnected"] + + @property + def is_group(self): + return self._node["isGroupUnit"] + + @property + def statistics(self): + return self._node["statistics"] + + @property + def unit_type(self): + return self._node["unitType"] + + # General + def get_present(self): + """Check if the unit is present.""" + return self.is_connected diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 0c2ca1b..f6de485 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -6,6 +6,7 @@ import hashlib import logging import time +import json from xml.etree import ElementTree from cryptography.hazmat.primitives import hashes @@ -15,6 +16,7 @@ from .errors import InvalidError, LoginError, NotLoggedInError from .fritzhomedevice import FritzhomeDevice +from .fritzhomedevice import FritzhomeUnit from .fritzhomedevice import FritzhomeTemplate from .fritzhomedevice import FritzhomeTrigger from typing import Dict, Optional @@ -27,6 +29,7 @@ class Fritzhome(object): _sid = None _session = None + _units: Optional[Dict[str, FritzhomeUnit]] = None _devices: Optional[Dict[str, FritzhomeDevice]] = None _templates: Optional[Dict[str, FritzhomeTemplate]] = None _triggers: Optional[Dict[str, FritzhomeTrigger]] = None @@ -44,11 +47,12 @@ def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10) self.base_url = f"{host}:{port}" if port else host else: self.base_url = f"http://{host}:{port}" if port else f"http://{host}" + self.rest_url = f"{self.base_url}/api/v0/smarthome" - def _request(self, url, params=None): + def _request(self, url, params=None, headers=None): """Send a request with parameters.""" rsp = self._session.get( - url, params=params, timeout=self._timeout, verify=self._ssl_verify + url, params=params, headers=headers, timeout=self._timeout, verify=self._ssl_verify ) rsp.raise_for_status() return rsp.text.strip() @@ -130,6 +134,26 @@ def _aha_request(self, cmd, ain=None, param=None, rf=str): return bool(int(plain)) return rf(plain) + def _rest_request(self, endpoint, param=None): + """Send an REST API request""" + url = f"{self.rest_url}/{endpoint}" + + _LOGGER.debug("self._sid:%s", self._sid) + + if not self._sid: + raise NotLoggedInError + + params = {"Authorization": f"AVM-SID {self._sid}"} + if param: + params.update(param) + + response = self._request(url, headers=params) + data = json.loads(response) + #~ if data.contains("errors"): + #~ raise InvalidError + + return data + def login(self): """Login and get a valid session ID.""" (sid, challenge, blocktime) = self._login_request() @@ -156,6 +180,37 @@ def logout(self): self._logout_request() self._sid = None + def update_unit(self, uid): + if self._units is None: + self._units = {} + + _LOGGER.info("Updating units ...") + data = self._rest_request("overview/units/{uid}") + if uid in self._units.keys(): + _LOGGER.info( + "Updating already existing unit " + uid + ) + self._units[uid]._update_from_node(data) + else: + raise RuntimeError + + def update_units(self): + if self._units is None: + self._units = {} + + _LOGGER.info("Updating units ...") + data = self._rest_request("overview/units") + for element in data: + ain = element["ain"] + if ain in self._units.keys(): + _LOGGER.info( + "Updating already existing unit " + ain + ) + self._units[ain]._update_from_node(element) + else: + _LOGGER.info("Adding new unit " + ain) + self._units[ain] = FritzhomeUnit(self, node=element) + def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") @@ -164,15 +219,21 @@ def update_devices(self, ignore_removed=True): device_elements = self.get_device_elements() for element in device_elements: - if element.attrib["identifier"] in self._devices.keys(): + uid = element["UID"] + if uid in self._devices.keys(): _LOGGER.info( - "Updating already existing Device " + element.attrib["identifier"] + "Updating already existing Device " + uid ) - self._devices[element.attrib["identifier"]]._update_from_node(element) + self._devices[uid]._update_from_node(element) else: - _LOGGER.info("Adding new Device " + element.attrib["identifier"]) - device = FritzhomeDevice(self, node=element) - self._devices[device.ain] = device + _LOGGER.info("Adding new Device " + uid) + self._devices[uid] = FritzhomeDevice(self, node=element) + self._devices[uid].clear_units() + for unit_ain in element["unitUids"]: + if unit_ain in self._units.keys(): + self._devices[uid].add_unit(self._units[unit_ain]) + else: + _LOGGER.warning(f"Unknown unit {unit_ain}") if not ignore_removed: for identifier in list(self._devices.keys()): @@ -222,11 +283,12 @@ def wait_device_txbusy(self, ain, retries=10): return False def get_device_elements(self): - """Get the DOM elements for the device list.""" - return self._get_listinfo_elements("device") + """Get the JSON elements for the device list.""" + resp = self._rest_request("overview/devices") + return resp def get_device_element(self, ain): - """Get the DOM element for the specified device.""" + """Get the JSON element for the specified device.""" elements = self.get_device_elements() for element in elements: if element.attrib["identifier"] == ain: @@ -239,6 +301,8 @@ def get_devices(self): def get_devices_as_dict(self): """Get the list of all known devices.""" + if self._units is None: + self.update_units() if self._devices is None: self.update_devices() return self._devices diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 59648f1..95f18df 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -2,35 +2,15 @@ # -*- coding: utf-8 -*- +from .devicetypes import FritzhomeUnit # noqa: F401 from .devicetypes import FritzhomeTemplate # noqa: F401 from .devicetypes import FritzhomeTrigger # noqa: F401 -from .devicetypes import ( - FritzhomeDeviceAlarm, - FritzhomeDeviceBlind, - FritzhomeDeviceButton, - FritzhomeDeviceHumidity, - FritzhomeDeviceLevel, - FritzhomeDeviceLightBulb, - FritzhomeDevicePowermeter, - FritzhomeDeviceRepeater, - FritzhomeDeviceSwitch, - FritzhomeDeviceTemperature, - FritzhomeDeviceThermostat, -) - +from .devicetypes import FritzhomeDeviceBase +from .devicetypes import FritzhomeOnOffMixin class FritzhomeDevice( - FritzhomeDeviceAlarm, - FritzhomeDeviceBlind, - FritzhomeDeviceButton, - FritzhomeDeviceHumidity, - FritzhomeDeviceLevel, - FritzhomeDeviceLightBulb, - FritzhomeDevicePowermeter, - FritzhomeDeviceRepeater, - FritzhomeDeviceSwitch, - FritzhomeDeviceTemperature, - FritzhomeDeviceThermostat, + FritzhomeDeviceBase, + FritzhomeOnOffMixin ): """The Fritzhome Device class.""" From b586e305e2e2e582168a8c1c5452d3d938dcf6d7 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sat, 7 Feb 2026 00:05:38 +0100 Subject: [PATCH 02/29] TMP testdata --- pyfritzhome/__init__.py | 2 +- pyfritzhome/cli.py | 7 +++++++ pyfritzhome/fritzhome.py | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyfritzhome/__init__.py b/pyfritzhome/__init__.py index 312fead..be0ac19 100644 --- a/pyfritzhome/__init__.py +++ b/pyfritzhome/__init__.py @@ -6,7 +6,7 @@ from .fritzhome import Fritzhome from .fritzhomedevice import FritzhomeDevice -__version__ = version(__name__) +__version__ = "0.6.18" #version(__name__) __all__ = ( "Fritzhome", diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 6b0669a..7469588 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -231,6 +231,12 @@ def main(args=None): version="{version}".format(version=__version__), help="Print version", ) + parser.add_argument( + "--test-data", + action="store_true", + dest="testdata", + help="Use offline test data" + ) _sub = parser.add_subparsers(title="Commands") @@ -396,6 +402,7 @@ def main(args=None): password=args.password, port=args.port or None, ssl_verify=not args.insecure, + use_testdata=args.testdata ) fritzbox.login() args.func(fritzbox, args) diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index f6de485..b997dce 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -34,7 +34,7 @@ class Fritzhome(object): _templates: Optional[Dict[str, FritzhomeTemplate]] = None _triggers: Optional[Dict[str, FritzhomeTrigger]] = None - def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10): + def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10, use_testdata=False): """Create a fritzhome object.""" self._user = user self._password = password @@ -43,6 +43,7 @@ def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10) self._timeout = timeout self._has_getdeviceinfos = True self._has_txbusy = True + self._use_testdata = use_testdata if host.startswith("https://") or host.startswith("http://"): self.base_url = f"{host}:{port}" if port else host else: @@ -136,6 +137,8 @@ def _aha_request(self, cmd, ain=None, param=None, rf=str): def _rest_request(self, endpoint, param=None): """Send an REST API request""" + if self._use_testdata: + return json.load(open(f"testdata/{endpoint.replace("/", "_")}.json.txt", "r")) url = f"{self.rest_url}/{endpoint}" _LOGGER.debug("self._sid:%s", self._sid) @@ -156,6 +159,8 @@ def _rest_request(self, endpoint, param=None): def login(self): """Login and get a valid session ID.""" + if self._use_testdata: + return (sid, challenge, blocktime) = self._login_request() _LOGGER.info("sid:%s, challenge:%s, blocktime:%s", sid, challenge, blocktime) if sid == "0000000000000000": From 226e50c112032a021f0ddd8a451e3095b570d97b Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sun, 8 Feb 2026 22:55:53 +0100 Subject: [PATCH 03/29] multimeter and temperature --- pyfritzhome/cli.py | 19 ++--- pyfritzhome/devicetypes/__init__.py | 4 + .../devicetypes/fritzhomedevicebase.py | 5 +- .../devicetypes/fritzhomedevicepowermeter.py | 68 ----------------- .../devicetypes/fritzhomedevicetemperature.py | 47 ------------ pyfritzhome/devicetypes/fritzhomeinterface.py | 6 +- .../fritzhomemultimeterinterface.py | 76 +++++++++++++++++++ .../fritzhometemperatureinterface.py | 58 ++++++++++++++ pyfritzhome/devicetypes/fritzhomeunit.py | 8 +- pyfritzhome/fritzhomedevice.py | 6 +- 10 files changed, 168 insertions(+), 129 deletions(-) delete mode 100644 pyfritzhome/devicetypes/fritzhomedevicepowermeter.py delete mode 100644 pyfritzhome/devicetypes/fritzhomedevicetemperature.py create mode 100644 pyfritzhome/devicetypes/fritzhomemultimeterinterface.py create mode 100644 pyfritzhome/devicetypes/fritzhometemperatureinterface.py diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 7469588..4494ac3 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -33,15 +33,16 @@ def list_all(fritz, args): if device.has_switch: print(" Switch:") print(" switch_state=%s" % device.switch_state) - #~ if device.has_powermeter: - #~ print(" Powermeter:") - #~ print(" power=%s" % device.power) - #~ print(" energy=%s" % device.energy) - #~ print(" voltage=%s" % device.voltage) - #~ if device.has_temperature_sensor: - #~ print(" Temperature:") - #~ print(" temperature=%s" % device.temperature) - #~ print(" offset=%s" % device.offset) + if device.has_powermeter: + print(" Powermeter:") + print(" power=%s" % device.power) + print(" energy=%s" % device.energy) + print(" voltage=%s" % device.voltage) + print(" current=%s" % device.current) + if device.has_temperature_sensor: + print(" Temperature:") + print(" temperature=%s" % device.temperature) + print(" offset=%s" % device.offset) #~ if device.has_thermostat: #~ print(" Thermostat:") #~ print(" battery_low=%s" % device.battery_low) diff --git a/pyfritzhome/devicetypes/__init__.py b/pyfritzhome/devicetypes/__init__.py index c84c52f..2766ab7 100644 --- a/pyfritzhome/devicetypes/__init__.py +++ b/pyfritzhome/devicetypes/__init__.py @@ -6,6 +6,8 @@ from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomeinterface import FritzhomeInterface from .fritzhomeonoffinterface import * +from .fritzhomemultimeterinterface import * +from .fritzhometemperatureinterface import * __all__ = ( "FritzhomeUnit", @@ -13,4 +15,6 @@ "FritzhomeTrigger", "FritzhomeInterface", "FritzhomeOnOffInterface", "FritzhomeOnOffMixin", + "FritzhomeMultimeterInterface", "FritzhomeMultimeterMixin", + "FritzhomeTemperatureInterface", "FritzhomeTemperatureMixin", ) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 1014417..5b2b4f6 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -29,12 +29,15 @@ def __repr__(self): def update(self): """Update the device values.""" - self._fritz.update_devices() + self._fritz.update_device() def _update_from_node(self, node): _LOGGER.debug("update base device") super()._update_from_node(node) + def get_config(): + self._fritz.update_device_config(self.ain) + @property def uid(self): return self._node["UID"] diff --git a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py deleted file mode 100644 index a4e651a..0000000 --- a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py +++ /dev/null @@ -1,68 +0,0 @@ -"""The powermeter device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDevicePowermeter(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - power = None - energy = None - voltage = None - current = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_powermeter: - self._update_powermeter_from_node(node) - - # Power Meter - @property - def has_powermeter(self): - """Check if the device has powermeter function.""" - return self._has_feature(FritzhomeDeviceFeatures.POWER_METER) - - def _update_powermeter_from_node(self, node): - _LOGGER.debug("update powermeter device") - val = node.find("powermeter") - - try: - self.power = int(val.findtext("power")) - except Exception: - pass - - try: - self.energy = int(val.findtext("energy")) - except Exception: - pass - - try: - self.voltage = int(val.findtext("voltage")) - except Exception: - pass - - if ( - isinstance(self.power, int) - and isinstance(self.voltage, int) - and self.voltage > 0 - ): - self.current = self.power / self.voltage * 1000 - else: - self.current = None - - def get_switch_power(self): - """Get the switch state.""" - return self._fritz.get_switch_power(self.ain) - - def get_switch_energy(self): - """Get the switch energy.""" - return self._fritz.get_switch_energy(self.ain) diff --git a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py deleted file mode 100644 index 9ca1c4e..0000000 --- a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py +++ /dev/null @@ -1,47 +0,0 @@ -"""The temperature device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDeviceTemperature(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - offset = None - temperature = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_temperature_sensor: - self._update_temperature_from_node(node) - - # Temperature - @property - def has_temperature_sensor(self): - """Check if the device has temperature function.""" - return self._has_feature(FritzhomeDeviceFeatures.TEMPERATURE) - - def _update_temperature_from_node(self, node): - _LOGGER.debug("update temperature device") - temperature_element = node.find("temperature") - try: - self.offset = ( - self.get_node_value_as_int(temperature_element, "offset") / 10.0 - ) - except ValueError: - pass - - try: - self.temperature = ( - self.get_node_value_as_int(temperature_element, "celsius") / 10.0 - ) - except ValueError: - pass diff --git a/pyfritzhome/devicetypes/fritzhomeinterface.py b/pyfritzhome/devicetypes/fritzhomeinterface.py index 8d451b5..11186ab 100644 --- a/pyfritzhome/devicetypes/fritzhomeinterface.py +++ b/pyfritzhome/devicetypes/fritzhomeinterface.py @@ -11,10 +11,14 @@ from .fritzhomeinterfacebase import FritzhomeInterfaceBase from .fritzhomeonoffinterface import FritzhomeOnOffInterface +from .fritzhomemultimeterinterface import FritzhomeMultimeterInterface +from .fritzhometemperatureinterface import FritzhomeTemperatureInterface _LOGGER = logging.getLogger(__name__) -class FritzhomeInterface(FritzhomeOnOffInterface): +class FritzhomeInterface(FritzhomeOnOffInterface, + FritzhomeMultimeterInterface, + FritzhomeTemperatureInterface): """The Fritzhome Interface class.""" def __init__(self, type, node = None): diff --git a/pyfritzhome/devicetypes/fritzhomemultimeterinterface.py b/pyfritzhome/devicetypes/fritzhomemultimeterinterface.py new file mode 100644 index 0000000..dad3261 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomemultimeterinterface.py @@ -0,0 +1,76 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .fritzhomeinterfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeMultimeterInterface(FritzhomeInterfaceBase): + """The Fritzhome Device class.""" + + power = None + energy = None + voltage = None + current = None + + @property + def is_powermeter(self): + return self.type == "multimeterInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_powermeter: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.power = self._node["power"] + self.energy = self._node["energy"] + self.voltage = self._node["voltage"] + self.current = self._node["current"] + + + +class FritzhomeMultimeterMixin(): + """The Fritzhome Multimeter mixin.""" + + def find_multimeter_interface(self): + #~ return next((unit for unit in self._units if unit.is_switch), None) + for unit in self.units(): + if interface := unit.interfaces.get("multimeterInterface"): + return (unit, interface) + return None + + @property + def has_powermeter(self): + """Check if the device has powermeter sensors.""" + return self.find_multimeter_interface() != None + + @property + def power(self): + """ Get the current powermeter power """ + if pair := self.find_multimeter_interface(): + return pair[1].current + + @property + def energy(self): + """ Get the current currentmeter energy """ + if pair := self.find_multimeter_interface(): + return pair[1].energy + + @property + def voltage(self): + """ Get the current voltagemeter voltage """ + if pair := self.find_multimeter_interface(): + return pair[1].voltage + + @property + def current(self): + """ Get the current currentmeter current """ + if pair := self.find_multimeter_interface(): + return pair[1].current + + diff --git a/pyfritzhome/devicetypes/fritzhometemperatureinterface.py b/pyfritzhome/devicetypes/fritzhometemperatureinterface.py new file mode 100644 index 0000000..fde8452 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhometemperatureinterface.py @@ -0,0 +1,58 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .fritzhomeinterfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeTemperatureInterface(FritzhomeInterfaceBase): + """The Fritzhome Device class.""" + + celsius = None + + @property + def is_temperature(self): + return self.type == "temperatureInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_temperature: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.celsius = self._node["celsius"] + # offset is not always exposed (for Thermo 302 the offset is in the thermostatInterface) + self.offset = self._node.get("offset") or 0.0 + + + +class FritzhomeTemperatureMixin(): + """The Fritzhome Temperature mixin.""" + + def find_temperature_interface(self): + #~ return next((unit for unit in self._units if unit.is_switch), None) + for unit in self.units(): + if interface := unit.interfaces.get("temperatureInterface"): + return (unit, interface) + return None + + @property + def has_temperature_sensor(self): + """Check if the device has temperature sensors.""" + return self.find_temperature_interface() != None + + @property + def temperature(self): + """ Get the current temperature """ + if pair := self.find_temperature_interface(): + return pair[1].celsius + + @property + def offset(self): + """ Get the current temperature offset """ + if pair := self.find_temperature_interface(): + return pair[1].offset diff --git a/pyfritzhome/devicetypes/fritzhomeunit.py b/pyfritzhome/devicetypes/fritzhomeunit.py index 894c5ac..41206e9 100644 --- a/pyfritzhome/devicetypes/fritzhomeunit.py +++ b/pyfritzhome/devicetypes/fritzhomeunit.py @@ -6,12 +6,16 @@ import logging from .fritzhomeunitbase import FritzhomeUnitBase -from .fritzhomeonoffinterface import * +from .fritzhomeonoffinterface import FritzhomeOnOffMixin +from .fritzhomemultimeterinterface import FritzhomeMultimeterMixin +from .fritzhometemperatureinterface import FritzhomeTemperatureMixin _LOGGER = logging.getLogger(__name__) class FritzhomeUnit(FritzhomeUnitBase, - FritzhomeOnOffMixin): + FritzhomeOnOffMixin, + FritzhomeMultimeterMixin, + FritzhomeTemperatureMixin): """The Fritzhome Device class.""" def __init__(self, fritz=None, node=None): diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 95f18df..5e5644d 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -7,10 +7,14 @@ from .devicetypes import FritzhomeTrigger # noqa: F401 from .devicetypes import FritzhomeDeviceBase from .devicetypes import FritzhomeOnOffMixin +from .devicetypes import FritzhomeMultimeterMixin +from .devicetypes import FritzhomeTemperatureMixin class FritzhomeDevice( FritzhomeDeviceBase, - FritzhomeOnOffMixin + FritzhomeOnOffMixin, + FritzhomeMultimeterMixin, + FritzhomeTemperatureMixin, ): """The Fritzhome Device class.""" From 0299b0a0f43bdba47d3207f0d87d9a932a00c688 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Tue, 24 Feb 2026 20:59:30 +0100 Subject: [PATCH 04/29] moved interfaces # Conflicts: # pyfritzhome/devicetypes/fritzhomeunitbase.py --- pyfritzhome/devicetypes/__init__.py | 8 -------- pyfritzhome/devicetypes/fritzhomeunit.py | 10 ++++------ pyfritzhome/devicetypes/fritzhomeunitbase.py | 4 ++-- pyfritzhome/fritzhomedevice.py | 4 +--- pyfritzhome/interfaces/__init__.py | 13 +++++++++++++ .../interface.py} | 8 ++++---- .../interfacebase.py} | 0 .../multimeterinterface.py} | 2 +- .../onoffinterface.py} | 2 +- .../temperatureinterface.py} | 2 +- 10 files changed, 27 insertions(+), 26 deletions(-) create mode 100644 pyfritzhome/interfaces/__init__.py rename pyfritzhome/{devicetypes/fritzhomeinterface.py => interfaces/interface.py} (72%) rename pyfritzhome/{devicetypes/fritzhomeinterfacebase.py => interfaces/interfacebase.py} (100%) rename pyfritzhome/{devicetypes/fritzhomemultimeterinterface.py => interfaces/multimeterinterface.py} (97%) rename pyfritzhome/{devicetypes/fritzhomeonoffinterface.py => interfaces/onoffinterface.py} (97%) rename pyfritzhome/{devicetypes/fritzhometemperatureinterface.py => interfaces/temperatureinterface.py} (96%) diff --git a/pyfritzhome/devicetypes/__init__.py b/pyfritzhome/devicetypes/__init__.py index 2766ab7..a057e02 100644 --- a/pyfritzhome/devicetypes/__init__.py +++ b/pyfritzhome/devicetypes/__init__.py @@ -4,17 +4,9 @@ from .fritzhometemplate import FritzhomeTemplate from .fritzhometrigger import FritzhomeTrigger from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomeinterface import FritzhomeInterface -from .fritzhomeonoffinterface import * -from .fritzhomemultimeterinterface import * -from .fritzhometemperatureinterface import * __all__ = ( "FritzhomeUnit", "FritzhomeTemplate", "FritzhomeTrigger", - "FritzhomeInterface", - "FritzhomeOnOffInterface", "FritzhomeOnOffMixin", - "FritzhomeMultimeterInterface", "FritzhomeMultimeterMixin", - "FritzhomeTemperatureInterface", "FritzhomeTemperatureMixin", ) diff --git a/pyfritzhome/devicetypes/fritzhomeunit.py b/pyfritzhome/devicetypes/fritzhomeunit.py index 41206e9..b392839 100644 --- a/pyfritzhome/devicetypes/fritzhomeunit.py +++ b/pyfritzhome/devicetypes/fritzhomeunit.py @@ -6,16 +6,14 @@ import logging from .fritzhomeunitbase import FritzhomeUnitBase -from .fritzhomeonoffinterface import FritzhomeOnOffMixin -from .fritzhomemultimeterinterface import FritzhomeMultimeterMixin -from .fritzhometemperatureinterface import FritzhomeTemperatureMixin +from .. import interfaces _LOGGER = logging.getLogger(__name__) class FritzhomeUnit(FritzhomeUnitBase, - FritzhomeOnOffMixin, - FritzhomeMultimeterMixin, - FritzhomeTemperatureMixin): + interfaces.FritzhomeOnOffMixin, + interfaces.FritzhomeMultimeterMixin, + interfaces.FritzhomeTemperatureMixin): """The Fritzhome Device class.""" def __init__(self, fritz=None, node=None): diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index 2a0f055..0454981 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -6,7 +6,7 @@ import logging from .fritzhomeentitybase import FritzhomeEntityBase -from .fritzhomeinterface import FritzhomeInterface +from .. import interfaces _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ def _update_from_node(self, node): # unshare class attribute on write self.interfaces = {} for iface, node in node["interfaces"].items(): - self.interfaces[iface] = FritzhomeInterface(iface, node) + self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) def units(self): return [self] diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 5e5644d..32a724e 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -6,9 +6,7 @@ from .devicetypes import FritzhomeTemplate # noqa: F401 from .devicetypes import FritzhomeTrigger # noqa: F401 from .devicetypes import FritzhomeDeviceBase -from .devicetypes import FritzhomeOnOffMixin -from .devicetypes import FritzhomeMultimeterMixin -from .devicetypes import FritzhomeTemperatureMixin +from .interfaces import * class FritzhomeDevice( FritzhomeDeviceBase, diff --git a/pyfritzhome/interfaces/__init__.py b/pyfritzhome/interfaces/__init__.py new file mode 100644 index 0000000..fb24299 --- /dev/null +++ b/pyfritzhome/interfaces/__init__.py @@ -0,0 +1,13 @@ +"""Init file for the device types.""" + +__all__ = ( + "FritzhomeInterface", + "FritzhomeOnOffInterface", "FritzhomeOnOffMixin", + "FritzhomeMultimeterInterface", "FritzhomeMultimeterMixin", + "FritzhomeTemperatureInterface", "FritzhomeTemperatureMixin", +) + +from .interface import FritzhomeInterface +from .onoffinterface import FritzhomeOnOffInterface, FritzhomeOnOffMixin +from .multimeterinterface import FritzhomeMultimeterInterface, FritzhomeMultimeterMixin +from .temperatureinterface import FritzhomeTemperatureInterface, FritzhomeTemperatureMixin diff --git a/pyfritzhome/devicetypes/fritzhomeinterface.py b/pyfritzhome/interfaces/interface.py similarity index 72% rename from pyfritzhome/devicetypes/fritzhomeinterface.py rename to pyfritzhome/interfaces/interface.py index 11186ab..5b761b2 100644 --- a/pyfritzhome/devicetypes/fritzhomeinterface.py +++ b/pyfritzhome/interfaces/interface.py @@ -9,10 +9,10 @@ import logging import json -from .fritzhomeinterfacebase import FritzhomeInterfaceBase -from .fritzhomeonoffinterface import FritzhomeOnOffInterface -from .fritzhomemultimeterinterface import FritzhomeMultimeterInterface -from .fritzhometemperatureinterface import FritzhomeTemperatureInterface +from .interfacebase import FritzhomeInterfaceBase +from .onoffinterface import FritzhomeOnOffInterface +from .multimeterinterface import FritzhomeMultimeterInterface +from .temperatureinterface import FritzhomeTemperatureInterface _LOGGER = logging.getLogger(__name__) diff --git a/pyfritzhome/devicetypes/fritzhomeinterfacebase.py b/pyfritzhome/interfaces/interfacebase.py similarity index 100% rename from pyfritzhome/devicetypes/fritzhomeinterfacebase.py rename to pyfritzhome/interfaces/interfacebase.py diff --git a/pyfritzhome/devicetypes/fritzhomemultimeterinterface.py b/pyfritzhome/interfaces/multimeterinterface.py similarity index 97% rename from pyfritzhome/devicetypes/fritzhomemultimeterinterface.py rename to pyfritzhome/interfaces/multimeterinterface.py index dad3261..6197970 100644 --- a/pyfritzhome/devicetypes/fritzhomemultimeterinterface.py +++ b/pyfritzhome/interfaces/multimeterinterface.py @@ -3,7 +3,7 @@ import logging -from .fritzhomeinterfacebase import FritzhomeInterfaceBase +from .interfacebase import FritzhomeInterfaceBase _LOGGER = logging.getLogger(__name__) diff --git a/pyfritzhome/devicetypes/fritzhomeonoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py similarity index 97% rename from pyfritzhome/devicetypes/fritzhomeonoffinterface.py rename to pyfritzhome/interfaces/onoffinterface.py index 161e1f6..9d1462b 100644 --- a/pyfritzhome/devicetypes/fritzhomeonoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -3,7 +3,7 @@ import logging -from .fritzhomeinterfacebase import FritzhomeInterfaceBase +from .interfacebase import FritzhomeInterfaceBase _LOGGER = logging.getLogger(__name__) diff --git a/pyfritzhome/devicetypes/fritzhometemperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py similarity index 96% rename from pyfritzhome/devicetypes/fritzhometemperatureinterface.py rename to pyfritzhome/interfaces/temperatureinterface.py index fde8452..c7c7048 100644 --- a/pyfritzhome/devicetypes/fritzhometemperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -3,7 +3,7 @@ import logging -from .fritzhomeinterfacebase import FritzhomeInterfaceBase +from .interfacebase import FritzhomeInterfaceBase _LOGGER = logging.getLogger(__name__) From d9ea9b61b8f2b287417a278309c0e9c75039ce93 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sun, 8 Feb 2026 21:47:48 +0100 Subject: [PATCH 05/29] moved interfaces --- .../devicetypes/fritzhomedevicebase.py | 8 +- pyfritzhome/devicetypes/fritzhomeunitbase.py | 4 +- pyfritzhome/fritzhome.py | 160 +++++++++++------- pyfritzhome/interfaces/interface.py | 4 +- pyfritzhome/interfaces/interfacebase.py | 9 +- pyfritzhome/interfaces/onoffinterface.py | 6 +- 6 files changed, 115 insertions(+), 76 deletions(-) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 5b2b4f6..6833781 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -16,7 +16,7 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): def __init__(self, fritz=None, node=None): super().__init__(fritz, node) - self._units = [] + self._units = {} def __repr__(self): """Return a string.""" @@ -66,10 +66,10 @@ def present(self): return self.is_connected def clear_units(self): - self._units = [] + self._units = {} - def add_unit(self, unit): - self._units.append(unit) + def add_or_update_unit(self, unit): + self._units[unit.ain] = unit def units(self): return self._units.values() diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index 0454981..40da80b 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -41,8 +41,8 @@ def _update_from_node(self, node): def units(self): return [self] - def update(self): - pass + def update_interface(self, interface): + self._fritz.put_unit(self.ain, {"interfaces": {interface.type: interface._node}}) @property def parent(self): diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index b997dce..ee59b31 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -29,8 +29,8 @@ class Fritzhome(object): _sid = None _session = None - _units: Optional[Dict[str, FritzhomeUnit]] = None - _devices: Optional[Dict[str, FritzhomeDevice]] = None + _units: Dict[str, FritzhomeUnit] + _devices: Dict[str, FritzhomeDevice] _templates: Optional[Dict[str, FritzhomeTemplate]] = None _triggers: Optional[Dict[str, FritzhomeTrigger]] = None @@ -43,6 +43,8 @@ def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10, self._timeout = timeout self._has_getdeviceinfos = True self._has_txbusy = True + self._devices = {} + self._units = {} self._use_testdata = use_testdata if host.startswith("https://") or host.startswith("http://"): self.base_url = f"{host}:{port}" if port else host @@ -58,6 +60,23 @@ def _request(self, url, params=None, headers=None): rsp.raise_for_status() return rsp.text.strip() + def _request2(self, url, params=None, headers=None, timeout=10): + """Send a request with parameters.""" + rsp = self._session.get( + url, params=params, headers=headers, timeout=timeout, verify=self._ssl_verify + ) + rsp.raise_for_status() + return rsp + + def _put(self, url, data, params=None, headers=None, timeout=10): + """Send a request with parameters.""" + rsp = self._session.put( + url, params=params, headers=headers, timeout=timeout, verify=self._ssl_verify, + json=data + ) + rsp.raise_for_status() + return rsp.text.strip() + def _login_request(self, username=None, secret=None): """Send a login request with paramerters.""" url = f"{self.base_url}/login_sid.lua?version=2" @@ -150,12 +169,11 @@ def _rest_request(self, endpoint, param=None): if param: params.update(param) - response = self._request(url, headers=params) - data = json.loads(response) - #~ if data.contains("errors"): - #~ raise InvalidError + response = self._request2(url, headers=params) + if response.ok: + return response.json() - return data + return None def login(self): """Login and get a valid session ID.""" @@ -185,58 +203,73 @@ def logout(self): self._logout_request() self._sid = None - def update_unit(self, uid): + def _update_unit_from_node(self, node): + ain = node["ain"] + if unit := self._units.get(ain): + _LOGGER.info("Updating already existing unit " + ain) + unit._update_from_node(node) + else: + _LOGGER.info("Adding new unit " + ain) + self._units[ain] = FritzhomeUnit(self, node=node) + + def _update_unit(self, ain): if self._units is None: self._units = {} - _LOGGER.info("Updating units ...") - data = self._rest_request("overview/units/{uid}") - if uid in self._units.keys(): - _LOGGER.info( - "Updating already existing unit " + uid - ) - self._units[uid]._update_from_node(data) - else: - raise RuntimeError + self._update_unit_from_node(self.get_unit_element(ain)) - def update_units(self): + def _update_units(self): if self._units is None: self._units = {} _LOGGER.info("Updating units ...") - data = self._rest_request("overview/units") - for element in data: - ain = element["ain"] - if ain in self._units.keys(): - _LOGGER.info( - "Updating already existing unit " + ain - ) - self._units[ain]._update_from_node(element) - else: - _LOGGER.info("Adding new unit " + ain) - self._units[ain] = FritzhomeUnit(self, node=element) + for element in self.get_unit_elements(): + self._update_unit_from_node(element) + + def put_unit(self, ain, node): + if self._units is None: + self._units = {} + + _LOGGER.info("put units ...\n" + json.dumps(node)) + params = {"Authorization": f"AVM-SID {self._sid}"} + data = self._put(f"{self.rest_url}/configuration/units/{ain}", node, headers=params) + + def _update_device_from_node(self, node): + ain = node["ain"] + if dev := self._devices.get(ain): + _LOGGER.info("Updating already existing device " + ain) + dev._update_from_node(node) + else: + _LOGGER.info("Adding new device " + ain) + self._devices[ain] = FritzhomeDevice(self, node=node) + + def update_device(self, ain): + """Update the device.""" + _LOGGER.info(f"Updating Device {ain} ...") + + if element := self.get_device_element(ain): + self._update_device_from_node(element) + for unit_ain in element["unitUids"]: + self._update_unit(unit_ain) + if unit := self._units.get(unit_ain): + self._devices[ain].add_or_update_unit(unit) + else: + _LOGGER.warning(f"Unknown unit {unit_ain}") + return True + return False def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") - if self._devices is None: - self._devices = {} - + self._update_units() device_elements = self.get_device_elements() for element in device_elements: - uid = element["UID"] - if uid in self._devices.keys(): - _LOGGER.info( - "Updating already existing Device " + uid - ) - self._devices[uid]._update_from_node(element) - else: - _LOGGER.info("Adding new Device " + uid) - self._devices[uid] = FritzhomeDevice(self, node=element) - self._devices[uid].clear_units() + ain = element["ain"] + self._update_device_from_node(element) + self._devices[ain].clear_units() for unit_ain in element["unitUids"]: - if unit_ain in self._units.keys(): - self._devices[uid].add_unit(self._units[unit_ain]) + if unit := self._units.get(unit_ain): + self._devices[ain].add_or_update_unit(unit) else: _LOGGER.warning(f"Unknown unit {unit_ain}") @@ -287,18 +320,21 @@ def wait_device_txbusy(self, ain, retries=10): time.sleep(0.2) return False + def get_unit_elements(self): + """Get the JSON elements for the unit list.""" + return self._rest_request("overview/units") + + def get_unit_element(self, ain): + """Get the JSON element for the specified unit.""" + return self._rest_request(f"overview/units/{ain}") + def get_device_elements(self): """Get the JSON elements for the device list.""" - resp = self._rest_request("overview/devices") - return resp + return self._rest_request("overview/devices") def get_device_element(self, ain): """Get the JSON element for the specified device.""" - elements = self.get_device_elements() - for element in elements: - if element.attrib["identifier"] == ain: - return element - return None + return self._rest_request(f"overview/devices/{ain}") def get_devices(self): """Get the list of all known devices.""" @@ -306,9 +342,7 @@ def get_devices(self): def get_devices_as_dict(self): """Get the list of all known devices.""" - if self._units is None: - self.update_units() - if self._devices is None: + if not self._devices: self.update_devices() return self._devices @@ -334,21 +368,21 @@ def get_switch_state(self, ain): def set_switch_state_on(self, ain, wait=False): """Set the switch to on state.""" - result = self._aha_request("setswitchon", ain=ain, rf=bool) - wait and self.wait_device_txbusy(ain) - return result + if self.update_device(ain): + self._devices[ain].set_switch_state_on() + return None def set_switch_state_off(self, ain, wait=False): """Set the switch to off state.""" - result = self._aha_request("setswitchoff", ain=ain, rf=bool) - wait and self.wait_device_txbusy(ain) - return result + if self.update_device(ain): + self._devices[ain].set_switch_state_off() + return None def set_switch_state_toggle(self, ain, wait=False): """Toggle the switch state.""" - result = self._aha_request("setswitchtoggle", ain=ain, rf=bool) - wait and self.wait_device_txbusy(ain) - return result + if self.update_device(ain): + self._devices[ain].set_switch_state_toggle() + return None def get_switch_power(self, ain): """Get the switch power consumption.""" diff --git a/pyfritzhome/interfaces/interface.py b/pyfritzhome/interfaces/interface.py index 5b761b2..acb01cc 100644 --- a/pyfritzhome/interfaces/interface.py +++ b/pyfritzhome/interfaces/interface.py @@ -21,9 +21,9 @@ class FritzhomeInterface(FritzhomeOnOffInterface, FritzhomeTemperatureInterface): """The Fritzhome Interface class.""" - def __init__(self, type, node = None): + def __init__(self, unit, type, node = None): """Create an entity base object.""" - super().__init__(type, node) + super().__init__(unit, type, node) # interfaces are not entities, only their parent units are, therefore this is # called with the unit REST node diff --git a/pyfritzhome/interfaces/interfacebase.py b/pyfritzhome/interfaces/interfacebase.py index 769b5ce..e6b5fa6 100644 --- a/pyfritzhome/interfaces/interfacebase.py +++ b/pyfritzhome/interfaces/interfacebase.py @@ -6,6 +6,7 @@ from abc import ABC +import weakref import logging import json @@ -15,20 +16,24 @@ class FritzhomeInterfaceBase(): """The Fritzhome Interface class.""" - def __init__(self, type, node): + def __init__(self, unit, type, node): """Create an entity base object.""" self.type = type self._node = node + self._unit_ref = weakref.ref(unit) if node is not None: self._update_from_node(node) def __repr__(self): """Return a string.""" - return f"{self.type} of {self._unit.ain}" + return f"{self.type} of {self._unit_ref().ain}" def _update_from_node(self, node): pass + def update(self): + self._unit_ref().update_interface(self) + @property def node(self): return self._node; diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index 9d1462b..2cde45e 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -58,17 +58,17 @@ def set_switch_state_on(self): """Set the switch state to on.""" if pair := self.find_switch_interface(): pair[1].set_switch_state_on() - pair[0].update() + pair[1].update() def set_switch_state_off(self): """Set the switch state to off.""" if pair := self.find_switch_interface(): pair[1].set_switch_state_off() - pair[0].update() + pair[1].update() def set_switch_state_toggle(self): """Toggle the switch state.""" if pair := self.find_switch_interface(): pair[1].set_switch_state_toggle() - pair[0].update() + pair[1].update() From e9813d9db3f747560bb975cbf8b8c24c823c647d Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Mon, 9 Feb 2026 00:31:08 +0100 Subject: [PATCH 06/29] humidity --- pyfritzhome/cli.py | 3 ++ .../devicetypes/fritzhomedevicehumidity.py | 39 --------------- pyfritzhome/devicetypes/fritzhomeunit.py | 3 +- pyfritzhome/fritzhomedevice.py | 1 + pyfritzhome/interfaces/__init__.py | 2 + pyfritzhome/interfaces/humidityinterface.py | 50 +++++++++++++++++++ pyfritzhome/interfaces/interface.py | 4 +- 7 files changed, 61 insertions(+), 41 deletions(-) delete mode 100644 pyfritzhome/devicetypes/fritzhomedevicehumidity.py create mode 100644 pyfritzhome/interfaces/humidityinterface.py diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 4494ac3..a16f4fb 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -43,6 +43,9 @@ def list_all(fritz, args): print(" Temperature:") print(" temperature=%s" % device.temperature) print(" offset=%s" % device.offset) + if device.has_humidity_sensor: + print(" Humidity:") + print(" relative_humidity=%s" % device.rel_humidity) #~ if device.has_thermostat: #~ print(" Thermostat:") #~ print(" battery_low=%s" % device.battery_low) diff --git a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py deleted file mode 100644 index fe56ec0..0000000 --- a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py +++ /dev/null @@ -1,39 +0,0 @@ -"""The humidity device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDeviceHumidity(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - rel_humidity = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_humidity_sensor: - self._update_humidity_from_node(node) - - # Humidity - @property - def has_humidity_sensor(self): - """Check if the device has humidity function.""" - return self._has_feature(FritzhomeDeviceFeatures.HUMIDITY) - - def _update_humidity_from_node(self, node): - _LOGGER.debug("update humidity device") - humidity_element = node.find("humidity") - try: - self.rel_humidity = self.get_node_value_as_int( - humidity_element, "rel_humidity" - ) - except ValueError: - pass diff --git a/pyfritzhome/devicetypes/fritzhomeunit.py b/pyfritzhome/devicetypes/fritzhomeunit.py index b392839..d7d5ce2 100644 --- a/pyfritzhome/devicetypes/fritzhomeunit.py +++ b/pyfritzhome/devicetypes/fritzhomeunit.py @@ -13,7 +13,8 @@ class FritzhomeUnit(FritzhomeUnitBase, interfaces.FritzhomeOnOffMixin, interfaces.FritzhomeMultimeterMixin, - interfaces.FritzhomeTemperatureMixin): + interfaces.FritzhomeTemperatureMixin, + interfaces.FritzhomeHumidityMixin): """The Fritzhome Device class.""" def __init__(self, fritz=None, node=None): diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 32a724e..0a6e55c 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -13,6 +13,7 @@ class FritzhomeDevice( FritzhomeOnOffMixin, FritzhomeMultimeterMixin, FritzhomeTemperatureMixin, + FritzhomeHumidityMixin, ): """The Fritzhome Device class.""" diff --git a/pyfritzhome/interfaces/__init__.py b/pyfritzhome/interfaces/__init__.py index fb24299..7437683 100644 --- a/pyfritzhome/interfaces/__init__.py +++ b/pyfritzhome/interfaces/__init__.py @@ -5,9 +5,11 @@ "FritzhomeOnOffInterface", "FritzhomeOnOffMixin", "FritzhomeMultimeterInterface", "FritzhomeMultimeterMixin", "FritzhomeTemperatureInterface", "FritzhomeTemperatureMixin", + "FritzhomeHumidityInterface", "FritzhomeHumidityMixin", ) from .interface import FritzhomeInterface from .onoffinterface import FritzhomeOnOffInterface, FritzhomeOnOffMixin from .multimeterinterface import FritzhomeMultimeterInterface, FritzhomeMultimeterMixin from .temperatureinterface import FritzhomeTemperatureInterface, FritzhomeTemperatureMixin +from .humidityinterface import FritzhomeHumidityInterface, FritzhomeHumidityMixin diff --git a/pyfritzhome/interfaces/humidityinterface.py b/pyfritzhome/interfaces/humidityinterface.py new file mode 100644 index 0000000..455d1a9 --- /dev/null +++ b/pyfritzhome/interfaces/humidityinterface.py @@ -0,0 +1,50 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeHumidityInterface(FritzhomeInterfaceBase): + """The Fritzhome Device class.""" + + rel_humidity = None + + @property + def is_humidity(self): + return self.type == "humidityInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_humidity: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.rel_humidity = self._node["relativeHumidity"] + + + +class FritzhomeHumidityMixin(): + """The Fritzhome Humidity mixin.""" + + def find_humidity_interface(self): + #~ return next((unit for unit in self._units if unit.is_switch), None) + for unit in self.units(): + if interface := unit.interfaces.get("humidityInterface"): + return (unit, interface) + return None + + @property + def has_humidity_sensor(self): + """Check if the device has humidity sensors.""" + return self.find_humidity_interface() != None + + @property + def rel_humidity(self): + """ Get the current humidity """ + if pair := self.find_humidity_interface(): + return pair[1].rel_humidity diff --git a/pyfritzhome/interfaces/interface.py b/pyfritzhome/interfaces/interface.py index acb01cc..23af395 100644 --- a/pyfritzhome/interfaces/interface.py +++ b/pyfritzhome/interfaces/interface.py @@ -13,12 +13,14 @@ from .onoffinterface import FritzhomeOnOffInterface from .multimeterinterface import FritzhomeMultimeterInterface from .temperatureinterface import FritzhomeTemperatureInterface +from .humidityinterface import FritzhomeHumidityInterface _LOGGER = logging.getLogger(__name__) class FritzhomeInterface(FritzhomeOnOffInterface, FritzhomeMultimeterInterface, - FritzhomeTemperatureInterface): + FritzhomeTemperatureInterface, + FritzhomeHumidityInterface): """The Fritzhome Interface class.""" def __init__(self, unit, type, node = None): From 30800739838b557600f62bfac35abcbeb7bc408c Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Tue, 24 Feb 2026 20:33:15 +0100 Subject: [PATCH 07/29] introduce facility to force aha api Also switch to REST for some APIs while preserving aha compat using that facility. --- pyfritzhome/cli.py | 4 +++ pyfritzhome/fritzhome.py | 60 +++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index a16f4fb..e9f3e24 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -203,6 +203,9 @@ def main(args=None): parser.add_argument( "-v", action="store_true", dest="verbose", help="be more verbose" ) + parser.add_argument( + "-A", "--aha", action="store_true", dest="aha_api", help="Use legacy AHA API" + ) parser.add_argument( "-f", "--fritzbox", @@ -406,6 +409,7 @@ def main(args=None): password=args.password, port=args.port or None, ssl_verify=not args.insecure, + force_aha_api=args.aha_api, use_testdata=args.testdata ) fritzbox.login() diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index ee59b31..72c9dde 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -34,7 +34,7 @@ class Fritzhome(object): _templates: Optional[Dict[str, FritzhomeTemplate]] = None _triggers: Optional[Dict[str, FritzhomeTrigger]] = None - def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10, use_testdata=False): + def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10, force_aha_api=False, use_testdata=False): """Create a fritzhome object.""" self._user = user self._password = password @@ -45,6 +45,7 @@ def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10, self._has_txbusy = True self._devices = {} self._units = {} + self._use_aha = force_aha_api self._use_testdata = use_testdata if host.startswith("https://") or host.startswith("http://"): self.base_url = f"{host}:{port}" if port else host @@ -356,15 +357,25 @@ def get_device_infos(self, ain): def get_device_present(self, ain): """Get the device presence.""" - return self._aha_request("getswitchpresent", ain=ain, rf=bool) + if self._use_aha: + return self._aha_request("getswitchpresent", ain=ain) + return self._update_device_config(ain).is_connected def get_device_name(self, ain): """Get the device name.""" - return self._aha_request("getswitchname", ain=ain) + if self._use_aha: + return self._aha_request("getswitchname", ain=ain) + return self._update_device_config(ain).name def get_switch_state(self, ain): """Get the switch state.""" - return self._aha_request("getswitchstate", ain=ain, rf=bool) + if self._use_aha: + return self._aha_request("getswitchstate", ain=ain, rf=bool) + if dev := self._update_device_config(ain): + if not dev.has_switch: + _LOGGER.error(f"Device {dev.name} is not a switch") + return None + return dev.switch_state def set_switch_state_on(self, ain, wait=False): """Set the switch to on state.""" @@ -386,15 +397,33 @@ def set_switch_state_toggle(self, ain, wait=False): def get_switch_power(self, ain): """Get the switch power consumption.""" - return self._aha_request("getswitchpower", ain=ain, rf=int) + if self._use_aha: + return self._aha_request("getswitchpower", ain=ain, rf=int) + if dev := self._update_device_config(ain): + if not dev.has_powermeter: + _LOGGER.error(f"Device {dev.name} is not a powermeter") + return None + return dev.power def get_switch_energy(self, ain): """Get the switch energy.""" - return self._aha_request("getswitchenergy", ain=ain, rf=int) + if self._use_aha: + return self._aha_request("getswitchenergy", ain=ain, rf=int) + if dev := self._update_device_config(ain): + if not dev.has_powermeter: + _LOGGER.error(f"Device {dev.name} is not a powermeter") + return None + return dev.energy def get_temperature(self, ain): """Get the device temperature sensor value.""" - return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 + if self._use_aha: + return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 + if dev := self._update_device_config(ain): + if not dev.has_temperature_sensor: + _LOGGER.error(f"Device {dev.name} is not a thermometer") + return None + return float(dev.temperature) def _get_temperature(self, ain, name): plain = self._aha_request(name, ain=ain, rf=float) @@ -402,7 +431,13 @@ def _get_temperature(self, ain, name): def get_target_temperature(self, ain): """Get the thermostate target temperature.""" - return self._get_temperature(ain, "gethkrtsoll") + if self._use_aha: + return self._get_temperature(ain, "gethkrtsoll") + if dev := self._update_device_config(ain): + if not dev.has_temperature_sensor: + _LOGGER.error(f"Device {dev.name} is not a thermometer") + return None + return float(dev.temperature) def set_target_temperature(self, ain, temperature, wait=False): """Set the thermostate target temperature.""" @@ -442,8 +477,13 @@ def get_eco_temperature(self, ain): def get_device_statistics(self, ain): """Get device statistics.""" - plain = self._aha_request("getbasicdevicestats", ain=ain) - return plain + if self._use_aha: + return self._aha_request("getbasicdevicestats", ain=ain) + stats = {"statistics":[]} + for unit in self._update_device_config(ain).units(): + if s := unit.statistics: + stats["statistics"].append(s) + return json.dumps(stats) # Lightbulb-related commands From 1766334737b7b249df3fe27bb432ecf9f1f0f3c9 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Tue, 24 Feb 2026 20:51:47 +0100 Subject: [PATCH 08/29] first steps for querying configuration nodes --- pyfritzhome/fritzhome.py | 118 ++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 72c9dde..b6750bc 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -211,21 +211,9 @@ def _update_unit_from_node(self, node): unit._update_from_node(node) else: _LOGGER.info("Adding new unit " + ain) - self._units[ain] = FritzhomeUnit(self, node=node) - - def _update_unit(self, ain): - if self._units is None: - self._units = {} - - self._update_unit_from_node(self.get_unit_element(ain)) - - def _update_units(self): - if self._units is None: - self._units = {} - - _LOGGER.info("Updating units ...") - for element in self.get_unit_elements(): - self._update_unit_from_node(element) + unit = FritzhomeUnit(self, node=node) + self._units[ain] = unit + return unit def put_unit(self, ain, node): if self._units is None: @@ -237,42 +225,92 @@ def put_unit(self, ain, node): def _update_device_from_node(self, node): ain = node["ain"] - if dev := self._devices.get(ain): + if device := self._devices.get(ain): _LOGGER.info("Updating already existing device " + ain) - dev._update_from_node(node) + device._update_from_node(node) else: _LOGGER.info("Adding new device " + ain) - self._devices[ain] = FritzhomeDevice(self, node=node) + device = FritzhomeDevice(self, node=node) + self._devices[ain] = device + return device + + def _update_device_units(self, ain, unit_ains): + if self._units is None: + self._units = {} + + for unit_ain in unit_ains: + self._update_unit_from_node(self.get_unit_element(unit_ain)) + if unit := self._units.get(unit_ain): + self._devices[ain].add_or_update_unit(unit) + else: + _LOGGER.warning(f"Unknown unit {unit_ain}") def update_device(self, ain): """Update the device.""" _LOGGER.info(f"Updating Device {ain} ...") - - if element := self.get_device_element(ain): + if node := self._rest_request(f"overview/devices/{ain}"): self._update_device_from_node(element) - for unit_ain in element["unitUids"]: - self._update_unit(unit_ain) - if unit := self._units.get(unit_ain): - self._devices[ain].add_or_update_unit(unit) - else: - _LOGGER.warning(f"Unknown unit {unit_ain}") - return True + for unit_ain in node["unitUids"]: + if node := self._rest_request(f"overview/unit/{unit_ain}"): + self._update_device_from_node(node) + return True return False + def _update_device_config(self, ain): + """Update the device, using its configuration endpoint.""" + _LOGGER.info(f"Updating Device {ain} ...") + device = None + units = [] + if node := self._rest_request(f"configuration/devices/{ain}"): + units = node.pop("units") + device = self._update_device_from_node(node) + for node in units: + unit = self._update_unit_from_node(node) + device.add_or_update_unit(unit) + return device + def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") - self._update_units() + devices = self._rest_request("overview/devices") + for node in devices: + self._update_device_from_node(node) + units = self._rest_request("overview/units") + for node in units: + self._update_unit_from_node(node) + + for device in self._devices.values(): + units = device.node["unitUids"] + for unit_ain in units: + if unit := self._units.get(unit_ain): + device.add_or_update_unit(unit) + + if not ignore_removed: + for ain in list(self._devices.keys()): + if ain not in [ + element.attrib["ain"] for element in devices + ]: + _LOGGER.info("Removing no more existing device " + ain) + self._devices.pop(ain) + for ain in list(self._units.keys()): + if ain not in [ + element.attrib["ain"] for element in units + ]: + _LOGGER.info("Removing no more existing device " + ain) + self._units.pop(ain) + + return True + + def update_units_devices(self, ignore_removed=True, with_units=False): + """Update the device.""" + _LOGGER.info("Updating Devices ...") device_elements = self.get_device_elements() for element in device_elements: ain = element["ain"] self._update_device_from_node(element) - self._devices[ain].clear_units() - for unit_ain in element["unitUids"]: - if unit := self._units.get(unit_ain): - self._devices[ain].add_or_update_unit(unit) - else: - _LOGGER.warning(f"Unknown unit {unit_ain}") + if with_units: + self._devices[ain].clear_units() + self._update_device_units(ain, element["unitUids"]) if not ignore_removed: for identifier in list(self._devices.keys()): @@ -321,22 +359,10 @@ def wait_device_txbusy(self, ain, retries=10): time.sleep(0.2) return False - def get_unit_elements(self): - """Get the JSON elements for the unit list.""" - return self._rest_request("overview/units") - - def get_unit_element(self, ain): - """Get the JSON element for the specified unit.""" - return self._rest_request(f"overview/units/{ain}") - def get_device_elements(self): """Get the JSON elements for the device list.""" return self._rest_request("overview/devices") - def get_device_element(self, ain): - """Get the JSON element for the specified device.""" - return self._rest_request(f"overview/devices/{ain}") - def get_devices(self): """Get the list of all known devices.""" return list(self.get_devices_as_dict().values()) From 088b7356ac60147d518d34f66acbce0d88310b7f Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 25 Feb 2026 22:23:59 +0100 Subject: [PATCH 09/29] bring back AHA in a few places --- pyfritzhome/devicetypes/__init__.py | 30 ++- .../devicetypes/fritzhomedevicebase.py | 41 ++-- .../devicetypes/fritzhomeentitybase.py | 44 ++++- pyfritzhome/devicetypes/fritzhomeunitbase.py | 4 +- pyfritzhome/fritzhome.py | 186 +++++++++++------- pyfritzhome/fritzhomedevice.py | 44 ++++- pyfritzhome/interfaces/onoffinterface.py | 2 + .../interfaces/temperatureinterface.py | 1 + 8 files changed, 236 insertions(+), 116 deletions(-) diff --git a/pyfritzhome/devicetypes/__init__.py b/pyfritzhome/devicetypes/__init__.py index a057e02..a66c529 100644 --- a/pyfritzhome/devicetypes/__init__.py +++ b/pyfritzhome/devicetypes/__init__.py @@ -1,12 +1,36 @@ """Init file for the device types.""" -from .fritzhomeunit import FritzhomeUnit +from .fritzhomedevicebase import FritzhomeDeviceBase +from .fritzhomedevicealarm import FritzhomeDeviceAlarm +from .fritzhomedevicebutton import FritzhomeDeviceButton +from .fritzhomedevicehumidity import FritzhomeDeviceHumidity +from .fritzhomedevicelevel import FritzhomeDeviceLevel +from .fritzhomedevicepowermeter import FritzhomeDevicePowermeter +from .fritzhomedevicerepeater import FritzhomeDeviceRepeater +from .fritzhomedeviceswitch import FritzhomeDeviceSwitch +from .fritzhomedevicetemperature import FritzhomeDeviceTemperature +from .fritzhomedevicethermostat import FritzhomeDeviceThermostat +from .fritzhomedevicelightbulb import FritzhomeDeviceLightBulb +from .fritzhomedeviceblind import FritzhomeDeviceBlind from .fritzhometemplate import FritzhomeTemplate from .fritzhometrigger import FritzhomeTrigger -from .fritzhomedevicebase import FritzhomeDeviceBase +from .fritzhomeunit import FritzhomeUnit + __all__ = ( - "FritzhomeUnit", + "FritzhomeDeviceBase", + "FritzhomeDeviceAlarm", + "FritzhomeDeviceButton", + "FritzhomeDeviceHumidity", + "FritzhomeDeviceLevel", + "FritzhomeDevicePowermeter", + "FritzhomeDeviceRepeater", + "FritzhomeDeviceSwitch", + "FritzhomeDeviceTemperature", + "FritzhomeDeviceThermostat", + "FritzhomeDeviceLightBulb", + "FritzhomeDeviceBlind", "FritzhomeTemplate", "FritzhomeTrigger", + "FritzhomeUnit", ) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 6833781..7ae6216 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -3,7 +3,6 @@ from __future__ import print_function - import logging from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase @@ -14,9 +13,9 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): """The Fritzhome Device class.""" - def __init__(self, fritz=None, node=None): - super().__init__(fritz, node) - self._units = {} + manufacturer = None + product_name = None + is_connected = None def __repr__(self): """Return a string.""" @@ -27,39 +26,27 @@ def __repr__(self): name=self.name, ) - def update(self): - """Update the device values.""" - self._fritz.update_device() - def _update_from_node(self, node): _LOGGER.debug("update base device") super()._update_from_node(node) + self._units = {} + if self._fritz._use_aha: + self.manufacturer = node.attrib["manufacturer"] + self.product_name = node.attrib["productname"] + self.is_connected = self.get_node_value_as_int_as_bool(node, "present") + else: + self.manufacturer = self._node["manufacturer"] + self.product_name = self._node["productName"] + self.is_connected = self._node["isConnected"] def get_config(): self._fritz.update_device_config(self.ain) - @property - def uid(self): - return self._node["UID"] - - @property - def manufacturer(self): - return self._node["manufacturer"] - - @property - def product_name(self): - return self._node["productName"] - # legacy @property def productname(self): return self.product_name - # legacy - @property - def is_connected(self): - return self._node["isConnected"] - # legacy @property def present(self): @@ -71,5 +58,9 @@ def clear_units(self): def add_or_update_unit(self, unit): self._units[unit.ain] = unit + # with aha, there are no units and interfaces. Emulated interfaces become directly attached + def add_or_update_unit(self, unit): + self._units[unit.ain] = unit + def units(self): return self._units.values() diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index 0774b51..092e4c7 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -20,6 +20,9 @@ def __init__(self, fritz=None, node=None): """Create an entity base object.""" self._fritz = fritz self._node = node + self.ain = None + self.name = None + self._functionsbitmask = 0 if node is not None: self._update_from_node(node) @@ -27,20 +30,43 @@ def __repr__(self): """Return a string.""" return f"{self.ain} {self.name}" + def _has_feature(self, feature: FritzhomeDeviceFeatures) -> bool: + return feature in FritzhomeDeviceFeatures(self._functionsbitmask) + def _update_from_node(self, node): - _LOGGER.debug(json.dumps(node)) - if self.ain != node["ain"]: - raise ValueError("updating invalid ain") self._node = node + if self._fritz._use_aha: + if self.ain is not None and self.ain != node.attrib["identifier"]: + raise ValueError("updating invalid ain") + self.ain = node.attrib["identifier"] + self.name = self.get_node_value(node, "name") + self._functionsbitmask = int(node.attrib["functionbitmask"]) + else: + if self.ain is not None and self.ain != node["ain"]: + raise ValueError("updating invalid ain") + self.ain = node["ain"] + self.name = node["name"] @property def node(self): return self._node; - @property - def ain(self): - return self._node["ain"]; - @property - def name(self): - return self._node["name"]; + # XML Helpers + + def get_node_value(self, elem, node): + """Get the node value.""" + return elem.findtext(node) + + def get_node_value_as_int(self, elem, node) -> int: + """Get the node value as integer.""" + return int(self.get_node_value(elem, node)) + + def get_node_value_as_int_as_bool(self, elem, node) -> bool: + """Get the node value as boolean.""" + return bool(self.get_node_value_as_int(elem, node)) + + def get_temp_from_node(self, elem, node): + """Get the node temp value as float.""" + return float(self.get_node_value(elem, node)) / 2 + return x diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index 40da80b..cd7b12f 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -14,9 +14,7 @@ class FritzhomeUnitBase(FritzhomeEntityBase): """The Fritzhome Device class.""" - def __init__(self, fritz=None, node=None): - super().__init__(fritz, node) - interfaces = {} + interfaces = None def __repr__(self): """Return a string.""" diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index b6750bc..6a8358f 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -15,10 +15,12 @@ from requests import exceptions, Session from .errors import InvalidError, LoginError, NotLoggedInError -from .fritzhomedevice import FritzhomeDevice +from .fritzhomedevice import FritzhomeDeviceAHA +from .fritzhomedevice import FritzhomeDeviceREST from .fritzhomedevice import FritzhomeUnit from .fritzhomedevice import FritzhomeTemplate from .fritzhomedevice import FritzhomeTrigger +from .fritzhomedevice import get_device_class from typing import Dict, Optional _LOGGER = logging.getLogger(__name__) @@ -30,7 +32,7 @@ class Fritzhome(object): _sid = None _session = None _units: Dict[str, FritzhomeUnit] - _devices: Dict[str, FritzhomeDevice] + _devices: Dict[str, FritzhomeDeviceREST | FritzhomeDeviceAHA] _templates: Optional[Dict[str, FritzhomeTemplate]] = None _triggers: Optional[Dict[str, FritzhomeTrigger]] = None @@ -47,6 +49,7 @@ def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10, self._units = {} self._use_aha = force_aha_api self._use_testdata = use_testdata + get_device_class(self._use_aha) if host.startswith("https://") or host.startswith("http://"): self.base_url = f"{host}:{port}" if port else host else: @@ -75,8 +78,13 @@ def _put(self, url, data, params=None, headers=None, timeout=10): url, params=params, headers=headers, timeout=timeout, verify=self._ssl_verify, json=data ) - rsp.raise_for_status() - return rsp.text.strip() + try: + rsp.raise_for_status() + return rsp.text.strip() + except exceptions.HTTPError as e: + _LOGGER.warning(e) + _LOGGER.warning("Error response: " + rsp.text) + return None def _login_request(self, username=None, secret=None): """Send a login request with paramerters.""" @@ -134,6 +142,7 @@ def _create_login_secret_md5(challenge, password): def _aha_request(self, cmd, ain=None, param=None, rf=str): """Send an AHA request.""" + _LOGGER.debug("HTTP request using AHA API") url = f"{self.base_url}/webservices/homeautoswitch.lua" _LOGGER.debug("self._sid:%s", self._sid) @@ -157,6 +166,7 @@ def _aha_request(self, cmd, ain=None, param=None, rf=str): def _rest_request(self, endpoint, param=None): """Send an REST API request""" + _LOGGER.debug("HTTP request using REST API") if self._use_testdata: return json.load(open(f"testdata/{endpoint.replace("/", "_")}.json.txt", "r")) url = f"{self.rest_url}/{endpoint}" @@ -230,7 +240,7 @@ def _update_device_from_node(self, node): device._update_from_node(node) else: _LOGGER.info("Adding new device " + ain) - device = FritzhomeDevice(self, node=node) + device = FritzhomeDeviceREST(self, node=node) self._devices[ain] = device return device @@ -245,17 +255,6 @@ def _update_device_units(self, ain, unit_ains): else: _LOGGER.warning(f"Unknown unit {unit_ain}") - def update_device(self, ain): - """Update the device.""" - _LOGGER.info(f"Updating Device {ain} ...") - if node := self._rest_request(f"overview/devices/{ain}"): - self._update_device_from_node(element) - for unit_ain in node["unitUids"]: - if node := self._rest_request(f"overview/unit/{unit_ain}"): - self._update_device_from_node(node) - return True - return False - def _update_device_config(self, ain): """Update the device, using its configuration endpoint.""" _LOGGER.info(f"Updating Device {ain} ...") @@ -272,18 +271,30 @@ def _update_device_config(self, ain): def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") - devices = self._rest_request("overview/devices") - for node in devices: - self._update_device_from_node(node) - units = self._rest_request("overview/units") - for node in units: - self._update_unit_from_node(node) - - for device in self._devices.values(): - units = device.node["unitUids"] - for unit_ain in units: - if unit := self._units.get(unit_ain): - device.add_or_update_unit(unit) + if self._use_aha: + for element in self._get_listinfo_elements("device"): + if element.attrib["identifier"] in self._devices.keys(): + _LOGGER.info( + "Updating already existing Device " + element.attrib["identifier"] + ) + self._devices[element.attrib["identifier"]]._update_from_node(element) + else: + _LOGGER.info("Adding new Device " + element.attrib["identifier"]) + device = FritzhomeDeviceAHA(self, node=element) + self._devices[device.ain] = device + else: + devices = self._rest_request("overview/devices") + for node in devices: + self._update_device_from_node(node) + units = self._rest_request("overview/units") + for node in units: + self._update_unit_from_node(node) + + for device in self._devices.values(): + units = device.node["unitUids"] + for unit_ain in units: + if unit := self._units.get(unit_ain): + device.add_or_update_unit(unit) if not ignore_removed: for ain in list(self._devices.keys()): @@ -326,7 +337,6 @@ def _get_listinfo_elements(self, entity_type): """Get the DOM elements for the entity list.""" plain = self._aha_request("get" + entity_type + "listinfos") dom = ElementTree.fromstring(plain) - _LOGGER.debug(dom) return dom.findall("*") def wait_device_txbusy(self, ain, retries=10): @@ -403,23 +413,39 @@ def get_switch_state(self, ain): return None return dev.switch_state + def _switch_action(self, ain, action): + if dev := self._update_device_config(ain): + if dev.has_switch: + action(dev) + return dev.switch_state + _LOGGER.error(f"Device {dev.name} is not a switch") + else: + _LOGGER.error(f"Device {ain} not found") + return None + def set_switch_state_on(self, ain, wait=False): """Set the switch to on state.""" - if self.update_device(ain): - self._devices[ain].set_switch_state_on() - return None + if self._use_aha: + result = self._aha_request("setswitchon", ain=ain, rf=bool) + wait and self.wait_device_txbusy(ain) + return result + return self._switch_action(ain, lambda dev: dev.set_switch_state_on()) def set_switch_state_off(self, ain, wait=False): """Set the switch to off state.""" - if self.update_device(ain): - self._devices[ain].set_switch_state_off() - return None + if self._use_aha: + result = self._aha_request("setswitchoff", ain=ain, rf=bool) + wait and self.wait_device_txbusy(ain) + return result + return self._switch_action(ain, lambda dev: dev.set_switch_state_off()) def set_switch_state_toggle(self, ain, wait=False): """Toggle the switch state.""" - if self.update_device(ain): - self._devices[ain].set_switch_state_toggle() - return None + if self._use_aha: + result = self._aha_request("setswitchtoggle", ain=ain, rf=bool) + wait and self.wait_device_txbusy(ain) + return result + return self._switch_action(ain, lambda dev: dev.set_switch_state_toggle()) def get_switch_power(self, ain): """Get the switch power consumption.""" @@ -511,46 +537,60 @@ def get_device_statistics(self, ain): stats["statistics"].append(s) return json.dumps(stats) - # Lightbulb-related commands - + # Lightbulb-related commands (for on/off/toggle there is no difference switch devices in REST) def set_state_off(self, ain, wait=False): """Set the switch/actuator/lightbulb to on state.""" - self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 0}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 0}) + wait and self.wait_device_txbusy(ain) + return self._switch_action(ain, lambda dev: dev.set_switch_state_on()) def set_state_on(self, ain, wait=False): """Set the switch/actuator/lightbulb to on state.""" - self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 1}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 1}) + wait and self.wait_device_txbusy(ain) + return self._switch_action(ain, lambda dev: dev.set_switch_state_off()) def set_state_toggle(self, ain, wait=False): """Toggle the switch/actuator/lightbulb state.""" - self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 2}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 2}) + wait and self.wait_device_txbusy(ain) + return self._switch_action(ain, lambda dev: dev.set_switch_state_toggle()) def set_level(self, ain, level, wait=False): """Set level/brightness/height in interval [0,255].""" - if level < 0: - level = 0 # 0% - elif level > 255: - level = 255 # 100 % + if self._use_aha: + if level < 0: + level = 0 # 0% + elif level > 255: + level = 255 # 100 % - self._aha_request("setlevel", ain=ain, param={"level": int(level)}) - wait and self.wait_device_txbusy(ain) + self._aha_request("setlevel", ain=ain, param={"level": int(level)}) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") def set_level_percentage(self, ain, level, wait=False): """Set level/brightness/height in interval [0,100].""" - if level < 0: - level = 0 - elif level > 100: - level = 100 + if self._use_aha: + if level < 0: + level = 0 + elif level > 100: + level = 100 - self._aha_request("setlevelpercentage", ain=ain, param={"level": int(level)}) - wait and self.wait_device_txbusy(ain) + self._aha_request("setlevelpercentage", ain=ain, param={"level": int(level)}) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") def _get_colordefaults(self, ain): - plain = self._aha_request("getcolordefaults", ain=ain) - return ElementTree.fromstring(plain) + if self._use_aha: + plain = self._aha_request("getcolordefaults", ain=ain) + return ElementTree.fromstring(plain) + else: + raise NotImplementedError("missing REST api impl") def get_colors(self, ain): """Get colors (HSV-space) supported by this lightbulb.""" @@ -596,29 +636,33 @@ def set_color_temp(self, ain, temperature, duration=0, wait=False): temperature: temperature element obtained from get_temperatures() duration: Speed of change in seconds, 0 = instant """ - params = {"temperature": int(temperature), "duration": int(duration) * 10} - self._aha_request("setcolortemperature", ain=ain, param=params) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + params = {"temperature": int(temperature), "duration": int(duration) * 10} + self._aha_request("setcolortemperature", ain=ain, param=params) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") # blinds # states: open, close, stop - def _set_blind_state(self, ain, state): - self._aha_request("setblind", ain=ain, param={"target": state}) + def _set_blind_state(self, ain, state, wait): + if self._use_aha: + self._aha_request("setblind", ain=ain, param={"target": state}) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") def set_blind_open(self, ain, wait=False): """Set the blind state to open.""" - self._set_blind_state(ain, "open") - wait and self.wait_device_txbusy(ain) + self._set_blind_state(ain, "open", wait) def set_blind_close(self, ain, wait=False): """Set the blind state to close.""" - self._set_blind_state(ain, "close") - wait and self.wait_device_txbusy(ain) + self._set_blind_state(ain, "close", wait) def set_blind_stop(self, ain, wait=False): """Set the blind state to stop.""" - self._set_blind_state(ain, "stop") - wait and self.wait_device_txbusy(ain) + self._set_blind_state(ain, "stop", wait) # Template-related commands diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 0a6e55c..530a74b 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -2,13 +2,32 @@ # -*- coding: utf-8 -*- -from .devicetypes import FritzhomeUnit # noqa: F401 -from .devicetypes import FritzhomeTemplate # noqa: F401 -from .devicetypes import FritzhomeTrigger # noqa: F401 -from .devicetypes import FritzhomeDeviceBase +from .devicetypes import * from .interfaces import * -class FritzhomeDevice( +class FritzhomeDeviceAHA( + FritzhomeDeviceAlarm, + FritzhomeDeviceBlind, + FritzhomeDeviceButton, + FritzhomeDeviceHumidity, + FritzhomeDeviceLevel, + FritzhomeDeviceLightBulb, + FritzhomeDevicePowermeter, + FritzhomeDeviceRepeater, + FritzhomeDeviceSwitch, + FritzhomeDeviceTemperature, + FritzhomeDeviceThermostat, +): + """The Fritzhome Device class.""" + + def __init__(self, fritz=None, node=None): + """Create a device object.""" + super().__init__(fritz, node) + + def _update_from_node(self, node): + super()._update_from_node(node) + +class FritzhomeDeviceREST( FritzhomeDeviceBase, FritzhomeOnOffMixin, FritzhomeMultimeterMixin, @@ -23,3 +42,18 @@ def __init__(self, fritz=None, node=None): def _update_from_node(self, node): super()._update_from_node(node) + +FritzhomeDevice = None + +def __get_defice_class(base): + class FritzhomeDevice(base): + pass + return FritzhomeDevice + +def get_device_class(is_aha): + global FritzhomeDevice + if is_aha: + FritzhomeDevice = __get_defice_class(FritzhomeDeviceAHA) + else: + FritzhomeDevice = __get_defice_class(FritzhomeDeviceREST) + return FritzhomeDevice diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index 2cde45e..e8f115b 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -11,6 +11,8 @@ class FritzhomeOnOffInterface(FritzhomeInterfaceBase): """The Fritzhome OnOff interface class.""" + switch_state = None + # Switch @property def is_switch(self): diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py index c7c7048..bd385dd 100644 --- a/pyfritzhome/interfaces/temperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -12,6 +12,7 @@ class FritzhomeTemperatureInterface(FritzhomeInterfaceBase): """The Fritzhome Device class.""" celsius = None + offset = None @property def is_temperature(self): From 4c098fdcd2fd924034070495c6685e10182c264c Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 25 Feb 2026 22:33:08 +0100 Subject: [PATCH 10/29] simplified interface mixins --- .../devicetypes/fritzhomedevicebase.py | 6 +++++ pyfritzhome/devicetypes/fritzhomeunitbase.py | 6 ++--- pyfritzhome/interfaces/humidityinterface.py | 9 ++----- pyfritzhome/interfaces/multimeterinterface.py | 18 ++++---------- pyfritzhome/interfaces/onoffinterface.py | 24 +++++++------------ .../interfaces/temperatureinterface.py | 12 +++------- 6 files changed, 27 insertions(+), 48 deletions(-) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 7ae6216..4f56429 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -39,6 +39,12 @@ def _update_from_node(self, node): self.product_name = self._node["productName"] self.is_connected = self._node["isConnected"] + def find_interface(self, interface): + for unit in self._units.values(): + if interface := unit.find_interface(interface): + return interface + return None + def get_config(): self._fritz.update_device_config(self.ain) diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index cd7b12f..0c04540 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -36,12 +36,12 @@ def _update_from_node(self, node): for iface, node in node["interfaces"].items(): self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) - def units(self): - return [self] - def update_interface(self, interface): self._fritz.put_unit(self.ain, {"interfaces": {interface.type: interface._node}}) + def find_interface(self, interface): + return self.interfaces.get(interface); + @property def parent(self): return self._node["parentUid"] diff --git a/pyfritzhome/interfaces/humidityinterface.py b/pyfritzhome/interfaces/humidityinterface.py index 455d1a9..8ca8361 100644 --- a/pyfritzhome/interfaces/humidityinterface.py +++ b/pyfritzhome/interfaces/humidityinterface.py @@ -32,11 +32,7 @@ class FritzhomeHumidityMixin(): """The Fritzhome Humidity mixin.""" def find_humidity_interface(self): - #~ return next((unit for unit in self._units if unit.is_switch), None) - for unit in self.units(): - if interface := unit.interfaces.get("humidityInterface"): - return (unit, interface) - return None + return self.find_interface("humidityInterface") @property def has_humidity_sensor(self): @@ -46,5 +42,4 @@ def has_humidity_sensor(self): @property def rel_humidity(self): """ Get the current humidity """ - if pair := self.find_humidity_interface(): - return pair[1].rel_humidity + return self.find_humidity_interface().rel_humidity diff --git a/pyfritzhome/interfaces/multimeterinterface.py b/pyfritzhome/interfaces/multimeterinterface.py index 6197970..92bcbb4 100644 --- a/pyfritzhome/interfaces/multimeterinterface.py +++ b/pyfritzhome/interfaces/multimeterinterface.py @@ -38,11 +38,7 @@ class FritzhomeMultimeterMixin(): """The Fritzhome Multimeter mixin.""" def find_multimeter_interface(self): - #~ return next((unit for unit in self._units if unit.is_switch), None) - for unit in self.units(): - if interface := unit.interfaces.get("multimeterInterface"): - return (unit, interface) - return None + return self.find_interface("multimeterInterface") @property def has_powermeter(self): @@ -52,25 +48,21 @@ def has_powermeter(self): @property def power(self): """ Get the current powermeter power """ - if pair := self.find_multimeter_interface(): - return pair[1].current + self.find_multimeter_interface().current @property def energy(self): """ Get the current currentmeter energy """ - if pair := self.find_multimeter_interface(): - return pair[1].energy + self.find_multimeter_interface().energy @property def voltage(self): """ Get the current voltagemeter voltage """ - if pair := self.find_multimeter_interface(): - return pair[1].voltage + self.find_multimeter_interface().voltage @property def current(self): """ Get the current currentmeter current """ - if pair := self.find_multimeter_interface(): - return pair[1].current + self.find_multimeter_interface().current diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index e8f115b..f37b2f2 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -29,22 +29,21 @@ def _update_from_node(self, node): def set_switch_state_on(self, wait=False): self._node["active"] = True + return self def set_switch_state_off(self, wait=False): self._node["active"] = False + return self def set_switch_state_toggle(self, wait=False): self._node["active"] = not self._node["active"] + return self class FritzhomeOnOffMixin(): def find_switch_interface(self): - #~ return next((unit for unit in self._units if unit.is_switch), None) - for unit in self.units(): - if interface := unit.interfaces.get("onOffInterface"): - return (unit, interface) - return None + return self.find_interface("onOffInterface") @property def has_switch(self): @@ -53,24 +52,17 @@ def has_switch(self): @property def switch_state(self): """ Get the current switch state """ - if pair := self.find_switch_interface(): - return pair[1].switch_state + return self.find_switch_interface().switch_state def set_switch_state_on(self): """Set the switch state to on.""" - if pair := self.find_switch_interface(): - pair[1].set_switch_state_on() - pair[1].update() + self.find_switch_interface().set_switch_state_on().update() def set_switch_state_off(self): """Set the switch state to off.""" - if pair := self.find_switch_interface(): - pair[1].set_switch_state_off() - pair[1].update() + self.find_switch_interface().set_switch_state_off().update() def set_switch_state_toggle(self): """Toggle the switch state.""" - if pair := self.find_switch_interface(): - pair[1].set_switch_state_toggle() - pair[1].update() + self.find_switch_interface().set_switch_state_toggle().update() diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py index bd385dd..294d6a7 100644 --- a/pyfritzhome/interfaces/temperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -35,11 +35,7 @@ class FritzhomeTemperatureMixin(): """The Fritzhome Temperature mixin.""" def find_temperature_interface(self): - #~ return next((unit for unit in self._units if unit.is_switch), None) - for unit in self.units(): - if interface := unit.interfaces.get("temperatureInterface"): - return (unit, interface) - return None + return self.find_interface("temperatureInterface") @property def has_temperature_sensor(self): @@ -49,11 +45,9 @@ def has_temperature_sensor(self): @property def temperature(self): """ Get the current temperature """ - if pair := self.find_temperature_interface(): - return pair[1].celsius + self.find_temperature_interface().celsius @property def offset(self): """ Get the current temperature offset """ - if pair := self.find_temperature_interface(): - return pair[1].offset + self.find_temperature_interface().offset From 124e7538e6ddfb9c43fa5437c0b1612c52a2a956 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Mon, 9 Mar 2026 08:06:09 +0100 Subject: [PATCH 11/29] WIP --- pyfritzhome/cli.py | 11 ++ .../devicetypes/fritzhomedevicebase.py | 41 ++++++- .../devicetypes/fritzhomedevicehumidity.py | 39 ++++++ .../devicetypes/fritzhomedevicepowermeter.py | 68 +++++++++++ .../devicetypes/fritzhomedeviceswitch.py | 81 +++++++++++++ .../devicetypes/fritzhomedevicetemperature.py | 47 ++++++++ .../devicetypes/fritzhomeentitybase.py | 1 + pyfritzhome/devicetypes/fritzhomeunit.py | 3 +- pyfritzhome/devicetypes/fritzhomeunitbase.py | 31 ++++- pyfritzhome/fritzhome.py | 74 +++++++++--- pyfritzhome/fritzhomedevice.py | 1 + pyfritzhome/interfaces/__init__.py | 2 + pyfritzhome/interfaces/interface.py | 4 +- pyfritzhome/interfaces/interfacebase.py | 16 ++- pyfritzhome/interfaces/onoffinterface.py | 6 +- pyfritzhome/interfaces/thermostatinterface.py | 113 ++++++++++++++++++ 16 files changed, 509 insertions(+), 29 deletions(-) create mode 100644 pyfritzhome/devicetypes/fritzhomedevicehumidity.py create mode 100644 pyfritzhome/devicetypes/fritzhomedevicepowermeter.py create mode 100644 pyfritzhome/devicetypes/fritzhomedeviceswitch.py create mode 100644 pyfritzhome/devicetypes/fritzhomedevicetemperature.py create mode 100644 pyfritzhome/interfaces/thermostatinterface.py diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index e9f3e24..20ff14e 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -110,6 +110,10 @@ def blind_set_level_percentage(fritz, args): fritz.set_level_percentage(args.ain, args.level) +def thermostat_get_info(fritz, args): + """DOC-TODO""" + print(fritz.get_device_infos(args.ain)) + def thermostat_set_target_temperature(fritz, args): """Command that sets the thermostat temperature.""" fritz.set_target_temperature(args.ain, args.temperature) @@ -298,6 +302,13 @@ def main(args=None): subparser = _sub.add_parser("thermostat", help="Thermostat commands") _sub_switch = subparser.add_subparsers() + # thermostat target temperature + subparser = _sub_switch.add_parser( + "get_info", help="Get thermostat information" + ) + subparser.add_argument("ain", type=str, metavar="AIN", help="Actor Identification") + subparser.set_defaults(func=thermostat_get_info) + # thermostat target temperature subparser = _sub_switch.add_parser( "set_target_temperature", help="set target temperature" diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 4f56429..c87508a 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -4,6 +4,7 @@ from __future__ import print_function import logging +import json from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase @@ -28,8 +29,8 @@ def __repr__(self): def _update_from_node(self, node): _LOGGER.debug("update base device") - super()._update_from_node(node) self._units = {} + super()._update_from_node(node) if self._fritz._use_aha: self.manufacturer = node.attrib["manufacturer"] self.product_name = node.attrib["productname"] @@ -39,15 +40,45 @@ def _update_from_node(self, node): self.product_name = self._node["productName"] self.is_connected = self._node["isConnected"] + def update_unit(self, unit, node): + """DOC-TODO (REST)""" + assert unit.ain in self._units.keys(), "unknown unit to update" + self._updates |= {"ain": self.ain, "units": node or unit._node} + self._updates["units"] |= { "ain": unit.ain } + if self.commit_now: + self.trigger_update() + def find_interface(self, interface): - for unit in self._units.values(): + """DOC-TODO (REST)""" + found = None + for unit in self.units(): if interface := unit.find_interface(interface): - return interface - return None + if found is None: + found = interface + else: + _LOGGER.warning("ambigious device, multiple interface candidates, using first") + return found + + def begin(self, unit_ain=None): + """DOC-TODO (REST)""" + if unit_ain: + return self.units[unit_ain].begin() + else: + if len(self.units()) > 1: + _LOGGER.warning("ambigious device, multiple units, using first") + for unit in self._units.values(): + return unit.begin() - def get_config(): + def get_device_config(): self._fritz.update_device_config(self.ain) + def get_json(self): + r = self._node.copy() + r["units"] = {} + for unit in self._node["unitUids"]: + r["units"][unit] = self._units[unit]._node + return json.dumps(r) + # legacy @property def productname(self): diff --git a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py new file mode 100644 index 0000000..fe56ec0 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py @@ -0,0 +1,39 @@ +"""The humidity device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .fritzhomedevicebase import FritzhomeDeviceBase +from .fritzhomedevicefeatures import FritzhomeDeviceFeatures + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeDeviceHumidity(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + rel_humidity = None + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_humidity_sensor: + self._update_humidity_from_node(node) + + # Humidity + @property + def has_humidity_sensor(self): + """Check if the device has humidity function.""" + return self._has_feature(FritzhomeDeviceFeatures.HUMIDITY) + + def _update_humidity_from_node(self, node): + _LOGGER.debug("update humidity device") + humidity_element = node.find("humidity") + try: + self.rel_humidity = self.get_node_value_as_int( + humidity_element, "rel_humidity" + ) + except ValueError: + pass diff --git a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py new file mode 100644 index 0000000..a4e651a --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py @@ -0,0 +1,68 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .fritzhomedevicebase import FritzhomeDeviceBase +from .fritzhomedevicefeatures import FritzhomeDeviceFeatures + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeDevicePowermeter(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + power = None + energy = None + voltage = None + current = None + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_powermeter: + self._update_powermeter_from_node(node) + + # Power Meter + @property + def has_powermeter(self): + """Check if the device has powermeter function.""" + return self._has_feature(FritzhomeDeviceFeatures.POWER_METER) + + def _update_powermeter_from_node(self, node): + _LOGGER.debug("update powermeter device") + val = node.find("powermeter") + + try: + self.power = int(val.findtext("power")) + except Exception: + pass + + try: + self.energy = int(val.findtext("energy")) + except Exception: + pass + + try: + self.voltage = int(val.findtext("voltage")) + except Exception: + pass + + if ( + isinstance(self.power, int) + and isinstance(self.voltage, int) + and self.voltage > 0 + ): + self.current = self.power / self.voltage * 1000 + else: + self.current = None + + def get_switch_power(self): + """Get the switch state.""" + return self._fritz.get_switch_power(self.ain) + + def get_switch_energy(self): + """Get the switch energy.""" + return self._fritz.get_switch_energy(self.ain) diff --git a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py new file mode 100644 index 0000000..92f1de3 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py @@ -0,0 +1,81 @@ +"""The switch device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .fritzhomedevicebase import FritzhomeDeviceBase +from .fritzhomedevicefeatures import FritzhomeDeviceFeatures + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeDeviceSwitch(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + switch_state = None + switch_mode = None + lock = None + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_switch: + self._update_switch_from_node(node) + + # Switch + @property + def has_switch(self): + """Check if the device has switch function.""" + if self._has_feature(FritzhomeDeviceFeatures.SWITCH): + # for AVM plugs like FRITZ!DECT 200 and FRITZ!DECT 210 + return True + if self._has_feature( + FritzhomeDeviceFeatures.SWITCHABLE + ) and not self._has_feature(FritzhomeDeviceFeatures.LIGHTBULB): + # for HAN-FUN plugs + return True + return False + + def _update_switch_from_node(self, node): + _LOGGER.debug("update switch device") + if self._has_feature(FritzhomeDeviceFeatures.SWITCH): + val = node.find("switch") + try: + self.switch_state = self.get_node_value_as_int_as_bool(val, "state") + except Exception: + self.switch_state = None + self.switch_mode = self.get_node_value(val, "mode") + try: + self.lock = self.get_node_value_as_int_as_bool(val, "lock") + except Exception: + self.lock = None + + # optional value + try: + self.device_lock = self.get_node_value_as_int_as_bool(val, "devicelock") + except Exception: + pass + else: + val = node.find("simpleonoff") + try: + self.switch_state = self.get_node_value_as_int_as_bool(val, "state") + except Exception: + self.switch_state = None + + def get_switch_state(self): + """Get the switch state.""" + return self._fritz.get_switch_state(self.ain) + + def set_switch_state_on(self, wait=False): + """Set the switch state to on.""" + return self._fritz.set_switch_state_on(self.ain, wait) + + def set_switch_state_off(self, wait=False): + """Set the switch state to off.""" + return self._fritz.set_switch_state_off(self.ain, wait) + + def set_switch_state_toggle(self, wait=False): + """Toggle the switch state.""" + return self._fritz.set_switch_state_toggle(self.ain, wait) diff --git a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py new file mode 100644 index 0000000..9ca1c4e --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py @@ -0,0 +1,47 @@ +"""The temperature device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .fritzhomedevicebase import FritzhomeDeviceBase +from .fritzhomedevicefeatures import FritzhomeDeviceFeatures + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeDeviceTemperature(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + offset = None + temperature = None + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_temperature_sensor: + self._update_temperature_from_node(node) + + # Temperature + @property + def has_temperature_sensor(self): + """Check if the device has temperature function.""" + return self._has_feature(FritzhomeDeviceFeatures.TEMPERATURE) + + def _update_temperature_from_node(self, node): + _LOGGER.debug("update temperature device") + temperature_element = node.find("temperature") + try: + self.offset = ( + self.get_node_value_as_int(temperature_element, "offset") / 10.0 + ) + except ValueError: + pass + + try: + self.temperature = ( + self.get_node_value_as_int(temperature_element, "celsius") / 10.0 + ) + except ValueError: + pass diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index 092e4c7..2acecb6 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -23,6 +23,7 @@ def __init__(self, fritz=None, node=None): self.ain = None self.name = None self._functionsbitmask = 0 + self.commit_now = True if node is not None: self._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomeunit.py b/pyfritzhome/devicetypes/fritzhomeunit.py index d7d5ce2..b0e23fe 100644 --- a/pyfritzhome/devicetypes/fritzhomeunit.py +++ b/pyfritzhome/devicetypes/fritzhomeunit.py @@ -14,7 +14,8 @@ class FritzhomeUnit(FritzhomeUnitBase, interfaces.FritzhomeOnOffMixin, interfaces.FritzhomeMultimeterMixin, interfaces.FritzhomeTemperatureMixin, - interfaces.FritzhomeHumidityMixin): + interfaces.FritzhomeHumidityMixin, + interfaces.FritzhomeThermostatMixin): """The Fritzhome Device class.""" def __init__(self, fritz=None, node=None): diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index 0c04540..04adc28 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -4,6 +4,7 @@ from __future__ import print_function import logging +import weakref from .fritzhomeentitybase import FritzhomeEntityBase from .. import interfaces @@ -16,6 +17,11 @@ class FritzhomeUnitBase(FritzhomeEntityBase): interfaces = None + def __init__(self, fritz=None, node=None): + """Create a unit object (REST-only).""" + super().__init__(fritz, node) + self._updates = {} + def __repr__(self): """Return a string.""" return f"{self.ain} of device {self.parent}" @@ -36,8 +42,29 @@ def _update_from_node(self, node): for iface, node in node["interfaces"].items(): self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) - def update_interface(self, interface): - self._fritz.put_unit(self.ain, {"interfaces": {interface.type: interface._node}}) + def update_interface(self, interface, node=None): + import traceback + if self.commit_now: + traceback.print_stack() + self._fritz.put_unit(self.ain, {"interfaces": {interface.type: node or interface._node}}) + else: + self._updates |= {"interfaces": {interface.type: node or interface._node}} + + def begin(self): + """DOC-TODO (REST)""" + assert self.commit_now, "nested begin() not permitted" + print(f"A {self}") + self.commit_now = False + return self + + def end(self): + """DOC-TODO (REST)""" + print(f"Z {self}") + self.commit_now = True + self.trigger_update() + + def trigger_update(self): + self._fritz.put_unit(self.ain, self._updates) def find_interface(self, interface): return self.interfaces.get(interface); diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 6a8358f..440fa25 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -226,6 +226,7 @@ def _update_unit_from_node(self, node): return unit def put_unit(self, ain, node): + """DOC-TODO (REST)""" if self._units is None: self._units = {} @@ -233,6 +234,15 @@ def put_unit(self, ain, node): params = {"Authorization": f"AVM-SID {self._sid}"} data = self._put(f"{self.rest_url}/configuration/units/{ain}", node, headers=params) + def put_device(self, ain, node): + """DOC-TODO (REST)""" + if self._devices is None: + self._devices = {} + + _LOGGER.info("put devices ...\n" + json.dumps(node)) + params = {"Authorization": f"AVM-SID {self._sid}"} + data = self._put(f"{self.rest_url}/configuration/devices/{ain}", node, headers=params) + def _update_device_from_node(self, node): ain = node["ain"] if device := self._devices.get(ain): @@ -268,6 +278,10 @@ def _update_device_config(self, ain): device.add_or_update_unit(unit) return device + def get_config(self, ain): + """ DOC-TODO """ + return self._update_device_config(ain) + def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") @@ -389,7 +403,9 @@ def get_device_by_ain(self, ain): def get_device_infos(self, ain): """Get the device infos.""" - return self._aha_request("getdeviceinfos", ain=ain) + if self._use_aha: + return self._aha_request("getdeviceinfos", ain=ain) + return self._update_device_config(ain).get_json() def get_device_present(self, ain): """Get the device presence.""" @@ -467,6 +483,17 @@ def get_switch_energy(self, ain): return None return dev.energy + def get_thermostat_config(self, ain): + """Get the thermostat .""" + if self._use_aha: + return None + if dev := self._update_device_config(ain): + if not dev.has_temperature: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + return float(dev.has_temperature) + + def get_temperature(self, ain): """Get the device temperature sensor value.""" if self._use_aha: @@ -493,31 +520,48 @@ def get_target_temperature(self, ain): def set_target_temperature(self, ain, temperature, wait=False): """Set the thermostate target temperature.""" - temp = int(temperature * 2) - - if temp < 16: - temp = 253 - elif temp > 56: - temp = 254 + if self._use_aha: + temp = int(temperature * 2) - self._aha_request("sethkrtsoll", ain=ain, param={"param": temp}) - wait and self.wait_device_txbusy(ain) + if temp < 16: + temp = 253 + elif temp > 56: + temp = 254 + self._aha_request("sethkrtsoll", ain=ain, param={"param": temp}) + wait and self.wait_device_txbusy(ain) + elif dev := self._update_device_config(ain): + if not dev.has_thermostat: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + dev.set_target_temperature(seconds) def set_window_open(self, ain, seconds, wait=False): """Set the thermostate target temperature.""" endtimestamp = int(time.time() + seconds) - self._aha_request( - "sethkrwindowopen", ain=ain, param={"endtimestamp": endtimestamp} - ) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request( + "sethkrwindowopen", ain=ain, param={"endtimestamp": endtimestamp} + ) + wait and self.wait_device_txbusy(ain) + elif dev := self._update_device_config(ain): + if not dev.has_thermostat: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + dev.set_window_open(seconds) def set_boost_mode(self, ain, seconds, wait=False): """Set the thermostate to boost mode.""" endtimestamp = int(time.time() + seconds) - self._aha_request("sethkrboost", ain=ain, param={"endtimestamp": endtimestamp}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("sethkrboost", ain=ain, param={"endtimestamp": endtimestamp}) + wait and self.wait_device_txbusy(ain) + elif dev := self._update_device_config(ain): + if not dev.has_thermostat: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + dev.set_boost_mode(seconds) def get_comfort_temperature(self, ain): """Get the thermostate comfort temperature.""" diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 530a74b..90b8b1f 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -33,6 +33,7 @@ class FritzhomeDeviceREST( FritzhomeMultimeterMixin, FritzhomeTemperatureMixin, FritzhomeHumidityMixin, + FritzhomeThermostatMixin, ): """The Fritzhome Device class.""" diff --git a/pyfritzhome/interfaces/__init__.py b/pyfritzhome/interfaces/__init__.py index 7437683..39a4319 100644 --- a/pyfritzhome/interfaces/__init__.py +++ b/pyfritzhome/interfaces/__init__.py @@ -6,6 +6,7 @@ "FritzhomeMultimeterInterface", "FritzhomeMultimeterMixin", "FritzhomeTemperatureInterface", "FritzhomeTemperatureMixin", "FritzhomeHumidityInterface", "FritzhomeHumidityMixin", + "FritzhomeThermostatInterface", "FritzhomeThermostatMixin", ) from .interface import FritzhomeInterface @@ -13,3 +14,4 @@ from .multimeterinterface import FritzhomeMultimeterInterface, FritzhomeMultimeterMixin from .temperatureinterface import FritzhomeTemperatureInterface, FritzhomeTemperatureMixin from .humidityinterface import FritzhomeHumidityInterface, FritzhomeHumidityMixin +from .thermostatinterface import FritzhomeThermostatInterface, FritzhomeThermostatMixin diff --git a/pyfritzhome/interfaces/interface.py b/pyfritzhome/interfaces/interface.py index 23af395..d4c9668 100644 --- a/pyfritzhome/interfaces/interface.py +++ b/pyfritzhome/interfaces/interface.py @@ -14,13 +14,15 @@ from .multimeterinterface import FritzhomeMultimeterInterface from .temperatureinterface import FritzhomeTemperatureInterface from .humidityinterface import FritzhomeHumidityInterface +from .thermostatinterface import FritzhomeThermostatInterface _LOGGER = logging.getLogger(__name__) class FritzhomeInterface(FritzhomeOnOffInterface, FritzhomeMultimeterInterface, FritzhomeTemperatureInterface, - FritzhomeHumidityInterface): + FritzhomeHumidityInterface, + FritzhomeThermostatInterface): """The Fritzhome Interface class.""" def __init__(self, unit, type, node = None): diff --git a/pyfritzhome/interfaces/interfacebase.py b/pyfritzhome/interfaces/interfacebase.py index e6b5fa6..8a8daae 100644 --- a/pyfritzhome/interfaces/interfacebase.py +++ b/pyfritzhome/interfaces/interfacebase.py @@ -16,10 +16,18 @@ class FritzhomeInterfaceBase(): """The Fritzhome Interface class.""" + @staticmethod + def node_property(node_name, value_name): + return property( + fget = lambda self: self._node[node_name].get(value_name), + fset = lambda self, v: self._node[node_name].update(value_name, v) + ) + def __init__(self, unit, type, node): """Create an entity base object.""" self.type = type self._node = node + self._node_set = {} self._unit_ref = weakref.ref(unit) if node is not None: self._update_from_node(node) @@ -31,9 +39,13 @@ def __repr__(self): def _update_from_node(self, node): pass - def update(self): - self._unit_ref().update_interface(self) + def changed(self): + assert self._node_set.keys() <= self._node.keys(), "_node_set must be a strict subset" + self._unit_ref().update_interface(self, self._node_set) + self._node |= self._node_set + self._node_set = {} @property def node(self): return self._node; + diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index f37b2f2..f956477 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -56,13 +56,13 @@ def switch_state(self): def set_switch_state_on(self): """Set the switch state to on.""" - self.find_switch_interface().set_switch_state_on().update() + self.find_switch_interface().set_switch_state_on().commit() def set_switch_state_off(self): """Set the switch state to off.""" - self.find_switch_interface().set_switch_state_off().update() + self.find_switch_interface().set_switch_state_off().commit() def set_switch_state_toggle(self): """Toggle the switch state.""" - self.find_switch_interface().set_switch_state_toggle().update() + self.find_switch_interface().set_switch_state_toggle().commit() diff --git a/pyfritzhome/interfaces/thermostatinterface.py b/pyfritzhome/interfaces/thermostatinterface.py new file mode 100644 index 0000000..e44d613 --- /dev/null +++ b/pyfritzhome/interfaces/thermostatinterface.py @@ -0,0 +1,113 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeThermostatInterface(FritzhomeInterfaceBase): + """The Fritzhome Device class.""" + + def __init__(self, unit, type, node): + super().__init__(unit, type, node) + if type == "thermostatInterface": + FritzhomeInterfaceBase.node_property("setPointTemperature", "celsius") + FritzhomeInterfaceBase.node_property("comfortTemperature", "celsius") + FritzhomeInterfaceBase.node_property("reducedTemperature", "celsius") + + @property + def is_thermostat(self): + return self.type == "thermostatInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + + def set_target_temperature(self, v): + self._node_set["setPointTemperature"]= {} + self._node_set["setPointTemperature"]["celsius"] = v + self._node_set["setPointTemperature"]["mode"] = "temperature" + self.changed() + + def set_comfort_temperature(self, v): + self._node_set["comfortTemperature"]= {} + self._node_set["comfortTemperature"]["celsius"] = v + self._node_set["comfortTemperature"]["mode"] = "temperature" + self.changed() + + def set_reduced_temperature(self, v): + self._node_set["reducedTemperature"]= {} + self._node_set["reducedTemperature"]["celsius"] = v + self._node_set["reducedTemperature"]["mode"] = "temperature" + self.changed() + + def set_boost_mode(self, duration_secs): + import time + self._node_set["boost"] = {} + self._node_set["boost"]["enabled"] = True + self._node_set["boost"]["endTime"] = int(time.time() + duration_secs) + self.changed() + + def set_window_open(self, duration_secs): + import time + self._node_set["windowOpenMode"] = {} + self._node_set["windowOpenMode"]["enabled"] = True + self._node_set["windowOpenMode"]["endTime"] = int(time.time() + duration_secs) + self.changed() + +class FritzhomeThermostatMixin(): + """The Fritzhome Thermostat mixin.""" + + def find_thermostat_interface(self): + return self.find_interface("thermostatInterface") + + @property + def has_thermostat(self): + """Check if the device has thermostat sensors.""" + return self.find_thermostat_interface() != None + + @property + def target_temperature(self): + """ Get the current thermostat """ + self.find_thermostat_interface().target_temperature + + def set_target_temperature(self, v): + """ Get the current thermostat """ + self.find_thermostat_interface().set_target_temperature(v) + return self + + @property + def reduced_temperature(self): + """ Get the current thermostat offset """ + self.find_thermostat_interface().reduced_temperature + + def set_reduced_temperature(self, v): + """ Get the current thermostat """ + self.find_thermostat_interface().set_reduced_temperature(v) + return self + + @property + def comfort_temperature(self): + self.find_thermostat_interface().comfort_temperature + + def set_comfort_temperature(self, v): + """ Get the current thermostat """ + self.find_thermostat_interface().set_comfort_temperature(v) + return self + + @property + def eco_temperature(self): + return self.reduced_temperature + + def set_boost_mode(self, seconds, wait=False): + """Set the thermostate into boost mode.""" + self.find_thermostat_interface().set_boost_mode(seconds) + return self + + def set_window_open(self, seconds, wait=False): + """Set the thermostate in open window mode.""" + self.find_thermostat_interface().set_window_open(seconds) + return self From e3a3839f938d9551eb54d03dab1a6c3d58e8894c Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 11 Mar 2026 07:22:33 +0100 Subject: [PATCH 12/29] fixup! bring back AHA in a few places --- pyfritzhome/cli.py | 10 +++++++--- pyfritzhome/devicetypes/fritzhomeentitybase.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 20ff14e..512f465 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -170,9 +170,13 @@ def list_templates(fritz, args): print(" color=%s" % template.apply_color) print(" dialhelper=%s" % template.apply_dialhelper) - print(" Devices:") - for device_id in template.devices: - print(" %s=%s" % (device_id, devices[device_id].name)) + # REST api does not expose group devices in the devices list + # (TODO: there's a groups endpoint) + template_devs = template.devices & devices.keys() + if len(template_devs) > 0: + print(" Devices:") + for device_id in template_devs.devices: + print(" %s=%s" % (device_id, devices[device_id].name)) def template_apply(fritz, args): diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index 2acecb6..3a56736 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -36,13 +36,13 @@ def _has_feature(self, feature: FritzhomeDeviceFeatures) -> bool: def _update_from_node(self, node): self._node = node - if self._fritz._use_aha: + if node.get("identifier") is not None: # AHA if self.ain is not None and self.ain != node.attrib["identifier"]: raise ValueError("updating invalid ain") self.ain = node.attrib["identifier"] self.name = self.get_node_value(node, "name") self._functionsbitmask = int(node.attrib["functionbitmask"]) - else: + else: # REST if self.ain is not None and self.ain != node["ain"]: raise ValueError("updating invalid ain") self.ain = node["ain"] From afde4cf8604b47f0c3f5d52420576a196c91f964 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 11 Mar 2026 07:23:09 +0100 Subject: [PATCH 13/29] stubs for HASS --- pyfritzhome/devicetypes/fritzhomedevicebase.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index c87508a..03ce895 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -101,3 +101,7 @@ def add_or_update_unit(self, unit): def units(self): return self._units.values() + + @property + def has_color(self): + return False From ed80302f8e8155681c2a4a45a566fa10496e1ffd Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 11 Mar 2026 07:24:16 +0100 Subject: [PATCH 14/29] HASS: repair ignore_removed=False --- pyfritzhome/fritzhome.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 440fa25..280620d 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -285,6 +285,8 @@ def get_config(self, ain): def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") + old_devs = self._devices.keys() + old_units = self._units.keys() if self._use_aha: for element in self._get_listinfo_elements("device"): if element.attrib["identifier"] in self._devices.keys(): @@ -311,18 +313,12 @@ def update_devices(self, ignore_removed=True): device.add_or_update_unit(unit) if not ignore_removed: - for ain in list(self._devices.keys()): - if ain not in [ - element.attrib["ain"] for element in devices - ]: - _LOGGER.info("Removing no more existing device " + ain) - self._devices.pop(ain) - for ain in list(self._units.keys()): - if ain not in [ - element.attrib["ain"] for element in units - ]: - _LOGGER.info("Removing no more existing device " + ain) - self._units.pop(ain) + for ain in old_devs - self._devices.keys(): + _LOGGER.info("Removing no more existing device " + ain) + self._devices.pop(ain) + for ain in old_units - self._units.keys(): + _LOGGER.info("Removing no more existing device " + ain) + self._units.pop(ain) return True From a6b6f98ee3826b99bad7a74b4f16dd887e2e705e Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 13 Mar 2026 22:49:45 +0100 Subject: [PATCH 15/29] implement fw_version --- pyfritzhome/devicetypes/fritzhomedevicebase.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 03ce895..5e1f226 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -16,6 +16,7 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): manufacturer = None product_name = None + fw_version = None is_connected = None def __repr__(self): @@ -34,10 +35,12 @@ def _update_from_node(self, node): if self._fritz._use_aha: self.manufacturer = node.attrib["manufacturer"] self.product_name = node.attrib["productname"] + self.fw_version = node.attrib["fwversion"] self.is_connected = self.get_node_value_as_int_as_bool(node, "present") else: self.manufacturer = self._node["manufacturer"] self.product_name = self._node["productName"] + self.fw_version = self._node["firmwareVersion"] self.is_connected = self._node["isConnected"] def update_unit(self, unit, node): From 20f3a3bf84d11568cd1da71b4039bcf3895e5f85 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 13 Mar 2026 22:50:34 +0100 Subject: [PATCH 16/29] fixup! WIP --- pyfritzhome/interfaces/thermostatinterface.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pyfritzhome/interfaces/thermostatinterface.py b/pyfritzhome/interfaces/thermostatinterface.py index e44d613..4020f6d 100644 --- a/pyfritzhome/interfaces/thermostatinterface.py +++ b/pyfritzhome/interfaces/thermostatinterface.py @@ -26,18 +26,30 @@ def _update_from_node(self, node): _LOGGER.debug("update switch device") super()._update_from_node(node) + @property + def target_temperature(self): + return self._node["setPointTemperature"]["celsius"] + def set_target_temperature(self, v): - self._node_set["setPointTemperature"]= {} + self._node_set["setPointTemperature"] = {} self._node_set["setPointTemperature"]["celsius"] = v self._node_set["setPointTemperature"]["mode"] = "temperature" self.changed() + @property + def comfort_temperature(self): + return self._node["comfortTemperature"]["celsius"] + def set_comfort_temperature(self, v): self._node_set["comfortTemperature"]= {} self._node_set["comfortTemperature"]["celsius"] = v self._node_set["comfortTemperature"]["mode"] = "temperature" self.changed() + @property + def reduced_temperature(self): + return self._node["reducedTemperature"]["celsius"] + def set_reduced_temperature(self, v): self._node_set["reducedTemperature"]= {} self._node_set["reducedTemperature"]["celsius"] = v From 7d163f0b838cad2bd6798f762350c6fc457e6434 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 13 Mar 2026 22:51:28 +0100 Subject: [PATCH 17/29] provide device_and_unit_id although really a bad fit for rest --- pyfritzhome/devicetypes/fritzhomeentitybase.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index 3a56736..f318ccc 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -52,6 +52,21 @@ def _update_from_node(self, node): def node(self): return self._node; + @property + def device_and_unit_id(self): + """Get the device and possible unit id.""" + # These three are not exposed in the overview/devices endpoint in the REST api + if ( + self.ain.startswith("tmp") + or self.ain.startswith("grp") + or self.ain.startswith("trg") + ): + return (self.ain, None) + elif self.ain.startswith("Z") and len(self.ain) == 19: + return (self.ain[0:17], self.ain[17:]) + elif "-" in self.ain: + return tuple(self.ain.split("-")) + return (self.ain, None) # XML Helpers From 90e7f07065644505cb97f0e263d659ddf0213829 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 13 Mar 2026 22:52:19 +0100 Subject: [PATCH 18/29] temperatureinterface: add actual_temperature --- pyfritzhome/interfaces/temperatureinterface.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py index 294d6a7..52fac47 100644 --- a/pyfritzhome/interfaces/temperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -47,6 +47,11 @@ def temperature(self): """ Get the current temperature """ self.find_temperature_interface().celsius + @property + def actual_temperature(self): + """ Get the current temperature (legacy) """ + self.find_temperature_interface().celsius + @property def offset(self): """ Get the current temperature offset """ From 96490fdb4cc9fe045256968b1c3d2ba79ab0b9fa Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 13 Mar 2026 22:52:33 +0100 Subject: [PATCH 19/29] fixup! stubs for HASS --- .../devicetypes/fritzhomedevicebase.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 5e1f226..5f3acd7 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -108,3 +108,46 @@ def units(self): @property def has_color(self): return False + + @property + def has_blind(self): + return False + + @property + def has_alarm(self): + return False + + @property + def has_lightbulb(self): + return False + @property + def holiday_active(self): + return False + @property + def summer_active(self): + return False + @property + def lock(self): + return False + @property + def device_lock(self): + return False + + @property + def battery_level(self): + return False + + @property + def battery_low(self): + return False + + @property + def window_open(self): + return False + + @property + def nextchange_temperature(self): + return False + @property + def nextchange_endperiod(self): + return False From 620d5730279d50c3cf630787364851b7dc258c99 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Tue, 17 Mar 2026 07:09:02 +0100 Subject: [PATCH 20/29] adapt onoffinterface to change interface --- pyfritzhome/interfaces/onoffinterface.py | 27 +++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index f956477..0ab095c 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -28,16 +28,16 @@ def _update_from_node(self, node): self.switch_state = self._node["active"] def set_switch_state_on(self, wait=False): - self._node["active"] = True - return self + self._node_set["active"] = True + self.changed(wait) def set_switch_state_off(self, wait=False): - self._node["active"] = False - return self + self._node_set["active"] = False + self.changed(wait) def set_switch_state_toggle(self, wait=False): - self._node["active"] = not self._node["active"] - return self + self._node_set["active"] = not self._node["active"] + self.changed(wait) class FritzhomeOnOffMixin(): @@ -54,15 +54,18 @@ def switch_state(self): """ Get the current switch state """ return self.find_switch_interface().switch_state - def set_switch_state_on(self): + def set_switch_state_on(self, wait=False): """Set the switch state to on.""" - self.find_switch_interface().set_switch_state_on().commit() + self.find_switch_interface().set_switch_state_on() + return self - def set_switch_state_off(self): + def set_switch_state_off(self, wait=False): """Set the switch state to off.""" - self.find_switch_interface().set_switch_state_off().commit() + self.find_switch_interface().set_switch_state_off() + return self - def set_switch_state_toggle(self): + def set_switch_state_toggle(self, wait=False): """Toggle the switch state.""" - self.find_switch_interface().set_switch_state_toggle().commit() + self.find_switch_interface().set_switch_state_toggle() + return self From 34668b9a4c58659954ee4bcc1aadf8f0105dc356 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Tue, 17 Mar 2026 07:09:59 +0100 Subject: [PATCH 21/29] allow updating interface classes Previously, when units were updated, interfaces would re-created (new objects) which does not allow polling for completion (also F!B does not reflect updated values in GET immediately, thus re-created interfaced were stale). --- pyfritzhome/devicetypes/fritzhomeunitbase.py | 17 +++++++------ pyfritzhome/fritzhome.py | 7 ++++++ pyfritzhome/interfaces/interfacebase.py | 26 +++++++++++++++++--- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index 04adc28..5d431b0 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -15,12 +15,11 @@ class FritzhomeUnitBase(FritzhomeEntityBase): """The Fritzhome Device class.""" - interfaces = None - def __init__(self, fritz=None, node=None): """Create a unit object (REST-only).""" - super().__init__(fritz, node) self._updates = {} + self.interfaces = {} + super().__init__(fritz, node) def __repr__(self): """Return a string.""" @@ -37,19 +36,21 @@ def _update_from_node(self, node): if self.ain != node["ain"]: raise ValueError - # unshare class attribute on write - self.interfaces = {} for iface, node in node["interfaces"].items(): - self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) + if n := self.interfaces.get(iface): + self.interfaces[iface]._update_from_node(node) + else: + self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) def update_interface(self, interface, node=None): - import traceback if self.commit_now: - traceback.print_stack() self._fritz.put_unit(self.ain, {"interfaces": {interface.type: node or interface._node}}) else: self._updates |= {"interfaces": {interface.type: node or interface._node}} + def update(self): + self._fritz._update_unit(self.ain) + def begin(self): """DOC-TODO (REST)""" assert self.commit_now, "nested begin() not permitted" diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 280620d..073c23e 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -278,6 +278,13 @@ def _update_device_config(self, ain): device.add_or_update_unit(unit) return device + def _update_unit(self, ain): + """Update the unit, using its configuration endpoint.""" + _LOGGER.info(f"Updating Unit {ain} ...") + if node := self._rest_request(f"configuration/units/{ain}"): + return self._update_unit_from_node(node) + return None + def get_config(self, ain): """ DOC-TODO """ return self._update_device_config(ain) diff --git a/pyfritzhome/interfaces/interfacebase.py b/pyfritzhome/interfaces/interfacebase.py index 8a8daae..770f149 100644 --- a/pyfritzhome/interfaces/interfacebase.py +++ b/pyfritzhome/interfaces/interfacebase.py @@ -16,6 +16,8 @@ class FritzhomeInterfaceBase(): """The Fritzhome Interface class.""" + update_intervals = [ 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 10, 15 ] + @staticmethod def node_property(node_name, value_name): return property( @@ -37,13 +39,29 @@ def __repr__(self): return f"{self.type} of {self._unit_ref().ain}" def _update_from_node(self, node): - pass + self._node = node - def changed(self): + def changed(self, wait=False): + import sys assert self._node_set.keys() <= self._node.keys(), "_node_set must be a strict subset" self._unit_ref().update_interface(self, self._node_set) - self._node |= self._node_set - self._node_set = {} + if not wait: + self._node_set = {} + return + import time + off = 0 + for i in self.update_intervals: + self._unit_ref().update() + if self._node | self._node_set == self._node: + self._node_set = {} + return + time.sleep(i - off) + off = i + self._unit_ref().update() + if self._node | self._node_set == self._node: + self._node_set = {} + return + raise RuntimeError("Failed to fetch updated interface") @property def node(self): From 44a167d180dbbca856e85603f7fba3d95cd7c68b Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 18 Mar 2026 16:02:12 +0100 Subject: [PATCH 22/29] fixup! allow updating interface classes updating interface is now delegated to the unit --- pyfritzhome/devicetypes/fritzhomeunitbase.py | 55 +++++++++++++++----- pyfritzhome/fritzhome.py | 7 ++- pyfritzhome/interfaces/interfacebase.py | 23 +------- pyfritzhome/interfaces/onoffinterface.py | 6 +-- 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index 5d431b0..2fa2cbe 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -11,10 +11,23 @@ _LOGGER = logging.getLogger(__name__) +def dict_deepmerge(base, override): + """ Returns a new dict which is a recursive merge of two input dicts. """ + result = base.copy() + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = dict_deepmerge(result[key], value) + elif isinstance(value, dict): + result[key] = value.copy() + else: + result[key] = value + return result class FritzhomeUnitBase(FritzhomeEntityBase): """The Fritzhome Device class.""" + _update_intervals = [ 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 10, 15 ] + def __init__(self, fritz=None, node=None): """Create a unit object (REST-only).""" self._updates = {} @@ -42,14 +55,34 @@ def _update_from_node(self, node): else: self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) - def update_interface(self, interface, node=None): - if self.commit_now: - self._fritz.put_unit(self.ain, {"interfaces": {interface.type: node or interface._node}}) - else: - self._updates |= {"interfaces": {interface.type: node or interface._node}} - def update(self): - self._fritz._update_unit(self.ain) + def _update_unit(self, node, wait=False): + self._fritz.put_unit(self.ain, node) + if not wait: + return + import time + off = 0 + for i in self._update_intervals: + # this calls our _update_from_node eventually + self._fritz._update_unit(self.ain) + if dict_deepmerge(self._node, node) == self._node: + return + time.sleep(i - off) + off = i + self._unit_ref().update() + if self._node | node == self._node: + return + raise RuntimeError("Failed to fetch updated unit") + + def interface_changed(self, interface, node=None, wait=False): + import sys + + if not self.commit_now: + if wait: + LOGGER.err("wait=True not supported in bulk update") + self._updates |= {"interfaces": {interface.type: node or interface._node}} + return + self._update_unit({"interfaces": {interface.type: node or interface._node}}, wait) def begin(self): """DOC-TODO (REST)""" @@ -58,14 +91,12 @@ def begin(self): self.commit_now = False return self - def end(self): + def end(self, wait=False): """DOC-TODO (REST)""" print(f"Z {self}") self.commit_now = True - self.trigger_update() - - def trigger_update(self): - self._fritz.put_unit(self.ain, self._updates) + self._update_unit(self._updates, wait) + self._updates = {} def find_interface(self, interface): return self.interfaces.get(interface); diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 073c23e..edcba68 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -230,7 +230,6 @@ def put_unit(self, ain, node): if self._units is None: self._units = {} - _LOGGER.info("put units ...\n" + json.dumps(node)) params = {"Authorization": f"AVM-SID {self._sid}"} data = self._put(f"{self.rest_url}/configuration/units/{ain}", node, headers=params) @@ -448,7 +447,7 @@ def set_switch_state_on(self, ain, wait=False): result = self._aha_request("setswitchon", ain=ain, rf=bool) wait and self.wait_device_txbusy(ain) return result - return self._switch_action(ain, lambda dev: dev.set_switch_state_on()) + return self._switch_action(ain, lambda dev: dev.set_switch_state_on(wait)) def set_switch_state_off(self, ain, wait=False): """Set the switch to off state.""" @@ -456,7 +455,7 @@ def set_switch_state_off(self, ain, wait=False): result = self._aha_request("setswitchoff", ain=ain, rf=bool) wait and self.wait_device_txbusy(ain) return result - return self._switch_action(ain, lambda dev: dev.set_switch_state_off()) + return self._switch_action(ain, lambda dev: dev.set_switch_state_off(wait)) def set_switch_state_toggle(self, ain, wait=False): """Toggle the switch state.""" @@ -464,7 +463,7 @@ def set_switch_state_toggle(self, ain, wait=False): result = self._aha_request("setswitchtoggle", ain=ain, rf=bool) wait and self.wait_device_txbusy(ain) return result - return self._switch_action(ain, lambda dev: dev.set_switch_state_toggle()) + return self._switch_action(ain, lambda dev: dev.set_switch_state_toggle(wait)) def get_switch_power(self, ain): """Get the switch power consumption.""" diff --git a/pyfritzhome/interfaces/interfacebase.py b/pyfritzhome/interfaces/interfacebase.py index 770f149..b2429d1 100644 --- a/pyfritzhome/interfaces/interfacebase.py +++ b/pyfritzhome/interfaces/interfacebase.py @@ -16,8 +16,6 @@ class FritzhomeInterfaceBase(): """The Fritzhome Interface class.""" - update_intervals = [ 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 10, 15 ] - @staticmethod def node_property(node_name, value_name): return property( @@ -42,26 +40,9 @@ def _update_from_node(self, node): self._node = node def changed(self, wait=False): - import sys assert self._node_set.keys() <= self._node.keys(), "_node_set must be a strict subset" - self._unit_ref().update_interface(self, self._node_set) - if not wait: - self._node_set = {} - return - import time - off = 0 - for i in self.update_intervals: - self._unit_ref().update() - if self._node | self._node_set == self._node: - self._node_set = {} - return - time.sleep(i - off) - off = i - self._unit_ref().update() - if self._node | self._node_set == self._node: - self._node_set = {} - return - raise RuntimeError("Failed to fetch updated interface") + self._unit_ref().interface_changed(self, self._node_set, wait) + self._node_set = {} @property def node(self): diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index 0ab095c..596be7a 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -56,16 +56,16 @@ def switch_state(self): def set_switch_state_on(self, wait=False): """Set the switch state to on.""" - self.find_switch_interface().set_switch_state_on() + self.find_switch_interface().set_switch_state_on(wait) return self def set_switch_state_off(self, wait=False): """Set the switch state to off.""" - self.find_switch_interface().set_switch_state_off() + self.find_switch_interface().set_switch_state_off(wait) return self def set_switch_state_toggle(self, wait=False): """Toggle the switch state.""" - self.find_switch_interface().set_switch_state_toggle() + self.find_switch_interface().set_switch_state_toggle(wait) return self From a99c1f5f144b6a22f66f7214474589eb0e89b86e Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 20 Mar 2026 08:14:39 +0100 Subject: [PATCH 23/29] fixup! simplified interface mixins --- pyfritzhome/interfaces/multimeterinterface.py | 8 ++++---- pyfritzhome/interfaces/temperatureinterface.py | 8 ++++---- pyfritzhome/interfaces/thermostatinterface.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyfritzhome/interfaces/multimeterinterface.py b/pyfritzhome/interfaces/multimeterinterface.py index 92bcbb4..60a64ca 100644 --- a/pyfritzhome/interfaces/multimeterinterface.py +++ b/pyfritzhome/interfaces/multimeterinterface.py @@ -48,21 +48,21 @@ def has_powermeter(self): @property def power(self): """ Get the current powermeter power """ - self.find_multimeter_interface().current + return self.find_multimeter_interface().power @property def energy(self): """ Get the current currentmeter energy """ - self.find_multimeter_interface().energy + return self.find_multimeter_interface().energy @property def voltage(self): """ Get the current voltagemeter voltage """ - self.find_multimeter_interface().voltage + return self.find_multimeter_interface().voltage @property def current(self): """ Get the current currentmeter current """ - self.find_multimeter_interface().current + return self.find_multimeter_interface().current diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py index 52fac47..aa8b3c0 100644 --- a/pyfritzhome/interfaces/temperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -45,14 +45,14 @@ def has_temperature_sensor(self): @property def temperature(self): """ Get the current temperature """ - self.find_temperature_interface().celsius + return self.find_temperature_interface().celsius @property def actual_temperature(self): """ Get the current temperature (legacy) """ - self.find_temperature_interface().celsius + return self.temperature @property def offset(self): - """ Get the current temperature offset """ - self.find_temperature_interface().offset + """ Get the current temperature offset (maybe None!) """ + return self.find_temperature_interface().offset diff --git a/pyfritzhome/interfaces/thermostatinterface.py b/pyfritzhome/interfaces/thermostatinterface.py index 4020f6d..d81de4f 100644 --- a/pyfritzhome/interfaces/thermostatinterface.py +++ b/pyfritzhome/interfaces/thermostatinterface.py @@ -84,7 +84,7 @@ def has_thermostat(self): @property def target_temperature(self): """ Get the current thermostat """ - self.find_thermostat_interface().target_temperature + return self.find_thermostat_interface().target_temperature def set_target_temperature(self, v): """ Get the current thermostat """ @@ -94,7 +94,7 @@ def set_target_temperature(self, v): @property def reduced_temperature(self): """ Get the current thermostat offset """ - self.find_thermostat_interface().reduced_temperature + return self.find_thermostat_interface().reduced_temperature def set_reduced_temperature(self, v): """ Get the current thermostat """ @@ -103,7 +103,7 @@ def set_reduced_temperature(self, v): @property def comfort_temperature(self): - self.find_thermostat_interface().comfort_temperature + return self.find_thermostat_interface().comfort_temperature def set_comfort_temperature(self, v): """ Get the current thermostat """ From 8c7c0ae6105df35233eaa974e732576a4bccc720 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 20 Mar 2026 08:17:41 +0100 Subject: [PATCH 24/29] HASS stubs are now None, except battery-related properties are actually implemented --- .../devicetypes/fritzhomedevicebase.py | 52 +++++++++++-------- .../devicetypes/fritzhomeentitybase.py | 4 +- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 5f3acd7..d8c894c 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -14,10 +14,13 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): """The Fritzhome Device class.""" - manufacturer = None - product_name = None - fw_version = None - is_connected = None + manufacturer = None + product_name = None + fw_version = None + is_connected = None + has_battery = None + battery_low = None + battery_level = None def __repr__(self): """Return a string.""" @@ -37,12 +40,23 @@ def _update_from_node(self, node): self.product_name = node.attrib["productname"] self.fw_version = node.attrib["fwversion"] self.is_connected = self.get_node_value_as_int_as_bool(node, "present") + + self._battery = self.get_node_value_as_int(node, "battery") + self.has_battery = self._battery is not None or self.battery_low is not None + if self.has_battery and self.is_connected: + self.battery_low = self.get_node_value_as_int(node, "batterylow") + self.battery_level = self._battery or 0 else: self.manufacturer = self._node["manufacturer"] self.product_name = self._node["productName"] self.fw_version = self._node["firmwareVersion"] self.is_connected = self._node["isConnected"] + self.has_battery = self._node["isBatteryPowered"] + if self.has_battery and self.is_connected: + self.battery_low = self._node["isBatteryLow"] + self.battery_level = self._node["batteryValue"] + def update_unit(self, unit, node): """DOC-TODO (REST)""" assert unit.ain in self._units.keys(), "unknown unit to update" @@ -107,47 +121,39 @@ def units(self): @property def has_color(self): - return False + return None @property def has_blind(self): - return False + return None @property def has_alarm(self): - return False + return None @property def has_lightbulb(self): - return False + return None @property def holiday_active(self): - return False + return None @property def summer_active(self): - return False + return None @property def lock(self): - return False + return None @property def device_lock(self): - return False - - @property - def battery_level(self): - return False - - @property - def battery_low(self): - return False + return None @property def window_open(self): - return False + return None @property def nextchange_temperature(self): - return False + return None @property def nextchange_endperiod(self): - return False + return None diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index f318ccc..ee49250 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -76,7 +76,9 @@ def get_node_value(self, elem, node): def get_node_value_as_int(self, elem, node) -> int: """Get the node value as integer.""" - return int(self.get_node_value(elem, node)) + if value := self.get_node_value(elem, node): + return int(value) + return None def get_node_value_as_int_as_bool(self, elem, node) -> bool: """Get the node value as boolean.""" From b25928be341bd51e8aeadceb3cbdbe374f77aee4 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 20 Mar 2026 08:18:15 +0100 Subject: [PATCH 25/29] Fritzhome.get_config() implemented for AHA, needed by HASS --- pyfritzhome/fritzhome.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index edcba68..3c4baf2 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -286,7 +286,10 @@ def _update_unit(self, ain): def get_config(self, ain): """ DOC-TODO """ - return self._update_device_config(ain) + if self._use_aha: + return self._devices[ain] + else: + return self._update_device_config(ain) def update_devices(self, ignore_removed=True): """Update the device.""" From 68b1f782bdad9580873efb6274126285edb6b907 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 20 Mar 2026 08:18:58 +0100 Subject: [PATCH 26/29] onoff: don't pretend 0.0 offset --- pyfritzhome/interfaces/temperatureinterface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py index aa8b3c0..4714989 100644 --- a/pyfritzhome/interfaces/temperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -26,8 +26,8 @@ def _update_from_node(self, node): _LOGGER.warning("interface state not valid") else: self.celsius = self._node["celsius"] - # offset is not always exposed (for Thermo 302 the offset is in the thermostatInterface) - self.offset = self._node.get("offset") or 0.0 + # offset is not always exposed (only through /smarthome/configuration/… endpoints) + self.offset = self._node.get("offset") From d4c0476a05410f3d0e5610e3f2a03ad48fe0c68f Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 20 Mar 2026 08:22:08 +0100 Subject: [PATCH 27/29] fix debug output --- pyfritzhome/interfaces/humidityinterface.py | 2 +- pyfritzhome/interfaces/multimeterinterface.py | 2 +- pyfritzhome/interfaces/onoffinterface.py | 2 +- pyfritzhome/interfaces/temperatureinterface.py | 2 +- pyfritzhome/interfaces/thermostatinterface.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyfritzhome/interfaces/humidityinterface.py b/pyfritzhome/interfaces/humidityinterface.py index 8ca8361..59e25d1 100644 --- a/pyfritzhome/interfaces/humidityinterface.py +++ b/pyfritzhome/interfaces/humidityinterface.py @@ -18,8 +18,8 @@ def is_humidity(self): return self.type == "humidityInterface" def _update_from_node(self, node): - _LOGGER.debug("update switch device") super()._update_from_node(node) + _LOGGER.debug(f"update {self.type}") if self.is_humidity: if self._node["state"] != "valid": _LOGGER.warning("interface state not valid") diff --git a/pyfritzhome/interfaces/multimeterinterface.py b/pyfritzhome/interfaces/multimeterinterface.py index 60a64ca..1f74eda 100644 --- a/pyfritzhome/interfaces/multimeterinterface.py +++ b/pyfritzhome/interfaces/multimeterinterface.py @@ -21,8 +21,8 @@ def is_powermeter(self): return self.type == "multimeterInterface" def _update_from_node(self, node): - _LOGGER.debug("update switch device") super()._update_from_node(node) + _LOGGER.debug(f"update {self.type}") if self.is_powermeter: if self._node["state"] != "valid": _LOGGER.warning("interface state not valid") diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index 596be7a..599a852 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -19,8 +19,8 @@ def is_switch(self): return self.type == "onOffInterface" def _update_from_node(self, node): - _LOGGER.debug("update switch device") super()._update_from_node(node) + _LOGGER.debug(f"update {self.type}") if self.is_switch: if self._node["state"] != "valid": _LOGGER.warning("interface state not valid") diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py index 4714989..0036523 100644 --- a/pyfritzhome/interfaces/temperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -19,8 +19,8 @@ def is_temperature(self): return self.type == "temperatureInterface" def _update_from_node(self, node): - _LOGGER.debug("update switch device") super()._update_from_node(node) + _LOGGER.debug(f"update {self.type}") if self.is_temperature: if self._node["state"] != "valid": _LOGGER.warning("interface state not valid") diff --git a/pyfritzhome/interfaces/thermostatinterface.py b/pyfritzhome/interfaces/thermostatinterface.py index d81de4f..d0dba94 100644 --- a/pyfritzhome/interfaces/thermostatinterface.py +++ b/pyfritzhome/interfaces/thermostatinterface.py @@ -23,8 +23,8 @@ def is_thermostat(self): return self.type == "thermostatInterface" def _update_from_node(self, node): - _LOGGER.debug("update switch device") super()._update_from_node(node) + _LOGGER.debug(f"update {self.type}") @property def target_temperature(self): From 00f33cb457848c9ee7ab239e98edd7e8adbb7c76 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Mon, 23 Mar 2026 15:16:43 +0100 Subject: [PATCH 28/29] thermostat: fix lots of missing gaps --- pyfritzhome/cli.py | 30 ++-- .../devicetypes/fritzhomedevicebase.py | 10 -- pyfritzhome/interfaces/thermostatinterface.py | 152 +++++++++++++++--- 3 files changed, 145 insertions(+), 47 deletions(-) diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 512f465..16adcc3 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -46,21 +46,21 @@ def list_all(fritz, args): if device.has_humidity_sensor: print(" Humidity:") print(" relative_humidity=%s" % device.rel_humidity) - #~ if device.has_thermostat: - #~ print(" Thermostat:") - #~ print(" battery_low=%s" % device.battery_low) - #~ print(" battery_level=%s" % device.battery_level) - #~ print(" actual=%s" % device.actual_temperature) - #~ print(" target=%s" % device.target_temperature) - #~ print(" comfort=%s" % device.comfort_temperature) - #~ print(" eco=%s" % device.eco_temperature) - #~ print(" window=%s" % device.window_open) - #~ print(" window_until=%s" % device.window_open_endtime) - #~ print(" boost=%s" % device.boost_active) - #~ print(" boost_until=%s" % device.boost_active_endtime) - #~ print(" adaptive_heating_running=%s" % device.adaptive_heating_running) - #~ print(" summer=%s" % device.summer_active) - #~ print(" holiday=%s" % device.holiday_active) + if device.has_thermostat: + print(" Thermostat:") + print(" battery_low=%s" % device.battery_low) + print(" battery_level=%s" % device.battery_level) + print(" actual=%s" % device.actual_temperature) + print(" target=%s" % device.target_temperature) + print(" comfort=%s" % device.comfort_temperature) + print(" eco=%s" % device.eco_temperature) + print(" window=%s" % device.window_open) + print(" window_until=%s" % device.window_open_endtime) + print(" boost=%s" % device.boost_active) + print(" boost_until=%s" % device.boost_active_endtime) + print(" adaptive_heating_running=%s" % device.adaptive_heating_running) + print(" summer=%s" % device.summer_active) + print(" holiday=%s" % device.holiday_active) #~ if device.has_alarm: #~ print(" Alert:") #~ print(" alert=%s" % device.alert_state) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index d8c894c..294c1e4 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -135,22 +135,12 @@ def has_alarm(self): def has_lightbulb(self): return None @property - def holiday_active(self): - return None - @property - def summer_active(self): - return None - @property def lock(self): return None @property def device_lock(self): return None - @property - def window_open(self): - return None - @property def nextchange_temperature(self): return None diff --git a/pyfritzhome/interfaces/thermostatinterface.py b/pyfritzhome/interfaces/thermostatinterface.py index d0dba94..28b01fe 100644 --- a/pyfritzhome/interfaces/thermostatinterface.py +++ b/pyfritzhome/interfaces/thermostatinterface.py @@ -28,47 +28,99 @@ def _update_from_node(self, node): @property def target_temperature(self): - return self._node["setPointTemperature"]["celsius"] + if self._node["setPointTemperature"]["mode"] == "temperature": + return self._node["setPointTemperature"]["celsius"] + return None - def set_target_temperature(self, v): + @property + def thermostat_mode(self): + return self._node["setPointTemperature"]["mode"] + + def set_thermostat_mode(self, v, wait=False): + self._node_set["setPointTemperature"] = {} + self._node_set["setPointTemperature"]["mode"] = v + self.changed(wait) + + def set_target_temperature(self, v, wait=False): self._node_set["setPointTemperature"] = {} self._node_set["setPointTemperature"]["celsius"] = v self._node_set["setPointTemperature"]["mode"] = "temperature" - self.changed() + self.changed(wait) @property def comfort_temperature(self): return self._node["comfortTemperature"]["celsius"] - def set_comfort_temperature(self, v): + def set_comfort_temperature(self, v, wait=False): self._node_set["comfortTemperature"]= {} self._node_set["comfortTemperature"]["celsius"] = v self._node_set["comfortTemperature"]["mode"] = "temperature" - self.changed() + self.changed(wait) @property def reduced_temperature(self): return self._node["reducedTemperature"]["celsius"] - def set_reduced_temperature(self, v): + def set_reduced_temperature(self, v, wait=False): self._node_set["reducedTemperature"]= {} self._node_set["reducedTemperature"]["celsius"] = v self._node_set["reducedTemperature"]["mode"] = "temperature" - self.changed() + self.changed(wait) + + @property + def holiday_active(self): + return self._node["isHolidayActive"] + + @property + def adaptive_heating_active(self): + return self._node["isAdaptiveActive"] + + @property + def adaptive_heating_running(self): + # not always exposed (only through /smarthome/configuration/… endpoints) + return self._node.get("adaptiveHeatingModeEnabled") + + @property + def summer_active(self): + return self._node["isSummertimeActive"] + + @property + def window_open(self): + return self._node["windowOpenMode"]["enabled"] + + @property + def window_open_endtime(self): + return self._node["windowOpenMode"]["endTime"] + + @property + def boost_active(self): + return self._node["boost"]["enabled"] + + @property + def boost_active_endtime(self): + return self._node["boost"]["endTime"] - def set_boost_mode(self, duration_secs): + def set_boost_mode(self, duration_secs, wait=False): import time self._node_set["boost"] = {} - self._node_set["boost"]["enabled"] = True - self._node_set["boost"]["endTime"] = int(time.time() + duration_secs) - self.changed() - - def set_window_open(self, duration_secs): + if duration_secs > 0: + self._node_set["boost"]["enabled"] = True + self._node_set["boost"]["endTime"] = int(time.time() + duration_secs) + else: + self._node_set["boost"]["enabled"] = False + self._node_set["boost"]["endTime"] = 0 + self.changed(wait) + + def set_window_open(self, duration_secs, wait=False): import time self._node_set["windowOpenMode"] = {} - self._node_set["windowOpenMode"]["enabled"] = True - self._node_set["windowOpenMode"]["endTime"] = int(time.time() + duration_secs) - self.changed() + if duration_secs > 0: + self._node_set["windowOpenMode"]["enabled"] = True + self._node_set["windowOpenMode"]["endTime"] = int(time.time() + duration_secs) + else: + self._node_set["windowOpenMode"]["enabled"] = False + self._node_set["windowOpenMode"]["endTime"] = 0 + self.changed(wait) class FritzhomeThermostatMixin(): """The Fritzhome Thermostat mixin.""" @@ -86,9 +138,9 @@ def target_temperature(self): """ Get the current thermostat """ return self.find_thermostat_interface().target_temperature - def set_target_temperature(self, v): + def set_target_temperature(self, v, wait=False): """ Get the current thermostat """ - self.find_thermostat_interface().set_target_temperature(v) + self.find_thermostat_interface().set_target_temperature(v, wait) return self @property @@ -96,24 +148,56 @@ def reduced_temperature(self): """ Get the current thermostat offset """ return self.find_thermostat_interface().reduced_temperature - def set_reduced_temperature(self, v): + def set_reduced_temperature(self, v, wait=False): """ Get the current thermostat """ - self.find_thermostat_interface().set_reduced_temperature(v) + self.find_thermostat_interface().set_reduced_temperature(v, wait) return self @property def comfort_temperature(self): return self.find_thermostat_interface().comfort_temperature - def set_comfort_temperature(self, v): + def set_comfort_temperature(self, v, wait=False): """ Get the current thermostat """ - self.find_thermostat_interface().set_comfort_temperature(v) + self.find_thermostat_interface().set_comfort_temperature(v, wait) return self @property def eco_temperature(self): return self.reduced_temperature + @property + def adaptive_heating_active(self): + return self.find_thermostat_interface().adaptive_heating_active + + @property + def adaptive_heating_running(self): + return self.find_thermostat_interface().adaptive_heating_running + + @property + def holiday_active(self): + return self.find_thermostat_interface().holiday_active + + @property + def summer_active(self): + return self.find_thermostat_interface().summer_active + + @property + def window_open(self): + return self.find_thermostat_interface().window_open + + @property + def window_open_endtime(self): + return self.find_thermostat_interface().window_open_endtime + + @property + def boost_active(self): + return self.find_thermostat_interface().boost_active + + @property + def boost_active_endtime(self): + return self.find_thermostat_interface().boost_active_endtime + def set_boost_mode(self, seconds, wait=False): """Set the thermostate into boost mode.""" self.find_thermostat_interface().set_boost_mode(seconds) @@ -123,3 +207,27 @@ def set_window_open(self, seconds, wait=False): """Set the thermostate in open window mode.""" self.find_thermostat_interface().set_window_open(seconds) return self + + def get_hkr_state(self): + """Get the thermostate state.""" + mode = self.find_thermostat_interface().thermostat_mode + if mode in ["on", "off"]: + return mode + elif self.target_temperature == self.eco_temperature: + return "eco" + elif self.target_temperature == self.comfort_temperature: + return "comfort" + return "manual" + + def set_hkr_state(self, state, wait=False): + """Set the state of the thermostat. + + Possible values for state are: 'on', 'off', 'comfort', 'eco'. + """ + if state in ["on", "off"]: + self.find_thermostat_interface().set_thermostat_mode(state, wait) + elif state == "eco": + self.find_thermostat_interface().set_target_temperature(self.eco_temperature, wait) + elif state == "comfort": + self.find_thermostat_interface().set_target_temperature(self.comfort_temperature, wait) + return self From d2bc7fb74fe0d0f4c98b5d5673fa6ae792c47b45 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Tue, 24 Mar 2026 07:40:33 +0100 Subject: [PATCH 29/29] streamlined interface, get rid of all-in-one interface --- pyfritzhome/devicetypes/fritzhomeunitbase.py | 2 +- pyfritzhome/interfaces/humidityinterface.py | 19 ++--------- pyfritzhome/interfaces/interface.py | 33 ++++++++++--------- pyfritzhome/interfaces/interfacebase.py | 13 ++------ pyfritzhome/interfaces/multimeterinterface.py | 33 ++++++++----------- pyfritzhome/interfaces/onoffinterface.py | 18 ++-------- .../interfaces/temperatureinterface.py | 25 ++++---------- pyfritzhome/interfaces/thermostatinterface.py | 19 ++--------- 8 files changed, 51 insertions(+), 111 deletions(-) diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py index 2fa2cbe..8a7cb7b 100644 --- a/pyfritzhome/devicetypes/fritzhomeunitbase.py +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -53,7 +53,7 @@ def _update_from_node(self, node): if n := self.interfaces.get(iface): self.interfaces[iface]._update_from_node(node) else: - self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) + self.interfaces[iface] = interfaces.FritzhomeInterface.create(self, iface, node) def _update_unit(self, node, wait=False): diff --git a/pyfritzhome/interfaces/humidityinterface.py b/pyfritzhome/interfaces/humidityinterface.py index 59e25d1..5cd544d 100644 --- a/pyfritzhome/interfaces/humidityinterface.py +++ b/pyfritzhome/interfaces/humidityinterface.py @@ -9,24 +9,11 @@ class FritzhomeHumidityInterface(FritzhomeInterfaceBase): - """The Fritzhome Device class.""" - - rel_humidity = None + """The Fritzhome Humidity interface class.""" @property - def is_humidity(self): - return self.type == "humidityInterface" - - def _update_from_node(self, node): - super()._update_from_node(node) - _LOGGER.debug(f"update {self.type}") - if self.is_humidity: - if self._node["state"] != "valid": - _LOGGER.warning("interface state not valid") - else: - self.rel_humidity = self._node["relativeHumidity"] - - + def rel_humidity(self): + return self._node["relativeHumidity"] class FritzhomeHumidityMixin(): """The Fritzhome Humidity mixin.""" diff --git a/pyfritzhome/interfaces/interface.py b/pyfritzhome/interfaces/interface.py index d4c9668..b64d048 100644 --- a/pyfritzhome/interfaces/interface.py +++ b/pyfritzhome/interfaces/interface.py @@ -18,18 +18,21 @@ _LOGGER = logging.getLogger(__name__) -class FritzhomeInterface(FritzhomeOnOffInterface, - FritzhomeMultimeterInterface, - FritzhomeTemperatureInterface, - FritzhomeHumidityInterface, - FritzhomeThermostatInterface): - """The Fritzhome Interface class.""" - - def __init__(self, unit, type, node = None): - """Create an entity base object.""" - super().__init__(unit, type, node) - - # interfaces are not entities, only their parent units are, therefore this is - # called with the unit REST node - def _update_from_node(self, node): - super()._update_from_node(node) +class FritzhomeInterface(): + """The Fritzhome Interface factory.""" + + @staticmethod + def create(unit, type, node = None): + """Create a specific interface object.""" + cls = { + "onOffInterface": FritzhomeOnOffInterface, + "multimeterInterface": FritzhomeMultimeterInterface, + "temperatureInterface": FritzhomeTemperatureInterface, + "humidityInterface": FritzhomeHumidityInterface, + "thermostatInterface" : FritzhomeThermostatInterface, + } + try: + return cls[type](unit, type, node) + except KeyError: + return FritzhomeInterfaceBase(unit, type, node) + diff --git a/pyfritzhome/interfaces/interfacebase.py b/pyfritzhome/interfaces/interfacebase.py index b2429d1..bdd44bb 100644 --- a/pyfritzhome/interfaces/interfacebase.py +++ b/pyfritzhome/interfaces/interfacebase.py @@ -16,21 +16,12 @@ class FritzhomeInterfaceBase(): """The Fritzhome Interface class.""" - @staticmethod - def node_property(node_name, value_name): - return property( - fget = lambda self: self._node[node_name].get(value_name), - fset = lambda self, v: self._node[node_name].update(value_name, v) - ) - def __init__(self, unit, type, node): """Create an entity base object.""" self.type = type self._node = node self._node_set = {} self._unit_ref = weakref.ref(unit) - if node is not None: - self._update_from_node(node) def __repr__(self): """Return a string.""" @@ -38,6 +29,9 @@ def __repr__(self): def _update_from_node(self, node): self._node = node + _LOGGER.debug(f"update {self.type}") + if self._node["state"] != "valid": + _LOGGER.warning(f"{self.type} state not valid") def changed(self, wait=False): assert self._node_set.keys() <= self._node.keys(), "_node_set must be a strict subset" @@ -47,4 +41,3 @@ def changed(self, wait=False): @property def node(self): return self._node; - diff --git a/pyfritzhome/interfaces/multimeterinterface.py b/pyfritzhome/interfaces/multimeterinterface.py index 1f74eda..09dae32 100644 --- a/pyfritzhome/interfaces/multimeterinterface.py +++ b/pyfritzhome/interfaces/multimeterinterface.py @@ -9,28 +9,23 @@ class FritzhomeMultimeterInterface(FritzhomeInterfaceBase): - """The Fritzhome Device class.""" + """The Fritzhome Multimeter interface class.""" - power = None - energy = None - voltage = None - current = None + @property + def power(self): + return self._node["power"] + + @property + def energy(self): + return self._node["energy"] @property - def is_powermeter(self): - return self.type == "multimeterInterface" + def voltage(self): + return self._node["voltage"] - def _update_from_node(self, node): - super()._update_from_node(node) - _LOGGER.debug(f"update {self.type}") - if self.is_powermeter: - if self._node["state"] != "valid": - _LOGGER.warning("interface state not valid") - else: - self.power = self._node["power"] - self.energy = self._node["energy"] - self.voltage = self._node["voltage"] - self.current = self._node["current"] + @property + def current(self): + return self._node["current"] @@ -43,7 +38,7 @@ def find_multimeter_interface(self): @property def has_powermeter(self): """Check if the device has powermeter sensors.""" - return self.find_multimeter_interface() != None + return self.find_multimeter_interface() is not None @property def power(self): diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py index 599a852..9592e96 100644 --- a/pyfritzhome/interfaces/onoffinterface.py +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -9,23 +9,11 @@ class FritzhomeOnOffInterface(FritzhomeInterfaceBase): - """The Fritzhome OnOff interface class.""" + """The Fritzhome OnOff (switch) interface class.""" - switch_state = None - - # Switch @property - def is_switch(self): - return self.type == "onOffInterface" - - def _update_from_node(self, node): - super()._update_from_node(node) - _LOGGER.debug(f"update {self.type}") - if self.is_switch: - if self._node["state"] != "valid": - _LOGGER.warning("interface state not valid") - else: - self.switch_state = self._node["active"] + def switch_state(self): + return self._node["active"] def set_switch_state_on(self, wait=False): self._node_set["active"] = True diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py index 0036523..5a588ee 100644 --- a/pyfritzhome/interfaces/temperatureinterface.py +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -9,27 +9,16 @@ class FritzhomeTemperatureInterface(FritzhomeInterfaceBase): - """The Fritzhome Device class.""" - - celsius = None - offset = None + """The Fritzhome Temperature interface class.""" @property - def is_temperature(self): - return self.type == "temperatureInterface" - - def _update_from_node(self, node): - super()._update_from_node(node) - _LOGGER.debug(f"update {self.type}") - if self.is_temperature: - if self._node["state"] != "valid": - _LOGGER.warning("interface state not valid") - else: - self.celsius = self._node["celsius"] - # offset is not always exposed (only through /smarthome/configuration/… endpoints) - self.offset = self._node.get("offset") - + def celsius(self): + return self._node["celsius"] + @property + def offset(self): + # offset is not always exposed (only through /smarthome/configuration/… endpoints) + return self._node.get("offset") class FritzhomeTemperatureMixin(): """The Fritzhome Temperature mixin.""" diff --git a/pyfritzhome/interfaces/thermostatinterface.py b/pyfritzhome/interfaces/thermostatinterface.py index 28b01fe..7497a59 100644 --- a/pyfritzhome/interfaces/thermostatinterface.py +++ b/pyfritzhome/interfaces/thermostatinterface.py @@ -9,22 +9,7 @@ class FritzhomeThermostatInterface(FritzhomeInterfaceBase): - """The Fritzhome Device class.""" - - def __init__(self, unit, type, node): - super().__init__(unit, type, node) - if type == "thermostatInterface": - FritzhomeInterfaceBase.node_property("setPointTemperature", "celsius") - FritzhomeInterfaceBase.node_property("comfortTemperature", "celsius") - FritzhomeInterfaceBase.node_property("reducedTemperature", "celsius") - - @property - def is_thermostat(self): - return self.type == "thermostatInterface" - - def _update_from_node(self, node): - super()._update_from_node(node) - _LOGGER.debug(f"update {self.type}") + """The Fritzhome Thermostat interface class.""" @property def target_temperature(self): @@ -131,7 +116,7 @@ def find_thermostat_interface(self): @property def has_thermostat(self): """Check if the device has thermostat sensors.""" - return self.find_thermostat_interface() != None + return self.find_thermostat_interface() is not None @property def target_temperature(self):