From 56cb1ce0159fc0512196910999498f51067d7024 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sat, 3 Oct 2026 17:21:29 +0000 Subject: [PATCH 1/3] make the lib fully strict typed Co-authored-by: Copilot --- .pre-commit-config.yaml | 7 + pyfritzhome/cli.py | 64 ++--- .../devicetypes/fritzhomedevicealarm.py | 10 +- .../devicetypes/fritzhomedevicebase.py | 39 +-- .../devicetypes/fritzhomedeviceblind.py | 16 +- .../devicetypes/fritzhomedevicebutton.py | 36 ++- .../devicetypes/fritzhomedevicehumidity.py | 10 +- .../devicetypes/fritzhomedevicelevel.py | 22 +- .../devicetypes/fritzhomedevicelightbulb.py | 61 +++-- .../devicetypes/fritzhomedevicepowermeter.py | 34 ++- .../devicetypes/fritzhomedevicerepeater.py | 5 +- .../devicetypes/fritzhomedeviceswitch.py | 23 +- .../devicetypes/fritzhomedevicetemperature.py | 12 +- .../devicetypes/fritzhomedevicethermostat.py | 67 ++--- .../devicetypes/fritzhomeentitybase.py | 56 ++-- pyfritzhome/devicetypes/fritzhometemplate.py | 35 ++- pyfritzhome/devicetypes/fritzhometrigger.py | 7 +- pyfritzhome/errors.py | 12 +- pyfritzhome/fritzhome.py | 250 ++++++++++++------ pyfritzhome/fritzhomedevice.py | 14 +- pyfritzhome/py.typed | 0 requirements_dev.txt | 2 + setup.cfg | 7 + tests/test_fritzhome.py | 11 + 24 files changed, 514 insertions(+), 286 deletions(-) create mode 100644 pyfritzhome/py.typed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 610034f..6ead4bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,3 +17,10 @@ repos: rev: v2.2.1 hooks: - id: prettier + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.19.1 + hooks: + - id: mypy + additional_dependencies: + - types-requests + files: ^pyfritzhome/.+\.py$ diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index e98af80..6c8eaca 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -5,13 +5,14 @@ from __future__ import print_function import logging import argparse +from typing import Optional, Sequence from pyfritzhome import Fritzhome, __version__ _LOGGER = logging.getLogger(__name__) -def list_all(fritz, args): +def list_all(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that prints all device information.""" devices = fritz.get_devices() @@ -77,73 +78,75 @@ def list_all(fritz, args): print(" endpositionset=%s" % device.endpositionsset) -def device_name(fritz, args): +def device_name(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that prints the device name.""" print(fritz.get_device_name(args.ain)) -def device_presence(fritz, args): +def device_presence(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that prints the device presence.""" print(int(fritz.get_device_present(args.ain))) -def device_statistics(fritz, args): +def device_statistics(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that prints the device statistics.""" stats = fritz.get_device_statistics(args.ain) print(stats) -def blind_set_open(fritz, args): +def blind_set_open(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command to open the blinds.""" fritz.set_blind_open(args.ain) -def blind_set_close(fritz, args): +def blind_set_close(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command close the blinds.""" fritz.set_blind_close(args.ain) -def blind_set_level_percentage(fritz, args): +def blind_set_level_percentage(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that sets the blind level as percentage.""" fritz.set_level_percentage(args.ain, args.level) -def thermostat_set_target_temperature(fritz, args): +def thermostat_set_target_temperature( + fritz: Fritzhome, args: argparse.Namespace +) -> None: """Command that sets the thermostat temperature.""" fritz.set_target_temperature(args.ain, args.temperature) -def thermostat_set_window_open(fritz, args): +def thermostat_set_window_open(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that sets the thermostats window state.""" fritz.set_window_open(args.ain, args.timespan) -def thermostat_set_boost_mode(fritz, args): +def thermostat_set_boost_mode(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that sets the thermostats into boost mode.""" fritz.set_boost_mode(args.ain, args.timespan) -def switch_get(fritz, args): +def switch_get(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that get the device switch state.""" print(fritz.get_switch_state(args.ain)) -def switch_on(fritz, args): +def switch_on(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that set the device switch state to on.""" fritz.set_switch_state_on(args.ain) -def switch_off(fritz, args): +def switch_off(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that set the device switch state to off.""" fritz.set_switch_state_off(args.ain) -def switch_toggle(fritz, args): +def switch_toggle(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that toggles the device switch state.""" fritz.set_switch_state_toggle(args.ain) -def list_templates(fritz, args): +def list_templates(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that prints all template information.""" templates = fritz.get_templates() devices = fritz.get_devices_as_dict() @@ -165,16 +168,17 @@ def list_templates(fritz, args): print(" dialhelper=%s" % template.apply_dialhelper) print(" Devices:") - for device_id in template.devices: - print(" %s=%s" % (device_id, devices[device_id].name)) + if template.devices is not None: + for device_id in template.devices: + print(" %s=%s" % (device_id, devices[device_id].name)) -def template_apply(fritz, args): +def template_apply(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that applies a template.""" fritz.apply_template(args.ain) -def list_triggers(fritz, args): +def list_triggers(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that prints all trigger information.""" triggers = fritz.get_triggers() @@ -185,17 +189,17 @@ def list_triggers(fritz, args): print(" active=%s" % trigger.active) -def trigger_set_active(fritz, args): +def trigger_set_active(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that enables a trigger.""" fritz.set_trigger_active(args.ain) -def trigger_set_inactive(fritz, args): +def trigger_set_inactive(fritz: Fritzhome, args: argparse.Namespace) -> None: """Command that disables a trigger.""" fritz.set_trigger_inactive(args.ain) -def main(args=None): +def main(args: Optional[Sequence[str]] = None) -> None: """Enter the main function of the CLI tool.""" parser = argparse.ArgumentParser(description="Fritz!Box Smarthome CLI tool.") parser.add_argument( @@ -384,23 +388,23 @@ def main(args=None): subparser.add_argument("ain", type=str, metavar="AIN", help="Actor Identification") subparser.set_defaults(func=trigger_set_inactive) - args = parser.parse_args(args) + parsed_args = parser.parse_args(args) logging.basicConfig() - if args.verbose: + if parsed_args.verbose: logging.getLogger("pyfritzhome").setLevel(logging.DEBUG) fritzbox = None try: fritzbox = Fritzhome( - host=args.host, - user=args.user, - password=args.password, - port=args.port or None, - ssl_verify=not args.insecure, + host=parsed_args.host, + user=parsed_args.user, + password=parsed_args.password, + port=parsed_args.port or None, + ssl_verify=not parsed_args.insecure, ) fritzbox.login() - args.func(fritzbox, args) + parsed_args.func(fritzbox, parsed_args) finally: if fritzbox is not None: fritzbox.logout() diff --git a/pyfritzhome/devicetypes/fritzhomedevicealarm.py b/pyfritzhome/devicetypes/fritzhomedevicealarm.py index c84a685..a7e8fec 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicealarm.py +++ b/pyfritzhome/devicetypes/fritzhomedevicealarm.py @@ -1,6 +1,8 @@ """The alarm device class.""" import logging +from typing import Optional +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,9 +13,9 @@ class FritzhomeDeviceAlarm(FritzhomeDeviceBase): """The Fritzhome Device class.""" - alert_state = None + alert_state: Optional[bool] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -23,11 +25,11 @@ def _update_from_node(self, node): # Alarm @property - def has_alarm(self): + def has_alarm(self) -> bool: """Check if the device has alarm function.""" return self._has_feature(FritzhomeDeviceFeatures.ALARM) - def _update_alarm_from_node(self, node): + def _update_alarm_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update alert device") val = node.find("alert") try: diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 2eadcee..134aed0 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -4,6 +4,8 @@ import logging +from typing import Optional +from xml.etree import ElementTree from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase @@ -13,18 +15,18 @@ 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 __repr__(self): + battery_level: Optional[int] = None + battery_low: Optional[bool] = None + identifier: Optional[str] = None + is_group: Optional[bool] = None + fw_version: Optional[str] = None + group_members: Optional[list[str]] = None + manufacturer: Optional[str] = None + productname: Optional[str] = None + present: Optional[bool] = None + tx_busy: Optional[bool] = None + + def __repr__(self) -> str: """Return a string.""" return "{ain} {identifier} {manuf} {prod} {name}".format( ain=self.ain, @@ -34,11 +36,11 @@ def __repr__(self): name=self.name, ) - def update(self): + def update(self) -> None: """Update the device values.""" self._fritz.update_devices() - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update base device") super()._update_from_node(node) self.ain = node.attrib["identifier"] @@ -47,11 +49,14 @@ def _update_from_node(self, node): self.manufacturer = node.attrib["manufacturer"] self.productname = node.attrib["productname"] - self.present = bool(int(node.findtext("present"))) + present_value = node.findtext("present") + if present_value is None: + raise ValueError("device node is missing present state") + self.present = bool(int(present_value)) groupinfo = node.find("groupinfo") self.is_group = groupinfo is not None - if self.is_group: + if groupinfo is not None: self.group_members = str(groupinfo.findtext("members")).split(",") try: @@ -66,6 +71,6 @@ def _update_from_node(self, node): pass # General - def get_present(self): + def get_present(self) -> bool: """Check if the device is present.""" return self._fritz.get_device_present(self.ain) diff --git a/pyfritzhome/devicetypes/fritzhomedeviceblind.py b/pyfritzhome/devicetypes/fritzhomedeviceblind.py index 8bf08a1..22d7271 100644 --- a/pyfritzhome/devicetypes/fritzhomedeviceblind.py +++ b/pyfritzhome/devicetypes/fritzhomedeviceblind.py @@ -1,6 +1,8 @@ """The blind device class.""" import logging +from typing import Optional +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,9 +13,9 @@ class FritzhomeDeviceBlind(FritzhomeDeviceBase): """The Fritzhome Device class.""" - endpositionsset = None + endpositionsset: Optional[bool] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -23,11 +25,11 @@ def _update_from_node(self, node): # Blind @property - def has_blind(self): + def has_blind(self) -> bool: """Check if the device has blind function.""" return self._has_feature(FritzhomeDeviceFeatures.BLIND) - def _update_blind_from_node(self, node): + def _update_blind_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update blind device") blind_element = node.find("blind") try: @@ -37,14 +39,14 @@ def _update_blind_from_node(self, node): except Exception: pass - def set_blind_open(self, wait=False): + def set_blind_open(self, wait: bool = False) -> None: """Open the blind.""" self._fritz.set_blind_open(self.ain, wait) - def set_blind_close(self, wait=False): + def set_blind_close(self, wait: bool = False) -> None: """Close the blind.""" self._fritz.set_blind_close(self.ain, wait) - def set_blind_stop(self, wait=False): + def set_blind_stop(self, wait: bool = False) -> None: """Stop the blind.""" self._fritz.set_blind_stop(self.ain, wait) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebutton.py b/pyfritzhome/devicetypes/fritzhomedevicebutton.py index 4d12d8a..82c0741 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebutton.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebutton.py @@ -1,6 +1,7 @@ """The button device class.""" import logging +from typing import Optional from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -12,7 +13,7 @@ class FritzhomeDeviceButton(FritzhomeDeviceBase): """The Fritzhome Device class.""" - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -22,11 +23,11 @@ def _update_from_node(self, node): # Button @property - def has_button(self): + def has_button(self) -> bool: """Check if the device has button function.""" return self._has_feature(FritzhomeDeviceFeatures.BUTTON) - def _update_button_from_node(self, node): + def _update_button_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update button device") self.buttons = {} @@ -34,7 +35,7 @@ def _update_button_from_node(self, node): button = FritzhomeButton(element) self.buttons[button.ain] = button - def get_button_by_ain(self, ain): + def get_button_by_ain(self, ain: str) -> "FritzhomeButton": """Return the button by AIN.""" return self.buttons[ain] @@ -42,17 +43,17 @@ def get_button_by_ain(self, ain): class FritzhomeButton(object): """The Fritzhome Button Device class.""" - ain = None - identifier = None - name = None - last_pressed = None + ain: Optional[str] = None + identifier: Optional[str] = None + name: Optional[str] = None + last_pressed: Optional[int] = None - def __init__(self, node=None): + def __init__(self, node: Optional[ElementTree.Element] = None) -> None: """Create a button object.""" if node is not None: self._update_from_node(node) - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug(ElementTree.tostring(node)) self.ain = node.attrib["identifier"] self.identifier = node.attrib["id"] @@ -62,10 +63,19 @@ def _update_from_node(self, node): except ValueError: pass - def get_node_value(self, elem, node): + def get_node_value( + self, elem: Optional[ElementTree.Element], node: str + ) -> Optional[str]: """Get the node value.""" + if elem is None: + return None return elem.findtext(node) - def get_node_value_as_int(self, elem, node) -> int: + def get_node_value_as_int( + self, elem: Optional[ElementTree.Element], node: str + ) -> int: """Get the node value as integer.""" - return int(self.get_node_value(elem, node)) + value = self.get_node_value(elem, node) + if value is None: + raise TypeError("node value is missing") + return int(value) diff --git a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py index 7876018..9a27bdc 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py +++ b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py @@ -1,6 +1,8 @@ """The humidity device class.""" import logging +from typing import Optional +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,9 +13,9 @@ class FritzhomeDeviceHumidity(FritzhomeDeviceBase): """The Fritzhome Device class.""" - rel_humidity = None + rel_humidity: Optional[int] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -23,11 +25,11 @@ def _update_from_node(self, node): # Humidity @property - def has_humidity_sensor(self): + def has_humidity_sensor(self) -> bool: """Check if the device has humidity function.""" return self._has_feature(FritzhomeDeviceFeatures.HUMIDITY) - def _update_humidity_from_node(self, node): + def _update_humidity_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update humidity device") humidity_element = node.find("humidity") try: diff --git a/pyfritzhome/devicetypes/fritzhomedevicelevel.py b/pyfritzhome/devicetypes/fritzhomedevicelevel.py index e9cc15e..c415228 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicelevel.py +++ b/pyfritzhome/devicetypes/fritzhomedevicelevel.py @@ -1,6 +1,8 @@ """The level device class.""" import logging +from typing import Optional, Union +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,10 +13,10 @@ class FritzhomeDeviceLevel(FritzhomeDeviceBase): """The Fritzhome Device class.""" - level = None - levelpercentage = None + level: Optional[int] = None + levelpercentage: Optional[int] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -24,11 +26,11 @@ def _update_from_node(self, node): # Level @property - def has_level(self): + def has_level(self) -> bool: """Check if the device has level function.""" return self._has_feature(FritzhomeDeviceFeatures.LEVEL) - def _update_level_from_node(self, node): + def _update_level_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update level device") levelcontrol_element = node.find("levelcontrol") try: @@ -39,18 +41,20 @@ def _update_level_from_node(self, node): except Exception: pass - def get_level(self): + def get_level(self) -> Optional[int]: """Get the level.""" return self.level - def get_level_percentage(self): + def get_level_percentage(self) -> Optional[int]: """Get the level in percentage.""" return self.levelpercentage - def set_level(self, level, wait=False): + def set_level(self, level: Union[int, float], wait: bool = False) -> None: """Set the level.""" self._fritz.set_level(self.ain, level, wait) - def set_level_percentage(self, levelpercentage, wait=False): + def set_level_percentage( + self, levelpercentage: Union[int, float], wait: bool = False + ) -> None: """Set the level in percentage.""" self._fritz.set_level_percentage(self.ain, levelpercentage, wait) diff --git a/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py b/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py index 58a9720..b4a8bff 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py +++ b/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py @@ -1,6 +1,8 @@ """The light bulb device class.""" import logging +from typing import Optional, Sequence, SupportsInt, Union +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,17 +13,17 @@ class FritzhomeDeviceLightBulb(FritzhomeDeviceBase): """The Fritzhome Device class.""" - state = None - hue = None - saturation = None - unmapped_hue = None - unmapped_saturation = None - color_temp = None - color_mode = None - supported_color_mode = None + state: Optional[bool] = None + hue: Optional[int] = None + saturation: Optional[int] = None + unmapped_hue: Optional[int] = None + unmapped_saturation: Optional[int] = None + color_temp: Optional[int] = None + color_mode: Optional[str] = None + supported_color_mode: Optional[str] = None fullcolorsupport: bool = False - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -31,16 +33,16 @@ def _update_from_node(self, node): # Light Bulb @property - def has_lightbulb(self): + def has_lightbulb(self) -> bool: """Check if the device has LightBulb function.""" return self._has_feature(FritzhomeDeviceFeatures.LIGHTBULB) @property - def has_color(self): + def has_color(self) -> bool: """Check if the device has LightBulb function.""" return self._has_feature(FritzhomeDeviceFeatures.COLOR) - def _update_lightbulb_from_node(self, node): + def _update_lightbulb_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update light bulb device") state_element = node.find("simpleonoff") try: @@ -51,6 +53,8 @@ def _update_lightbulb_from_node(self, node): if self.has_color: colorcontrol_element = node.find("colorcontrol") + if colorcontrol_element is None: + return try: self.color_mode = colorcontrol_element.attrib.get("current_mode") @@ -95,46 +99,63 @@ def _update_lightbulb_from_node(self, node): # reset values after color mode changed self.color_temp = None - def set_state_off(self, wait=False): + def set_state_off(self, wait: bool = False) -> None: """Switch light bulb off.""" self.state = True self._fritz.set_state_off(self.ain, wait) - def set_state_on(self, wait=False): + def set_state_on(self, wait: bool = False) -> None: """Switch light bulb on.""" self.state = True self._fritz.set_state_on(self.ain, wait) - def set_state_toggle(self, wait=False): + def set_state_toggle(self, wait: bool = False) -> None: """Toogle light bulb state.""" self.state = True self._fritz.set_state_toggle(self.ain, wait) - def get_colors(self): + def get_colors( + self, + ) -> dict[str, list[tuple[Optional[str], Optional[str], Optional[str]]]]: """Get the supported colors.""" if self.has_color: return self._fritz.get_colors(self.ain) else: return {} - def set_color(self, hsv, duration=0, wait=False): + def set_color( + self, + hsv: Sequence[Union[str, SupportsInt]], + duration: int = 0, + wait: bool = False, + ) -> None: """Set HSV color.""" if self.has_color: self._fritz.set_color(self.ain, hsv, duration, True, wait) - def set_unmapped_color(self, hsv, duration=0, wait=False): + def set_unmapped_color( + self, + hsv: Sequence[Union[str, SupportsInt]], + duration: int = 0, + wait: bool = False, + ) -> None: """Set unmapped HSV color (Free color selection).""" if self.has_color and self.fullcolorsupport: self._fritz.set_color(self.ain, hsv, duration, False, wait) - def get_color_temps(self): + def get_color_temps(self) -> list[Optional[str]]: """Get the supported color temperatures energy.""" if self.has_color: return self._fritz.get_color_temps(self.ain) else: return [] - def set_color_temp(self, temperature, duration=0, wait=False): + def set_color_temp( + self, + temperature: Union[str, int, float], + duration: int = 0, + wait: bool = False, + ) -> None: """Set white color temperature.""" if self.has_color: self._fritz.set_color_temp(self.ain, temperature, duration, wait) diff --git a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py index fe4b434..ffdcf55 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py +++ b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py @@ -1,6 +1,8 @@ """The powermeter device class.""" import logging +from typing import Optional +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,12 +13,12 @@ class FritzhomeDevicePowermeter(FritzhomeDeviceBase): """The Fritzhome Device class.""" - power = None - energy = None - voltage = None - current = None + power: Optional[int] = None + energy: Optional[int] = None + voltage: Optional[int] = None + current: Optional[float] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -26,26 +28,34 @@ def _update_from_node(self, node): # Power Meter @property - def has_powermeter(self): + def has_powermeter(self) -> bool: """Check if the device has powermeter function.""" return self._has_feature(FritzhomeDeviceFeatures.POWER_METER) - def _update_powermeter_from_node(self, node): + def _update_powermeter_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update powermeter device") val = node.find("powermeter") + if val is None: + return try: - self.power = int(val.findtext("power")) + power = val.findtext("power") + if power is not None: + self.power = int(power) except Exception: pass try: - self.energy = int(val.findtext("energy")) + energy = val.findtext("energy") + if energy is not None: + self.energy = int(energy) except Exception: pass try: - self.voltage = int(val.findtext("voltage")) + voltage = val.findtext("voltage") + if voltage is not None: + self.voltage = int(voltage) except Exception: pass @@ -58,10 +68,10 @@ def _update_powermeter_from_node(self, node): else: self.current = None - def get_switch_power(self): + def get_switch_power(self) -> int: """Get the switch state.""" return self._fritz.get_switch_power(self.ain) - def get_switch_energy(self): + def get_switch_energy(self) -> int: """Get the switch energy.""" return self._fritz.get_switch_energy(self.ain) diff --git a/pyfritzhome/devicetypes/fritzhomedevicerepeater.py b/pyfritzhome/devicetypes/fritzhomedevicerepeater.py index 12abc54..e6840d3 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicerepeater.py +++ b/pyfritzhome/devicetypes/fritzhomedevicerepeater.py @@ -1,6 +1,7 @@ """The repeater device class.""" import logging +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,13 +12,13 @@ class FritzhomeDeviceRepeater(FritzhomeDeviceBase): """The Fritzhome Device class.""" - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return # Repeater @property - def has_repeater(self): + def has_repeater(self) -> bool: """Check if the device has repeater function.""" return self._has_feature(FritzhomeDeviceFeatures.DECT_REPEATER) diff --git a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py index fe70965..397fc4e 100644 --- a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py +++ b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py @@ -1,6 +1,8 @@ """The switch device class.""" import logging +from typing import Optional +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,11 +13,12 @@ class FritzhomeDeviceSwitch(FritzhomeDeviceBase): """The Fritzhome Device class.""" - switch_state = None - switch_mode = None - lock = None + switch_state: Optional[bool] = None + switch_mode: Optional[str] = None + lock: Optional[bool] = None + device_lock: Optional[bool] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -25,7 +28,7 @@ def _update_from_node(self, node): # Switch @property - def has_switch(self): + def has_switch(self) -> bool: """Check if the device has switch function.""" if self._has_feature(FritzhomeDeviceFeatures.SWITCH): # for AVM plugs like FRITZ!DECT 200 and FRITZ!DECT 210 @@ -37,7 +40,7 @@ def has_switch(self): return True return False - def _update_switch_from_node(self, node): + def _update_switch_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update switch device") if self._has_feature(FritzhomeDeviceFeatures.SWITCH): val = node.find("switch") @@ -63,18 +66,18 @@ def _update_switch_from_node(self, node): except Exception: self.switch_state = None - def get_switch_state(self): + def get_switch_state(self) -> bool: """Get the switch state.""" return self._fritz.get_switch_state(self.ain) - def set_switch_state_on(self, wait=False): + def set_switch_state_on(self, wait: bool = False) -> bool: """Set the switch state to on.""" return self._fritz.set_switch_state_on(self.ain, wait) - def set_switch_state_off(self, wait=False): + def set_switch_state_off(self, wait: bool = False) -> bool: """Set the switch state to off.""" return self._fritz.set_switch_state_off(self.ain, wait) - def set_switch_state_toggle(self, wait=False): + def set_switch_state_toggle(self, wait: bool = False) -> bool: """Toggle the switch state.""" return self._fritz.set_switch_state_toggle(self.ain, wait) diff --git a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py index fe89b48..3ff5f4f 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py +++ b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py @@ -1,6 +1,8 @@ """The temperature device class.""" import logging +from typing import Optional +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,10 +13,10 @@ class FritzhomeDeviceTemperature(FritzhomeDeviceBase): """The Fritzhome Device class.""" - offset = None - temperature = None + offset: Optional[float] = None + temperature: Optional[float] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -24,11 +26,11 @@ def _update_from_node(self, node): # Temperature @property - def has_temperature_sensor(self): + def has_temperature_sensor(self) -> bool: """Check if the device has temperature function.""" return self._has_feature(FritzhomeDeviceFeatures.TEMPERATURE) - def _update_temperature_from_node(self, node): + def _update_temperature_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update temperature device") temperature_element = node.find("temperature") try: diff --git a/pyfritzhome/devicetypes/fritzhomedevicethermostat.py b/pyfritzhome/devicetypes/fritzhomedevicethermostat.py index 1015fd1..5119921 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicethermostat.py +++ b/pyfritzhome/devicetypes/fritzhomedevicethermostat.py @@ -2,6 +2,8 @@ import logging import time +from typing import Optional +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -12,25 +14,25 @@ class FritzhomeDeviceThermostat(FritzhomeDeviceBase): """The Fritzhome Device class.""" - actual_temperature = None - target_temperature = None - eco_temperature = None - comfort_temperature = None - device_lock = None - lock = None - error_code = None - window_open = None - window_open_endtime = None - boost_active = None - boost_active_endtime = None - adaptive_heating_active = None - adaptive_heating_running = None - summer_active = None - holiday_active = None - nextchange_endperiod = None - nextchange_temperature = None - - def _update_from_node(self, node): + actual_temperature: Optional[float] = None + target_temperature: Optional[float] = None + eco_temperature: Optional[float] = None + comfort_temperature: Optional[float] = None + device_lock: Optional[bool] = None + lock: Optional[bool] = None + error_code: Optional[int] = None + window_open: Optional[bool] = None + window_open_endtime: Optional[float] = None + boost_active: Optional[bool] = None + boost_active_endtime: Optional[float] = None + adaptive_heating_active: Optional[bool] = None + adaptive_heating_running: Optional[bool] = None + summer_active: Optional[bool] = None + holiday_active: Optional[bool] = None + nextchange_endperiod: Optional[int] = None + nextchange_temperature: Optional[float] = None + + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -40,13 +42,15 @@ def _update_from_node(self, node): # Thermostat @property - def has_thermostat(self): + def has_thermostat(self) -> bool: """Check if the device has thermostat function.""" return self._has_feature(FritzhomeDeviceFeatures.THERMOSTAT) - def _update_hkr_from_node(self, node): + def _update_hkr_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update thermostat device") hkr_element = node.find("hkr") + if hkr_element is None: + return try: self.actual_temperature = self.get_temp_from_node(hkr_element, "tist") @@ -123,35 +127,35 @@ def _update_hkr_from_node(self, node): except Exception: pass - def get_temperature(self): + def get_temperature(self) -> float: """Get the device temperature value.""" return self._fritz.get_temperature(self.ain) - def get_target_temperature(self): + def get_target_temperature(self) -> float: """Get the thermostate target temperature.""" return self._fritz.get_target_temperature(self.ain) - def set_target_temperature(self, temperature, wait=False): + def set_target_temperature(self, temperature: float, wait: bool = False) -> None: """Set the thermostate target temperature.""" return self._fritz.set_target_temperature(self.ain, temperature, wait) - def set_window_open(self, seconds, wait=False): + def set_window_open(self, seconds: float, wait: bool = False) -> None: """Set the thermostate to window open.""" return self._fritz.set_window_open(self.ain, seconds, wait) - def set_boost_mode(self, seconds, wait=False): + def set_boost_mode(self, seconds: float, wait: bool = False) -> None: """Set the thermostate into boost mode.""" return self._fritz.set_boost_mode(self.ain, seconds, wait) - def get_comfort_temperature(self): + def get_comfort_temperature(self) -> float: """Get the thermostate comfort temperature.""" return self._fritz.get_comfort_temperature(self.ain) - def get_eco_temperature(self): + def get_eco_temperature(self) -> float: """Get the thermostate eco temperature.""" return self._fritz.get_eco_temperature(self.ain) - def get_hkr_state(self): + def get_hkr_state(self) -> str: """Get the thermostate state.""" try: return { @@ -163,7 +167,7 @@ def get_hkr_state(self): except KeyError: return "manual" - def set_hkr_state(self, state, wait=False): + def set_hkr_state(self, state: str, wait: bool = False) -> None: """Set the state of the thermostat. Possible values for state are: 'on', 'off', 'comfort', 'eco'. @@ -178,4 +182,5 @@ def set_hkr_state(self, state, wait=False): except KeyError: return - self.set_target_temperature(value, wait) + if value is not None: + self.set_target_temperature(value, wait) diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index dab7c3a..702465e 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -1,7 +1,8 @@ """The entity base class.""" -from __future__ import print_function +from __future__ import annotations, print_function from abc import ABC +from typing import TYPE_CHECKING, Optional, cast import logging @@ -10,23 +11,30 @@ _LOGGER = logging.getLogger(__name__) +if TYPE_CHECKING: + from pyfritzhome.fritzhome import Fritzhome + class FritzhomeEntityBase(ABC): """The Fritzhome Entity class.""" - _fritz = None + _fritz: Fritzhome = cast("Fritzhome", None) ain: str _functionsbitmask: int = 0 - supported_features = None + supported_features: list[FritzhomeDeviceFeatures] | None = None - def __init__(self, fritz=None, node=None): + def __init__( + self, + fritz: Optional["Fritzhome"] = None, + node: Optional[ElementTree.Element] = None, + ) -> None: """Create an entity base object.""" if fritz is not None: self._fritz = fritz if node is not None: self._update_from_node(node) - def __repr__(self): + def __repr__(self) -> str: """Return a string.""" return "{ain} {name}".format( ain=self.ain, @@ -36,12 +44,12 @@ def __repr__(self): def _has_feature(self, feature: FritzhomeDeviceFeatures) -> bool: return feature in FritzhomeDeviceFeatures(self._functionsbitmask) - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug(ElementTree.tostring(node)) self.ain = node.attrib["identifier"] self._functionsbitmask = int(node.attrib["functionbitmask"]) - self.name = node.findtext("name").strip() + self.name = node.findtext("name", "").strip() self.supported_features = [] for feature in FritzhomeDeviceFeatures: @@ -49,7 +57,7 @@ def _update_from_node(self, node): self.supported_features.append(feature) @property - def device_and_unit_id(self): + def device_and_unit_id(self) -> tuple[Optional[str], Optional[str]]: """Get the device and possible unit id.""" if ( self.ain.startswith("tmp") @@ -60,23 +68,39 @@ def device_and_unit_id(self): 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.split("-")[0], self.ain.split("-")[1]) return (self.ain, None) # XML Helpers - def get_node_value(self, elem, node): + def get_node_value( + self, elem: Optional[ElementTree.Element], node: str + ) -> Optional[str]: """Get the node value.""" + if elem is None: + return None return elem.findtext(node) - def get_node_value_as_int(self, elem, node) -> int: + def get_node_value_as_int( + self, elem: Optional[ElementTree.Element], node: str + ) -> 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: + value = self.get_node_value(elem, node) + if value is None: + raise TypeError("node value is missing") + return int(value) + + def get_node_value_as_int_as_bool( + self, elem: Optional[ElementTree.Element], node: str + ) -> 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): + def get_temp_from_node( + self, elem: Optional[ElementTree.Element], node: str + ) -> float: """Get the node temp value as float.""" - return float(self.get_node_value(elem, node)) / 2 + value = self.get_node_value(elem, node) + if value is None: + raise TypeError("node value is missing") + return float(value) / 2 diff --git a/pyfritzhome/devicetypes/fritzhometemplate.py b/pyfritzhome/devicetypes/fritzhometemplate.py index cef9bee..b2245c4 100644 --- a/pyfritzhome/devicetypes/fritzhometemplate.py +++ b/pyfritzhome/devicetypes/fritzhometemplate.py @@ -1,6 +1,8 @@ """The template class.""" import logging +from typing import Optional +from xml.etree import ElementTree from .fritzhomeentitybase import FritzhomeEntityBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,25 +13,27 @@ class FritzhomeTemplate(FritzhomeEntityBase): """The Fritzhome Template class.""" - devices = None - features = None - apply_hkr_summer = None - apply_hkr_temperature = None - apply_hkr_holidays = None - apply_hkr_time_table = None - apply_relay_manual = None - apply_relay_automatic = None - apply_level = None - apply_color = None - apply_dialhelper = None - - def _update_from_node(self, node): + devices: Optional[list[str]] = None + features: Optional[FritzhomeDeviceFeatures] = None + apply_hkr_summer: Optional[bool] = None + apply_hkr_temperature: Optional[bool] = None + apply_hkr_holidays: Optional[bool] = None + apply_hkr_time_table: Optional[bool] = None + apply_relay_manual: Optional[bool] = None + apply_relay_automatic: Optional[bool] = None + apply_level: Optional[bool] = None + apply_color: Optional[bool] = None + apply_dialhelper: Optional[bool] = None + + def _update_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update template") super()._update_from_node(node) self.features = FritzhomeDeviceFeatures(self._functionsbitmask) applymask = node.find("applymask") + if applymask is None: + raise ValueError("template node is missing applymask") self.apply_hkr_summer = applymask.find("hkr_summer") is not None self.apply_hkr_temperature = applymask.find("hkr_temperature") is not None self.apply_hkr_holidays = applymask.find("hkr_holidays") is not None @@ -40,6 +44,9 @@ def _update_from_node(self, node): self.apply_color = applymask.find("color") is not None self.apply_dialhelper = applymask.find("dialhelper") is not None + devices = node.find("devices") + if devices is None: + raise ValueError("template node is missing devices") self.devices = [] - for device in node.find("devices").findall("device"): + for device in devices.findall("device"): self.devices.append(device.attrib["identifier"]) diff --git a/pyfritzhome/devicetypes/fritzhometrigger.py b/pyfritzhome/devicetypes/fritzhometrigger.py index 9871f84..7d354d2 100644 --- a/pyfritzhome/devicetypes/fritzhometrigger.py +++ b/pyfritzhome/devicetypes/fritzhometrigger.py @@ -1,6 +1,7 @@ """The trigger class.""" import logging +from typing import Optional from xml.etree import ElementTree from .fritzhomeentitybase import FritzhomeEntityBase @@ -11,11 +12,11 @@ class FritzhomeTrigger(FritzhomeEntityBase): """The Fritzhome Trigger class.""" - active = None + active: Optional[bool] = None - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update trigger") _LOGGER.debug(ElementTree.tostring(node)) self.ain = node.attrib["identifier"] - self.name = node.findtext("name").strip() + self.name = node.findtext("name", "").strip() self.active = node.attrib["active"] == "1" diff --git a/pyfritzhome/errors.py b/pyfritzhome/errors.py index 374e82b..c52b2f0 100644 --- a/pyfritzhome/errors.py +++ b/pyfritzhome/errors.py @@ -4,19 +4,23 @@ class LoginError(Exception): """The LoginError Exception.""" - def __init__(self, user): + def __init__(self, user: str, detail: str = "") -> None: """Initialize the an loginError.""" self.user = user + self.detail = detail - def __str__(self): + def __str__(self) -> str: """Return the error.""" - return 'login for user="{}" failed'.format(self.user) + message = 'login for user="{}" failed'.format(self.user) + if self.detail: + message += ": " + self.detail + return message class NotLoggedInError(Exception): """The NotLoggedInError Exception.""" - def __str__(self): + def __str__(self) -> str: """Return the error.""" return "not logged in, login before doing any requests." diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 7e3f1f5..cfde1e6 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -16,37 +16,48 @@ from .errors import InvalidError, LoginError, NotLoggedInError from .fritzhomedevice import FritzhomeDevice -from .fritzhomedevice import FritzhomeTemplate -from .fritzhomedevice import FritzhomeTrigger -from typing import Dict, Optional +from .devicetypes import FritzhomeTemplate, FritzhomeTrigger +from typing import Dict, Mapping, Optional, Sequence, SupportsInt, Union, overload -_LOGGER = logging.getLogger(__name__) +_LOGGER: logging.Logger = logging.getLogger(__name__) class Fritzhome(object): """Fritzhome object to communicate with the device.""" - _sid = None - _session = None + _sid: Optional[str] = None + _session: Session _devices: Optional[Dict[str, FritzhomeDevice]] = None _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: str, + user: str, + password: str, + port: Optional[int] = None, + ssl_verify: bool = True, + timeout: int = 10, + ) -> None: """Create a fritzhome object.""" - self._user = user - self._password = password + self._user: str = user + self._password: str = password self._session = Session() - self._ssl_verify = ssl_verify - self._timeout = timeout - self._has_getdeviceinfos = True - self._has_txbusy = True + self._ssl_verify: bool = ssl_verify + self._timeout: int = timeout + self._has_getdeviceinfos: bool = True + self._has_txbusy: bool = True if host.startswith("https://") or host.startswith("http://"): - self.base_url = f"{host}:{port}" if port else host + self.base_url: str = f"{host}:{port}" if port else host else: self.base_url = f"http://{host}:{port}" if port else f"http://{host}" - def _request(self, url, params=None): + def _request( + self, + url: str, + params: Optional[Mapping[str, Union[str, int, None]]] = None, + ) -> str: """Send a request with parameters.""" rsp = self._session.get( url, params=params, timeout=self._timeout, verify=self._ssl_verify @@ -54,7 +65,9 @@ def _request(self, url, params=None): rsp.raise_for_status() return rsp.text.strip() - def _login_request(self, username=None, secret=None): + def _login_request( + self, username: Optional[str] = None, secret: Optional[str] = None + ) -> tuple[Optional[str], Optional[str], int]: """Send a login request with paramerters.""" url = f"{self.base_url}/login_sid.lua?version=2" params = {} @@ -66,7 +79,7 @@ def _login_request(self, username=None, secret=None): plain = self._request(url, params) dom = ElementTree.fromstring(plain) sid = dom.findtext("SID") - blocktime = int(dom.findtext("BlockTime")) + blocktime = int(dom.findtext("BlockTime") or 0) challenge = dom.findtext("Challenge") return (sid, challenge, blocktime) @@ -115,7 +128,7 @@ def has_smarthome_capabilities(self) -> Optional[bool]: ) return None - def _logout_request(self): + def _logout_request(self) -> None: """Send a logout request.""" _LOGGER.debug("logout") url = f"{self.base_url}/login_sid.lua" @@ -124,7 +137,7 @@ def _logout_request(self): self._request(url, params) @staticmethod - def _create_login_secrete_pbkdf2(challenge, password): + def _create_login_secrete_pbkdf2(challenge: str, password: str) -> str: challenge_parts = challenge.split("$") # Extract all necessary values encoded into the challenge iter1 = int(challenge_parts[1]) @@ -146,13 +159,55 @@ def _create_login_secrete_pbkdf2(challenge, password): return f"{challenge_parts[4]}${hash2.hex()}" @staticmethod - def _create_login_secret_md5(challenge, password): + def _create_login_secret_md5(challenge: str, password: str) -> str: """Create a login secret.""" to_hash = (challenge + "-" + password).encode("UTF-16LE") hashed = hashlib.md5(to_hash).hexdigest() return "{0}-{1}".format(challenge, hashed) - def _aha_request(self, cmd, ain=None, param=None, rf=str): + @overload + def _aha_request( + self, + cmd: str, + ain: Optional[str] = None, + param: Optional[Mapping[str, Union[str, int]]] = None, + rf: type[str] = str, + ) -> str: ... + + @overload + def _aha_request( + self, + cmd: str, + ain: Optional[str] = None, + param: Optional[Mapping[str, Union[str, int]]] = None, + rf: type[bool] = bool, + ) -> bool: ... + + @overload + def _aha_request( + self, + cmd: str, + ain: Optional[str] = None, + param: Optional[Mapping[str, Union[str, int]]] = None, + rf: type[int] = int, + ) -> int: ... + + @overload + def _aha_request( + self, + cmd: str, + ain: Optional[str] = None, + param: Optional[Mapping[str, Union[str, int]]] = None, + rf: type[float] = float, + ) -> float: ... + + def _aha_request( + self, + cmd: str, + ain: Optional[str] = None, + param: Optional[Mapping[str, Union[str, int]]] = None, + rf: Union[type[str], type[bool], type[int], type[float]] = str, + ) -> Union[str, bool, int, float]: """Send an AHA request.""" url = f"{self.base_url}/webservices/homeautoswitch.lua" @@ -161,9 +216,10 @@ def _aha_request(self, cmd, ain=None, param=None, rf=str): if not self._sid: raise NotLoggedInError - params = {"switchcmd": cmd, "sid": self._sid} + params: Dict[str, Union[str, int]] = {"switchcmd": cmd, "sid": self._sid} if param: - params.update(param) + for key, value in param.items(): + params[key] = value if ain: params["ain"] = ain @@ -175,13 +231,15 @@ def _aha_request(self, cmd, ain=None, param=None, rf=str): return bool(int(plain)) return rf(plain) - def login(self): + def login(self) -> None: """Login and get a valid session ID.""" (sid, challenge, blocktime) = self._login_request() _LOGGER.info("sid:%s, challenge:%s, blocktime:%s", sid, challenge, blocktime) if sid == "0000000000000000": if blocktime > 0: time.sleep(blocktime) + if challenge is None: + raise LoginError(self._user, "challenge missing from login response") # PBKDF2 (FRITZ!OS 7.24 or later) if challenge.startswith("2$"): secret = self._create_login_secrete_pbkdf2(challenge, self._password) @@ -196,12 +254,12 @@ def login(self): raise LoginError(self._user) self._sid = sid2 - def logout(self): + def logout(self) -> None: """Logout.""" self._logout_request() self._sid = None - def update_devices(self, ignore_removed=True): + def update_devices(self, ignore_removed: bool = True) -> bool: """Update the device.""" _LOGGER.info("Updating Devices ...") if self._devices is None: @@ -229,14 +287,14 @@ def update_devices(self, ignore_removed=True): return True - def _get_listinfo_elements(self, entity_type): + def _get_listinfo_elements(self, entity_type: str) -> list[ElementTree.Element]: """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): + def wait_device_txbusy(self, ain: str, retries: int = 10) -> bool: """Wait for device to finish command execution.""" if not self._has_txbusy: return True @@ -252,7 +310,10 @@ def wait_device_txbusy(self, ain, retries=10): self._has_getdeviceinfos = False if not self._has_getdeviceinfos: - dom = self.get_device_element(ain) + fallback_dom = self.get_device_element(ain) + if fallback_dom is None: + return False + dom = fallback_dom txbusy = dom.findall("txbusy") if not txbusy: @@ -266,11 +327,11 @@ def wait_device_txbusy(self, ain, retries=10): time.sleep(0.2) return False - def get_device_elements(self): + def get_device_elements(self) -> list[ElementTree.Element]: """Get the DOM elements for the device list.""" return self._get_listinfo_elements("device") - def get_device_element(self, ain): + def get_device_element(self, ain: str) -> Optional[ElementTree.Element]: """Get the DOM element for the specified device.""" elements = self.get_device_elements() for element in elements: @@ -278,75 +339,78 @@ def get_device_element(self, ain): return element return None - def get_devices(self): + def get_devices(self) -> list[FritzhomeDevice]: """Get the list of all known devices.""" return list(self.get_devices_as_dict().values()) - def get_devices_as_dict(self): + def get_devices_as_dict(self) -> Dict[str, FritzhomeDevice]: """Get the list of all known devices.""" if self._devices is None: self.update_devices() + assert self._devices is not None return self._devices - def get_device_by_ain(self, ain): + def get_device_by_ain(self, ain: str) -> FritzhomeDevice: """Return a device specified by the AIN.""" return self.get_devices_as_dict()[ain] - def get_device_infos(self, ain): + def get_device_infos(self, ain: str) -> str: """Get the device infos.""" return self._aha_request("getdeviceinfos", ain=ain) - def get_device_present(self, ain): + def get_device_present(self, ain: str) -> bool: """Get the device presence.""" return self._aha_request("getswitchpresent", ain=ain, rf=bool) - def get_device_name(self, ain): + def get_device_name(self, ain: str) -> str: """Get the device name.""" return self._aha_request("getswitchname", ain=ain) - def get_switch_state(self, ain): + def get_switch_state(self, ain: str) -> bool: """Get the switch state.""" return self._aha_request("getswitchstate", ain=ain, rf=bool) - def set_switch_state_on(self, ain, wait=False): + def set_switch_state_on(self, ain: str, wait: bool = False) -> bool: """Set the switch to on state.""" result = self._aha_request("setswitchon", ain=ain, rf=bool) wait and self.wait_device_txbusy(ain) return result - def set_switch_state_off(self, ain, wait=False): + def set_switch_state_off(self, ain: str, wait: bool = False) -> bool: """Set the switch to off state.""" result = self._aha_request("setswitchoff", ain=ain, rf=bool) wait and self.wait_device_txbusy(ain) return result - def set_switch_state_toggle(self, ain, wait=False): + def set_switch_state_toggle(self, ain: str, wait: bool = False) -> bool: """Toggle the switch state.""" result = self._aha_request("setswitchtoggle", ain=ain, rf=bool) wait and self.wait_device_txbusy(ain) return result - def get_switch_power(self, ain): + def get_switch_power(self, ain: str) -> int: """Get the switch power consumption.""" return self._aha_request("getswitchpower", ain=ain, rf=int) - def get_switch_energy(self, ain): + def get_switch_energy(self, ain: str) -> int: """Get the switch energy.""" return self._aha_request("getswitchenergy", ain=ain, rf=int) - def get_temperature(self, ain): + def get_temperature(self, ain: str) -> float: """Get the device temperature sensor value.""" return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 - def _get_temperature(self, ain, name): + def _get_temperature(self, ain: str, name: str) -> float: plain = self._aha_request(name, ain=ain, rf=float) return plain / 2 - def get_target_temperature(self, ain): + def get_target_temperature(self, ain: str) -> float: """Get the thermostate target temperature.""" return self._get_temperature(ain, "gethkrtsoll") - def set_target_temperature(self, ain, temperature, wait=False): + def set_target_temperature( + self, ain: str, temperature: float, wait: bool = False + ) -> None: """Set the thermostate target temperature.""" temp = int(temperature * 2) @@ -358,7 +422,7 @@ def set_target_temperature(self, ain, temperature, wait=False): self._aha_request("sethkrtsoll", ain=ain, param={"param": temp}) wait and self.wait_device_txbusy(ain) - def set_window_open(self, ain, seconds, wait=False): + def set_window_open(self, ain: str, seconds: float, wait: bool = False) -> None: """Set the thermostate target temperature.""" endtimestamp = int(time.time() + seconds) @@ -367,44 +431,44 @@ def set_window_open(self, ain, seconds, wait=False): ) wait and self.wait_device_txbusy(ain) - def set_boost_mode(self, ain, seconds, wait=False): + def set_boost_mode(self, ain: str, seconds: float, wait: bool = False) -> None: """Set the thermostate to boost mode.""" endtimestamp = int(time.time() + seconds) self._aha_request("sethkrboost", ain=ain, param={"endtimestamp": endtimestamp}) wait and self.wait_device_txbusy(ain) - def get_comfort_temperature(self, ain): + def get_comfort_temperature(self, ain: str) -> float: """Get the thermostate comfort temperature.""" return self._get_temperature(ain, "gethkrkomfort") - def get_eco_temperature(self, ain): + def get_eco_temperature(self, ain: str) -> float: """Get the thermostate eco temperature.""" return self._get_temperature(ain, "gethkrabsenk") - def get_device_statistics(self, ain): + def get_device_statistics(self, ain: str) -> str: """Get device statistics.""" plain = self._aha_request("getbasicdevicestats", ain=ain) return plain # Lightbulb-related commands - def set_state_off(self, ain, wait=False): + def set_state_off(self, ain: str, wait: bool = False) -> None: """Set the switch/actuator/lightbulb to on state.""" self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 0}) wait and self.wait_device_txbusy(ain) - def set_state_on(self, ain, wait=False): + def set_state_on(self, ain: str, wait: bool = False) -> None: """Set the switch/actuator/lightbulb to on state.""" self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 1}) wait and self.wait_device_txbusy(ain) - def set_state_toggle(self, ain, wait=False): + def set_state_toggle(self, ain: str, wait: bool = False) -> None: """Toggle the switch/actuator/lightbulb state.""" self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 2}) wait and self.wait_device_txbusy(ain) - def set_level(self, ain, level, wait=False): + def set_level(self, ain: str, level: float, wait: bool = False) -> None: """Set level/brightness/height in interval [0,255].""" if level < 0: level = 0 # 0% @@ -414,7 +478,7 @@ def set_level(self, ain, level, wait=False): self._aha_request("setlevel", ain=ain, param={"level": int(level)}) wait and self.wait_device_txbusy(ain) - def set_level_percentage(self, ain, level, wait=False): + def set_level_percentage(self, ain: str, level: float, wait: bool = False) -> None: """Set level/brightness/height in interval [0,100].""" if level < 0: level = 0 @@ -424,23 +488,35 @@ def set_level_percentage(self, ain, level, wait=False): self._aha_request("setlevelpercentage", ain=ain, param={"level": int(level)}) wait and self.wait_device_txbusy(ain) - def _get_colordefaults(self, ain): + def _get_colordefaults(self, ain: str) -> ElementTree.Element: plain = self._aha_request("getcolordefaults", ain=ain) return ElementTree.fromstring(plain) - def get_colors(self, ain): + def get_colors( + self, ain: str + ) -> dict[str, list[tuple[Optional[str], Optional[str], Optional[str]]]]: """Get colors (HSV-space) supported by this lightbulb.""" colordefaults = self._get_colordefaults(ain) colors = {} for hs in colordefaults.iter("hs"): - name = hs.find("name").text.strip() + name_element = hs.find("name") + if name_element is None or name_element.text is None: + continue + name = name_element.text.strip() values = [] for st in hs.iter("color"): values.append((st.get("hue"), st.get("sat"), st.get("val"))) colors[name] = values return colors - def set_color(self, ain, hsv, duration=0, mapped=True, wait=False): + def set_color( + self, + ain: str, + hsv: Sequence[Union[str, SupportsInt]], + duration: int = 0, + mapped: bool = True, + wait: bool = False, + ) -> None: """Set hue and saturation. hsv: HUE colorspace element obtained from get_colors() @@ -458,7 +534,7 @@ def set_color(self, ain, hsv, duration=0, mapped=True, wait=False): self._aha_request("setunmappedcolor", ain=ain, param=params) wait and self.wait_device_txbusy(ain) - def get_color_temps(self, ain): + def get_color_temps(self, ain: str) -> list[Optional[str]]: """Get temperatures supported by this lightbulb.""" colordefaults = self._get_colordefaults(ain) temperatures = [] @@ -466,7 +542,13 @@ def get_color_temps(self, ain): temperatures.append(temp.get("value")) return temperatures - def set_color_temp(self, ain, temperature, duration=0, wait=False): + def set_color_temp( + self, + ain: str, + temperature: Union[str, int, float], + duration: int = 0, + wait: bool = False, + ) -> None: """Set color temperature. temperature: temperature element obtained from get_temperatures() @@ -478,27 +560,27 @@ def set_color_temp(self, ain, temperature, duration=0, wait=False): # blinds # states: open, close, stop - def _set_blind_state(self, ain, state): + def _set_blind_state(self, ain: str, state: str) -> None: self._aha_request("setblind", ain=ain, param={"target": state}) - def set_blind_open(self, ain, wait=False): + def set_blind_open(self, ain: str, wait: bool = False) -> None: """Set the blind state to open.""" self._set_blind_state(ain, "open") wait and self.wait_device_txbusy(ain) - def set_blind_close(self, ain, wait=False): + def set_blind_close(self, ain: str, wait: bool = False) -> None: """Set the blind state to close.""" self._set_blind_state(ain, "close") wait and self.wait_device_txbusy(ain) - def set_blind_stop(self, ain, wait=False): + def set_blind_stop(self, ain: str, wait: bool = False) -> None: """Set the blind state to stop.""" self._set_blind_state(ain, "stop") wait and self.wait_device_txbusy(ain) # Template-related commands - def has_templates(self): + def has_templates(self) -> bool: """Check if the Fritz!Box supports smarthome templates.""" plain = self._aha_request("gettemplatelistinfos") try: @@ -507,7 +589,7 @@ def has_templates(self): return False return True - def update_templates(self, ignore_removed=True): + def update_templates(self, ignore_removed: bool = True) -> bool: """Update the template.""" _LOGGER.info("Updating Templates ...") if self._templates is None: @@ -535,31 +617,32 @@ def update_templates(self, ignore_removed=True): return True - def get_template_elements(self): + def get_template_elements(self) -> list[ElementTree.Element]: """Get the DOM elements for the template list.""" return self._get_listinfo_elements("template") - def get_templates(self): + def get_templates(self) -> list[FritzhomeTemplate]: """Get the list of all known templates.""" return list(self.get_templates_as_dict().values()) - def get_templates_as_dict(self): + def get_templates_as_dict(self) -> Dict[str, FritzhomeTemplate]: """Get the list of all known templates.""" if self._templates is None: self.update_templates() + assert self._templates is not None return self._templates - def get_template_by_ain(self, ain): + def get_template_by_ain(self, ain: str) -> FritzhomeTemplate: """Return a template specified by the AIN.""" return self.get_templates_as_dict()[ain] - def apply_template(self, ain): + def apply_template(self, ain: str) -> None: """Appliy a template.""" self._aha_request("applytemplate", ain=ain) # Trigger-related commands - def has_triggers(self): + def has_triggers(self) -> bool: """Check if the Fritz!Box supports smarthome triggers.""" plain = self._aha_request("gettriggerlistinfos") try: @@ -568,7 +651,7 @@ def has_triggers(self): return False return True - def update_triggers(self, ignore_removed=True): + def update_triggers(self, ignore_removed: bool = True) -> bool: """Update the triger.""" _LOGGER.info("Updating Trigers ...") if self._triggers is None: @@ -596,31 +679,32 @@ def update_triggers(self, ignore_removed=True): return True - def get_trigger_elements(self): + def get_trigger_elements(self) -> list[ElementTree.Element]: """Get the DOM elements for the trigger list.""" return self._get_listinfo_elements("trigger") - def get_triggers(self): + def get_triggers(self) -> list[FritzhomeTrigger]: """Get the list of all known triggers.""" return list(self.get_triggers_as_dict().values()) - def get_triggers_as_dict(self): + def get_triggers_as_dict(self) -> Dict[str, FritzhomeTrigger]: """Get all known triggers as dictionary.""" if self._triggers is None: self.update_triggers() + assert self._triggers is not None return self._triggers - def get_trigger_by_ain(self, ain): + def get_trigger_by_ain(self, ain: str) -> FritzhomeTrigger: """Return a trigger specified by the AIN.""" return self.get_triggers_as_dict()[ain] - def _set_trigger_state(self, ain, state): + def _set_trigger_state(self, ain: str, state: str) -> None: self._aha_request("settriggeractive", ain=ain, param={"active": state}) - def set_trigger_active(self, ain): + def set_trigger_active(self, ain: str) -> None: """Set the trigger to active state.""" self._set_trigger_state(ain, "1") - def set_trigger_inactive(self, ain): + def set_trigger_inactive(self, ain: str) -> None: """Set the trigger to inactive state.""" self._set_trigger_state(ain, "0") diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 1f824bf..a2c2f0c 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -1,5 +1,8 @@ """Toplevel device for pyfritzhome.""" +from typing import Optional, TYPE_CHECKING +from xml.etree import ElementTree + from .devicetypes import FritzhomeTemplate # noqa: F401 from .devicetypes import FritzhomeTrigger # noqa: F401 from .devicetypes import ( @@ -16,6 +19,9 @@ FritzhomeDeviceThermostat, ) +if TYPE_CHECKING: + from .fritzhome import Fritzhome + class FritzhomeDevice( FritzhomeDeviceAlarm, @@ -32,9 +38,13 @@ class FritzhomeDevice( ): """The Fritzhome Device class.""" - def __init__(self, fritz=None, node=None): + def __init__( + self, + fritz: Optional["Fritzhome"] = None, + node: Optional[ElementTree.Element] = None, + ) -> None: """Create a device object.""" super().__init__(fritz, node) - def _update_from_node(self, node): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/py.typed b/pyfritzhome/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/requirements_dev.txt b/requirements_dev.txt index a860a00..648784c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,8 +4,10 @@ coverage docutils flake8 future +mypy pre-commit pytest pytest-cov python-coveralls rstcheck +types-requests diff --git a/setup.cfg b/setup.cfg index 9284161..2d93d7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,9 @@ test_suite = tests install_requires = requests; cryptography tests_requires = pytest +[options.package_data] +* = py.typed + [options.entry_points] console_scripts = fritzhome=pyfritzhome.cli:main @@ -66,3 +69,7 @@ match-dir = pyfritzhome/* [pydocstyle] ignore = D203,D213 match-dir = pyfritzhome/* + +[mypy] +files = pyfritzhome/**/*.py +strict = true diff --git a/tests/test_fritzhome.py b/tests/test_fritzhome.py index b3974d4..77009ca 100644 --- a/tests/test_fritzhome.py +++ b/tests/test_fritzhome.py @@ -25,6 +25,17 @@ def test_login_fail(self): self.fritz.login() assert str(ex.value) == 'login for user="user" failed' + def test_login_missing_challenge(self): + self.fritz._login_request = MagicMock( + return_value=("0000000000000000", None, 0) + ) + + with pytest.raises(LoginError) as ex: + self.fritz.login() + assert str(ex.value) == ( + 'login for user="user" failed: challenge missing from login response' + ) + def test_login_connection_error(self): self.mock.side_effect = ConnectionError From a66bc9355074bd2607c9fa2bce45db7d339a18de Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 4 Oct 2026 18:00:55 +0000 Subject: [PATCH 2/3] adjust pre-commit settings --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ead4bd..41f2ae9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,8 @@ repos: rev: v1.19.1 hooks: - id: mypy + args: [] additional_dependencies: + - cryptography - types-requests files: ^pyfritzhome/.+\.py$ From 8915dbcb9160994638a84f33d5a80262606f2d4a Mon Sep 17 00:00:00 2001 From: mib1185 Date: Tue, 6 Oct 2026 15:27:29 +0000 Subject: [PATCH 3/3] modernize type annotations Co-authored-by: Copilot --- pyfritzhome/cli.py | 7 +- .../devicetypes/fritzhomedevicealarm.py | 6 +- .../devicetypes/fritzhomedevicebase.py | 25 ++++--- .../devicetypes/fritzhomedeviceblind.py | 6 +- .../devicetypes/fritzhomedevicebutton.py | 22 +++--- .../devicetypes/fritzhomedevicehumidity.py | 6 +- .../devicetypes/fritzhomedevicelevel.py | 15 +++-- .../devicetypes/fritzhomedevicelightbulb.py | 32 +++++---- .../devicetypes/fritzhomedevicepowermeter.py | 12 ++-- .../devicetypes/fritzhomedeviceswitch.py | 12 ++-- .../devicetypes/fritzhomedevicetemperature.py | 8 ++- .../devicetypes/fritzhomedevicethermostat.py | 38 ++++++----- .../devicetypes/fritzhomeentitybase.py | 26 +++---- pyfritzhome/devicetypes/fritzhometemplate.py | 26 +++---- pyfritzhome/devicetypes/fritzhometrigger.py | 6 +- pyfritzhome/fritzhome.py | 67 ++++++++++--------- pyfritzhome/fritzhomedevice.py | 10 ++- 17 files changed, 173 insertions(+), 151 deletions(-) diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 6c8eaca..2b10a06 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -1,11 +1,12 @@ #!/usr/bin/env python """A simple CLI tool.""" +from __future__ import annotations -from __future__ import print_function +from collections.abc import Sequence import logging import argparse -from typing import Optional, Sequence + from pyfritzhome import Fritzhome, __version__ @@ -199,7 +200,7 @@ def trigger_set_inactive(fritz: Fritzhome, args: argparse.Namespace) -> None: fritz.set_trigger_inactive(args.ain) -def main(args: Optional[Sequence[str]] = None) -> None: +def main(args: Sequence[str] | None = None) -> None: """Enter the main function of the CLI tool.""" parser = argparse.ArgumentParser(description="Fritz!Box Smarthome CLI tool.") parser.add_argument( diff --git a/pyfritzhome/devicetypes/fritzhomedevicealarm.py b/pyfritzhome/devicetypes/fritzhomedevicealarm.py index a7e8fec..7dad8ea 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicealarm.py +++ b/pyfritzhome/devicetypes/fritzhomedevicealarm.py @@ -1,7 +1,9 @@ """The alarm device class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,7 +15,7 @@ class FritzhomeDeviceAlarm(FritzhomeDeviceBase): """The Fritzhome Device class.""" - alert_state: Optional[bool] = None + alert_state: bool | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 134aed0..354ba73 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -1,10 +1,9 @@ """The base device class.""" -from __future__ import print_function - +from __future__ import annotations import logging -from typing import Optional + from xml.etree import ElementTree from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase @@ -15,16 +14,16 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): """The Fritzhome Device class.""" - battery_level: Optional[int] = None - battery_low: Optional[bool] = None - identifier: Optional[str] = None - is_group: Optional[bool] = None - fw_version: Optional[str] = None - group_members: Optional[list[str]] = None - manufacturer: Optional[str] = None - productname: Optional[str] = None - present: Optional[bool] = None - tx_busy: Optional[bool] = None + battery_level: int | None = None + battery_low: bool | None = None + identifier: str | None = None + is_group: bool | None = None + fw_version: str | None = None + group_members: list[str] | None = None + manufacturer: str | None = None + productname: str | None = None + present: bool | None = None + tx_busy: bool | None = None def __repr__(self) -> str: """Return a string.""" diff --git a/pyfritzhome/devicetypes/fritzhomedeviceblind.py b/pyfritzhome/devicetypes/fritzhomedeviceblind.py index 22d7271..3972ef4 100644 --- a/pyfritzhome/devicetypes/fritzhomedeviceblind.py +++ b/pyfritzhome/devicetypes/fritzhomedeviceblind.py @@ -1,7 +1,9 @@ """The blind device class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,7 +15,7 @@ class FritzhomeDeviceBlind(FritzhomeDeviceBase): """The Fritzhome Device class.""" - endpositionsset: Optional[bool] = None + endpositionsset: bool | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebutton.py b/pyfritzhome/devicetypes/fritzhomedevicebutton.py index 82c0741..28a9544 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebutton.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebutton.py @@ -1,7 +1,9 @@ """The button device class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -43,12 +45,12 @@ def get_button_by_ain(self, ain: str) -> "FritzhomeButton": class FritzhomeButton(object): """The Fritzhome Button Device class.""" - ain: Optional[str] = None - identifier: Optional[str] = None - name: Optional[str] = None - last_pressed: Optional[int] = None + ain: str | None = None + identifier: str | None = None + name: str | None = None + last_pressed: int | None = None - def __init__(self, node: Optional[ElementTree.Element] = None) -> None: + def __init__(self, node: ElementTree.Element | None = None) -> None: """Create a button object.""" if node is not None: self._update_from_node(node) @@ -63,17 +65,13 @@ def _update_from_node(self, node: ElementTree.Element) -> None: except ValueError: pass - def get_node_value( - self, elem: Optional[ElementTree.Element], node: str - ) -> Optional[str]: + def get_node_value(self, elem: ElementTree.Element | None, node: str) -> str | None: """Get the node value.""" if elem is None: return None return elem.findtext(node) - def get_node_value_as_int( - self, elem: Optional[ElementTree.Element], node: str - ) -> int: + def get_node_value_as_int(self, elem: ElementTree.Element | None, node: str) -> int: """Get the node value as integer.""" value = self.get_node_value(elem, node) if value is None: diff --git a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py index 9a27bdc..ec8bb4f 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py +++ b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py @@ -1,7 +1,9 @@ """The humidity device class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,7 +15,7 @@ class FritzhomeDeviceHumidity(FritzhomeDeviceBase): """The Fritzhome Device class.""" - rel_humidity: Optional[int] = None + rel_humidity: int | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomedevicelevel.py b/pyfritzhome/devicetypes/fritzhomedevicelevel.py index c415228..a856bd1 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicelevel.py +++ b/pyfritzhome/devicetypes/fritzhomedevicelevel.py @@ -1,7 +1,8 @@ """The level device class.""" +from __future__ import annotations + import logging -from typing import Optional, Union from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,8 +14,8 @@ class FritzhomeDeviceLevel(FritzhomeDeviceBase): """The Fritzhome Device class.""" - level: Optional[int] = None - levelpercentage: Optional[int] = None + level: int | None = None + levelpercentage: int | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) @@ -41,20 +42,20 @@ def _update_level_from_node(self, node: ElementTree.Element) -> None: except Exception: pass - def get_level(self) -> Optional[int]: + def get_level(self) -> int | None: """Get the level.""" return self.level - def get_level_percentage(self) -> Optional[int]: + def get_level_percentage(self) -> int | None: """Get the level in percentage.""" return self.levelpercentage - def set_level(self, level: Union[int, float], wait: bool = False) -> None: + def set_level(self, level: int | float, wait: bool = False) -> None: """Set the level.""" self._fritz.set_level(self.ain, level, wait) def set_level_percentage( - self, levelpercentage: Union[int, float], wait: bool = False + self, levelpercentage: int | float, wait: bool = False ) -> None: """Set the level in percentage.""" self._fritz.set_level_percentage(self.ain, levelpercentage, wait) diff --git a/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py b/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py index b4a8bff..44782a4 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py +++ b/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py @@ -1,7 +1,11 @@ """The light bulb device class.""" +from __future__ import annotations + +from collections.abc import Sequence +from typing import SupportsInt + import logging -from typing import Optional, Sequence, SupportsInt, Union from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,14 +17,14 @@ class FritzhomeDeviceLightBulb(FritzhomeDeviceBase): """The Fritzhome Device class.""" - state: Optional[bool] = None - hue: Optional[int] = None - saturation: Optional[int] = None - unmapped_hue: Optional[int] = None - unmapped_saturation: Optional[int] = None - color_temp: Optional[int] = None - color_mode: Optional[str] = None - supported_color_mode: Optional[str] = None + state: bool | None = None + hue: int | None = None + saturation: int | None = None + unmapped_hue: int | None = None + unmapped_saturation: int | None = None + color_temp: int | None = None + color_mode: str | None = None + supported_color_mode: str | None = None fullcolorsupport: bool = False def _update_from_node(self, node: ElementTree.Element) -> None: @@ -116,7 +120,7 @@ def set_state_toggle(self, wait: bool = False) -> None: def get_colors( self, - ) -> dict[str, list[tuple[Optional[str], Optional[str], Optional[str]]]]: + ) -> dict[str, list[tuple[str | None, str | None, str | None]]]: """Get the supported colors.""" if self.has_color: return self._fritz.get_colors(self.ain) @@ -125,7 +129,7 @@ def get_colors( def set_color( self, - hsv: Sequence[Union[str, SupportsInt]], + hsv: Sequence[str | SupportsInt], duration: int = 0, wait: bool = False, ) -> None: @@ -135,7 +139,7 @@ def set_color( def set_unmapped_color( self, - hsv: Sequence[Union[str, SupportsInt]], + hsv: Sequence[str | SupportsInt], duration: int = 0, wait: bool = False, ) -> None: @@ -143,7 +147,7 @@ def set_unmapped_color( if self.has_color and self.fullcolorsupport: self._fritz.set_color(self.ain, hsv, duration, False, wait) - def get_color_temps(self) -> list[Optional[str]]: + def get_color_temps(self) -> list[str | None]: """Get the supported color temperatures energy.""" if self.has_color: return self._fritz.get_color_temps(self.ain) @@ -152,7 +156,7 @@ def get_color_temps(self) -> list[Optional[str]]: def set_color_temp( self, - temperature: Union[str, int, float], + temperature: str | int | float, duration: int = 0, wait: bool = False, ) -> None: diff --git a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py index ffdcf55..e5deb40 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py +++ b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py @@ -1,7 +1,9 @@ """The powermeter device class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,10 +15,10 @@ class FritzhomeDevicePowermeter(FritzhomeDeviceBase): """The Fritzhome Device class.""" - power: Optional[int] = None - energy: Optional[int] = None - voltage: Optional[int] = None - current: Optional[float] = None + power: int | None = None + energy: int | None = None + voltage: int | None = None + current: float | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py index 397fc4e..42a4dcc 100644 --- a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py +++ b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py @@ -1,7 +1,9 @@ """The switch device class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,10 +15,10 @@ class FritzhomeDeviceSwitch(FritzhomeDeviceBase): """The Fritzhome Device class.""" - switch_state: Optional[bool] = None - switch_mode: Optional[str] = None - lock: Optional[bool] = None - device_lock: Optional[bool] = None + switch_state: bool | None = None + switch_mode: str | None = None + lock: bool | None = None + device_lock: bool | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py index 3ff5f4f..d487bc9 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py +++ b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py @@ -1,7 +1,9 @@ """The temperature device class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -13,8 +15,8 @@ class FritzhomeDeviceTemperature(FritzhomeDeviceBase): """The Fritzhome Device class.""" - offset: Optional[float] = None - temperature: Optional[float] = None + offset: float | None = None + temperature: float | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomedevicethermostat.py b/pyfritzhome/devicetypes/fritzhomedevicethermostat.py index 5119921..ab461f5 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicethermostat.py +++ b/pyfritzhome/devicetypes/fritzhomedevicethermostat.py @@ -1,8 +1,10 @@ """The thermostat device class.""" +from __future__ import annotations + import logging import time -from typing import Optional + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase @@ -14,23 +16,23 @@ class FritzhomeDeviceThermostat(FritzhomeDeviceBase): """The Fritzhome Device class.""" - actual_temperature: Optional[float] = None - target_temperature: Optional[float] = None - eco_temperature: Optional[float] = None - comfort_temperature: Optional[float] = None - device_lock: Optional[bool] = None - lock: Optional[bool] = None - error_code: Optional[int] = None - window_open: Optional[bool] = None - window_open_endtime: Optional[float] = None - boost_active: Optional[bool] = None - boost_active_endtime: Optional[float] = None - adaptive_heating_active: Optional[bool] = None - adaptive_heating_running: Optional[bool] = None - summer_active: Optional[bool] = None - holiday_active: Optional[bool] = None - nextchange_endperiod: Optional[int] = None - nextchange_temperature: Optional[float] = None + actual_temperature: float | None = None + target_temperature: float | None = None + eco_temperature: float | None = None + comfort_temperature: float | None = None + device_lock: bool | None = None + lock: bool | None = None + error_code: int | None = None + window_open: bool | None = None + window_open_endtime: float | None = None + boost_active: bool | None = None + boost_active_endtime: float | None = None + adaptive_heating_active: bool | None = None + adaptive_heating_running: bool | None = None + summer_active: bool | None = None + holiday_active: bool | None = None + nextchange_endperiod: int | None = None + nextchange_temperature: float | None = None def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index 702465e..41ae3a8 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -1,9 +1,9 @@ """The entity base class.""" -from __future__ import annotations, print_function -from abc import ABC -from typing import TYPE_CHECKING, Optional, cast +from __future__ import annotations +from abc import ABC +from typing import TYPE_CHECKING, cast import logging from xml.etree import ElementTree @@ -25,8 +25,8 @@ class FritzhomeEntityBase(ABC): def __init__( self, - fritz: Optional["Fritzhome"] = None, - node: Optional[ElementTree.Element] = None, + fritz: "Fritzhome" | None = None, + node: ElementTree.Element | None = None, ) -> None: """Create an entity base object.""" if fritz is not None: @@ -57,7 +57,7 @@ def _update_from_node(self, node: ElementTree.Element) -> None: self.supported_features.append(feature) @property - def device_and_unit_id(self) -> tuple[Optional[str], Optional[str]]: + def device_and_unit_id(self) -> tuple[str | None, str | None]: """Get the device and possible unit id.""" if ( self.ain.startswith("tmp") @@ -73,17 +73,13 @@ def device_and_unit_id(self) -> tuple[Optional[str], Optional[str]]: # XML Helpers - def get_node_value( - self, elem: Optional[ElementTree.Element], node: str - ) -> Optional[str]: + def get_node_value(self, elem: ElementTree.Element | None, node: str) -> str | None: """Get the node value.""" if elem is None: return None return elem.findtext(node) - def get_node_value_as_int( - self, elem: Optional[ElementTree.Element], node: str - ) -> int: + def get_node_value_as_int(self, elem: ElementTree.Element | None, node: str) -> int: """Get the node value as integer.""" value = self.get_node_value(elem, node) if value is None: @@ -91,14 +87,12 @@ def get_node_value_as_int( return int(value) def get_node_value_as_int_as_bool( - self, elem: Optional[ElementTree.Element], node: str + self, elem: ElementTree.Element | None, node: str ) -> bool: """Get the node value as boolean.""" return bool(self.get_node_value_as_int(elem, node)) - def get_temp_from_node( - self, elem: Optional[ElementTree.Element], node: str - ) -> float: + def get_temp_from_node(self, elem: ElementTree.Element | None, node: str) -> float: """Get the node temp value as float.""" value = self.get_node_value(elem, node) if value is None: diff --git a/pyfritzhome/devicetypes/fritzhometemplate.py b/pyfritzhome/devicetypes/fritzhometemplate.py index b2245c4..4722f83 100644 --- a/pyfritzhome/devicetypes/fritzhometemplate.py +++ b/pyfritzhome/devicetypes/fritzhometemplate.py @@ -1,7 +1,9 @@ """The template class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomeentitybase import FritzhomeEntityBase @@ -13,17 +15,17 @@ class FritzhomeTemplate(FritzhomeEntityBase): """The Fritzhome Template class.""" - devices: Optional[list[str]] = None - features: Optional[FritzhomeDeviceFeatures] = None - apply_hkr_summer: Optional[bool] = None - apply_hkr_temperature: Optional[bool] = None - apply_hkr_holidays: Optional[bool] = None - apply_hkr_time_table: Optional[bool] = None - apply_relay_manual: Optional[bool] = None - apply_relay_automatic: Optional[bool] = None - apply_level: Optional[bool] = None - apply_color: Optional[bool] = None - apply_dialhelper: Optional[bool] = None + devices: list[str] | None = None + features: FritzhomeDeviceFeatures | None = None + apply_hkr_summer: bool | None = None + apply_hkr_temperature: bool | None = None + apply_hkr_holidays: bool | None = None + apply_hkr_time_table: bool | None = None + apply_relay_manual: bool | None = None + apply_relay_automatic: bool | None = None + apply_level: bool | None = None + apply_color: bool | None = None + apply_dialhelper: bool | None = None def _update_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update template") diff --git a/pyfritzhome/devicetypes/fritzhometrigger.py b/pyfritzhome/devicetypes/fritzhometrigger.py index 7d354d2..bee3bf5 100644 --- a/pyfritzhome/devicetypes/fritzhometrigger.py +++ b/pyfritzhome/devicetypes/fritzhometrigger.py @@ -1,7 +1,9 @@ """The trigger class.""" +from __future__ import annotations + import logging -from typing import Optional + from xml.etree import ElementTree from .fritzhomeentitybase import FritzhomeEntityBase @@ -12,7 +14,7 @@ class FritzhomeTrigger(FritzhomeEntityBase): """The Fritzhome Trigger class.""" - active: Optional[bool] = None + active: bool | None = None def _update_from_node(self, node: ElementTree.Element) -> None: _LOGGER.debug("update trigger") diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index cfde1e6..9c5833c 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -1,6 +1,10 @@ """The main fritzhome handling class.""" -from __future__ import print_function +from __future__ import annotations + + +from collections.abc import Mapping, Sequence +from typing import SupportsInt, overload import hashlib import json @@ -17,7 +21,6 @@ from .errors import InvalidError, LoginError, NotLoggedInError from .fritzhomedevice import FritzhomeDevice from .devicetypes import FritzhomeTemplate, FritzhomeTrigger -from typing import Dict, Mapping, Optional, Sequence, SupportsInt, Union, overload _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -25,18 +28,18 @@ class Fritzhome(object): """Fritzhome object to communicate with the device.""" - _sid: Optional[str] = None + _sid: str | None = None _session: Session - _devices: Optional[Dict[str, FritzhomeDevice]] = None - _templates: Optional[Dict[str, FritzhomeTemplate]] = None - _triggers: Optional[Dict[str, FritzhomeTrigger]] = None + _devices: dict[str, FritzhomeDevice] | None = None + _templates: dict[str, FritzhomeTemplate] | None = None + _triggers: dict[str, FritzhomeTrigger] | None = None def __init__( self, host: str, user: str, password: str, - port: Optional[int] = None, + port: int | None = None, ssl_verify: bool = True, timeout: int = 10, ) -> None: @@ -56,7 +59,7 @@ def __init__( def _request( self, url: str, - params: Optional[Mapping[str, Union[str, int, None]]] = None, + params: Mapping[str, str | int | None] | None = None, ) -> str: """Send a request with parameters.""" rsp = self._session.get( @@ -66,8 +69,8 @@ def _request( return rsp.text.strip() def _login_request( - self, username: Optional[str] = None, secret: Optional[str] = None - ) -> tuple[Optional[str], Optional[str], int]: + self, username: str | None = None, secret: str | None = None + ) -> tuple[str | None, str | None, int]: """Send a login request with paramerters.""" url = f"{self.base_url}/login_sid.lua?version=2" params = {} @@ -84,7 +87,7 @@ def _login_request( return (sid, challenge, blocktime) - def has_smarthome_capabilities(self) -> Optional[bool]: + def has_smarthome_capabilities(self) -> bool | None: """Check if the device offers smart home capabilities. Tries the TR-064 device description first and, if that could not be @@ -169,8 +172,8 @@ def _create_login_secret_md5(challenge: str, password: str) -> str: def _aha_request( self, cmd: str, - ain: Optional[str] = None, - param: Optional[Mapping[str, Union[str, int]]] = None, + ain: str | None = None, + param: Mapping[str, str | int] | None = None, rf: type[str] = str, ) -> str: ... @@ -178,8 +181,8 @@ def _aha_request( def _aha_request( self, cmd: str, - ain: Optional[str] = None, - param: Optional[Mapping[str, Union[str, int]]] = None, + ain: str | None = None, + param: Mapping[str, str | int] | None = None, rf: type[bool] = bool, ) -> bool: ... @@ -187,8 +190,8 @@ def _aha_request( def _aha_request( self, cmd: str, - ain: Optional[str] = None, - param: Optional[Mapping[str, Union[str, int]]] = None, + ain: str | None = None, + param: Mapping[str, str | int] | None = None, rf: type[int] = int, ) -> int: ... @@ -196,18 +199,18 @@ def _aha_request( def _aha_request( self, cmd: str, - ain: Optional[str] = None, - param: Optional[Mapping[str, Union[str, int]]] = None, + ain: str | None = None, + param: Mapping[str, str | int] | None = None, rf: type[float] = float, ) -> float: ... def _aha_request( self, cmd: str, - ain: Optional[str] = None, - param: Optional[Mapping[str, Union[str, int]]] = None, - rf: Union[type[str], type[bool], type[int], type[float]] = str, - ) -> Union[str, bool, int, float]: + ain: str | None = None, + param: Mapping[str, str | int] | None = None, + rf: type[str] | type[bool] | type[int] | type[float] = str, + ) -> str | bool | int | float: """Send an AHA request.""" url = f"{self.base_url}/webservices/homeautoswitch.lua" @@ -216,7 +219,7 @@ def _aha_request( if not self._sid: raise NotLoggedInError - params: Dict[str, Union[str, int]] = {"switchcmd": cmd, "sid": self._sid} + params: dict[str, str | int] = {"switchcmd": cmd, "sid": self._sid} if param: for key, value in param.items(): params[key] = value @@ -331,7 +334,7 @@ def get_device_elements(self) -> list[ElementTree.Element]: """Get the DOM elements for the device list.""" return self._get_listinfo_elements("device") - def get_device_element(self, ain: str) -> Optional[ElementTree.Element]: + def get_device_element(self, ain: str) -> ElementTree.Element | None: """Get the DOM element for the specified device.""" elements = self.get_device_elements() for element in elements: @@ -343,7 +346,7 @@ def get_devices(self) -> list[FritzhomeDevice]: """Get the list of all known devices.""" return list(self.get_devices_as_dict().values()) - def get_devices_as_dict(self) -> Dict[str, FritzhomeDevice]: + def get_devices_as_dict(self) -> dict[str, FritzhomeDevice]: """Get the list of all known devices.""" if self._devices is None: self.update_devices() @@ -494,7 +497,7 @@ def _get_colordefaults(self, ain: str) -> ElementTree.Element: def get_colors( self, ain: str - ) -> dict[str, list[tuple[Optional[str], Optional[str], Optional[str]]]]: + ) -> dict[str, list[tuple[str | None, str | None, str | None]]]: """Get colors (HSV-space) supported by this lightbulb.""" colordefaults = self._get_colordefaults(ain) colors = {} @@ -512,7 +515,7 @@ def get_colors( def set_color( self, ain: str, - hsv: Sequence[Union[str, SupportsInt]], + hsv: Sequence[str | SupportsInt], duration: int = 0, mapped: bool = True, wait: bool = False, @@ -534,7 +537,7 @@ def set_color( self._aha_request("setunmappedcolor", ain=ain, param=params) wait and self.wait_device_txbusy(ain) - def get_color_temps(self, ain: str) -> list[Optional[str]]: + def get_color_temps(self, ain: str) -> list[str | None]: """Get temperatures supported by this lightbulb.""" colordefaults = self._get_colordefaults(ain) temperatures = [] @@ -545,7 +548,7 @@ def get_color_temps(self, ain: str) -> list[Optional[str]]: def set_color_temp( self, ain: str, - temperature: Union[str, int, float], + temperature: str | int | float, duration: int = 0, wait: bool = False, ) -> None: @@ -625,7 +628,7 @@ def get_templates(self) -> list[FritzhomeTemplate]: """Get the list of all known templates.""" return list(self.get_templates_as_dict().values()) - def get_templates_as_dict(self) -> Dict[str, FritzhomeTemplate]: + def get_templates_as_dict(self) -> dict[str, FritzhomeTemplate]: """Get the list of all known templates.""" if self._templates is None: self.update_templates() @@ -687,7 +690,7 @@ def get_triggers(self) -> list[FritzhomeTrigger]: """Get the list of all known triggers.""" return list(self.get_triggers_as_dict().values()) - def get_triggers_as_dict(self) -> Dict[str, FritzhomeTrigger]: + def get_triggers_as_dict(self) -> dict[str, FritzhomeTrigger]: """Get all known triggers as dictionary.""" if self._triggers is None: self.update_triggers() diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index a2c2f0c..e737225 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -1,6 +1,10 @@ """Toplevel device for pyfritzhome.""" -from typing import Optional, TYPE_CHECKING +from __future__ import annotations + +from typing import TYPE_CHECKING + + from xml.etree import ElementTree from .devicetypes import FritzhomeTemplate # noqa: F401 @@ -40,8 +44,8 @@ class FritzhomeDevice( def __init__( self, - fritz: Optional["Fritzhome"] = None, - node: Optional[ElementTree.Element] = None, + fritz: "Fritzhome" | None = None, + node: ElementTree.Element | None = None, ) -> None: """Create a device object.""" super().__init__(fritz, node)