diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 610034f..41f2ae9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,3 +17,12 @@ repos: rev: v2.2.1 hooks: - id: prettier + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.19.1 + hooks: + - id: mypy + args: [] + additional_dependencies: + - cryptography + - types-requests + files: ^pyfritzhome/.+\.py$ diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index e98af80..2b10a06 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -1,17 +1,19 @@ #!/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 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 +79,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 +169,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 +190,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: 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( @@ -384,23 +389,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..7dad8ea 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicealarm.py +++ b/pyfritzhome/devicetypes/fritzhomedevicealarm.py @@ -1,7 +1,11 @@ """The alarm device class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree + from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,9 +15,9 @@ class FritzhomeDeviceAlarm(FritzhomeDeviceBase): """The Fritzhome Device class.""" - alert_state = None + alert_state: bool | None = 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 +27,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..354ba73 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -1,10 +1,11 @@ """The base device class.""" -from __future__ import print_function - +from __future__ import annotations import logging +from xml.etree import ElementTree + from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase _LOGGER = logging.getLogger(__name__) @@ -13,18 +14,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: 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.""" return "{ain} {identifier} {manuf} {prod} {name}".format( ain=self.ain, @@ -34,11 +35,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 +48,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 +70,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..3972ef4 100644 --- a/pyfritzhome/devicetypes/fritzhomedeviceblind.py +++ b/pyfritzhome/devicetypes/fritzhomedeviceblind.py @@ -1,7 +1,11 @@ """The blind device class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree + from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,9 +15,9 @@ class FritzhomeDeviceBlind(FritzhomeDeviceBase): """The Fritzhome Device class.""" - endpositionsset = None + endpositionsset: bool | None = 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 +27,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 +41,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..28a9544 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebutton.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebutton.py @@ -1,7 +1,10 @@ """The button device class.""" +from __future__ import annotations + import logging + from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -12,7 +15,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 +25,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 +37,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 +45,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: str | None = None + identifier: str | None = None + name: str | None = None + last_pressed: int | None = None - def __init__(self, node=None): + def __init__(self, node: ElementTree.Element | None = 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 +65,15 @@ def _update_from_node(self, node): except ValueError: pass - def get_node_value(self, elem, node): + 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, node) -> int: + def get_node_value_as_int(self, elem: ElementTree.Element | None, 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..ec8bb4f 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py +++ b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py @@ -1,7 +1,11 @@ """The humidity device class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree + from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,9 +15,9 @@ class FritzhomeDeviceHumidity(FritzhomeDeviceBase): """The Fritzhome Device class.""" - rel_humidity = None + rel_humidity: int | None = 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 +27,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..a856bd1 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicelevel.py +++ b/pyfritzhome/devicetypes/fritzhomedevicelevel.py @@ -1,6 +1,9 @@ """The level device class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,10 +14,10 @@ class FritzhomeDeviceLevel(FritzhomeDeviceBase): """The Fritzhome Device class.""" - level = None - levelpercentage = None + level: int | None = None + levelpercentage: int | None = 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 +27,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 +42,20 @@ def _update_level_from_node(self, node): except Exception: pass - def get_level(self): + def get_level(self) -> int | None: """Get the level.""" return self.level - def get_level_percentage(self): + def get_level_percentage(self) -> int | None: """Get the level in percentage.""" return self.levelpercentage - def set_level(self, level, wait=False): + 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, wait=False): + def set_level_percentage( + 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 58a9720..44782a4 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py +++ b/pyfritzhome/devicetypes/fritzhomedevicelightbulb.py @@ -1,6 +1,12 @@ """The light bulb device class.""" +from __future__ import annotations + +from collections.abc import Sequence +from typing import SupportsInt + import logging +from xml.etree import ElementTree from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,17 +17,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: 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): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -31,16 +37,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 +57,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 +103,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[str | None, str | None, str | None]]]: """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[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[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[str | None]: """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: 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..e5deb40 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py +++ b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py @@ -1,7 +1,11 @@ """The powermeter device class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree + from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,12 +15,12 @@ class FritzhomeDevicePowermeter(FritzhomeDeviceBase): """The Fritzhome Device class.""" - power = None - energy = None - voltage = None - current = None + power: int | None = None + energy: int | None = None + voltage: int | None = None + current: float | None = 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 +30,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 +70,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..42a4dcc 100644 --- a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py +++ b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py @@ -1,7 +1,11 @@ """The switch device class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree + from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,11 +15,12 @@ class FritzhomeDeviceSwitch(FritzhomeDeviceBase): """The Fritzhome Device class.""" - switch_state = None - switch_mode = None - lock = 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): + def _update_from_node(self, node: ElementTree.Element) -> None: super()._update_from_node(node) if self.present is False: return @@ -25,7 +30,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 +42,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 +68,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..d487bc9 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py +++ b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py @@ -1,7 +1,11 @@ """The temperature device class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree + from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,10 +15,10 @@ class FritzhomeDeviceTemperature(FritzhomeDeviceBase): """The Fritzhome Device class.""" - offset = None - temperature = None + offset: float | None = None + temperature: float | None = 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 +28,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..ab461f5 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicethermostat.py +++ b/pyfritzhome/devicetypes/fritzhomedevicethermostat.py @@ -1,8 +1,12 @@ """The thermostat device class.""" +from __future__ import annotations + import logging import time +from xml.etree import ElementTree + from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -12,25 +16,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: 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) if self.present is False: return @@ -40,13 +44,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 +129,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 +169,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 +184,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..41ae3a8 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -1,8 +1,9 @@ """The entity base class.""" -from __future__ import print_function -from abc import ABC +from __future__ import annotations +from abc import ABC +from typing import TYPE_CHECKING, cast import logging from xml.etree import ElementTree @@ -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: "Fritzhome" | None = None, + node: ElementTree.Element | None = 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[str | None, str | None]: """Get the device and possible unit id.""" if ( self.ain.startswith("tmp") @@ -60,23 +68,33 @@ 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: 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, node) -> int: + def get_node_value_as_int(self, elem: ElementTree.Element | None, 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: 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, node): + def get_temp_from_node(self, elem: ElementTree.Element | None, 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..4722f83 100644 --- a/pyfritzhome/devicetypes/fritzhometemplate.py +++ b/pyfritzhome/devicetypes/fritzhometemplate.py @@ -1,7 +1,11 @@ """The template class.""" +from __future__ import annotations + import logging +from xml.etree import ElementTree + from .fritzhomeentitybase import FritzhomeEntityBase from .fritzhomedevicefeatures import FritzhomeDeviceFeatures @@ -11,25 +15,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: 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") 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 +46,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..bee3bf5 100644 --- a/pyfritzhome/devicetypes/fritzhometrigger.py +++ b/pyfritzhome/devicetypes/fritzhometrigger.py @@ -1,6 +1,9 @@ """The trigger class.""" +from __future__ import annotations + import logging + from xml.etree import ElementTree from .fritzhomeentitybase import FritzhomeEntityBase @@ -11,11 +14,11 @@ class FritzhomeTrigger(FritzhomeEntityBase): """The Fritzhome Trigger class.""" - active = None + active: bool | None = 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..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 @@ -16,37 +20,47 @@ 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 -_LOGGER = logging.getLogger(__name__) +_LOGGER: logging.Logger = logging.getLogger(__name__) class Fritzhome(object): """Fritzhome object to communicate with the device.""" - _sid = None - _session = None - _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): + _sid: str | None = None + _session: Session + _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: int | None = 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: Mapping[str, str | int | None] | None = None, + ) -> str: """Send a request with parameters.""" rsp = self._session.get( url, params=params, timeout=self._timeout, verify=self._ssl_verify @@ -54,7 +68,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: 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 = {} @@ -66,12 +82,12 @@ 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) - 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 @@ -115,7 +131,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 +140,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 +162,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: str | None = None, + param: Mapping[str, str | int] | None = None, + rf: type[str] = str, + ) -> str: ... + + @overload + def _aha_request( + self, + cmd: str, + ain: str | None = None, + param: Mapping[str, str | int] | None = None, + rf: type[bool] = bool, + ) -> bool: ... + + @overload + def _aha_request( + self, + cmd: str, + ain: str | None = None, + param: Mapping[str, str | int] | None = None, + rf: type[int] = int, + ) -> int: ... + + @overload + def _aha_request( + self, + cmd: str, + ain: str | None = None, + param: Mapping[str, str | int] | None = None, + rf: type[float] = float, + ) -> float: ... + + def _aha_request( + self, + cmd: str, + 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" @@ -161,9 +219,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, 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 +234,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 +257,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 +290,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 +313,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 +330,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) -> ElementTree.Element | None: """Get the DOM element for the specified device.""" elements = self.get_device_elements() for element in elements: @@ -278,75 +342,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 +425,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 +434,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 +481,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 +491,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[str | None, str | None, str | None]]]: """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[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 +537,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[str | None]: """Get temperatures supported by this lightbulb.""" colordefaults = self._get_colordefaults(ain) temperatures = [] @@ -466,7 +545,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: str | int | float, + duration: int = 0, + wait: bool = False, + ) -> None: """Set color temperature. temperature: temperature element obtained from get_temperatures() @@ -478,27 +563,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 +592,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 +620,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 +654,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 +682,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..e737225 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -1,5 +1,12 @@ """Toplevel device for pyfritzhome.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + + +from xml.etree import ElementTree + from .devicetypes import FritzhomeTemplate # noqa: F401 from .devicetypes import FritzhomeTrigger # noqa: F401 from .devicetypes import ( @@ -16,6 +23,9 @@ FritzhomeDeviceThermostat, ) +if TYPE_CHECKING: + from .fritzhome import Fritzhome + class FritzhomeDevice( FritzhomeDeviceAlarm, @@ -32,9 +42,13 @@ class FritzhomeDevice( ): """The Fritzhome Device class.""" - def __init__(self, fritz=None, node=None): + def __init__( + self, + fritz: "Fritzhome" | None = None, + node: ElementTree.Element | None = 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