From 80e96db5de46d1389a905dfd08ab40de212b900b Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sat, 7 Feb 2026 00:00:57 +0100 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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