diff --git a/pyfritzhome/__init__.py b/pyfritzhome/__init__.py index 312fead..be0ac19 100644 --- a/pyfritzhome/__init__.py +++ b/pyfritzhome/__init__.py @@ -6,7 +6,7 @@ from .fritzhome import Fritzhome from .fritzhomedevice import FritzhomeDevice -__version__ = version(__name__) +__version__ = "0.6.18" #version(__name__) __all__ = ( "Fritzhome", diff --git a/pyfritzhome/cli.py b/pyfritzhome/cli.py index 9f1aae4..e9f3e24 100644 --- a/pyfritzhome/cli.py +++ b/pyfritzhome/cli.py @@ -20,15 +20,12 @@ def list_all(fritz, args): print("#" * 30) print("name=%s" % device.name) print(" ain=%s" % device.ain) - print(" id=%s" % device.identifier) print(" productname=%s" % device.productname) print(" manufacturer=%s" % device.manufacturer) print(" present=%s" % device.present) - print(" lock=%s" % device.lock) - print(" devicelock=%s" % device.device_lock) - print(" is_group=%s" % device.is_group) - if device.is_group: - print(" group_members=%s" % device.group_members) + #~ print(" is_group=%s" % device.is_group) + #~ if device.is_group: + #~ print(" group_members=%s" % device.group_members) if device.present is False: continue @@ -41,41 +38,45 @@ def list_all(fritz, args): print(" power=%s" % device.power) print(" energy=%s" % device.energy) print(" voltage=%s" % device.voltage) + print(" current=%s" % device.current) if device.has_temperature_sensor: print(" Temperature:") print(" temperature=%s" % device.temperature) print(" offset=%s" % device.offset) - if device.has_thermostat: - print(" Thermostat:") - print(" battery_low=%s" % device.battery_low) - print(" battery_level=%s" % device.battery_level) - print(" actual=%s" % device.actual_temperature) - print(" target=%s" % device.target_temperature) - print(" comfort=%s" % device.comfort_temperature) - print(" eco=%s" % device.eco_temperature) - print(" window=%s" % device.window_open) - print(" window_until=%s" % device.window_open_endtime) - print(" boost=%s" % device.boost_active) - print(" boost_until=%s" % device.boost_active_endtime) - print(" adaptive_heating_running=%s" % device.adaptive_heating_running) - print(" summer=%s" % device.summer_active) - print(" holiday=%s" % device.holiday_active) - if device.has_alarm: - print(" Alert:") - print(" alert=%s" % device.alert_state) - if device.has_lightbulb: - print(" Light bulb:") - print(" state=%s" % ("Off" if device.state == 0 else "On")) - if device.has_level: - print(" level=%s" % device.level) - if device.has_color: - print(" hue=%s" % device.hue) - print(" saturation=%s" % device.saturation) - if device.has_blind: - print(" Blind:") - print(" level=%s" % device.level) - print(" levelpercentage=%s" % device.levelpercentage) - print(" endpositionset=%s" % device.endpositionsset) + if device.has_humidity_sensor: + print(" Humidity:") + print(" relative_humidity=%s" % device.rel_humidity) + #~ if device.has_thermostat: + #~ print(" Thermostat:") + #~ print(" battery_low=%s" % device.battery_low) + #~ print(" battery_level=%s" % device.battery_level) + #~ print(" actual=%s" % device.actual_temperature) + #~ print(" target=%s" % device.target_temperature) + #~ print(" comfort=%s" % device.comfort_temperature) + #~ print(" eco=%s" % device.eco_temperature) + #~ print(" window=%s" % device.window_open) + #~ print(" window_until=%s" % device.window_open_endtime) + #~ print(" boost=%s" % device.boost_active) + #~ print(" boost_until=%s" % device.boost_active_endtime) + #~ print(" adaptive_heating_running=%s" % device.adaptive_heating_running) + #~ print(" summer=%s" % device.summer_active) + #~ print(" holiday=%s" % device.holiday_active) + #~ if device.has_alarm: + #~ print(" Alert:") + #~ print(" alert=%s" % device.alert_state) + #~ if device.has_lightbulb: + #~ print(" Light bulb:") + #~ print(" state=%s" % ("Off" if device.state == 0 else "On")) + #~ if device.has_level: + #~ print(" level=%s" % device.level) + #~ if device.has_color: + #~ print(" hue=%s" % device.hue) + #~ print(" saturation=%s" % device.saturation) + #~ if device.has_blind: + #~ print(" Blind:") + #~ print(" level=%s" % device.level) + #~ print(" levelpercentage=%s" % device.levelpercentage) + #~ print(" endpositionset=%s" % device.endpositionsset) def device_name(fritz, args): @@ -202,6 +203,9 @@ def main(args=None): parser.add_argument( "-v", action="store_true", dest="verbose", help="be more verbose" ) + parser.add_argument( + "-A", "--aha", action="store_true", dest="aha_api", help="Use legacy AHA API" + ) parser.add_argument( "-f", "--fritzbox", @@ -234,6 +238,12 @@ def main(args=None): version="{version}".format(version=__version__), help="Print version", ) + parser.add_argument( + "--test-data", + action="store_true", + dest="testdata", + help="Use offline test data" + ) _sub = parser.add_subparsers(title="Commands") @@ -399,6 +409,8 @@ def main(args=None): password=args.password, port=args.port or None, ssl_verify=not args.insecure, + force_aha_api=args.aha_api, + use_testdata=args.testdata ) fritzbox.login() args.func(fritzbox, args) diff --git a/pyfritzhome/devicetypes/__init__.py b/pyfritzhome/devicetypes/__init__.py index c84fb3b..a66c529 100644 --- a/pyfritzhome/devicetypes/__init__.py +++ b/pyfritzhome/devicetypes/__init__.py @@ -1,5 +1,6 @@ """Init file for the device types.""" +from .fritzhomedevicebase import FritzhomeDeviceBase from .fritzhomedevicealarm import FritzhomeDeviceAlarm from .fritzhomedevicebutton import FritzhomeDeviceButton from .fritzhomedevicehumidity import FritzhomeDeviceHumidity @@ -13,9 +14,11 @@ from .fritzhomedeviceblind import FritzhomeDeviceBlind from .fritzhometemplate import FritzhomeTemplate from .fritzhometrigger import FritzhomeTrigger +from .fritzhomeunit import FritzhomeUnit __all__ = ( + "FritzhomeDeviceBase", "FritzhomeDeviceAlarm", "FritzhomeDeviceButton", "FritzhomeDeviceHumidity", @@ -29,4 +32,5 @@ "FritzhomeDeviceBlind", "FritzhomeTemplate", "FritzhomeTrigger", + "FritzhomeUnit", ) diff --git a/pyfritzhome/devicetypes/fritzhomedevicebase.py b/pyfritzhome/devicetypes/fritzhomedevicebase.py index 3ebabc9..4f56429 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -3,7 +3,6 @@ from __future__ import print_function - import logging from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase @@ -14,59 +13,60 @@ 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 + product_name = None + is_connected = None def __repr__(self): """Return a string.""" - return "{ain} {identifier} {manuf} {prod} {name}".format( + return "{ain} {manuf} {prod} {name}".format( ain=self.ain, - identifier=self.identifier, manuf=self.manufacturer, prod=self.productname, name=self.name, ) - def update(self): - """Update the device values.""" - self._fritz.update_devices() - def _update_from_node(self, node): _LOGGER.debug("update base device") super()._update_from_node(node) - self.ain = node.attrib["identifier"] - self.identifier = node.attrib["id"] - self.fw_version = node.attrib["fwversion"] - self.manufacturer = node.attrib["manufacturer"] - self.productname = node.attrib["productname"] - - self.present = bool(int(node.findtext("present"))) - - groupinfo = node.find("groupinfo") - self.is_group = groupinfo is not None - if self.is_group: - self.group_members = str(groupinfo.findtext("members")).split(",") - - try: - self.tx_busy = self.get_node_value_as_int_as_bool(node, "txbusy") - except Exception: - pass - - try: - self.battery_low = self.get_node_value_as_int_as_bool(node, "batterylow") - self.battery_level = int(self.get_node_value_as_int(node, "battery")) - except Exception: - pass - - # General - def get_present(self): - """Check if the device is present.""" - return self._fritz.get_device_present(self.ain) + self._units = {} + if self._fritz._use_aha: + self.manufacturer = node.attrib["manufacturer"] + self.product_name = node.attrib["productname"] + self.is_connected = self.get_node_value_as_int_as_bool(node, "present") + else: + self.manufacturer = self._node["manufacturer"] + self.product_name = self._node["productName"] + self.is_connected = self._node["isConnected"] + + def find_interface(self, interface): + for unit in self._units.values(): + if interface := unit.find_interface(interface): + return interface + return None + + def get_config(): + self._fritz.update_device_config(self.ain) + + # legacy + @property + def productname(self): + return self.product_name + + # legacy + @property + def present(self): + return self.is_connected + + def clear_units(self): + self._units = {} + + def add_or_update_unit(self, unit): + self._units[unit.ain] = unit + + # with aha, there are no units and interfaces. Emulated interfaces become directly attached + def add_or_update_unit(self, unit): + self._units[unit.ain] = unit + + def units(self): + return self._units.values() diff --git a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py b/pyfritzhome/devicetypes/fritzhomedevicehumidity.py deleted file mode 100644 index fe56ec0..0000000 --- a/pyfritzhome/devicetypes/fritzhomedevicehumidity.py +++ /dev/null @@ -1,39 +0,0 @@ -"""The humidity device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDeviceHumidity(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - rel_humidity = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_humidity_sensor: - self._update_humidity_from_node(node) - - # Humidity - @property - def has_humidity_sensor(self): - """Check if the device has humidity function.""" - return self._has_feature(FritzhomeDeviceFeatures.HUMIDITY) - - def _update_humidity_from_node(self, node): - _LOGGER.debug("update humidity device") - humidity_element = node.find("humidity") - try: - self.rel_humidity = self.get_node_value_as_int( - humidity_element, "rel_humidity" - ) - except ValueError: - pass diff --git a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py b/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py deleted file mode 100644 index a4e651a..0000000 --- a/pyfritzhome/devicetypes/fritzhomedevicepowermeter.py +++ /dev/null @@ -1,68 +0,0 @@ -"""The powermeter device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDevicePowermeter(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - power = None - energy = None - voltage = None - current = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_powermeter: - self._update_powermeter_from_node(node) - - # Power Meter - @property - def has_powermeter(self): - """Check if the device has powermeter function.""" - return self._has_feature(FritzhomeDeviceFeatures.POWER_METER) - - def _update_powermeter_from_node(self, node): - _LOGGER.debug("update powermeter device") - val = node.find("powermeter") - - try: - self.power = int(val.findtext("power")) - except Exception: - pass - - try: - self.energy = int(val.findtext("energy")) - except Exception: - pass - - try: - self.voltage = int(val.findtext("voltage")) - except Exception: - pass - - if ( - isinstance(self.power, int) - and isinstance(self.voltage, int) - and self.voltage > 0 - ): - self.current = self.power / self.voltage * 1000 - else: - self.current = None - - def get_switch_power(self): - """Get the switch state.""" - return self._fritz.get_switch_power(self.ain) - - def get_switch_energy(self): - """Get the switch energy.""" - return self._fritz.get_switch_energy(self.ain) diff --git a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py b/pyfritzhome/devicetypes/fritzhomedeviceswitch.py deleted file mode 100644 index 92f1de3..0000000 --- a/pyfritzhome/devicetypes/fritzhomedeviceswitch.py +++ /dev/null @@ -1,81 +0,0 @@ -"""The switch device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDeviceSwitch(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - switch_state = None - switch_mode = None - lock = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_switch: - self._update_switch_from_node(node) - - # Switch - @property - def has_switch(self): - """Check if the device has switch function.""" - if self._has_feature(FritzhomeDeviceFeatures.SWITCH): - # for AVM plugs like FRITZ!DECT 200 and FRITZ!DECT 210 - return True - if self._has_feature( - FritzhomeDeviceFeatures.SWITCHABLE - ) and not self._has_feature(FritzhomeDeviceFeatures.LIGHTBULB): - # for HAN-FUN plugs - return True - return False - - def _update_switch_from_node(self, node): - _LOGGER.debug("update switch device") - if self._has_feature(FritzhomeDeviceFeatures.SWITCH): - val = node.find("switch") - try: - self.switch_state = self.get_node_value_as_int_as_bool(val, "state") - except Exception: - self.switch_state = None - self.switch_mode = self.get_node_value(val, "mode") - try: - self.lock = self.get_node_value_as_int_as_bool(val, "lock") - except Exception: - self.lock = None - - # optional value - try: - self.device_lock = self.get_node_value_as_int_as_bool(val, "devicelock") - except Exception: - pass - else: - val = node.find("simpleonoff") - try: - self.switch_state = self.get_node_value_as_int_as_bool(val, "state") - except Exception: - self.switch_state = None - - def get_switch_state(self): - """Get the switch state.""" - return self._fritz.get_switch_state(self.ain) - - def set_switch_state_on(self, wait=False): - """Set the switch state to on.""" - return self._fritz.set_switch_state_on(self.ain, wait) - - def set_switch_state_off(self, wait=False): - """Set the switch state to off.""" - return self._fritz.set_switch_state_off(self.ain, wait) - - def set_switch_state_toggle(self, wait=False): - """Toggle the switch state.""" - return self._fritz.set_switch_state_toggle(self.ain, wait) diff --git a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py b/pyfritzhome/devicetypes/fritzhomedevicetemperature.py deleted file mode 100644 index 9ca1c4e..0000000 --- a/pyfritzhome/devicetypes/fritzhomedevicetemperature.py +++ /dev/null @@ -1,47 +0,0 @@ -"""The temperature device class.""" -# -*- coding: utf-8 -*- - -import logging - -from .fritzhomedevicebase import FritzhomeDeviceBase -from .fritzhomedevicefeatures import FritzhomeDeviceFeatures - -_LOGGER = logging.getLogger(__name__) - - -class FritzhomeDeviceTemperature(FritzhomeDeviceBase): - """The Fritzhome Device class.""" - - offset = None - temperature = None - - def _update_from_node(self, node): - super()._update_from_node(node) - if self.present is False: - return - - if self.has_temperature_sensor: - self._update_temperature_from_node(node) - - # Temperature - @property - def has_temperature_sensor(self): - """Check if the device has temperature function.""" - return self._has_feature(FritzhomeDeviceFeatures.TEMPERATURE) - - def _update_temperature_from_node(self, node): - _LOGGER.debug("update temperature device") - temperature_element = node.find("temperature") - try: - self.offset = ( - self.get_node_value_as_int(temperature_element, "offset") / 10.0 - ) - except ValueError: - pass - - try: - self.temperature = ( - self.get_node_value_as_int(temperature_element, "celsius") / 10.0 - ) - except ValueError: - pass diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index c6d8de1..092e4c7 100644 --- a/pyfritzhome/devicetypes/fritzhomeentitybase.py +++ b/pyfritzhome/devicetypes/fritzhomeentitybase.py @@ -7,7 +7,7 @@ import logging -from xml.etree import ElementTree +import json from .fritzhomedevicefeatures import FritzhomeDeviceFeatures _LOGGER = logging.getLogger(__name__) @@ -16,54 +16,41 @@ class FritzhomeEntityBase(ABC): """The Fritzhome Entity class.""" - _fritz = None - ain: str - _functionsbitmask: int = 0 - supported_features = None - def __init__(self, fritz=None, node=None): """Create an entity base object.""" - if fritz is not None: - self._fritz = fritz + self._fritz = fritz + self._node = node + self.ain = None + self.name = None + self._functionsbitmask = 0 if node is not None: self._update_from_node(node) def __repr__(self): """Return a string.""" - return "{ain} {name}".format( - ain=self.ain, - name=self.name, - ) + return f"{self.ain} {self.name}" def _has_feature(self, feature: FritzhomeDeviceFeatures) -> bool: return feature in FritzhomeDeviceFeatures(self._functionsbitmask) def _update_from_node(self, node): - _LOGGER.debug(ElementTree.tostring(node)) - self.ain = node.attrib["identifier"] - self._functionsbitmask = int(node.attrib["functionbitmask"]) - - self.name = node.findtext("name").strip() - - self.supported_features = [] - for feature in FritzhomeDeviceFeatures: - if self._has_feature(feature): - self.supported_features.append(feature) + self._node = node + if self._fritz._use_aha: + if self.ain is not None and self.ain != node.attrib["identifier"]: + raise ValueError("updating invalid ain") + self.ain = node.attrib["identifier"] + self.name = self.get_node_value(node, "name") + self._functionsbitmask = int(node.attrib["functionbitmask"]) + else: + if self.ain is not None and self.ain != node["ain"]: + raise ValueError("updating invalid ain") + self.ain = node["ain"] + self.name = node["name"] @property - def device_and_unit_id(self): - """Get the device and possible unit id.""" - if ( - self.ain.startswith("tmp") - or self.ain.startswith("grp") - or self.ain.startswith("trg") - ): - return (self.ain, None) - elif self.ain.startswith("Z") and len(self.ain) == 19: - return (self.ain[0:17], self.ain[17:]) - elif "-" in self.ain: - return tuple(self.ain.split("-")) - return (self.ain, None) + def node(self): + return self._node; + # XML Helpers @@ -82,3 +69,4 @@ def get_node_value_as_int_as_bool(self, elem, node) -> bool: def get_temp_from_node(self, elem, node): """Get the node temp value as float.""" return float(self.get_node_value(elem, node)) / 2 + return x diff --git a/pyfritzhome/devicetypes/fritzhomeunit.py b/pyfritzhome/devicetypes/fritzhomeunit.py new file mode 100644 index 0000000..d7d5ce2 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeunit.py @@ -0,0 +1,25 @@ +"""The base device class.""" +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import logging + +from .fritzhomeunitbase import FritzhomeUnitBase +from .. import interfaces + +_LOGGER = logging.getLogger(__name__) + +class FritzhomeUnit(FritzhomeUnitBase, + interfaces.FritzhomeOnOffMixin, + interfaces.FritzhomeMultimeterMixin, + interfaces.FritzhomeTemperatureMixin, + interfaces.FritzhomeHumidityMixin): + """The Fritzhome Device class.""" + + def __init__(self, fritz=None, node=None): + """Create a device object.""" + super().__init__(fritz, node) + + def _update_from_node(self, node): + super()._update_from_node(node) diff --git a/pyfritzhome/devicetypes/fritzhomeunitbase.py b/pyfritzhome/devicetypes/fritzhomeunitbase.py new file mode 100644 index 0000000..0c04540 --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -0,0 +1,72 @@ +"""The base device class.""" +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import logging + +from .fritzhomeentitybase import FritzhomeEntityBase +from .. import interfaces + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeUnitBase(FritzhomeEntityBase): + """The Fritzhome Device class.""" + + interfaces = None + + def __repr__(self): + """Return a string.""" + return f"{self.ain} of device {self.parent}" + + def fetch(self): + """Update the device values.""" + # TODO: update specific unit (targeted REST endpoint) + self._fritz.update_units() + + def _update_from_node(self, node): + super()._update_from_node(node) + _LOGGER.debug("update unit base") + if self.ain != node["ain"]: + raise ValueError + + # unshare class attribute on write + self.interfaces = {} + for iface, node in node["interfaces"].items(): + self.interfaces[iface] = interfaces.FritzhomeInterface(self, iface, node) + + def update_interface(self, interface): + self._fritz.put_unit(self.ain, {"interfaces": {interface.type: interface._node}}) + + def find_interface(self, interface): + return self.interfaces.get(interface); + + @property + def parent(self): + return self._node["parentUid"] + + @property + def device(self): + return self._node["deviceUid"] + + @property + def is_connected(self): + return self._node["isConnected"] + + @property + def is_group(self): + return self._node["isGroupUnit"] + + @property + def statistics(self): + return self._node["statistics"] + + @property + def unit_type(self): + return self._node["unitType"] + + # General + def get_present(self): + """Check if the unit is present.""" + return self.is_connected diff --git a/pyfritzhome/fritzhome.py b/pyfritzhome/fritzhome.py index 0c2ca1b..6a8358f 100644 --- a/pyfritzhome/fritzhome.py +++ b/pyfritzhome/fritzhome.py @@ -6,6 +6,7 @@ import hashlib import logging import time +import json from xml.etree import ElementTree from cryptography.hazmat.primitives import hashes @@ -14,9 +15,12 @@ from requests import exceptions, Session from .errors import InvalidError, LoginError, NotLoggedInError -from .fritzhomedevice import FritzhomeDevice +from .fritzhomedevice import FritzhomeDeviceAHA +from .fritzhomedevice import FritzhomeDeviceREST +from .fritzhomedevice import FritzhomeUnit from .fritzhomedevice import FritzhomeTemplate from .fritzhomedevice import FritzhomeTrigger +from .fritzhomedevice import get_device_class from typing import Dict, Optional _LOGGER = logging.getLogger(__name__) @@ -27,11 +31,12 @@ class Fritzhome(object): _sid = None _session = None - _devices: Optional[Dict[str, FritzhomeDevice]] = None + _units: Dict[str, FritzhomeUnit] + _devices: Dict[str, FritzhomeDeviceREST | FritzhomeDeviceAHA] _templates: Optional[Dict[str, FritzhomeTemplate]] = None _triggers: Optional[Dict[str, FritzhomeTrigger]] = None - def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10): + def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10, force_aha_api=False, use_testdata=False): """Create a fritzhome object.""" self._user = user self._password = password @@ -40,19 +45,47 @@ def __init__(self, host, user, password, port=None, ssl_verify=True, timeout=10) self._timeout = timeout self._has_getdeviceinfos = True self._has_txbusy = True + self._devices = {} + self._units = {} + self._use_aha = force_aha_api + self._use_testdata = use_testdata + get_device_class(self._use_aha) if host.startswith("https://") or host.startswith("http://"): self.base_url = f"{host}:{port}" if port else host else: self.base_url = f"http://{host}:{port}" if port else f"http://{host}" + self.rest_url = f"{self.base_url}/api/v0/smarthome" - def _request(self, url, params=None): + def _request(self, url, params=None, headers=None): """Send a request with parameters.""" rsp = self._session.get( - url, params=params, timeout=self._timeout, verify=self._ssl_verify + url, params=params, headers=headers, timeout=self._timeout, verify=self._ssl_verify ) rsp.raise_for_status() return rsp.text.strip() + def _request2(self, url, params=None, headers=None, timeout=10): + """Send a request with parameters.""" + rsp = self._session.get( + url, params=params, headers=headers, timeout=timeout, verify=self._ssl_verify + ) + rsp.raise_for_status() + return rsp + + def _put(self, url, data, params=None, headers=None, timeout=10): + """Send a request with parameters.""" + rsp = self._session.put( + url, params=params, headers=headers, timeout=timeout, verify=self._ssl_verify, + json=data + ) + try: + rsp.raise_for_status() + return rsp.text.strip() + except exceptions.HTTPError as e: + _LOGGER.warning(e) + _LOGGER.warning("Error response: " + rsp.text) + return None + def _login_request(self, username=None, secret=None): """Send a login request with paramerters.""" url = f"{self.base_url}/login_sid.lua?version=2" @@ -109,6 +142,7 @@ def _create_login_secret_md5(challenge, password): def _aha_request(self, cmd, ain=None, param=None, rf=str): """Send an AHA request.""" + _LOGGER.debug("HTTP request using AHA API") url = f"{self.base_url}/webservices/homeautoswitch.lua" _LOGGER.debug("self._sid:%s", self._sid) @@ -130,8 +164,32 @@ def _aha_request(self, cmd, ain=None, param=None, rf=str): return bool(int(plain)) return rf(plain) + def _rest_request(self, endpoint, param=None): + """Send an REST API request""" + _LOGGER.debug("HTTP request using REST API") + if self._use_testdata: + return json.load(open(f"testdata/{endpoint.replace("/", "_")}.json.txt", "r")) + url = f"{self.rest_url}/{endpoint}" + + _LOGGER.debug("self._sid:%s", self._sid) + + if not self._sid: + raise NotLoggedInError + + params = {"Authorization": f"AVM-SID {self._sid}"} + if param: + params.update(param) + + response = self._request2(url, headers=params) + if response.ok: + return response.json() + + return None + def login(self): """Login and get a valid session ID.""" + if self._use_testdata: + return (sid, challenge, blocktime) = self._login_request() _LOGGER.info("sid:%s, challenge:%s, blocktime:%s", sid, challenge, blocktime) if sid == "0000000000000000": @@ -156,23 +214,114 @@ def logout(self): self._logout_request() self._sid = None + def _update_unit_from_node(self, node): + ain = node["ain"] + if unit := self._units.get(ain): + _LOGGER.info("Updating already existing unit " + ain) + unit._update_from_node(node) + else: + _LOGGER.info("Adding new unit " + ain) + unit = FritzhomeUnit(self, node=node) + self._units[ain] = unit + return unit + + def put_unit(self, ain, node): + if self._units is None: + self._units = {} + + _LOGGER.info("put units ...\n" + json.dumps(node)) + params = {"Authorization": f"AVM-SID {self._sid}"} + data = self._put(f"{self.rest_url}/configuration/units/{ain}", node, headers=params) + + def _update_device_from_node(self, node): + ain = node["ain"] + if device := self._devices.get(ain): + _LOGGER.info("Updating already existing device " + ain) + device._update_from_node(node) + else: + _LOGGER.info("Adding new device " + ain) + device = FritzhomeDeviceREST(self, node=node) + self._devices[ain] = device + return device + + def _update_device_units(self, ain, unit_ains): + if self._units is None: + self._units = {} + + for unit_ain in unit_ains: + self._update_unit_from_node(self.get_unit_element(unit_ain)) + if unit := self._units.get(unit_ain): + self._devices[ain].add_or_update_unit(unit) + else: + _LOGGER.warning(f"Unknown unit {unit_ain}") + + def _update_device_config(self, ain): + """Update the device, using its configuration endpoint.""" + _LOGGER.info(f"Updating Device {ain} ...") + device = None + units = [] + if node := self._rest_request(f"configuration/devices/{ain}"): + units = node.pop("units") + device = self._update_device_from_node(node) + for node in units: + unit = self._update_unit_from_node(node) + device.add_or_update_unit(unit) + return device + def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") - if self._devices is None: - self._devices = {} + if self._use_aha: + for element in self._get_listinfo_elements("device"): + if element.attrib["identifier"] in self._devices.keys(): + _LOGGER.info( + "Updating already existing Device " + element.attrib["identifier"] + ) + self._devices[element.attrib["identifier"]]._update_from_node(element) + else: + _LOGGER.info("Adding new Device " + element.attrib["identifier"]) + device = FritzhomeDeviceAHA(self, node=element) + self._devices[device.ain] = device + else: + devices = self._rest_request("overview/devices") + for node in devices: + self._update_device_from_node(node) + units = self._rest_request("overview/units") + for node in units: + self._update_unit_from_node(node) + + for device in self._devices.values(): + units = device.node["unitUids"] + for unit_ain in units: + if unit := self._units.get(unit_ain): + device.add_or_update_unit(unit) + + if not ignore_removed: + for ain in list(self._devices.keys()): + if ain not in [ + element.attrib["ain"] for element in devices + ]: + _LOGGER.info("Removing no more existing device " + ain) + self._devices.pop(ain) + for ain in list(self._units.keys()): + if ain not in [ + element.attrib["ain"] for element in units + ]: + _LOGGER.info("Removing no more existing device " + ain) + self._units.pop(ain) + return True + + def update_units_devices(self, ignore_removed=True, with_units=False): + """Update the device.""" + _LOGGER.info("Updating Devices ...") device_elements = self.get_device_elements() for element in device_elements: - if element.attrib["identifier"] in self._devices.keys(): - _LOGGER.info( - "Updating already existing Device " + element.attrib["identifier"] - ) - self._devices[element.attrib["identifier"]]._update_from_node(element) - else: - _LOGGER.info("Adding new Device " + element.attrib["identifier"]) - device = FritzhomeDevice(self, node=element) - self._devices[device.ain] = device + ain = element["ain"] + self._update_device_from_node(element) + if with_units: + self._devices[ain].clear_units() + self._update_device_units(ain, element["unitUids"]) if not ignore_removed: for identifier in list(self._devices.keys()): @@ -188,7 +337,6 @@ def _get_listinfo_elements(self, entity_type): """Get the DOM elements for the entity list.""" plain = self._aha_request("get" + entity_type + "listinfos") dom = ElementTree.fromstring(plain) - _LOGGER.debug(dom) return dom.findall("*") def wait_device_txbusy(self, ain, retries=10): @@ -222,16 +370,8 @@ def wait_device_txbusy(self, ain, retries=10): return False def get_device_elements(self): - """Get the DOM elements for the device list.""" - return self._get_listinfo_elements("device") - - def get_device_element(self, ain): - """Get the DOM element for the specified device.""" - elements = self.get_device_elements() - for element in elements: - if element.attrib["identifier"] == ain: - return element - return None + """Get the JSON elements for the device list.""" + return self._rest_request("overview/devices") def get_devices(self): """Get the list of all known devices.""" @@ -239,7 +379,7 @@ def get_devices(self): def get_devices_as_dict(self): """Get the list of all known devices.""" - if self._devices is None: + if not self._devices: self.update_devices() return self._devices @@ -253,45 +393,89 @@ def get_device_infos(self, ain): def get_device_present(self, ain): """Get the device presence.""" - return self._aha_request("getswitchpresent", ain=ain, rf=bool) + if self._use_aha: + return self._aha_request("getswitchpresent", ain=ain) + return self._update_device_config(ain).is_connected def get_device_name(self, ain): """Get the device name.""" - return self._aha_request("getswitchname", ain=ain) + if self._use_aha: + return self._aha_request("getswitchname", ain=ain) + return self._update_device_config(ain).name def get_switch_state(self, ain): """Get the switch state.""" - return self._aha_request("getswitchstate", ain=ain, rf=bool) + if self._use_aha: + return self._aha_request("getswitchstate", ain=ain, rf=bool) + if dev := self._update_device_config(ain): + if not dev.has_switch: + _LOGGER.error(f"Device {dev.name} is not a switch") + return None + return dev.switch_state + + def _switch_action(self, ain, action): + if dev := self._update_device_config(ain): + if dev.has_switch: + action(dev) + return dev.switch_state + _LOGGER.error(f"Device {dev.name} is not a switch") + else: + _LOGGER.error(f"Device {ain} not found") + return None def set_switch_state_on(self, ain, wait=False): """Set the switch to on state.""" - result = self._aha_request("setswitchon", ain=ain, rf=bool) - wait and self.wait_device_txbusy(ain) - return result + if self._use_aha: + result = self._aha_request("setswitchon", ain=ain, rf=bool) + wait and self.wait_device_txbusy(ain) + return result + return self._switch_action(ain, lambda dev: dev.set_switch_state_on()) def set_switch_state_off(self, ain, wait=False): """Set the switch to off state.""" - result = self._aha_request("setswitchoff", ain=ain, rf=bool) - wait and self.wait_device_txbusy(ain) - return result + if self._use_aha: + result = self._aha_request("setswitchoff", ain=ain, rf=bool) + wait and self.wait_device_txbusy(ain) + return result + return self._switch_action(ain, lambda dev: dev.set_switch_state_off()) def set_switch_state_toggle(self, ain, wait=False): """Toggle the switch state.""" - result = self._aha_request("setswitchtoggle", ain=ain, rf=bool) - wait and self.wait_device_txbusy(ain) - return result + if self._use_aha: + result = self._aha_request("setswitchtoggle", ain=ain, rf=bool) + wait and self.wait_device_txbusy(ain) + return result + return self._switch_action(ain, lambda dev: dev.set_switch_state_toggle()) def get_switch_power(self, ain): """Get the switch power consumption.""" - return self._aha_request("getswitchpower", ain=ain, rf=int) + if self._use_aha: + return self._aha_request("getswitchpower", ain=ain, rf=int) + if dev := self._update_device_config(ain): + if not dev.has_powermeter: + _LOGGER.error(f"Device {dev.name} is not a powermeter") + return None + return dev.power def get_switch_energy(self, ain): """Get the switch energy.""" - return self._aha_request("getswitchenergy", ain=ain, rf=int) + if self._use_aha: + return self._aha_request("getswitchenergy", ain=ain, rf=int) + if dev := self._update_device_config(ain): + if not dev.has_powermeter: + _LOGGER.error(f"Device {dev.name} is not a powermeter") + return None + return dev.energy def get_temperature(self, ain): """Get the device temperature sensor value.""" - return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 + if self._use_aha: + return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 + if dev := self._update_device_config(ain): + if not dev.has_temperature_sensor: + _LOGGER.error(f"Device {dev.name} is not a thermometer") + return None + return float(dev.temperature) def _get_temperature(self, ain, name): plain = self._aha_request(name, ain=ain, rf=float) @@ -299,7 +483,13 @@ def _get_temperature(self, ain, name): def get_target_temperature(self, ain): """Get the thermostate target temperature.""" - return self._get_temperature(ain, "gethkrtsoll") + if self._use_aha: + return self._get_temperature(ain, "gethkrtsoll") + if dev := self._update_device_config(ain): + if not dev.has_temperature_sensor: + _LOGGER.error(f"Device {dev.name} is not a thermometer") + return None + return float(dev.temperature) def set_target_temperature(self, ain, temperature, wait=False): """Set the thermostate target temperature.""" @@ -339,49 +529,68 @@ def get_eco_temperature(self, ain): def get_device_statistics(self, ain): """Get device statistics.""" - plain = self._aha_request("getbasicdevicestats", ain=ain) - return plain - - # Lightbulb-related commands - + if self._use_aha: + return self._aha_request("getbasicdevicestats", ain=ain) + stats = {"statistics":[]} + for unit in self._update_device_config(ain).units(): + if s := unit.statistics: + stats["statistics"].append(s) + return json.dumps(stats) + + # Lightbulb-related commands (for on/off/toggle there is no difference switch devices in REST) def set_state_off(self, ain, wait=False): """Set the switch/actuator/lightbulb to on state.""" - self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 0}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 0}) + wait and self.wait_device_txbusy(ain) + return self._switch_action(ain, lambda dev: dev.set_switch_state_on()) def set_state_on(self, ain, wait=False): """Set the switch/actuator/lightbulb to on state.""" - self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 1}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 1}) + wait and self.wait_device_txbusy(ain) + return self._switch_action(ain, lambda dev: dev.set_switch_state_off()) def set_state_toggle(self, ain, wait=False): """Toggle the switch/actuator/lightbulb state.""" - self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 2}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("setsimpleonoff", ain=ain, param={"onoff": 2}) + wait and self.wait_device_txbusy(ain) + return self._switch_action(ain, lambda dev: dev.set_switch_state_toggle()) def set_level(self, ain, level, wait=False): """Set level/brightness/height in interval [0,255].""" - if level < 0: - level = 0 # 0% - elif level > 255: - level = 255 # 100 % - - self._aha_request("setlevel", ain=ain, param={"level": int(level)}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + if level < 0: + level = 0 # 0% + elif level > 255: + level = 255 # 100 % + + self._aha_request("setlevel", ain=ain, param={"level": int(level)}) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") def set_level_percentage(self, ain, level, wait=False): """Set level/brightness/height in interval [0,100].""" - if level < 0: - level = 0 - elif level > 100: - level = 100 - - self._aha_request("setlevelpercentage", ain=ain, param={"level": int(level)}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + if level < 0: + level = 0 + elif level > 100: + level = 100 + + self._aha_request("setlevelpercentage", ain=ain, param={"level": int(level)}) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") def _get_colordefaults(self, ain): - plain = self._aha_request("getcolordefaults", ain=ain) - return ElementTree.fromstring(plain) + if self._use_aha: + plain = self._aha_request("getcolordefaults", ain=ain) + return ElementTree.fromstring(plain) + else: + raise NotImplementedError("missing REST api impl") def get_colors(self, ain): """Get colors (HSV-space) supported by this lightbulb.""" @@ -427,29 +636,33 @@ def set_color_temp(self, ain, temperature, duration=0, wait=False): temperature: temperature element obtained from get_temperatures() duration: Speed of change in seconds, 0 = instant """ - params = {"temperature": int(temperature), "duration": int(duration) * 10} - self._aha_request("setcolortemperature", ain=ain, param=params) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + params = {"temperature": int(temperature), "duration": int(duration) * 10} + self._aha_request("setcolortemperature", ain=ain, param=params) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") # blinds # states: open, close, stop - def _set_blind_state(self, ain, state): - self._aha_request("setblind", ain=ain, param={"target": state}) + def _set_blind_state(self, ain, state, wait): + if self._use_aha: + self._aha_request("setblind", ain=ain, param={"target": state}) + wait and self.wait_device_txbusy(ain) + else: + raise NotImplementedError("missing REST api impl") def set_blind_open(self, ain, wait=False): """Set the blind state to open.""" - self._set_blind_state(ain, "open") - wait and self.wait_device_txbusy(ain) + self._set_blind_state(ain, "open", wait) def set_blind_close(self, ain, wait=False): """Set the blind state to close.""" - self._set_blind_state(ain, "close") - wait and self.wait_device_txbusy(ain) + self._set_blind_state(ain, "close", wait) def set_blind_stop(self, ain, wait=False): """Set the blind state to stop.""" - self._set_blind_state(ain, "stop") - wait and self.wait_device_txbusy(ain) + self._set_blind_state(ain, "stop", wait) # Template-related commands diff --git a/pyfritzhome/fritzhomedevice.py b/pyfritzhome/fritzhomedevice.py index 59648f1..530a74b 100644 --- a/pyfritzhome/fritzhomedevice.py +++ b/pyfritzhome/fritzhomedevice.py @@ -2,9 +2,10 @@ # -*- coding: utf-8 -*- -from .devicetypes import FritzhomeTemplate # noqa: F401 -from .devicetypes import FritzhomeTrigger # noqa: F401 -from .devicetypes import ( +from .devicetypes import * +from .interfaces import * + +class FritzhomeDeviceAHA( FritzhomeDeviceAlarm, FritzhomeDeviceBlind, FritzhomeDeviceButton, @@ -16,21 +17,22 @@ FritzhomeDeviceSwitch, FritzhomeDeviceTemperature, FritzhomeDeviceThermostat, -) +): + """The Fritzhome Device class.""" + def __init__(self, fritz=None, node=None): + """Create a device object.""" + super().__init__(fritz, node) -class FritzhomeDevice( - FritzhomeDeviceAlarm, - FritzhomeDeviceBlind, - FritzhomeDeviceButton, - FritzhomeDeviceHumidity, - FritzhomeDeviceLevel, - FritzhomeDeviceLightBulb, - FritzhomeDevicePowermeter, - FritzhomeDeviceRepeater, - FritzhomeDeviceSwitch, - FritzhomeDeviceTemperature, - FritzhomeDeviceThermostat, + def _update_from_node(self, node): + super()._update_from_node(node) + +class FritzhomeDeviceREST( + FritzhomeDeviceBase, + FritzhomeOnOffMixin, + FritzhomeMultimeterMixin, + FritzhomeTemperatureMixin, + FritzhomeHumidityMixin, ): """The Fritzhome Device class.""" @@ -40,3 +42,18 @@ def __init__(self, fritz=None, node=None): def _update_from_node(self, node): super()._update_from_node(node) + +FritzhomeDevice = None + +def __get_defice_class(base): + class FritzhomeDevice(base): + pass + return FritzhomeDevice + +def get_device_class(is_aha): + global FritzhomeDevice + if is_aha: + FritzhomeDevice = __get_defice_class(FritzhomeDeviceAHA) + else: + FritzhomeDevice = __get_defice_class(FritzhomeDeviceREST) + return FritzhomeDevice diff --git a/pyfritzhome/interfaces/__init__.py b/pyfritzhome/interfaces/__init__.py new file mode 100644 index 0000000..7437683 --- /dev/null +++ b/pyfritzhome/interfaces/__init__.py @@ -0,0 +1,15 @@ +"""Init file for the device types.""" + +__all__ = ( + "FritzhomeInterface", + "FritzhomeOnOffInterface", "FritzhomeOnOffMixin", + "FritzhomeMultimeterInterface", "FritzhomeMultimeterMixin", + "FritzhomeTemperatureInterface", "FritzhomeTemperatureMixin", + "FritzhomeHumidityInterface", "FritzhomeHumidityMixin", +) + +from .interface import FritzhomeInterface +from .onoffinterface import FritzhomeOnOffInterface, FritzhomeOnOffMixin +from .multimeterinterface import FritzhomeMultimeterInterface, FritzhomeMultimeterMixin +from .temperatureinterface import FritzhomeTemperatureInterface, FritzhomeTemperatureMixin +from .humidityinterface import FritzhomeHumidityInterface, FritzhomeHumidityMixin diff --git a/pyfritzhome/interfaces/humidityinterface.py b/pyfritzhome/interfaces/humidityinterface.py new file mode 100644 index 0000000..8ca8361 --- /dev/null +++ b/pyfritzhome/interfaces/humidityinterface.py @@ -0,0 +1,45 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeHumidityInterface(FritzhomeInterfaceBase): + """The Fritzhome Device class.""" + + rel_humidity = None + + @property + def is_humidity(self): + return self.type == "humidityInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_humidity: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.rel_humidity = self._node["relativeHumidity"] + + + +class FritzhomeHumidityMixin(): + """The Fritzhome Humidity mixin.""" + + def find_humidity_interface(self): + return self.find_interface("humidityInterface") + + @property + def has_humidity_sensor(self): + """Check if the device has humidity sensors.""" + return self.find_humidity_interface() != None + + @property + def rel_humidity(self): + """ Get the current humidity """ + return self.find_humidity_interface().rel_humidity diff --git a/pyfritzhome/interfaces/interface.py b/pyfritzhome/interfaces/interface.py new file mode 100644 index 0000000..23af395 --- /dev/null +++ b/pyfritzhome/interfaces/interface.py @@ -0,0 +1,33 @@ +"""The entity base class.""" + +# -*- coding: utf-8 -*- + +from __future__ import print_function +from abc import ABC + + +import logging +import json + +from .interfacebase import FritzhomeInterfaceBase +from .onoffinterface import FritzhomeOnOffInterface +from .multimeterinterface import FritzhomeMultimeterInterface +from .temperatureinterface import FritzhomeTemperatureInterface +from .humidityinterface import FritzhomeHumidityInterface + +_LOGGER = logging.getLogger(__name__) + +class FritzhomeInterface(FritzhomeOnOffInterface, + FritzhomeMultimeterInterface, + FritzhomeTemperatureInterface, + FritzhomeHumidityInterface): + """The Fritzhome Interface class.""" + + def __init__(self, unit, type, node = None): + """Create an entity base object.""" + super().__init__(unit, type, node) + + # interfaces are not entities, only their parent units are, therefore this is + # called with the unit REST node + def _update_from_node(self, node): + super()._update_from_node(node) diff --git a/pyfritzhome/interfaces/interfacebase.py b/pyfritzhome/interfaces/interfacebase.py new file mode 100644 index 0000000..e6b5fa6 --- /dev/null +++ b/pyfritzhome/interfaces/interfacebase.py @@ -0,0 +1,39 @@ +"""The entity base class.""" + +# -*- coding: utf-8 -*- + +from __future__ import print_function +from abc import ABC + + +import weakref +import logging +import json + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeInterfaceBase(): + """The Fritzhome Interface class.""" + + def __init__(self, unit, type, node): + """Create an entity base object.""" + self.type = type + self._node = node + self._unit_ref = weakref.ref(unit) + if node is not None: + self._update_from_node(node) + + def __repr__(self): + """Return a string.""" + return f"{self.type} of {self._unit_ref().ain}" + + def _update_from_node(self, node): + pass + + def update(self): + self._unit_ref().update_interface(self) + + @property + def node(self): + return self._node; diff --git a/pyfritzhome/interfaces/multimeterinterface.py b/pyfritzhome/interfaces/multimeterinterface.py new file mode 100644 index 0000000..92bcbb4 --- /dev/null +++ b/pyfritzhome/interfaces/multimeterinterface.py @@ -0,0 +1,68 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeMultimeterInterface(FritzhomeInterfaceBase): + """The Fritzhome Device class.""" + + power = None + energy = None + voltage = None + current = None + + @property + def is_powermeter(self): + return self.type == "multimeterInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_powermeter: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.power = self._node["power"] + self.energy = self._node["energy"] + self.voltage = self._node["voltage"] + self.current = self._node["current"] + + + +class FritzhomeMultimeterMixin(): + """The Fritzhome Multimeter mixin.""" + + def find_multimeter_interface(self): + return self.find_interface("multimeterInterface") + + @property + def has_powermeter(self): + """Check if the device has powermeter sensors.""" + return self.find_multimeter_interface() != None + + @property + def power(self): + """ Get the current powermeter power """ + self.find_multimeter_interface().current + + @property + def energy(self): + """ Get the current currentmeter energy """ + self.find_multimeter_interface().energy + + @property + def voltage(self): + """ Get the current voltagemeter voltage """ + self.find_multimeter_interface().voltage + + @property + def current(self): + """ Get the current currentmeter current """ + self.find_multimeter_interface().current + + diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py new file mode 100644 index 0000000..f37b2f2 --- /dev/null +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -0,0 +1,68 @@ +"""The switch device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeOnOffInterface(FritzhomeInterfaceBase): + """The Fritzhome OnOff interface class.""" + + switch_state = None + + # Switch + @property + def is_switch(self): + return self.type == "onOffInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_switch: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.switch_state = self._node["active"] + + def set_switch_state_on(self, wait=False): + self._node["active"] = True + return self + + def set_switch_state_off(self, wait=False): + self._node["active"] = False + return self + + def set_switch_state_toggle(self, wait=False): + self._node["active"] = not self._node["active"] + return self + + +class FritzhomeOnOffMixin(): + + def find_switch_interface(self): + return self.find_interface("onOffInterface") + + @property + def has_switch(self): + return self.find_switch_interface() != None + + @property + def switch_state(self): + """ Get the current switch state """ + return self.find_switch_interface().switch_state + + def set_switch_state_on(self): + """Set the switch state to on.""" + self.find_switch_interface().set_switch_state_on().update() + + def set_switch_state_off(self): + """Set the switch state to off.""" + self.find_switch_interface().set_switch_state_off().update() + + def set_switch_state_toggle(self): + """Toggle the switch state.""" + self.find_switch_interface().set_switch_state_toggle().update() + diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py new file mode 100644 index 0000000..294d6a7 --- /dev/null +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -0,0 +1,53 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeTemperatureInterface(FritzhomeInterfaceBase): + """The Fritzhome Device class.""" + + celsius = None + offset = None + + @property + def is_temperature(self): + return self.type == "temperatureInterface" + + def _update_from_node(self, node): + _LOGGER.debug("update switch device") + super()._update_from_node(node) + if self.is_temperature: + if self._node["state"] != "valid": + _LOGGER.warning("interface state not valid") + else: + self.celsius = self._node["celsius"] + # offset is not always exposed (for Thermo 302 the offset is in the thermostatInterface) + self.offset = self._node.get("offset") or 0.0 + + + +class FritzhomeTemperatureMixin(): + """The Fritzhome Temperature mixin.""" + + def find_temperature_interface(self): + return self.find_interface("temperatureInterface") + + @property + def has_temperature_sensor(self): + """Check if the device has temperature sensors.""" + return self.find_temperature_interface() != None + + @property + def temperature(self): + """ Get the current temperature """ + self.find_temperature_interface().celsius + + @property + def offset(self): + """ Get the current temperature offset """ + self.find_temperature_interface().offset