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..16adcc3 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,10 +38,14 @@ 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_humidity_sensor: + print(" Humidity:") + print(" relative_humidity=%s" % device.rel_humidity) if device.has_thermostat: print(" Thermostat:") print(" battery_low=%s" % device.battery_low) @@ -60,22 +61,22 @@ def list_all(fritz, args): 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_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): @@ -109,6 +110,10 @@ def blind_set_level_percentage(fritz, args): fritz.set_level_percentage(args.ain, args.level) +def thermostat_get_info(fritz, args): + """DOC-TODO""" + print(fritz.get_device_infos(args.ain)) + def thermostat_set_target_temperature(fritz, args): """Command that sets the thermostat temperature.""" fritz.set_target_temperature(args.ain, args.temperature) @@ -165,9 +170,13 @@ def list_templates(fritz, args): print(" color=%s" % template.apply_color) print(" dialhelper=%s" % template.apply_dialhelper) - print(" Devices:") - for device_id in template.devices: - print(" %s=%s" % (device_id, devices[device_id].name)) + # REST api does not expose group devices in the devices list + # (TODO: there's a groups endpoint) + template_devs = template.devices & devices.keys() + if len(template_devs) > 0: + print(" Devices:") + for device_id in template_devs.devices: + print(" %s=%s" % (device_id, devices[device_id].name)) def template_apply(fritz, args): @@ -202,6 +211,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 +246,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") @@ -288,6 +306,13 @@ def main(args=None): subparser = _sub.add_parser("thermostat", help="Thermostat commands") _sub_switch = subparser.add_subparsers() + # thermostat target temperature + subparser = _sub_switch.add_parser( + "get_info", help="Get thermostat information" + ) + subparser.add_argument("ain", type=str, metavar="AIN", help="Actor Identification") + subparser.set_defaults(func=thermostat_get_info) + # thermostat target temperature subparser = _sub_switch.add_parser( "set_target_temperature", help="set target temperature" @@ -399,6 +424,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..294c1e4 100644 --- a/pyfritzhome/devicetypes/fritzhomedevicebase.py +++ b/pyfritzhome/devicetypes/fritzhomedevicebase.py @@ -3,8 +3,8 @@ from __future__ import print_function - import logging +import json from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase @@ -14,59 +14,136 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): """The Fritzhome Device class.""" + manufacturer = None + product_name = None + fw_version = None + is_connected = None + has_battery = None + battery_low = None 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): """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") + self._units = {} 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) + if self._fritz._use_aha: + self.manufacturer = node.attrib["manufacturer"] + self.product_name = node.attrib["productname"] + self.fw_version = node.attrib["fwversion"] + self.is_connected = self.get_node_value_as_int_as_bool(node, "present") + + self._battery = self.get_node_value_as_int(node, "battery") + self.has_battery = self._battery is not None or self.battery_low is not None + if self.has_battery and self.is_connected: + self.battery_low = self.get_node_value_as_int(node, "batterylow") + self.battery_level = self._battery or 0 + else: + self.manufacturer = self._node["manufacturer"] + self.product_name = self._node["productName"] + self.fw_version = self._node["firmwareVersion"] + self.is_connected = self._node["isConnected"] + + self.has_battery = self._node["isBatteryPowered"] + if self.has_battery and self.is_connected: + self.battery_low = self._node["isBatteryLow"] + self.battery_level = self._node["batteryValue"] + + def update_unit(self, unit, node): + """DOC-TODO (REST)""" + assert unit.ain in self._units.keys(), "unknown unit to update" + self._updates |= {"ain": self.ain, "units": node or unit._node} + self._updates["units"] |= { "ain": unit.ain } + if self.commit_now: + self.trigger_update() + + def find_interface(self, interface): + """DOC-TODO (REST)""" + found = None + for unit in self.units(): + if interface := unit.find_interface(interface): + if found is None: + found = interface + else: + _LOGGER.warning("ambigious device, multiple interface candidates, using first") + return found + + def begin(self, unit_ain=None): + """DOC-TODO (REST)""" + if unit_ain: + return self.units[unit_ain].begin() + else: + if len(self.units()) > 1: + _LOGGER.warning("ambigious device, multiple units, using first") + for unit in self._units.values(): + return unit.begin() + + def get_device_config(): + self._fritz.update_device_config(self.ain) + + def get_json(self): + r = self._node.copy() + r["units"] = {} + for unit in self._node["unitUids"]: + r["units"][unit] = self._units[unit]._node + return json.dumps(r) + + # legacy + @property + def productname(self): + 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() + + @property + def has_color(self): + return None + + @property + def has_blind(self): + return None + + @property + def has_alarm(self): + return None + + @property + def has_lightbulb(self): + return None + @property + def lock(self): + return None + @property + def device_lock(self): + return None + + @property + def nextchange_temperature(self): + return None + @property + def nextchange_endperiod(self): + return None diff --git a/pyfritzhome/devicetypes/fritzhomeentitybase.py b/pyfritzhome/devicetypes/fritzhomeentitybase.py index c6d8de1..ee49250 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,43 +16,46 @@ 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 + self.commit_now = True 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._node = node + if node.get("identifier") is not None: # AHA + if self.ain is not None and self.ain != node.attrib["identifier"]: + raise ValueError("updating invalid ain") + self.ain = node.attrib["identifier"] + self.name = self.get_node_value(node, "name") + self._functionsbitmask = int(node.attrib["functionbitmask"]) + else: # REST + if self.ain is not None and self.ain != node["ain"]: + raise ValueError("updating invalid ain") + self.ain = node["ain"] + self.name = node["name"] - self.supported_features = [] - for feature in FritzhomeDeviceFeatures: - if self._has_feature(feature): - self.supported_features.append(feature) + @property + def node(self): + return self._node; @property def device_and_unit_id(self): """Get the device and possible unit id.""" + # These three are not exposed in the overview/devices endpoint in the REST api if ( self.ain.startswith("tmp") or self.ain.startswith("grp") @@ -73,7 +76,9 @@ def get_node_value(self, elem, node): def get_node_value_as_int(self, elem, node) -> int: """Get the node value as integer.""" - return int(self.get_node_value(elem, node)) + if value := self.get_node_value(elem, node): + return int(value) + return None def get_node_value_as_int_as_bool(self, elem, node) -> bool: """Get the node value as boolean.""" @@ -82,3 +87,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..b0e23fe --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeunit.py @@ -0,0 +1,26 @@ +"""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, + interfaces.FritzhomeThermostatMixin): + """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..8a7cb7b --- /dev/null +++ b/pyfritzhome/devicetypes/fritzhomeunitbase.py @@ -0,0 +1,131 @@ +"""The base device class.""" +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import logging +import weakref + +from .fritzhomeentitybase import FritzhomeEntityBase +from .. import interfaces + +_LOGGER = logging.getLogger(__name__) + +def dict_deepmerge(base, override): + """ Returns a new dict which is a recursive merge of two input dicts. """ + result = base.copy() + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = dict_deepmerge(result[key], value) + elif isinstance(value, dict): + result[key] = value.copy() + else: + result[key] = value + return result + +class FritzhomeUnitBase(FritzhomeEntityBase): + """The Fritzhome Device class.""" + + _update_intervals = [ 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 10, 15 ] + + def __init__(self, fritz=None, node=None): + """Create a unit object (REST-only).""" + self._updates = {} + self.interfaces = {} + super().__init__(fritz, node) + + 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 + + for iface, node in node["interfaces"].items(): + if n := self.interfaces.get(iface): + self.interfaces[iface]._update_from_node(node) + else: + self.interfaces[iface] = interfaces.FritzhomeInterface.create(self, iface, node) + + + def _update_unit(self, node, wait=False): + self._fritz.put_unit(self.ain, node) + if not wait: + return + import time + off = 0 + for i in self._update_intervals: + # this calls our _update_from_node eventually + self._fritz._update_unit(self.ain) + if dict_deepmerge(self._node, node) == self._node: + return + time.sleep(i - off) + off = i + self._unit_ref().update() + if self._node | node == self._node: + return + raise RuntimeError("Failed to fetch updated unit") + + def interface_changed(self, interface, node=None, wait=False): + import sys + + if not self.commit_now: + if wait: + LOGGER.err("wait=True not supported in bulk update") + self._updates |= {"interfaces": {interface.type: node or interface._node}} + return + self._update_unit({"interfaces": {interface.type: node or interface._node}}, wait) + + def begin(self): + """DOC-TODO (REST)""" + assert self.commit_now, "nested begin() not permitted" + print(f"A {self}") + self.commit_now = False + return self + + def end(self, wait=False): + """DOC-TODO (REST)""" + print(f"Z {self}") + self.commit_now = True + self._update_unit(self._updates, wait) + self._updates = {} + + 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..3c4baf2 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,133 @@ 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): + """DOC-TODO (REST)""" + if self._units is None: + self._units = {} + + params = {"Authorization": f"AVM-SID {self._sid}"} + data = self._put(f"{self.rest_url}/configuration/units/{ain}", node, headers=params) + + def put_device(self, ain, node): + """DOC-TODO (REST)""" + if self._devices is None: + self._devices = {} + + _LOGGER.info("put devices ...\n" + json.dumps(node)) + params = {"Authorization": f"AVM-SID {self._sid}"} + data = self._put(f"{self.rest_url}/configuration/devices/{ain}", node, headers=params) + + def _update_device_from_node(self, node): + ain = node["ain"] + if device := self._devices.get(ain): + _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_unit(self, ain): + """Update the unit, using its configuration endpoint.""" + _LOGGER.info(f"Updating Unit {ain} ...") + if node := self._rest_request(f"configuration/units/{ain}"): + return self._update_unit_from_node(node) + return None + + def get_config(self, ain): + """ DOC-TODO """ + if self._use_aha: + return self._devices[ain] + else: + return self._update_device_config(ain) + def update_devices(self, ignore_removed=True): """Update the device.""" _LOGGER.info("Updating Devices ...") - if self._devices is None: - self._devices = {} + old_devs = self._devices.keys() + old_units = self._units.keys() + if self._use_aha: + for element in self._get_listinfo_elements("device"): + if element.attrib["identifier"] in self._devices.keys(): + _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 old_devs - self._devices.keys(): + _LOGGER.info("Removing no more existing device " + ain) + self._devices.pop(ain) + for ain in old_units - self._units.keys(): + _LOGGER.info("Removing no more existing device " + ain) + self._units.pop(ain) + + return True + 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 +356,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 +389,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 +398,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 @@ -249,49 +408,106 @@ def get_device_by_ain(self, ain): def get_device_infos(self, ain): """Get the device infos.""" - return self._aha_request("getdeviceinfos", ain=ain) + if self._use_aha: + return self._aha_request("getdeviceinfos", ain=ain) + return self._update_device_config(ain).get_json() def get_device_present(self, ain): """Get the device presence.""" - 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(wait)) 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(wait)) 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(wait)) 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_thermostat_config(self, ain): + """Get the thermostat .""" + if self._use_aha: + return None + if dev := self._update_device_config(ain): + if not dev.has_temperature: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + return float(dev.has_temperature) + def get_temperature(self, ain): """Get the device temperature sensor value.""" - 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,35 +515,58 @@ 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.""" - temp = int(temperature * 2) - - if temp < 16: - temp = 253 - elif temp > 56: - temp = 254 - - self._aha_request("sethkrtsoll", ain=ain, param={"param": temp}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + temp = int(temperature * 2) + + if temp < 16: + temp = 253 + elif temp > 56: + temp = 254 + self._aha_request("sethkrtsoll", ain=ain, param={"param": temp}) + wait and self.wait_device_txbusy(ain) + elif dev := self._update_device_config(ain): + if not dev.has_thermostat: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + dev.set_target_temperature(seconds) def set_window_open(self, ain, seconds, wait=False): """Set the thermostate target temperature.""" endtimestamp = int(time.time() + seconds) - self._aha_request( - "sethkrwindowopen", ain=ain, param={"endtimestamp": endtimestamp} - ) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request( + "sethkrwindowopen", ain=ain, param={"endtimestamp": endtimestamp} + ) + wait and self.wait_device_txbusy(ain) + elif dev := self._update_device_config(ain): + if not dev.has_thermostat: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + dev.set_window_open(seconds) def set_boost_mode(self, ain, seconds, wait=False): """Set the thermostate to boost mode.""" endtimestamp = int(time.time() + seconds) - self._aha_request("sethkrboost", ain=ain, param={"endtimestamp": endtimestamp}) - wait and self.wait_device_txbusy(ain) + if self._use_aha: + self._aha_request("sethkrboost", ain=ain, param={"endtimestamp": endtimestamp}) + wait and self.wait_device_txbusy(ain) + elif dev := self._update_device_config(ain): + if not dev.has_thermostat: + _LOGGER.error(f"Device {dev.name} is not a thermostat") + return None + dev.set_boost_mode(seconds) def get_comfort_temperature(self, ain): """Get the thermostate comfort temperature.""" @@ -339,49 +578,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 +685,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..90b8b1f 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,23 @@ 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, + FritzhomeThermostatMixin, ): """The Fritzhome Device class.""" @@ -40,3 +43,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..39a4319 --- /dev/null +++ b/pyfritzhome/interfaces/__init__.py @@ -0,0 +1,17 @@ +"""Init file for the device types.""" + +__all__ = ( + "FritzhomeInterface", + "FritzhomeOnOffInterface", "FritzhomeOnOffMixin", + "FritzhomeMultimeterInterface", "FritzhomeMultimeterMixin", + "FritzhomeTemperatureInterface", "FritzhomeTemperatureMixin", + "FritzhomeHumidityInterface", "FritzhomeHumidityMixin", + "FritzhomeThermostatInterface", "FritzhomeThermostatMixin", +) + +from .interface import FritzhomeInterface +from .onoffinterface import FritzhomeOnOffInterface, FritzhomeOnOffMixin +from .multimeterinterface import FritzhomeMultimeterInterface, FritzhomeMultimeterMixin +from .temperatureinterface import FritzhomeTemperatureInterface, FritzhomeTemperatureMixin +from .humidityinterface import FritzhomeHumidityInterface, FritzhomeHumidityMixin +from .thermostatinterface import FritzhomeThermostatInterface, FritzhomeThermostatMixin diff --git a/pyfritzhome/interfaces/humidityinterface.py b/pyfritzhome/interfaces/humidityinterface.py new file mode 100644 index 0000000..5cd544d --- /dev/null +++ b/pyfritzhome/interfaces/humidityinterface.py @@ -0,0 +1,32 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeHumidityInterface(FritzhomeInterfaceBase): + """The Fritzhome Humidity interface class.""" + + @property + def rel_humidity(self): + return 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..b64d048 --- /dev/null +++ b/pyfritzhome/interfaces/interface.py @@ -0,0 +1,38 @@ +"""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 +from .thermostatinterface import FritzhomeThermostatInterface + +_LOGGER = logging.getLogger(__name__) + +class FritzhomeInterface(): + """The Fritzhome Interface factory.""" + + @staticmethod + def create(unit, type, node = None): + """Create a specific interface object.""" + cls = { + "onOffInterface": FritzhomeOnOffInterface, + "multimeterInterface": FritzhomeMultimeterInterface, + "temperatureInterface": FritzhomeTemperatureInterface, + "humidityInterface": FritzhomeHumidityInterface, + "thermostatInterface" : FritzhomeThermostatInterface, + } + try: + return cls[type](unit, type, node) + except KeyError: + return FritzhomeInterfaceBase(unit, type, node) + diff --git a/pyfritzhome/interfaces/interfacebase.py b/pyfritzhome/interfaces/interfacebase.py new file mode 100644 index 0000000..bdd44bb --- /dev/null +++ b/pyfritzhome/interfaces/interfacebase.py @@ -0,0 +1,43 @@ +"""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._node_set = {} + self._unit_ref = weakref.ref(unit) + + def __repr__(self): + """Return a string.""" + return f"{self.type} of {self._unit_ref().ain}" + + def _update_from_node(self, node): + self._node = node + _LOGGER.debug(f"update {self.type}") + if self._node["state"] != "valid": + _LOGGER.warning(f"{self.type} state not valid") + + def changed(self, wait=False): + assert self._node_set.keys() <= self._node.keys(), "_node_set must be a strict subset" + self._unit_ref().interface_changed(self, self._node_set, wait) + self._node_set = {} + + @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..09dae32 --- /dev/null +++ b/pyfritzhome/interfaces/multimeterinterface.py @@ -0,0 +1,63 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeMultimeterInterface(FritzhomeInterfaceBase): + """The Fritzhome Multimeter interface class.""" + + @property + def power(self): + return self._node["power"] + + @property + def energy(self): + return self._node["energy"] + + @property + def voltage(self): + return self._node["voltage"] + + @property + def current(self): + return 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() is not None + + @property + def power(self): + """ Get the current powermeter power """ + return self.find_multimeter_interface().power + + @property + def energy(self): + """ Get the current currentmeter energy """ + return self.find_multimeter_interface().energy + + @property + def voltage(self): + """ Get the current voltagemeter voltage """ + return self.find_multimeter_interface().voltage + + @property + def current(self): + """ Get the current currentmeter current """ + return self.find_multimeter_interface().current + + diff --git a/pyfritzhome/interfaces/onoffinterface.py b/pyfritzhome/interfaces/onoffinterface.py new file mode 100644 index 0000000..9592e96 --- /dev/null +++ b/pyfritzhome/interfaces/onoffinterface.py @@ -0,0 +1,59 @@ +"""The switch device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeOnOffInterface(FritzhomeInterfaceBase): + """The Fritzhome OnOff (switch) interface class.""" + + @property + def switch_state(self): + return self._node["active"] + + def set_switch_state_on(self, wait=False): + self._node_set["active"] = True + self.changed(wait) + + def set_switch_state_off(self, wait=False): + self._node_set["active"] = False + self.changed(wait) + + def set_switch_state_toggle(self, wait=False): + self._node_set["active"] = not self._node["active"] + self.changed(wait) + + +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, wait=False): + """Set the switch state to on.""" + self.find_switch_interface().set_switch_state_on(wait) + return self + + def set_switch_state_off(self, wait=False): + """Set the switch state to off.""" + self.find_switch_interface().set_switch_state_off(wait) + return self + + def set_switch_state_toggle(self, wait=False): + """Toggle the switch state.""" + self.find_switch_interface().set_switch_state_toggle(wait) + return self + diff --git a/pyfritzhome/interfaces/temperatureinterface.py b/pyfritzhome/interfaces/temperatureinterface.py new file mode 100644 index 0000000..5a588ee --- /dev/null +++ b/pyfritzhome/interfaces/temperatureinterface.py @@ -0,0 +1,47 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeTemperatureInterface(FritzhomeInterfaceBase): + """The Fritzhome Temperature interface class.""" + + @property + def celsius(self): + return self._node["celsius"] + + @property + def offset(self): + # offset is not always exposed (only through /smarthome/configuration/… endpoints) + return self._node.get("offset") + +class FritzhomeTemperatureMixin(): + """The Fritzhome Temperature mixin.""" + + 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 """ + return self.find_temperature_interface().celsius + + @property + def actual_temperature(self): + """ Get the current temperature (legacy) """ + return self.temperature + + @property + def offset(self): + """ Get the current temperature offset (maybe None!) """ + return self.find_temperature_interface().offset diff --git a/pyfritzhome/interfaces/thermostatinterface.py b/pyfritzhome/interfaces/thermostatinterface.py new file mode 100644 index 0000000..7497a59 --- /dev/null +++ b/pyfritzhome/interfaces/thermostatinterface.py @@ -0,0 +1,218 @@ +"""The powermeter device class.""" +# -*- coding: utf-8 -*- + +import logging + +from .interfacebase import FritzhomeInterfaceBase + +_LOGGER = logging.getLogger(__name__) + + +class FritzhomeThermostatInterface(FritzhomeInterfaceBase): + """The Fritzhome Thermostat interface class.""" + + @property + def target_temperature(self): + if self._node["setPointTemperature"]["mode"] == "temperature": + return self._node["setPointTemperature"]["celsius"] + return None + + @property + def thermostat_mode(self): + return self._node["setPointTemperature"]["mode"] + + def set_thermostat_mode(self, v, wait=False): + self._node_set["setPointTemperature"] = {} + self._node_set["setPointTemperature"]["mode"] = v + self.changed(wait) + + def set_target_temperature(self, v, wait=False): + self._node_set["setPointTemperature"] = {} + self._node_set["setPointTemperature"]["celsius"] = v + self._node_set["setPointTemperature"]["mode"] = "temperature" + self.changed(wait) + + @property + def comfort_temperature(self): + return self._node["comfortTemperature"]["celsius"] + + def set_comfort_temperature(self, v, wait=False): + self._node_set["comfortTemperature"]= {} + self._node_set["comfortTemperature"]["celsius"] = v + self._node_set["comfortTemperature"]["mode"] = "temperature" + self.changed(wait) + + @property + def reduced_temperature(self): + return self._node["reducedTemperature"]["celsius"] + + def set_reduced_temperature(self, v, wait=False): + self._node_set["reducedTemperature"]= {} + self._node_set["reducedTemperature"]["celsius"] = v + self._node_set["reducedTemperature"]["mode"] = "temperature" + self.changed(wait) + + @property + def holiday_active(self): + return self._node["isHolidayActive"] + + @property + def adaptive_heating_active(self): + return self._node["isAdaptiveActive"] + + @property + def adaptive_heating_running(self): + # not always exposed (only through /smarthome/configuration/… endpoints) + return self._node.get("adaptiveHeatingModeEnabled") + + @property + def summer_active(self): + return self._node["isSummertimeActive"] + + @property + def window_open(self): + return self._node["windowOpenMode"]["enabled"] + + @property + def window_open_endtime(self): + return self._node["windowOpenMode"]["endTime"] + + @property + def boost_active(self): + return self._node["boost"]["enabled"] + + @property + def boost_active_endtime(self): + return self._node["boost"]["endTime"] + + def set_boost_mode(self, duration_secs, wait=False): + import time + self._node_set["boost"] = {} + if duration_secs > 0: + self._node_set["boost"]["enabled"] = True + self._node_set["boost"]["endTime"] = int(time.time() + duration_secs) + else: + self._node_set["boost"]["enabled"] = False + self._node_set["boost"]["endTime"] = 0 + self.changed(wait) + + def set_window_open(self, duration_secs, wait=False): + import time + self._node_set["windowOpenMode"] = {} + if duration_secs > 0: + self._node_set["windowOpenMode"]["enabled"] = True + self._node_set["windowOpenMode"]["endTime"] = int(time.time() + duration_secs) + else: + self._node_set["windowOpenMode"]["enabled"] = False + self._node_set["windowOpenMode"]["endTime"] = 0 + self.changed(wait) + +class FritzhomeThermostatMixin(): + """The Fritzhome Thermostat mixin.""" + + def find_thermostat_interface(self): + return self.find_interface("thermostatInterface") + + @property + def has_thermostat(self): + """Check if the device has thermostat sensors.""" + return self.find_thermostat_interface() is not None + + @property + def target_temperature(self): + """ Get the current thermostat """ + return self.find_thermostat_interface().target_temperature + + def set_target_temperature(self, v, wait=False): + """ Get the current thermostat """ + self.find_thermostat_interface().set_target_temperature(v, wait) + return self + + @property + def reduced_temperature(self): + """ Get the current thermostat offset """ + return self.find_thermostat_interface().reduced_temperature + + def set_reduced_temperature(self, v, wait=False): + """ Get the current thermostat """ + self.find_thermostat_interface().set_reduced_temperature(v, wait) + return self + + @property + def comfort_temperature(self): + return self.find_thermostat_interface().comfort_temperature + + def set_comfort_temperature(self, v, wait=False): + """ Get the current thermostat """ + self.find_thermostat_interface().set_comfort_temperature(v, wait) + return self + + @property + def eco_temperature(self): + return self.reduced_temperature + + @property + def adaptive_heating_active(self): + return self.find_thermostat_interface().adaptive_heating_active + + @property + def adaptive_heating_running(self): + return self.find_thermostat_interface().adaptive_heating_running + + @property + def holiday_active(self): + return self.find_thermostat_interface().holiday_active + + @property + def summer_active(self): + return self.find_thermostat_interface().summer_active + + @property + def window_open(self): + return self.find_thermostat_interface().window_open + + @property + def window_open_endtime(self): + return self.find_thermostat_interface().window_open_endtime + + @property + def boost_active(self): + return self.find_thermostat_interface().boost_active + + @property + def boost_active_endtime(self): + return self.find_thermostat_interface().boost_active_endtime + + def set_boost_mode(self, seconds, wait=False): + """Set the thermostate into boost mode.""" + self.find_thermostat_interface().set_boost_mode(seconds) + return self + + def set_window_open(self, seconds, wait=False): + """Set the thermostate in open window mode.""" + self.find_thermostat_interface().set_window_open(seconds) + return self + + def get_hkr_state(self): + """Get the thermostate state.""" + mode = self.find_thermostat_interface().thermostat_mode + if mode in ["on", "off"]: + return mode + elif self.target_temperature == self.eco_temperature: + return "eco" + elif self.target_temperature == self.comfort_temperature: + return "comfort" + return "manual" + + def set_hkr_state(self, state, wait=False): + """Set the state of the thermostat. + + Possible values for state are: 'on', 'off', 'comfort', 'eco'. + """ + if state in ["on", "off"]: + self.find_thermostat_interface().set_thermostat_mode(state, wait) + elif state == "eco": + self.find_thermostat_interface().set_target_temperature(self.eco_temperature, wait) + elif state == "comfort": + self.find_thermostat_interface().set_target_temperature(self.comfort_temperature, wait) + return self