From aa92a678307acc5150ba1c39c17b8712acec4615 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 19 Feb 2022 12:19:21 +0100 Subject: [PATCH 001/178] Initial commit --- .gitignore | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b6e47617d --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ From 26132157da12fc1072b63eaea735acc460fbfa1e Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 19 Feb 2022 12:20:03 +0100 Subject: [PATCH 002/178] Add files via upload --- __init__.py | 609 ++++++++++++++++++++++ locale.yaml | 10 + plugin.yaml | 84 +++ requirements.txt | 2 + user_doc.rst | 67 +++ webif/__init__.py | 104 ++++ webif/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 2240 bytes webif/static/img/readme.txt | 6 + webif/templates/index.html | 79 +++ 9 files changed, 961 insertions(+) create mode 100644 __init__.py create mode 100644 locale.yaml create mode 100644 plugin.yaml create mode 100644 requirements.txt create mode 100644 user_doc.rst create mode 100644 webif/__init__.py create mode 100644 webif/__pycache__/__init__.cpython-38.pyc create mode 100644 webif/static/img/readme.txt create mode 100644 webif/templates/index.html diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..5329f9393 --- /dev/null +++ b/__init__.py @@ -0,0 +1,609 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2020- +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# Sample plugin for new plugins to run with SmartHomeNG version 1.8 and +# upwards. +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + + +import logging +from requests.packages import urllib3 +from requests.auth import HTTPDigestAuth +from io import BytesIO +import lxml.etree as etree +import requests + +from lib.model.smartplugin import SmartPlugin +from lib.utils import Utils +from .webif import WebInterface + +TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} +TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} + +### Devices / Services +""" + InternetGatewayDevice + DeviceInfo + DeviceConfig + Layer3Forwarding + LANConfigSecurity + ManagementServer + Time + UserInterface + X_AVM-DE_Storage + X_AVM-DE_WebDAVClient + X_AVM-DE_UPnP + X_AVM-DE_Speedtest + X_AVM-DE_RemoteAccess + X_AVM-DE_MyFritz + X_VoIP + X_AVM-DE_OnTel + X_AVM-DE_Dect + X_AVM-DE_TAM + X_AVM-DE_AppSetup + X_AVM-DE_Homeauto + X_AVM-DE_Homeplug + X_AVM-DE_Filelinks + X_AVM-DE_Auth + X_AVM-DE_HostFilter + LANDevice + WLANConfiguration + Hosts + LANEthernetInterfaceConfig + LANHostConfigManagement + WANDevice + WANCommonInterfaceConfig + WANDSLInterfaceConfig + WANConnectionDevice + WANDSLLinkConfig + WANEthernetLinkConfig + WANPPPConnection + WANIPConnection +""" + + +class AVM2(SmartPlugin): + """ + Main class of the Plugin. Does all plugin specific stuff and provides + the update functions for the items + + HINT: Please have a look at the SmartPlugin class to see which + class properties and methods (class variables and class functions) + are already available! + """ + + PLUGIN_VERSION = '1.0.0' + + def __init__(self, sh): + """ + Initalizes the plugin. + """ + + # Call init code of parent class (SmartPlugin) + super().__init__() + + self.logger.info('Init AVM-Discover Plugin') + + self._session = requests.Session() + self._timeout = 10 + self._verify = self.get_parameter_value('verify') + ssl = self.get_parameter_value('ssl') + + if ssl and not self._verify: + urllib3.disable_warnings() + + self._fritz_device = FritzDevice(self.get_parameter_value('host'), self.get_parameter_value('port'), ssl, + self.get_parameter_value('username'), self.get_parameter_value('password'), + self.get_instance_name(), self) + + self._cycle = int(self.get_parameter_value('cycle')) + self.alive = False + + self.init_webinterface(WebInterface) + + + def run(self): + """ + Run method for the plugin + """ + self.logger.debug("Run method called") + # setup scheduler for device poll loop (disable the following line, if you don't need to poll the device. Rember to comment the self_cycle statement in __init__ as well) + # self.scheduler_add('poll_device', self.poll_device, cycle=self._cycle) + + self.alive = True + # self.logger.debug(f"manufacturer_name = {self._fritz_device.manufacturer_name}") + # self.logger.debug(f"model_name = {self._fritz_device.model_name}") + # self.logger.debug(f"safe_port = {self._fritz_device.safe_port}") + # self.logger.debug(f"network_devices = {self._fritz_device.get_network_devices()}") + + def stop(self): + """ + Stop method for the plugin + """ + self.logger.debug("Stop method called") + self.scheduler_remove('poll_device') + self.alive = False + + def parse_item(self, item): + """ + Default plugin parse_item method. Is called when the plugin is initialized. + The plugin can, corresponding to its attribute keywords, decide what to do with + the item in future, like adding it to an internal array for future reference + :param item: The item to process. + :return: If the plugin needs to be informed of an items change you should return a call back function + like the function update_item down below. An example when this is needed is the knx plugin + where parse_item returns the update_item function when the attribute knx_send is found. + This means that when the items value is about to be updated, the call back function is called + with the item, caller, source and dest as arguments and in case of the knx plugin the value + can be sent to the knx with a knx write function within the knx plugin. + """ + if self.has_iattr(item.conf, 'foo_itemtag'): + self.logger.debug(f"parse item: {item}") + + + def parse_logic(self, logic): + """ + Default plugin parse_logic method + """ + if 'xxx' in logic.conf: + # self.function(logic['name']) + pass + + def update_item(self, item, caller=None, source=None, dest=None): + """ + Item has been updated + + This method is called, if the value of an item has been updated by SmartHomeNG. + It should write the changed value out to the device (hardware/interface) that + is managed by this plugin. + + :param item: item to be updated towards the plugin + :param caller: if given it represents the callers name + :param source: if given it represents the source + :param dest: if given it represents the dest + """ + if self.alive and caller != self.get_shortname(): + # code to execute if the plugin is not stopped + # and only, if the item has not been changed by this this plugin: + self.logger.info(f"Update item: {item.property.path}, item has been changed outside this plugin") + + if self.has_iattr(item.conf, 'foo_itemtag'): + self.logger.debug(f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}") + pass + + +class FritzDevice: + """ + This class encapsulates information related to a specific FritzDevice, such has host, port, ssl, username, password, or related items + """ + + def __init__(self, host, port, ssl, username, password, identifier='default', plugin_instance=None): + + self._plugin_instance = plugin_instance + self._plugin_instance.logger.debug("Init FritzDevice") + + self._host = host + self._port = port + self._ssl = ssl + self._username = username + self._password = password + self._identifier = identifier + self._available = True + self._items = [] + self._smarthome_items = [] + self._smarthome_devices = {} + self.InternetGatewayDevice = None + self.LANDevice = None + self.WANDevice = None + self.WANConnectionDevice = None + + self.client = FritzDevice.Client(self._username, self._password, base_url=self._build_url(), plugin_instance=plugin_instance) + + self._network_devices = self.network_devices() + + + def _build_url(self): + """ + Builds a request url + + :return: string of the url, dependent on settings of the FritzDevice + """ + if self.is_ssl(): + url_prefix = "https" + else: + url_prefix = "http" + url = f"{url_prefix}://{self._host}:{self._port}" + + return url + + def get_identifier(self): + """ + Returns the internal identifier of the FritzDevice + + :return: identifier of the device, as set in plugin.conf + """ + return self._identifier + + def get_host(self): + """ + Returns the hostname / IP of the FritzDevice + + :return: hostname of the device, as set in plugin.conf + """ + return self._host + + def get_port(self): + """ + Returns the port of the FritzDevice + + :return: port of the device, as set in plugin.conf + """ + return self._port + + def get_items(self): + """ + Returns added items + + :return: array of items hold by the device + """ + return self._items + + def get_item_count(self): + """ + Returns number of added items + + :return: number of items hold by the device + """ + return len(self._items) + + def is_ssl(self): + """ + Returns information if SSL is enabled + + :return: is ssl enabled, as set in plugin.conf + """ + return self._ssl + + def is_available(self): + """ + Returns information if the device is currently available + + :return: boolean, if device is available + """ + return self._available + + def set_available(self, is_available): + """ + Sets the boolean, if the device is available + + :param is_available: boolean of the availability status + """ + self._available = is_available + + def get_user(self): + """ + Returns the user for the FritzDevice + + :return: user, as set in plugin.conf + """ + return self._username + + def get_password(self): + """ + Returns the password for the FritzDevice + + :return: password, as set in plugin.conf + """ + return self._password + + def get_smarthome_items(self): + """ + Returns added items + + :return: array of smarthome items hold by the plugin + """ + return self._smarthome_items + + def get_smarthome_devices(self): + """ + Returns added items + + :return: dict of smarthome devices hold by the plugin + """ + return self._smarthome_devices + + def get_network_devices(self): + return self._network_devices + + @property + def manufacturer_name(self): + if not self.InternetGatewayDevice: + self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self.InternetGatewayDevice.NewManufacturerName + + @property + def manufacturer_oui(self): + if not self.InternetGatewayDevice: + self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self.InternetGatewayDevice.NewManufacturerOUI + + @property + def model_name(self): + if not self.InternetGatewayDevice: + self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self.InternetGatewayDevice.NewModelName + + @property + def desciption(self): + if not self.InternetGatewayDevice: + self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self.InternetGatewayDevice.NewDescription + + @property + def safe_port(self): + return self.client.InternetGatewayDevice.DeviceInfo.GetSecurityPort().NewSecurityPort + + def network_devices(self): + devices = dict() + number_of_entries = self.client.LANDevice.Hosts.GetHostNumberOfEntries() + for index in range(int(number_of_entries.NewHostNumberOfEntries)): + host = self.client.LANDevice.Hosts.GetGenericHostEntry(NewIndex=index) + self._plugin_instance.logger.debug(f"host = {host}") + devices[index] = {'ip': host.NewIPAddress, 'mac': host.NewMACAddress, 'name': host.NewHostName} + return devices + + def wlan_info(self): + for index in range(3): + info = self.client.LANDevice.WLANConfiguration[index].GetInfo() + self._plugin_instance.logger.debug(f"WLAN info{index} = {info}") + + + class Client: + """TR-064 client. + :param str username: + Username with access to router. + :param str password: + Passwort to access router. + :param str base_url: + URL to router. + """ + + def __init__(self, username, password, base_url='https://192.168.178.1:49443', plugin_instance=None): + + # handle plugin instance + self._plugin_instance = plugin_instance + + self.base_url = base_url + self.auth = HTTPDigestAuth(username, password) + + self.devices = {} + self._plugin_instance.logger.debug(f"Init Client") + + def __getattr__(self, name): + if name not in self.devices: + self._fetch_devices() + + if name in self.devices: + return self.devices[name] + + def _fetch_devices(self, description_file='/tr64desc.xml'): + + """Fetch device description.""" + request = requests.get(f'{self.base_url}{description_file}') + + if request.status_code == 200: + xml = etree.parse(BytesIO(request.content)) + + for device in xml.findall('.//device', namespaces=TR064_DEVICE_NAMESPACE): + name = device.findtext('deviceType', namespaces=TR064_DEVICE_NAMESPACE).split(':')[-2] + if name not in self.devices: + self.devices[name] = FritzDevice.Device(device, self.auth, self.base_url) + + class Device: + """TR-064 device. + :param lxml.etree.Element xml: + XML device element + :param HTTPBasicAuthHandler auth: + HTTPBasicAuthHandler object, e.g. HTTPDigestAuth + :param str base_url: + URL to router. + """ + + def __init__(self, xml, auth, base_url): + self.logger = logging.getLogger(__name__) + self.services = {} + + for service in xml.findall('./serviceList/service', namespaces=TR064_DEVICE_NAMESPACE): + service_type = service.findtext('serviceType', namespaces=TR064_DEVICE_NAMESPACE) + service_id = service.findtext('serviceId', namespaces=TR064_DEVICE_NAMESPACE) + control_url = service.findtext('controlURL', namespaces=TR064_DEVICE_NAMESPACE) + event_sub_url = service.findtext('eventSubURL', namespaces=TR064_DEVICE_NAMESPACE) + scpdurl = service.findtext('SCPDURL', namespaces=TR064_DEVICE_NAMESPACE) + + name = service_type.split(':')[-2].replace('-', '_') + if name not in self.services: + self.services[name] = FritzDevice.ServiceList() + + self.services[name].append( + FritzDevice.Service( + auth, + base_url, + service_type, + service_id, + scpdurl, + control_url, + event_sub_url + ) + ) + + def __getattr__(self, name): + if name in self.services: + return self.services[name] + + class ServiceList(list): + """Service list.""" + + def __getattr__(self, name): + """Direct access to first list entry if brackets omit.""" + return self[0].__getattr__(name) + + def __getitem__(self, index): + """Overriden braket operator to return TR-064 exception.""" + if len(self) > index: + return super().__getitem__(index) + + class Service: + """TR-064 service.""" + + def __init__(self, auth, base_url, service_type, service_id, scpdurl, control_url, event_sub_url): + self.auth = auth + self.base_url = base_url + self.service_type = service_type + self.service_id = service_id + self.scpdurl = scpdurl + self.control_url = control_url + self.event_sub_url = event_sub_url + self.actions = {} + + def __getattr__(self, name): + if name not in self.actions: + self._fetch_actions(self.scpdurl) + + if name in self.actions: + return self.actions[name] + + def _fetch_actions(self, scpdurl): + """Fetch action description.""" + request = requests.get(f'{self.base_url}{scpdurl}') + if request.status_code == 200: + xml = etree.parse(BytesIO(request.content)) + + for action in xml.findall('./actionList/action', namespaces=TR064_SERVICE_NAMESPACE): + name = action.findtext('name', namespaces=TR064_SERVICE_NAMESPACE) + canonical_name = name.replace('-', '_') + self.actions[canonical_name] = FritzDevice.Action( + action, + self.auth, + self.base_url, + name, + self.service_type, + self.service_id, + self.control_url + ) + + class Action: + """TR-064 action. + :param lxml.etree.Element xml: + XML action element + :param HTTPBasicAuthHandler auth: + HTTPBasicAuthHandler object, e.g. HTTPDigestAuth + :param str base_url: + URL to router. + :param str name: + Action name + :param str service_type: + Service type + :param str service_id: + Service ID + :param str control_url: + Control URL + """ + + def __init__(self, xml, auth, base_url, name, service_type, service_id, control_url): + + self.logger = logging.getLogger(__name__) + + self.auth = auth + self.base_url = base_url + self.name = name + self.service_type = service_type + self.service_id = service_id + self.control_url = control_url + + etree.register_namespace('s', 'http://schemas.xmlsoap.org/soap/envelope/') + etree.register_namespace('h', 'http://soap-authentication.org/digest/2001/10/') + + self.headers = {'content-type': 'text/xml; charset="utf-8"'} + self.envelope = etree.Element( + '{http://schemas.xmlsoap.org/soap/envelope/}Envelope', + attrib={ + '{http://schemas.xmlsoap.org/soap/envelope/}encodingStyle': + 'http://schemas.xmlsoap.org/soap/encoding/'}) + self.body = etree.SubElement(self.envelope, '{http://schemas.xmlsoap.org/soap/envelope/}Body') + + self.in_arguments = {} + self.out_arguments = {} + + for argument in xml.findall('./argumentList/argument', namespaces=TR064_SERVICE_NAMESPACE): + name = argument.findtext('name', namespaces=TR064_SERVICE_NAMESPACE) + direction = argument.findtext('direction', namespaces=TR064_SERVICE_NAMESPACE) + + if direction == 'in': + self.in_arguments[name.replace('-', '_')] = name + + if direction == 'out': + self.out_arguments[name] = name.replace('-', '_') + + def __call__(self, **kwargs): + missing_arguments = self.in_arguments.keys() - kwargs.keys() + if missing_arguments: + self.logger.warning('Missing argument(s) \'' + "', '".join(missing_arguments) + '\'') + + unknown_arguments = kwargs.keys() - self.in_arguments.keys() + if unknown_arguments: + self.logger.warning('Unknown argument(s) \'' + "', '".join(unknown_arguments) + '\'') + + # Add SOAP action to header + self.headers['soapaction'] = '"{}#{}"'.format(self.service_type, self.name) + etree.register_namespace('u', self.service_type) + + # Prepare body for request + self.body.clear() + action = etree.SubElement(self.body, '{{{}}}{}'.format(self.service_type, self.name)) + for key in kwargs: + arg = etree.SubElement(action, self.in_arguments[key]) + arg.text = str(kwargs[key]) + + # soap._InitChallenge(header) + data = etree.tostring(self.envelope, encoding='utf-8', xml_declaration=True).decode() + self.logger.debug('post request in Action') + request = requests.post(f'{self.base_url}{self.control_url}', + headers=self.headers, + auth=self.auth, + data=data) + if request.status_code != 200: + return request.status_code + + # Translate response and prepare dict + xml = etree.parse(BytesIO(request.content)) + response = FritzDevice.AttributeDict() + for arg in list(xml.find('.//{{{}}}{}Response'.format(self.service_type, self.name))): + name = self.out_arguments[arg.tag] + response[name] = arg.text + return response + + class AttributeDict(dict): + """Direct access dict entries like attributes.""" + + def __getattr__(self, name): + return self[name] diff --git a/locale.yaml b/locale.yaml new file mode 100644 index 000000000..c0984a9ee --- /dev/null +++ b/locale.yaml @@ -0,0 +1,10 @@ +# translations for the web interface +plugin_translations: + # Translations for the plugin specially for the web interface + 'Wert 2': {'de': '=', 'en': 'Value 2'} + 'Wert 4': {'de': '=', 'en': 'Value 4'} + + # Alternative format for translations of longer texts: + 'Hier kommt der Inhalt des Webinterfaces hin.': + de: '=' + en: 'Here goes the content of the web interface.' diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 000000000..9d6d8c0c6 --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,84 @@ +# Metadata for the plugin +plugin: + # Global plugin attributes + type: unknown # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Beispiel Plugin für SmartHomeNG v1.5 und höher' + en: 'Sample plugin for SmartHomeNG v1.5 and up' + maintainer: +# tester: # Who tests this plugin? + state: develop # change to ready when done with development +# keywords: iot xyz +# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page +# support: https://knx-user-forum.de/forum/supportforen/smarthome-py + + version: 1.0.0 # Plugin version (must match the version specified in __init__.py) + sh_minversion: 1.8 # minimum shNG version to use this plugin +# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) +# py_minversion: 3.6 # minimum Python version to use for this plugin +# py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) + multi_instance: false # plugin supports multi instance + restartable: unknown + classname: AVM2 # class containing the plugin + +parameters: + # Definition of parameters to be configured in etc/plugin.yaml + username: + type: str + default: '' + description: + de: '(optional) Nutzername für den Login. Kann für manche Features benötigt werden! (Speziell für Fritz!OS 7 ist die Konfiguration der Fritz!Box auf `Anmeldung mit FRITZ!Box-Benutzernamen und Kennwort` notwendig' + en: '(optional) Login information (user). Can be needed to use some features of the AVM device. (Specially for Firtz!OS 7 the Fritz!Box should be configured for login with username and password' + password: + type: str + default: '' + hide: True + description: + de: '(optional) Passwort für den Login. Wird in der Regel immer benötigt und aus Sicherheitsgründen empfohlen.' + en: '(optional) Password for login. Is normally always needed and recommended due to security reasons' + host: + type: str + default: 'fritz.box' + description: + de: '(optional) Hostname oder IP-Adresse des FritzDevice.' + en: '(optional) Hostname or ip address of the FritzDevice.' + port: + type: int + default: 49443 + description: + de: '(optional) Port des FritzDevice, normalerweise 49433 für https oder 49000 für http' + en: '(optional) Port of the FritzDevice, typically 49433 for https or 49000 for http' + cycle: + type: int + default: 300 + description: + de: '(optional) Zeit zwischen zwei Updateläufen. Default ist 300 Sekunden.' + en: '(optional) Time period between two update cycles. Default is 300 seconds.' + ssl: + type: bool + default: True + description: + de: '(optional) Mit True wird das FritzDevice via https, mit False via http angesprochen.' + en: '(optional) True will add "https", False "http" to the URLs in the plugin.' + verify: + type: bool + default: False + description: + de: '(optional) Schaltet die Zertifikate-Prüfung an oder aus. Normalerweise False.' + en: '(optional) Turns certificate verification on or off. Typically False' + +item_attributes: NONE + # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) + +item_structs: NONE + # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) + +#item_attribute_prefixes: + # Definition of item attributes that only have a common prefix (enter 'item_attribute_prefixes: NONE' or ommit this section, if section should be empty) + # NOTE: This section should only be used, if really nessesary (e.g. for the stateengine plugin) + +plugin_functions: NONE + # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) + +logic_parameters: NONE + # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..6b9849bad --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +lxml \ No newline at end of file diff --git a/user_doc.rst b/user_doc.rst new file mode 100644 index 000000000..fdfdbd311 --- /dev/null +++ b/user_doc.rst @@ -0,0 +1,67 @@ +Sample Plugin <- hier den Namen des Plugins einsetzen +===================================================== + +Anforderungen +------------- +Anforderungen des Plugins auflisten. Werden spezielle Soft- oder Hardwarekomponenten benötigt? + +Notwendige Software +~~~~~~~~~~~~~~~~~~~ + +* die +* benötigte +* Software +* auflisten + +Dies beinhaltet Python- und SmartHomeNG-Module + +Unterstützte Geräte +~~~~~~~~~~~~~~~~~~~ + +* die +* unterstütze +* Hardware +* auflisten + + +Konfiguration +------------- + +plugin.yaml +~~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + + +items.yaml +~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + + +logic.yaml +~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + + +Funktionen +~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + + +Beispiele +--------- + +Hier können ausführlichere Beispiele und Anwendungsfälle beschrieben werden. + + +Web Interface +------------- + +Die Datei ``dev/sample_plugin/webif/templates/index.html`` sollte als Grundlage für Webinterfaces genutzt werden. Um Tabelleninhalte nach Spalten filtern und sortieren zu können, muss der entsprechende Code Block mit Referenz auf die relevante Table ID eingefügt werden (siehe Doku). + +SmartHomeNG liefert eine Reihe Komponenten von Drittherstellern mit, die für die Gestaltung des Webinterfaces genutzt werden können. Erweiterungen dieser Komponenten usw. finden sich im Ordner ``/modules/http/webif/gstatic``. + +Wenn das Plugin darüber hinaus noch Komponenten benötigt, werden diese im Ordner ``webif/static`` des Plugins abgelegt. diff --git a/webif/__init__.py b/webif/__init__.py new file mode 100644 index 000000000..cfe41ab94 --- /dev/null +++ b/webif/__init__.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2020- +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and +# upwards. +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + +import datetime +import time +import os + +from lib.item import Items +from lib.model.smartplugin import SmartPluginWebIf + + +# ------------------------------------------ +# Webinterface of the plugin +# ------------------------------------------ + +import cherrypy +import csv +from jinja2 import Environment, FileSystemLoader + + +class WebInterface(SmartPluginWebIf): + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + self.logger = plugin.logger + self.webif_dir = webif_dir + self.plugin = plugin + self.items = Items.get_instance() + + self.tplenv = self.init_template_environment() + + + @cherrypy.expose + def index(self, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + tmpl = self.tplenv.get_template('index.html') + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) + return tmpl.render(p=self.plugin, + items=sorted(self.items.return_items(), key=lambda k: str.lower(k['_path'])), + item_count=0) + + + @cherrypy.expose + def get_data_html(self, dataSet=None): + """ + Return data to update the webpage + + For the standard update mechanism of the web interface, the dataSet to return the data for is None + + :param dataSet: Dataset for which the data should be returned (standard: None) + :return: dict with the data needed to update the web page. + """ + if dataSet is None: + # get the new data + data = {} + + # data['item'] = {} + # for i in self.plugin.items: + # data['item'][i]['value'] = self.plugin.getitemvalue(i) + # + # return it as json the the web page + # try: + # return json.dumps(data) + # except Exception as e: + # self.logger.error("get_data_html exception: {}".format(e)) + return {} + diff --git a/webif/__pycache__/__init__.cpython-38.pyc b/webif/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..442ed75ec2dfe6ea02fab4c3bb91ba47d04cd005 GIT binary patch literal 2240 zcmZWq&5qkP5Edm_)_T3orf3c+dQgEJVj#7PpeG{;(!Uf71dAZsqJS?dEz)Z`)K5uz zH#VG8_L_G{j(w26LJz$5)N^k|QFMl~WbZ}>heL8WGo1Nm{L|4<&x7yBKOX)v_{j7A z#Kry>!r}=G&44hE5$R3pnM{aV`x75#Uj}(N2??GBvXe)X$nivWCtW8OPhyxud64%e zz5H-;NW33B)?v|=$08oR2_}6;&b&eQFR|JY-W*p{(LA~2 zQ!z`KP~!v$&rGRS$>p3Yo|rjz$ITN^Bkol$u!-VYFs^SZtWU?+{Mg;vNt;zwtKVAJ zM%_4UG?mi@_|+bs4q_Y0^8B1Dd(gUXqgq^HTy6iHo3x39?Fa;hy(=JCDX^8KhNpZF z5Rs`QtOtP&HJ39LgA#QB0z>A1c< zPO=ifRT$ksYiD8xbpy({D9*ta`oLN1`?|ODJ0neeU`8HG_Y8)H{^S`5&<5JjUGF_M z3;)!1(u$fnbc6s9XFIaIVZpYQ?5+SY_|*Z{y@#;Py8*S>(j z4tzR>r?t6wQ3HWdLowgW3b?j+RP>zh(*6_>hIqgR#i(M9aL%(iEriY+7(np=GPj-c zfxCnH&$vMod(d-M5x7Dpr)9BcYunzo*T%_ra9_iH6uz8`Y~E^3^t@a`DFAY7%>dl{ zjf-QacF?}IMJ^MWNiK!i(^GKHYS3Q~LV{5kwkl3tvYIq@w;tLMnVWe$!ajxvDHhpt z%qMmLuNV8_;ri2Sxx6MiAn=cfof5iEL35s_wwI=P$(9oNewzNeq_RFyw?U(go7%S# ze^r%Gub;!Vx`zZ?;2uF(DD>kC7!F{uAKwm1|5ixc{@}>Q;59cQ=KxToeyMFN#dL@u zuAB57T!Kqn4qZ##HgIfn>OK$~WctEJ3sEfSLxp9nI!Ms+o;S|9ZN=6PeEb. + +Extension can be png, svg or jpg + diff --git a/webif/templates/index.html b/webif/templates/index.html new file mode 100644 index 000000000..4c58a9f65 --- /dev/null +++ b/webif/templates/index.html @@ -0,0 +1,79 @@ +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} + + +{% set update_interval = 0 %} + + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Prompt 1{% if 1 == 2 %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}Prompt 4{{ _('Wert 4') }}
Prompt 2{{ _('Wert 2') }}Prompt 5-
Prompt 3-Prompt 6-
+{% endblock headtable %} + + + +{% block buttons %} +{% if 1==2 %} +
+ +
+{% endif %} +{% endblock %} + + +{% set tabcount = 4 %} + + + +{% if item_count==0 %} + {% set start_tab = 1 %} +{% endif %} + + + +{% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} +{% block bodytab1 %} +
+ {{ _('Hier kommt der Inhalt des Webinterfaces hin.') }}

+ {{ p._fritz_device.manufacturer_name }}

+ {{ p._fritz_device.safe_port }}

+ {{ p._fritz_device._network_devices }}

+ {{ p._fritz_device.client.LANDevice.WLANConfiguration[0].GetInfo() }}

+ +
+{% endblock bodytab1 %} From 17d1bd7c04c1912cfb5331869cd02dd1fda11430 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sun, 20 Feb 2022 18:51:52 +0100 Subject: [PATCH 003/178] - AHA Interface API added --- __init__.py | 1303 ++++++++++++++++++++++++++++++++---- plugin.yaml | 4 +- webif/templates/index.html | 66 +- 3 files changed, 1237 insertions(+), 136 deletions(-) diff --git a/__init__.py b/__init__.py index 5329f9393..fb32c0759 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### -# Copyright 2020- +# Copyright 2022- ######################################################################### # This file is part of SmartHomeNG. # https://www.smarthomeNG.de @@ -33,6 +33,13 @@ import lxml.etree as etree import requests +import hashlib +import time +from xml.etree import ElementTree +from typing import Dict +from enum import IntFlag +from abc import ABC + from lib.model.smartplugin import SmartPlugin from lib.utils import Utils from .webif import WebInterface @@ -40,47 +47,6 @@ TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} -### Devices / Services -""" - InternetGatewayDevice - DeviceInfo - DeviceConfig - Layer3Forwarding - LANConfigSecurity - ManagementServer - Time - UserInterface - X_AVM-DE_Storage - X_AVM-DE_WebDAVClient - X_AVM-DE_UPnP - X_AVM-DE_Speedtest - X_AVM-DE_RemoteAccess - X_AVM-DE_MyFritz - X_VoIP - X_AVM-DE_OnTel - X_AVM-DE_Dect - X_AVM-DE_TAM - X_AVM-DE_AppSetup - X_AVM-DE_Homeauto - X_AVM-DE_Homeplug - X_AVM-DE_Filelinks - X_AVM-DE_Auth - X_AVM-DE_HostFilter - LANDevice - WLANConfiguration - Hosts - LANEthernetInterfaceConfig - LANHostConfigManagement - WANDevice - WANCommonInterfaceConfig - WANDSLInterfaceConfig - WANConnectionDevice - WANDSLLinkConfig - WANEthernetLinkConfig - WANPPPConnection - WANIPConnection -""" - class AVM2(SmartPlugin): """ @@ -104,37 +70,40 @@ def __init__(self, sh): self.logger.info('Init AVM-Discover Plugin') - self._session = requests.Session() - self._timeout = 10 - self._verify = self.get_parameter_value('verify') - ssl = self.get_parameter_value('ssl') + _host = self.get_parameter_value('host') + _port = self.get_parameter_value('port') + _verify = self.get_parameter_value('verify') + _username = self.get_parameter_value('username') + _passwort = self.get_parameter_value('password') - if ssl and not self._verify: + ssl = self.get_parameter_value('ssl') + if ssl and not _verify: urllib3.disable_warnings() - - self._fritz_device = FritzDevice(self.get_parameter_value('host'), self.get_parameter_value('port'), ssl, - self.get_parameter_value('username'), self.get_parameter_value('password'), - self.get_instance_name(), self) - self._cycle = int(self.get_parameter_value('cycle')) self.alive = False - self.init_webinterface(WebInterface) + self._fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, self) + self._fritz_home = FritzHome(_host, ssl, _verify, _username, _passwort, self) + self.aha_devices = dict() + + # init WebIF + self.init_webinterface(WebInterface) def run(self): """ Run method for the plugin """ self.logger.debug("Run method called") - # setup scheduler for device poll loop (disable the following line, if you don't need to poll the device. Rember to comment the self_cycle statement in __init__ as well) - # self.scheduler_add('poll_device', self.poll_device, cycle=self._cycle) + # setup scheduler for device poll loop (disable the following line, if you don't need to poll the device. Rember to comment the self_cycle statement in __init__ as well) + self.scheduler_add('poll_device', self.poll_aha, prio=5, cycle=self._cycle, offset=2) self.alive = True + + # self.poll_aha() # self.logger.debug(f"manufacturer_name = {self._fritz_device.manufacturer_name}") # self.logger.debug(f"model_name = {self._fritz_device.model_name}") # self.logger.debug(f"safe_port = {self._fritz_device.safe_port}") - # self.logger.debug(f"network_devices = {self._fritz_device.get_network_devices()}") def stop(self): """ @@ -160,7 +129,6 @@ def parse_item(self, item): if self.has_iattr(item.conf, 'foo_itemtag'): self.logger.debug(f"parse item: {item}") - def parse_logic(self, logic): """ Default plugin parse_logic method @@ -191,13 +159,112 @@ def update_item(self, item, caller=None, source=None, dest=None): self.logger.debug(f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}") pass + def poll_aha(self): + + self.logger.debug(f'Starting AHA update loop for instance {self.get_instance_name()}.') + + # update devices + self._fritz_home.update_devices() + + # get device dict + _device_dict = self._fritz_home.get_devices_as_dict() + + for ain in _device_dict: + if not self.aha_devices.get(ain): + self.aha_devices[ain] = {} + self.aha_devices[ain]['connected_to_item'] = False + self.aha_devices[ain]['switch'] = {} + self.aha_devices[ain]['temperature_sensor'] = {} + self.aha_devices[ain]['thermostat'] = {} + self.aha_devices[ain]['alarm'] = {} + + self.aha_devices[ain]['online'] = bool(_device_dict[ain].present) + self.aha_devices[ain]['name'] = _device_dict[ain].name + self.aha_devices[ain]['productname'] = _device_dict[ain].productname + self.aha_devices[ain]['manufacturer'] = _device_dict[ain].manufacturer + self.aha_devices[ain]['fw_version'] = _device_dict[ain].fw_version + self.aha_devices[ain]['lock'] = bool(_device_dict[ain].lock) + self.aha_devices[ain]['device_lock'] = bool(_device_dict[ain].device_lock) + self.aha_devices[ain]['functions'] = [] + + if _device_dict[ain].has_thermostat: + self.aha_devices[ain]['functions'].append('thermostat') + self.aha_devices[ain]['thermostat']['actual_temperature'] = _device_dict[ain].actual_temperature + self.aha_devices[ain]['thermostat']['target_temperature'] = _device_dict[ain].target_temperature + self.aha_devices[ain]['thermostat']['comfort_temperature'] = _device_dict[ain].comfort_temperature + self.aha_devices[ain]['thermostat']['eco_temperature'] = _device_dict[ain].eco_temperature + self.aha_devices[ain]['thermostat']['battery_low'] = bool(_device_dict[ain].battery_low) + self.aha_devices[ain]['thermostat']['battery_level'] = _device_dict[ain].battery_level + self.aha_devices[ain]['thermostat']['window_open'] = bool(_device_dict[ain].window_open) + self.aha_devices[ain]['thermostat']['summer_active'] = bool(_device_dict[ain].summer_active) + self.aha_devices[ain]['thermostat']['holiday_active'] = bool(_device_dict[ain].holiday_active) + + if _device_dict[ain].has_switch: + self.aha_devices[ain]['functions'].append('switch') + self.aha_devices[ain]['switch']['switch_state'] = bool(_device_dict[ain].switch_state) + self.aha_devices[ain]['switch']['power'] = _device_dict[ain].power + self.aha_devices[ain]['switch']['energy'] = _device_dict[ain].energy + self.aha_devices[ain]['switch']['voltage'] = _device_dict[ain].voltage + + if _device_dict[ain].has_temperature_sensor: + self.aha_devices[ain]['functions'].append('temperature_sensor') + self.aha_devices[ain]['temperature_sensor']['temperature'] = _device_dict[ain].temperature + self.aha_devices[ain]['temperature_sensor']['offset'] = _device_dict[ain].offset + + if _device_dict[ain].has_alarm: + self.aha_devices[ain]['functions'].append('alarm') + self.aha_devices[ain]['alarm']['alert_state'] = bool(_device_dict[ain].alert_state) + class FritzDevice: """ - This class encapsulates information related to a specific FritzDevice, such has host, port, ssl, username, password, or related items + This class encapsulates information related to a specific FritzDevice + + # Devices / Services + InternetGatewayDevice + DeviceInfo + GetInfo + SetProvisioningCode + GetDeviceLog + GetSecurityPort + DeviceConfig + Layer3Forwarding + LANConfigSecurity + ManagementServer + Time + UserInterface + X_AVM-DE_Storage + X_AVM-DE_WebDAVClient + X_AVM-DE_UPnP + X_AVM-DE_Speedtest + X_AVM-DE_RemoteAccess + X_AVM-DE_MyFritz + X_VoIP + X_AVM-DE_OnTel + X_AVM-DE_Dect + X_AVM-DE_TAM + X_AVM-DE_AppSetup + X_AVM-DE_Homeauto + X_AVM-DE_Homeplug + X_AVM-DE_Filelinks + X_AVM-DE_Auth + X_AVM-DE_HostFilter + LANDevice + WLANConfiguration + Hosts + LANEthernetInterfaceConfig + LANHostConfigManagement + WANDevice + WANCommonInterfaceConfig + WANDSLInterfaceConfig + WANConnectionDevice + WANDSLLinkConfig + WANEthernetLinkConfig + WANPPPConnection + WANIPConnection """ - def __init__(self, host, port, ssl, username, password, identifier='default', plugin_instance=None): + def __init__(self, host, port, ssl, verify, username, password, plugin_instance=None): self._plugin_instance = plugin_instance self._plugin_instance.logger.debug("Init FritzDevice") @@ -205,22 +272,17 @@ def __init__(self, host, port, ssl, username, password, identifier='default', pl self._host = host self._port = port self._ssl = ssl + self._verify = verify self._username = username self._password = password - self._identifier = identifier self._available = True self._items = [] self._smarthome_items = [] self._smarthome_devices = {} - self.InternetGatewayDevice = None - self.LANDevice = None - self.WANDevice = None - self.WANConnectionDevice = None - - self.client = FritzDevice.Client(self._username, self._password, base_url=self._build_url(), plugin_instance=plugin_instance) - - self._network_devices = self.network_devices() + self._DeviceInfo = {} + self._Hosts = {} + self.client = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), plugin_instance=plugin_instance) def _build_url(self): """ @@ -236,14 +298,6 @@ def _build_url(self): return url - def get_identifier(self): - """ - Returns the internal identifier of the FritzDevice - - :return: identifier of the device, as set in plugin.conf - """ - return self._identifier - def get_host(self): """ Returns the hostname / IP of the FritzDevice @@ -260,6 +314,14 @@ def get_port(self): """ return self._port + def get_verify(self): + """ + Returns the wether to verify or not + + :return: verify + """ + return self._verify + def get_items(self): """ Returns added items @@ -316,68 +378,49 @@ def get_password(self): """ return self._password - def get_smarthome_items(self): - """ - Returns added items - - :return: array of smarthome items hold by the plugin - """ - return self._smarthome_items - - def get_smarthome_devices(self): - """ - Returns added items - - :return: dict of smarthome devices hold by the plugin - """ - return self._smarthome_devices - - def get_network_devices(self): - return self._network_devices - @property def manufacturer_name(self): - if not self.InternetGatewayDevice: - self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self.InternetGatewayDevice.NewManufacturerName + if 'GetInfo' not in self._DeviceInfo: + self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._DeviceInfo['GetInfo'].NewManufacturerName @property def manufacturer_oui(self): - if not self.InternetGatewayDevice: - self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self.InternetGatewayDevice.NewManufacturerOUI + if 'GetInfo' not in self._DeviceInfo: + self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._DeviceInfo['GetInfo'].NewManufacturerOUI @property def model_name(self): - if not self.InternetGatewayDevice: - self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self.InternetGatewayDevice.NewModelName + if 'GetInfo' not in self._DeviceInfo: + self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._DeviceInfo['GetInfo'].NewModelName @property def desciption(self): - if not self.InternetGatewayDevice: - self.InternetGatewayDevice = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self.InternetGatewayDevice.NewDescription + if 'GetInfo' not in self._DeviceInfo: + self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._DeviceInfo['GetInfo'].NewDescription @property def safe_port(self): - return self.client.InternetGatewayDevice.DeviceInfo.GetSecurityPort().NewSecurityPort - - def network_devices(self): - devices = dict() - number_of_entries = self.client.LANDevice.Hosts.GetHostNumberOfEntries() - for index in range(int(number_of_entries.NewHostNumberOfEntries)): - host = self.client.LANDevice.Hosts.GetGenericHostEntry(NewIndex=index) - self._plugin_instance.logger.debug(f"host = {host}") - devices[index] = {'ip': host.NewIPAddress, 'mac': host.NewMACAddress, 'name': host.NewHostName} - return devices + if 'GetSecurityPort' not in self._DeviceInfo: + self._DeviceInfo['GetSecurityPort'] = self.client.InternetGatewayDevice.DeviceInfo.GetSecurityPort() + return self._DeviceInfo['GetSecurityPort'].NewSecurityPort + + def GetGenericHostEntry(self, index): + if 'GetGenericHostEntry' not in self._Hosts: + self._Hosts['GetGenericHostEntry'] = {} + if index not in self._Hosts['GetGenericHostEntry']: + self._Hosts['GetGenericHostEntry'][index] = {} + self._Hosts['GetGenericHostEntry'][index].update(self.client.LANDevice.Hosts.GetGenericHostEntry(NewIndex=index)) + return self._Hosts['GetGenericHostEntry'][index] def wlan_info(self): for index in range(3): info = self.client.LANDevice.WLANConfiguration[index].GetInfo() self._plugin_instance.logger.debug(f"WLAN info{index} = {info}") - class Client: """TR-064 client. :param str username: @@ -388,13 +431,14 @@ class Client: URL to router. """ - def __init__(self, username, password, base_url='https://192.168.178.1:49443', plugin_instance=None): + def __init__(self, username, password, verify, base_url='https://192.168.178.1:49443', plugin_instance=None): # handle plugin instance self._plugin_instance = plugin_instance self.base_url = base_url self.auth = HTTPDigestAuth(username, password) + self.verify = verify self.devices = {} self._plugin_instance.logger.debug(f"Init Client") @@ -409,7 +453,7 @@ def __getattr__(self, name): def _fetch_devices(self, description_file='/tr64desc.xml'): """Fetch device description.""" - request = requests.get(f'{self.base_url}{description_file}') + request = requests.get(f'{self.base_url}{description_file}', verify=self.verify) if request.status_code == 200: xml = etree.parse(BytesIO(request.content)) @@ -417,7 +461,7 @@ def _fetch_devices(self, description_file='/tr64desc.xml'): for device in xml.findall('.//device', namespaces=TR064_DEVICE_NAMESPACE): name = device.findtext('deviceType', namespaces=TR064_DEVICE_NAMESPACE).split(':')[-2] if name not in self.devices: - self.devices[name] = FritzDevice.Device(device, self.auth, self.base_url) + self.devices[name] = FritzDevice.Device(device, self.auth, self.verify, self.base_url) class Device: """TR-064 device. @@ -429,9 +473,10 @@ class Device: URL to router. """ - def __init__(self, xml, auth, base_url): + def __init__(self, xml, auth, verify, base_url): self.logger = logging.getLogger(__name__) self.services = {} + self.verify = verify for service in xml.findall('./serviceList/service', namespaces=TR064_DEVICE_NAMESPACE): service_type = service.findtext('serviceType', namespaces=TR064_DEVICE_NAMESPACE) @@ -447,6 +492,7 @@ def __init__(self, xml, auth, base_url): self.services[name].append( FritzDevice.Service( auth, + self.verify, base_url, service_type, service_id, @@ -475,8 +521,9 @@ def __getitem__(self, index): class Service: """TR-064 service.""" - def __init__(self, auth, base_url, service_type, service_id, scpdurl, control_url, event_sub_url): + def __init__(self, auth, verify, base_url, service_type, service_id, scpdurl, control_url, event_sub_url): self.auth = auth + self.verify = verify self.base_url = base_url self.service_type = service_type self.service_id = service_id @@ -494,7 +541,7 @@ def __getattr__(self, name): def _fetch_actions(self, scpdurl): """Fetch action description.""" - request = requests.get(f'{self.base_url}{scpdurl}') + request = requests.get(f'{self.base_url}{scpdurl}', verify=self.verify) if request.status_code == 200: xml = etree.parse(BytesIO(request.content)) @@ -508,7 +555,8 @@ def _fetch_actions(self, scpdurl): name, self.service_type, self.service_id, - self.control_url + self.control_url, + self.verify ) class Action: @@ -529,11 +577,12 @@ class Action: Control URL """ - def __init__(self, xml, auth, base_url, name, service_type, service_id, control_url): + def __init__(self, xml, auth, base_url, name, service_type, service_id, control_url, verify): self.logger = logging.getLogger(__name__) self.auth = auth + self.verify = verify self.base_url = base_url self.name = name self.service_type = service_type @@ -586,11 +635,11 @@ def __call__(self, **kwargs): # soap._InitChallenge(header) data = etree.tostring(self.envelope, encoding='utf-8', xml_declaration=True).decode() - self.logger.debug('post request in Action') request = requests.post(f'{self.base_url}{self.control_url}', headers=self.headers, auth=self.auth, - data=data) + data=data, + verify=self.verify) if request.status_code != 200: return request.status_code @@ -607,3 +656,1001 @@ class AttributeDict(dict): def __getattr__(self, name): return self[name] + + +class FritzHome: + """Fritzhome object to communicate with the device.""" + + def __init__(self, host, ssl, verify, user, password, plugin_instance): + + self._plugin_instance = plugin_instance + self._plugin_instance.logger.debug("Init Fritzhome") + + self._host = host + self._ssl = ssl + self._verify = verify + self._user = user + self._password = password + + self._sid = None + self._devices: Dict[str, FritzHome.FritzhomeDevice] = None + self._templates: Dict[str, FritzHome.FritzhomeTemplate] = None + self._logged_in = False + + self._session = requests.Session() + + def _request(self, url, params=None, timeout=10): + """Send a request with parameters.""" + rsp = self._session.get(url, params=params, timeout=timeout, verify=self._verify) + rsp.raise_for_status() + return rsp.text.strip() + + def _login_request(self, username=None, secret=None): + """Send a login request with paramerters.""" + url = self.get_prefixed_host() + "/login_sid.lua" + params = {} + if username: + params["username"] = username + if secret: + params["response"] = secret + plain = self._request(url, params) + dom = ElementTree.fromstring(plain) + sid = dom.findtext("SID") + challenge = dom.findtext("Challenge") + return sid, challenge + + def _logout_request(self): + """Send a logout request.""" + url = self.get_prefixed_host() + "/login_sid.lua" + params = {"security:command/logout": "1", "sid": self._sid} + + self._request(url, params) + + @staticmethod + def _create_login_secret(challenge, password): + """Create a login secret.""" + to_hash = (challenge + "-" + password).encode("UTF-16LE") + hashed = hashlib.md5(to_hash).hexdigest() + return "{0}-{1}".format(challenge, hashed) + + def _aha_request(self, cmd, ain=None, param=None, rf=str): + """Send an AHA request.""" + + self._plugin_instance.logger.debug(f"_aha_request called with cmd={cmd}, ain={ain}, param={param}, rf={rf} ") + + if not self._logged_in: + self.login() + + url = self.get_prefixed_host() + "/webservices/homeautoswitch.lua" + params = {"switchcmd": cmd, "sid": self._sid} + if param: + params.update(param) + if ain: + params["ain"] = ain + + plain = self._request(url, params) + + self.logout() + + if plain == "inval": + self._plugin_instance.logger.error("InvalidError") + return + + if rf == bool: + return bool(int(plain)) + return rf(plain) + + def login(self): + """Login and get a valid session ID.""" + self._plugin_instance.logger.debug("AHA login called") + try: + (sid, challenge) = self._login_request() + if sid == "0000000000000000": + secret = self._create_login_secret(challenge, self._password) + (sid2, challenge) = self._login_request(username=self._user, secret=secret) + if sid2 == "0000000000000000": + self._plugin_instance.logger.warning("login failed %s", sid2) + self._plugin_instance.logger.error(f"LoginError for {self._user}") + return + self._sid = sid2 + except Exception as e: + self._plugin_instance.logger.error(f"LoginError {e} occurred for {self._user}") + else: + self._logged_in = True + + def logout(self): + """Logout.""" + self._plugin_instance.logger.debug("AHA logout called") + self._logout_request() + self._sid = None + self._logged_in = False + + def get_prefixed_host(self): + """Choose the correct protocol prefix for the host. + Supports three input formats: + - https://(requests use strict certificate validation by default) + - http:// (unecrypted) + - (unencrypted) + """ + host = self._host + if not host.startswith("https://") or not host.startswith("http://"): + if self._ssl: + host = "https://"+host + else: + host = "http://"+host + return host + + def update_devices(self): + self._plugin_instance.logger.info("Updating Devices ...") + if self._devices is None: + self._devices = {} + + for element in self.get_device_elements(): + if element.attrib["identifier"] in self._devices.keys(): + self._plugin_instance.logger.info("Updating already existing Device " + element.attrib["identifier"]) + self._devices[element.attrib["identifier"]]._update_from_node(element) + else: + self._plugin_instance.logger.info("Adding new Device " + element.attrib["identifier"]) + device = FritzHome.FritzhomeDevice(self, node=element) + self._devices[device.ain] = device + return True + + 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) + return dom.findall(entity_type) + + 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 + + def get_devices(self): + """Get the list of all known devices.""" + return list(self.get_devices_as_dict().values()) + + def get_devices_as_dict(self): + """Get the list of all known devices.""" + if self._devices is None: + self.update_devices() + return self._devices + + def get_device_by_ain(self, ain): + """Return a device specified by the AIN.""" + return self.get_devices_as_dict()[ain] + + def get_device_present(self, ain): + """Get the device presence.""" + return self._aha_request("getswitchpresent", ain=ain, rf=bool) + + def get_device_name(self, ain): + """Get the device name.""" + return self._aha_request("getswitchname", ain=ain) + + def get_switch_state(self, ain): + """Get the switch state.""" + return self._aha_request("getswitchstate", ain=ain, rf=bool) + + def set_switch_state_on(self, ain): + """Set the switch to on state.""" + return self._aha_request("setswitchon", ain=ain, rf=bool) + + def set_switch_state_off(self, ain): + """Set the switch to off state.""" + return self._aha_request("setswitchoff", ain=ain, rf=bool) + + def set_switch_state_toggle(self, ain): + """Toggle the switch state.""" + return self._aha_request("setswitchtoggle", ain=ain, rf=bool) + + def get_switch_power(self, ain): + """Get the switch power consumption.""" + return self._aha_request("getswitchpower", ain=ain, rf=int) + + def get_switch_energy(self, ain): + """Get the switch energy.""" + return self._aha_request("getswitchenergy", ain=ain, rf=int) + + def get_temperature(self, ain): + """Get the device temperature sensor value.""" + return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 + + def _get_temperature(self, ain, name): + plain = self._aha_request(name, ain=ain, rf=float) + return (plain - 16) / 2 + 8 + + def get_target_temperature(self, ain): + """Get the thermostate target temperature.""" + return self._get_temperature(ain, "gethkrtsoll") + + def set_target_temperature(self, ain, temperature): + """Set the thermostate target temperature.""" + temp = int(16 + ((float(temperature) - 8) * 2)) + + if temp < min(range(16, 56)): + temp = 253 + elif temp > max(range(16, 56)): + temp = 254 + + self._aha_request("sethkrtsoll", ain=ain, param={'param': temp}) + + def set_window_open(self, ain, seconds): + """Set the thermostate target temperature.""" + endtimestamp = int(time.time() + seconds) + + self._aha_request("sethkrwindowopen", ain=ain, param={'endtimestamp': endtimestamp}) + + def get_comfort_temperature(self, ain): + """Get the thermostate comfort temperature.""" + return self._get_temperature(ain, "gethkrkomfort") + + def get_eco_temperature(self, ain): + """Get the thermostate eco temperature.""" + return self._get_temperature(ain, "gethkrabsenk") + + def get_device_statistics(self, ain): + """Get device statistics.""" + plain = self._aha_request("getbasicdevicestats", ain=ain) + return plain + + # Lightbulb-related commands + + def set_state_off(self, ain): + """Set the switch/actuator/lightbulb to on state.""" + self._aha_request("setsimpleonoff", ain=ain, param={'onoff': 0}) + + def set_state_on(self, ain): + """Set the switch/actuator/lightbulb to on state.""" + self._aha_request("setsimpleonoff", ain=ain, param={'onoff': 1}) + + def set_state_toggle(self, ain): + """Toggle the switch/actuator/lightbulb state.""" + self._aha_request("setsimpleonoff", ain=ain, param={'onoff': 2}) + + def set_level(self, ain, level): + """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)}) + + def set_level_percentage(self, ain, level): + """Set level/brightness/height in interval [0,100].""" + # Scale percentage to [0,255] interval + self.set_level(ain, int(level * 2.55)) + + def _get_colordefaults(self, ain): + plain = self._aha_request("getcolordefaults", ain=ain) + return ElementTree.fromstring(plain) + + def get_colors(self, ain): + """Get colors (HSV-space) supported by this lightbulb.""" + colordefaults = self._get_colordefaults(ain) + colors = {} + for hs in colordefaults.iter('hs'): + name = hs.find("name").text.strip() + values = [] + for st in hs.iter("color"): + values.append( + ( + st.get("hue"), + st.get("sat"), + st.get("val") + ) + ) + colors[name] = values + return colors + + def set_color(self, ain, hsv, duration=0, mapped=True): + """Set hue and saturation. + hsv: HUE colorspace element obtained from get_colors() + duration: Speed of change in seconds, 0 = instant + """ + params = { + 'hue': int(hsv[0]), + 'saturation': int(hsv[1]), + "duration": int(duration) * 10 + } + if mapped: + self._aha_request("setcolor", ain=ain, param=params) + else: + # undocumented API method for free color selection + self._aha_request("setunmappedcolor", ain=ain, param=params) + + def get_color_temps(self, ain): + """Get temperatures supported by this lightbulb.""" + colordefaults = self._get_colordefaults(ain) + temperatures = [] + for temp in colordefaults.iter('temp'): + temperatures.append(temp.get("value")) + return temperatures + + def set_color_temp(self, ain, temperature, duration=0): + """Set color temperature. + 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) + + # Template-related commands + + def update_templates(self): + self._plugin_instance.logger.info("Updating Templates ...") + if self._templates is None: + self._templates = {} + + for element in self.get_template_elements(): + if element.attrib["identifier"] in self._templates.keys(): + self._plugin_instance.logger.info( + "Updating already existing Template " + element.attrib["identifier"] + ) + self._templates[element.attrib["identifier"]]._update_from_node(element) + else: + self._plugin_instance.logger.info("Adding new Template " + element.attrib["identifier"]) + template = FritzHome.FritzhomeTemplate(self, node=element) + self._templates[template.ain] = template + return True + + def get_template_elements(self): + """Get the DOM elements for the template list.""" + return self._get_listinfo_elements("template") + + def get_templates(self): + """Get the list of all known templates.""" + return list(self.get_templates_as_dict().values()) + + def get_templates_as_dict(self): + """Get the list of all known templates.""" + if self._templates is None: + self.update_templates() + return self._templates + + def get_template_by_ain(self, ain): + """Return a template specified by the AIN.""" + return self.get_templates_as_dict()[ain] + + def apply_template(self, ain): + """Applies a template.""" + self._aha_request("applytemplate", ain=ain) + + class FritzhomeDeviceFeatures(IntFlag): + ALARM = 0x0010 + UNKNOWN = 0x0020 + BUTTON = 0x0020 + THERMOSTAT = 0x0040 + POWER_METER = 0x0080 + TEMPERATURE = 0x0100 + SWITCH = 0x0200 + DECT_REPEATER = 0x0400 + MICROPHONE = 0x0800 + HANFUN = 0x2000 + SWITCHABLE = 0x8000 + DIMMABLE = 0x10000 + LIGHTBULB = 0x20000 + + class FritzhomeEntityBase(ABC): + """The Fritzhome Entity class.""" + + def __init__(self, fritz=None, node=None): + # init logger + self.logger = logging.getLogger(__name__) + + self._fritz = None + self.ain: str = None + self._functionsbitmask = None + + if fritz is not None: + self._fritz = fritz + if node is not None: + self._update_from_node(node) + + def __repr__(self): + """Return a string.""" + return f"{self.ain} {self.name}" + + def _has_feature(self, feature) -> bool: + return feature in FritzHome.FritzhomeDeviceFeatures(self._functionsbitmask) + + def _update_from_node(self, node): + self.logger.debug(ElementTree.tostring(node)) + self.ain = node.attrib["identifier"] + self._functionsbitmask = int(node.attrib["functionbitmask"]) + + self.name = node.findtext("name").strip() + + # XML Helpers + + def get_node_value(self, elem, node): + return elem.findtext(node) + + def get_node_value_as_int(self, elem, node) -> int: + return int(self.get_node_value(elem, node)) + + def get_node_value_as_int_as_bool(self, elem, node) -> bool: + return bool(self.get_node_value_as_int(elem, node)) + + def get_temp_from_node(self, elem, node): + return float(self.get_node_value(elem, node)) / 2 + + class FritzhomeTemplate(FritzhomeEntityBase): + """The Fritzhome Template class.""" + + devices = None + features = None + apply_hkr_summer = None + apply_hkr_temperature = None + apply_hkr_holidays = None + apply_hkr_time_table = None + apply_relay_manual = None + apply_relay_automatic = None + apply_level = None + apply_color = None + apply_dialhelper = None + + def _update_from_node(self, node): + super()._update_from_node(node) + + self.features = FritzHome.FritzhomeDeviceFeatures(self._functionsbitmask) + + applymask = node.find("applymask") + self.apply_hkr_summer = applymask.find("hkr_summer") is not None + self.apply_hkr_temperature = applymask.find("hkr_temperature") is not None + self.apply_hkr_holidays = applymask.find("hkr_holidays") is not None + self.apply_hkr_time_table = applymask.find("hkr_time_table") is not None + self.apply_relay_manual = applymask.find("relay_manual") is not None + self.apply_relay_automatic = applymask.find("relay_automatic") is not None + self.apply_level = applymask.find("level") is not None + self.apply_color = applymask.find("color") is not None + self.apply_dialhelper = applymask.find("dialhelper") is not None + + self.devices = [] + for device in node.find("devices").findall("device"): + self.devices.append(device.attrib["identifier"]) + + class FritzhomeDeviceBase(FritzhomeEntityBase): + """The Fritzhome Device class.""" + + identifier = None + fw_version = None + manufacturer = None + productname = None + present = None + + def __repr__(self): + """Return a string.""" + return "{ain} {identifier} {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): + 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"))) + + # General + def get_present(self): + """Check if the device is present.""" + return self._fritz.get_device_present(self.ain) + + class FritzhomeDeviceAlarm(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + alert_state = None + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_alarm: + self._update_alarm_from_node(node) + + # Alarm + @property + def has_alarm(self): + """Check if the device has alarm function.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.ALARM) + + def _update_alarm_from_node(self, node): + val = node.find("alert") + try: + self.alert_state = self.get_node_value_as_int_as_bool(val, "state") + except (Exception, ValueError): + pass + + class FritzhomeDeviceButton(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_button: + self._update_button_from_node(node) + + # Button + @property + def has_button(self): + """Check if the device has button function.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.BUTTON) + + def _update_button_from_node(self, node): + self.buttons = {} + + for element in node.findall("button"): + button = FritzHome.FritzhomeButton(element) + self.buttons[button.ain] = button + + try: + self.tx_busy = self.get_node_value_as_int_as_bool(node, "txbusy") + 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 + + def get_button_by_ain(self, ain): + return self.buttons[ain] + + class FritzhomeButton(object): + """The Fritzhome Button Device class.""" + + ain = None + identifier = None + name = None + last_pressed = None + + def __init__(self, node=None): + # init logger + self.logger = logging.getLogger(__name__) + + if node is not None: + self._update_from_node(node) + + def _update_from_node(self, node): + self.logger.debug(ElementTree.tostring(node)) + self.ain = node.attrib["identifier"] + self.identifier = node.attrib["id"] + self.name = node.findtext("name") + try: + self.last_pressed = self.get_node_value_as_int(node, "lastpressedtimestamp") + except ValueError: + pass + + def get_node_value(self, elem, node): + return elem.findtext(node) + + def get_node_value_as_int(self, elem, node) -> int: + return int(self.get_node_value(elem, node)) + + class FritzhomeDeviceLightBulb(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + state = None + level = None + hue = None + saturation = None + unmapped_hue = None + unmapped_saturation = None + color_temp = None + color_mode = None + supported_color_mode = None + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_lightbulb: + self._update_lightbulb_from_node(node) + + # Light Bulb + @property + def has_lightbulb(self): + """Check if the device has LightBulb function.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.LIGHTBULB) + + def _update_lightbulb_from_node(self, node): + state_element = node.find("simpleonoff") + try: + self.state = self.get_node_value_as_int_as_bool(state_element, "state") + + except ValueError: + pass + + level_element = node.find("levelcontrol") + try: + self.level = self.get_node_value_as_int(level_element, "level") + + self.level_percentage = int(self.level / 2.55) + except ValueError: + pass + + colorcontrol_element = node.find("colorcontrol") + try: + self.color_mode = colorcontrol_element.attrib.get("current_mode") + + self.supported_color_mode = colorcontrol_element.attrib.get( + "supported_modes") + + except ValueError: + pass + + try: + self.hue = self.get_node_value_as_int(colorcontrol_element, "hue") + + self.saturation = self.get_node_value_as_int(colorcontrol_element, + "saturation") + + self.unmapped_hue = self.get_node_value_as_int(colorcontrol_element, "unmapped_hue") + + self.unmapped_saturation = self.get_node_value_as_int(colorcontrol_element, + "unmapped_saturation") + except ValueError: + # reset values after color mode changed + self.hue = None + self.saturation = None + self.unmapped_hue = None + self.unmapped_saturation = None + + try: + self.color_temp = self.get_node_value_as_int(colorcontrol_element, + "temperature") + + except ValueError: + # reset values after color mode changed + self.color_temp = None + + def set_state_off(self): + """Switch light bulb off.""" + self.state = True + self._fritz.set_state_off(self.ain) + + def set_state_on(self): + """Switch light bulb on.""" + self.state = True + self._fritz.set_state_on(self.ain) + + def set_state_toggle(self): + """Toogle light bulb state.""" + self.state = True + self._fritz.set_state_toggle(self.ain) + + def set_level(self, level): + """Set HSV color.""" + self._fritz.set_level(self.ain, level) + + def get_colors(self): + """Get the supported colors.""" + return self._fritz.get_colors(self.ain) + + def set_color(self, hsv, duration=0): + """Set HSV color.""" + self._fritz.set_color(self.ain, hsv, duration, True) + + def set_unmapped_color(self, hsv, duration=0): + """Set unmapped HSV color (Free color selection).""" + self._fritz.set_color(self.ain, hsv, duration, False) + + def get_color_temps(self): + """Get the supported color temperatures energy.""" + return self._fritz.get_color_temps(self.ain) + + def set_color_temp(self, temperature, duration=0): + """Set white color temperature.""" + self._fritz.set_color_temp(self.ain, temperature, duration) + + class FritzhomeDevicePowermeter(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + power = None + energy = 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(FritzHome.FritzhomeDeviceFeatures.POWER_METER) + + def _update_powermeter_from_node(self, node): + val = node.find("powermeter") + self.power = int(val.findtext("power")) + self.energy = int(val.findtext("energy")) + try: + self.voltage = float(int(val.findtext("voltage")) / 1000) + except Exception: + pass + + def get_switch_power(self): + """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) + + class FritzhomeDeviceRepeater(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + # Repeater + @property + def has_repeater(self): + """Check if the device has repeater function.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.DECT_REPEATER) + + 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.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.SWITCH) + + def _update_switch_from_node(self, node): + val = node.find("switch") + self.switch_state = self.get_node_value_as_int_as_bool(val, "state") + self.switch_mode = self.get_node_value(val, "mode") + self.lock = bool(self.get_node_value(val, "lock")) + # optional value + try: + self.device_lock = self.get_node_value_as_int_as_bool(val, "devicelock") + except Exception: + pass + + def get_switch_state(self): + """Get the switch state.""" + return self._fritz.get_switch_state(self.ain) + + def set_switch_state_on(self): + """Set the switch state to on.""" + return self._fritz.set_switch_state_on(self.ain) + + def set_switch_state_off(self): + """Set the switch state to off.""" + return self._fritz.set_switch_state_off(self.ain) + + def set_switch_state_toggle(self): + """Toggle the switch state.""" + return self._fritz.set_switch_state_toggle(self.ain) + + class FritzhomeDeviceTemperature(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + offset = None + temperature = None + rel_humidity = 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(FritzHome.FritzhomeDeviceFeatures.TEMPERATURE) + + def _update_temperature_from_node(self, node): + 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 + + humidity_element = node.find("humidity") + if humidity_element is not None: + try: + self.rel_humidity = self.get_node_value_as_int(humidity_element, + "rel_humidity") + except ValueError: + pass + + class FritzhomeDeviceThermostat(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + actual_temperature = None + target_temperature = None + eco_temperature = None + comfort_temperature = None + device_lock = None + lock = None + error_code = None + battery_low = None + battery_level = None + window_open = None + summer_active = None + holiday_active = None + nextchange_endperiod = None + nextchange_temperature = None + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + if self.has_thermostat: + self._update_hkr_from_node(node) + + # Thermostat + @property + def has_thermostat(self): + """Check if the device has thermostat function.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.THERMOSTAT) + + def _update_hkr_from_node(self, node): + hkr_element = node.find("hkr") + + try: + self.actual_temperature = self.get_temp_from_node(hkr_element, "tist") + except ValueError: + pass + + self.target_temperature = self.get_temp_from_node(hkr_element, "tsoll") + self.eco_temperature = self.get_temp_from_node(hkr_element, "absenk") + self.comfort_temperature = self.get_temp_from_node(hkr_element, "komfort") + + # optional value + try: + self.device_lock = self.get_node_value_as_int_as_bool( + hkr_element, "devicelock" + ) + self.lock = self.get_node_value_as_int_as_bool(hkr_element, "lock") + self.error_code = self.get_node_value_as_int(hkr_element, "errorcode") + self.battery_low = self.get_node_value_as_int_as_bool( + hkr_element, "batterylow" + ) + self.battery_level = int(self.get_node_value_as_int(hkr_element, "battery")) + self.window_open = self.get_node_value_as_int_as_bool( + hkr_element, "windowopenactiv" + ) + self.summer_active = self.get_node_value_as_int_as_bool( + hkr_element, "summeractive" + ) + self.holiday_active = self.get_node_value_as_int_as_bool( + hkr_element, "holidayactive" + ) + nextchange_element = hkr_element.find("nextchange") + self.nextchange_endperiod = int( + self.get_node_value_as_int(nextchange_element, "endperiod") + ) + self.nextchange_temperature = self.get_temp_from_node( + nextchange_element, "tchange" + ) + except Exception: + pass + + def get_temperature(self): + """Get the device temperature value.""" + return self._fritz.get_temperature(self.ain) + + def get_target_temperature(self): + """Get the thermostate target temperature.""" + return self._fritz.get_target_temperature(self.ain) + + def set_target_temperature(self, temperature): + """Set the thermostate target temperature.""" + return self._fritz.set_target_temperature(self.ain, temperature) + + def set_window_open(self, seconds): + """Set the thermostate to window open.""" + return self._fritz.set_window_open(self.ain, seconds) + + def get_comfort_temperature(self): + """Get the thermostate comfort temperature.""" + return self._fritz.get_comfort_temperature(self.ain) + + def get_eco_temperature(self): + """Get the thermostate eco temperature.""" + return self._fritz.get_eco_temperature(self.ain) + + def get_hkr_state(self): + """Get the thermostate state.""" + try: + return { + 126.5: "off", + 127.0: "on", + self.eco_temperature: "eco", + self.comfort_temperature: "comfort", + }[self.target_temperature] + except KeyError: + return "manual" + + def set_hkr_state(self, state): + """Set the state of the thermostat. + Possible values for state are: 'on', 'off', 'comfort', 'eco'. + """ + try: + value = { + "off": 0, + "on": 100, + "eco": self.eco_temperature, + "comfort": self.comfort_temperature, + }[state] + except KeyError: + return + + self.set_target_temperature(value) + + class FritzhomeDevice( + FritzhomeDeviceAlarm, + FritzhomeDeviceButton, + FritzhomeDevicePowermeter, + FritzhomeDeviceRepeater, + FritzhomeDeviceSwitch, + FritzhomeDeviceTemperature, + FritzhomeDeviceThermostat, + FritzhomeDeviceLightBulb, + ): + """The Fritzhome Device class.""" + + def __init__(self, fritz=None, node=None): + super().__init__(fritz, node) + + def _update_from_node(self, node): + super()._update_from_node(node) diff --git a/plugin.yaml b/plugin.yaml index 9d6d8c0c6..631937a9b 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -46,8 +46,8 @@ parameters: type: int default: 49443 description: - de: '(optional) Port des FritzDevice, normalerweise 49433 für https oder 49000 für http' - en: '(optional) Port of the FritzDevice, typically 49433 for https or 49000 for http' + de: '(optional) Port des FritzDevice, normalerweise 49443 für https oder 49000 für http' + en: '(optional) Port of the FritzDevice, typically 49443 for https or 49000 for http' cycle: type: int default: 300 diff --git a/webif/templates/index.html b/webif/templates/index.html index 4c58a9f65..d2de1d7df 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -69,11 +69,65 @@ {% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} {% block bodytab1 %}
- {{ _('Hier kommt der Inhalt des Webinterfaces hin.') }}

- {{ p._fritz_device.manufacturer_name }}

- {{ p._fritz_device.safe_port }}

- {{ p._fritz_device._network_devices }}

- {{ p._fritz_device.client.LANDevice.WLANConfiguration[0].GetInfo() }}

- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _('model_name') }}{{ p._fritz_device.model_name }}
{{ _('GetInfo') }}{{ p._fritz_device._DeviceInfo['GetInfo'] }}
{{ _('safe_port') }}{{ p._fritz_device.safe_port }}
{{ _('GetSecurityPort') }}{{ p._fritz_device._DeviceInfo['GetSecurityPort'] }}
{{ _('GetGenericHostEntry') }}{{ p._fritz_device.GetGenericHostEntry(1) }}
{{ _('WLANConfiguration[0]') }}{{ p._fritz_device.client.LANDevice.WLANConfiguration[0].GetInfo() }}
{{ _('_Hosts') }}{{ p._fritz_device._Hosts }}
{{ _('WANPPP') }}{{ p._fritz_device.client.WANConnectionDevice.WANPPPConnection.GetExternalIPAddress() }}
{{ _('WANIP') }}{{ p._fritz_device.client.WANConnectionDevice.WANIPConnection.GetInfo() }}
{{ _('update_devices') }}{{ p._fritz_home.update_devices() }}
{{ _('devices') }}{{ p._fritz_home._devices }}
{{ _('aha_devices') }}{{ p.aha_devices }}
{{ _('get_device_by_ain') }}{{ p._fritz_home.get_device_by_ain('11963 0521424') }}
{{ _('poll_aha') }}{{ p.poll_aha() }}
{% endblock bodytab1 %} From d0f60b2055379537d0c6b2a550bb7ff83004560a Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Thu, 10 Mar 2022 08:56:01 +0100 Subject: [PATCH 004/178] CallMonitor added --- __init__.py | 2133 ++++++++++++++++++++++++++++++++---- plugin.yaml | 586 +++++++++- webif/templates/index.html | 52 +- 3 files changed, 2497 insertions(+), 274 deletions(-) diff --git a/__init__.py b/__init__.py index fb32c0759..0d557eab3 100644 --- a/__init__.py +++ b/__init__.py @@ -25,67 +25,382 @@ # ######################################################################### - +import socket +import threading +import datetime import logging -from requests.packages import urllib3 -from requests.auth import HTTPDigestAuth -from io import BytesIO -import lxml.etree as etree import requests - import hashlib import time +import lxml.etree as etree + +from typing import Union +from requests.packages import urllib3 +from requests.auth import HTTPDigestAuth +from io import BytesIO from xml.etree import ElementTree from typing import Dict from enum import IntFlag from abc import ABC +from json.decoder import JSONDecodeError from lib.model.smartplugin import SmartPlugin -from lib.utils import Utils +from lib.item import Items from .webif import WebInterface +""" +Definition of TR-064 details +""" + +FRITZ_TR64_DESC_FILE = "tr64desc.xml" +FRITZ_IGD_DESC_FILE = "igddesc.xml" +FRITZ_IGD2_DESC_FILE = "igd2desc.xml" +FRITZ_L2TPV3_FILE = "l2tpv3.xml" +FRITZ_FBOX_DESC_FILE = "fboxdesc.xml" + +IGD_DEVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:device-1-0'} +IGD_SERVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:service-1-0'} TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} +""" +Definition DeviceGroupDicts +""" + +InternetGatewayDevice = { + 'DeviceInfo': { + 'GetInfo': {}, + 'GetSecurityPort': {}, + }, + 'DeviceConfig': { + 'GetPersistentData': {}, + }, + 'X_AVM_DE_MyFritz': { + 'GetInfo': {}, + 'GetNumberOfServices': {}, + }, + 'X_VoIP': { + 'GetInfoEx': {}, + }, + 'X_AVM_DE_OnTel': { + 'GetInfo': {}, + 'GetInfoByIndex': {}, + 'GetNumberOfEntries': {}, + 'GetCallList': {}, + 'GetPhonebookList': {}, + 'GetPhonebook': {}, + 'GetNumberOfDeflections': {}, + 'GetDeflection': {}, + 'GetDeflections': {}, + 'GetDECTHandsetList': {}, + }, + 'X_AVM_DE_Dect': { + 'GetNumberOfDectEntries': {}, + 'GetGenericDectEntry': {}, + 'GetSpecificDectEntry': {}, + }, + 'X_AVM_DE_TAM': { + 'GetInfo': {}, + 'GetMessageList': {}, + 'GetList': {}, + }, + 'X_AVM_DE_Homeauto': { + 'GetInfo': {}, + 'GetGenericDeviceInfos': {}, + 'GetSpecificDeviceInfos': {}, + }, +} + +LANDevice = { + 'WLANConfiguration': { + 'GetInfo': {}, + 'GetGenericAssociatedDeviceInfo': {}, + 'X_AVM_DE_GetSpecificAssociatedDeviceInfoByIp': {}, + 'GetStatistics': {}, + 'GetPacketStatistics': {}, + 'X_AVM_DE_GetWLANHybridMode': {}, + 'X_AVM_DE_GetWLANExtInfo': {}, + }, + 'Hosts': { + 'GetHostNumberOfEntries': {}, + 'GetSpecificHostEntry': {}, + 'GetGenericHostEntry': {}, + 'X_AVM_DE_GetSpecificHostEntryByIp': {}, + }, + 'LANEthernetInterfaceConfig': { + 'GetInfo': {}, + 'GetStatistics': {}, + }, + 'LANHostConfigManagement': { + 'GetInfo': {}, + }, +} + +WANDevice = { + 'WANCommonInterfaceConfig': { + 'GetCommonLinkProperties': {}, + 'GetTotalBytesSent': {}, + 'GetTotalBytesReceived': {}, + 'GetTotalPacketsSent': {}, + 'GetTotalPacketsReceived': {}, + 'X_AVM_DE_GetOnlineMonitor': {}, + }, + 'WANDSLInterfaceConfig': { + 'GetInfo': {}, + 'GetStatisticsTotal': {}, + 'X_AVM_DE_GetDSLDiagnoseInfo': {}, + }, +} + +WANConnectionDevice = { + 'WANDSLLinkConfig': { + 'GetInfo': {}, + 'GetDSLLinkInfo': {}, + 'GetDestinationAddress': {} + }, + 'WANEthernetLinkConfig': { + 'GetEthernetLinkStatus': {} + }, + 'WANPPPConnection': { + 'GetInfo': {}, + 'GetConnectionTypeInfo': {}, + 'GetStatusInfo': {}, + 'GetGenericPortMappingEntry': {}, + 'GetSpecificPortMappingEntry': {}, + 'GetExternalIPAddress': {} + }, + 'WANIPConnection': { + 'GetInfo': {}, + 'GetConnectionTypeInfo': {}, + 'GetStatusInfo': {}, + 'GetGenericPortMappingEntry': {}, + 'GetSpecificPortMappingEntry': {}, + 'GetExternalIPAddress': {} + }, +} + +""" +Definition of attribute value groups of avm_data_type +""" +_aha_ro_attributes = ['device_id', + 'manufacturer', + 'product_name', + 'fw_version', + 'connected', + 'device_name', + 'tx_busy', + 'device_functions', + 'current_temperature', + 'temperature_reduced', + 'temperature_comfort', + 'temperature_offset', + 'windowopenactiveendtime', + 'boost_active', + 'boostactiveendtime', + 'summer_active', + 'holiday_active', + 'battery_low', + 'lock', + 'device_lock', + 'errorcode', + 'switch_mode', + 'power', 'energy', + 'voltage', + 'humidity', + 'alert_state'] + +_aha_wo_attributes = ['set_target_temperature', + 'set_window_open', + 'set_hkr_boost', + 'battery_level', + 'set_simpleonoff', + 'set_level', + 'set_levelpercentage', + 'set_hue', + 'set_saturation', + 'set_colortemperature', + 'switch_toggle'] + +_aha_rw_attributes = ['target_temperature', + 'window_open', + 'hkr_boost', + 'simpleonoff', + 'level', + 'levelpercentage', + 'hue', + 'saturation', + 'colortemperature', + 'switch_state'] + +_aha_attributes = [*_aha_ro_attributes, + *_aha_wo_attributes, + *_aha_rw_attributes] + +_avm_rw_attributes = ['wlanconfig', + 'tam', + 'deflection_enable'] + +_call_monitor_attributes_gen = ['call_event', + 'call_direction', + 'monitor_trigger'] + +_call_monitor_attributes_in = ['is_call_incoming', + 'last_caller_incoming', + 'last_call_date_incoming', + 'call_event_incoming', + 'last_number_incoming', + 'last_called_number_incoming'] + +_call_monitor_attributes_out = ['is_call_outgoing', + 'last_caller_outgoing', + 'last_call_date_outgoing', + 'call_event_outgoing', + 'last_number_outgoing', + 'last_called_number_outgoing'] + +_call_monitor_attributes = [*_call_monitor_attributes_gen, + *_call_monitor_attributes_in, + *_call_monitor_attributes_out] + +_call_duration_attributes = ['call_duration_incoming', + 'call_duration_outgoing'] + +_trigger_attributes = ['monitor_trigger'] + +_wan_connection_attributes = ['wan_connection_status', + 'wan_connection_error', + 'wan_is_connected', + 'wan_uptime', + 'wan_ip'] + +_tam_attributes = ['tam', + 'tam_name', + 'tam_new_message_number', + 'tam_total_message_number'] + +_wlan_config_attributes = ['wlanconfig', + 'wlanconfig_ssid', + 'wlan_guest_time_remaining'] + +_wan_common_interface_attributes = ['wan_total_packets_sent', + 'wan_total_packets_received', + 'wan_current_packets_sent', + 'wan_current_packets_received', + 'wan_total_bytes_sent', + 'wan_total_bytes_received', + 'wan_current_bytes_sent', + 'wan_current_bytes_received', + 'wan_link'] + +_fritz_device_attributes = ['uptime', + 'software_version', + 'hardware_version', + 'serial_number'] + +_host_attribute = ['network_device'] + +_host_child_attributes = ['device_ip', + 'device_connection_type', + 'device_hostname' + 'connection_status'] + +_host_attributes = [*_host_attribute, + *_host_child_attributes] + +_deflection_attributes = ['deflections_details', + 'deflection_enable', + 'deflection_type', + 'deflection_number', + 'deflection_to_number', + 'deflection_mode', + 'deflection_outgoing', + 'deflection_phonebook_id'] + +_wan_dsl_interface_attributes = ['wan_upstream', + 'wan_downstream'] + +_homeauto_ro_attributes = ['hkr_device', + 'set_temperature', + 'temperature', + 'set_temperature_reduced', + 'set_temperature_comfort', + 'firmware_version'] + +_homeauto_rw_attributes = ['aha_device'] + +_homeauto_attributes = [*_homeauto_ro_attributes, + *_homeauto_rw_attributes] + +_myfritz_attributes = ['myfritz_status'] + +_deprecated_attributes = ['temperature', + 'set_temperature_reduced', + 'set_temperature_comfort', + 'firmware_version', + 'aha_device', + 'hkr_device'] + +_tr064_attributes = [*_wan_connection_attributes, + *_tam_attributes, + *_wlan_config_attributes, + *_wan_common_interface_attributes, + *_fritz_device_attributes, + *_host_attributes, + *_deflection_attributes, + *_wan_dsl_interface_attributes, + *_homeauto_attributes, + *_myfritz_attributes, + *_avm_rw_attributes] + + class AVM2(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items - - HINT: Please have a look at the SmartPlugin class to see which - class properties and methods (class variables and class functions) - are already available! """ PLUGIN_VERSION = '1.0.0' def __init__(self, sh): """ - Initalizes the plugin. + Initializes the plugin. """ # Call init code of parent class (SmartPlugin) super().__init__() - self.logger.info('Init AVM-Discover Plugin') + self.logger.info('Init AVM2 Plugin') + # Get/Define Properties _host = self.get_parameter_value('host') _port = self.get_parameter_value('port') _verify = self.get_parameter_value('verify') _username = self.get_parameter_value('username') _passwort = self.get_parameter_value('password') - + _call_monitor_incoming_filter = self.get_parameter_value('call_monitor_incoming_filter') + self._call_monitor = self.get_parameter_value('call_monitor') + self._aha_http_interface = self.get_parameter_value('avm_home_automation') + self._cycle = self.get_parameter_value('cycle') + self.alive = False ssl = self.get_parameter_value('ssl') if ssl and not _verify: urllib3.disable_warnings() - self._cycle = int(self.get_parameter_value('cycle')) - self.alive = False - self._fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, self) - self._fritz_home = FritzHome(_host, ssl, _verify, _username, _passwort, self) + # init FritzDevice + self._fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, _call_monitor_incoming_filter, self) - self.aha_devices = dict() + # init FritzHome + if self._aha_http_interface: + self._fritz_home = FritzHome(_host, ssl, _verify, _username, _passwort, self) + + # init Call Monitor + if self._call_monitor: + self._monitoring_service = Callmonitor(_host, + 1012, + self._fritz_device.get_contact_name_by_phone_number, + _call_monitor_incoming_filter, + self) # init WebIF self.init_webinterface(WebInterface) @@ -95,22 +410,28 @@ def run(self): Run method for the plugin """ self.logger.debug("Run method called") + self.scheduler_add('poll_tr064', self._fritz_device.update_items, prio=5, cycle=self._cycle, offset=4) + if self._aha_http_interface: + # add scheduler for updating items + self.scheduler_add('poll_aha', self._fritz_home.update_items, prio=5, cycle=self._cycle, offset=2) + # add scheduler for checking validity of session id + self.scheduler_add('check_sid', self._fritz_home.check_sid, prio=5, cycle=900, offset=30) - # setup scheduler for device poll loop (disable the following line, if you don't need to poll the device. Rember to comment the self_cycle statement in __init__ as well) - self.scheduler_add('poll_device', self.poll_aha, prio=5, cycle=self._cycle, offset=2) self.alive = True - # self.poll_aha() - # self.logger.debug(f"manufacturer_name = {self._fritz_device.manufacturer_name}") - # self.logger.debug(f"model_name = {self._fritz_device.model_name}") - # self.logger.debug(f"safe_port = {self._fritz_device.safe_port}") - def stop(self): """ Stop method for the plugin """ self.logger.debug("Stop method called") - self.scheduler_remove('poll_device') + self.scheduler_remove('poll_tr064') + if self._aha_http_interface: + self.scheduler_remove('poll_aha') + self.scheduler_remove('check_sid') + self._fritz_home.logout() + if self._call_monitor: + self._monitoring_service.disconnect() + self.alive = False def parse_item(self, item): @@ -126,9 +447,142 @@ def parse_item(self, item): with the item, caller, source and dest as arguments and in case of the knx plugin the value can be sent to the knx with a knx write function within the knx plugin. """ - if self.has_iattr(item.conf, 'foo_itemtag'): + if self.has_iattr(item.conf, 'avm2_data_type'): self.logger.debug(f"parse item: {item}") + # get avm_data_type + avm_data_type = self.get_iattr_value(item.conf, 'avm2_data_type') + + # handle items specific to call monitor + if avm_data_type in _call_monitor_attributes: + # initially - if item empty - get data from calllist + if avm_data_type == 'last_caller_incoming' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['1', '2']: + if 'Name' in element: + item(element['Name'], self.get_shortname()) + else: + item(element['Caller'], self.get_shortname()) + break + elif avm_data_type == 'last_number_incoming' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['1', '2']: + if 'Caller' in element: + item(element['Caller'], self.get_shortname()) + else: + item("", self.get_shortname()) + break + elif avm_data_type == 'last_called_number_incoming' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['1', '2']: + item(element['CalledNumber'], self.get_shortname()) + break + elif avm_data_type == 'last_call_date_incoming' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['1', '2']: + date = str(element['Date']) + date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] + item(date, self.get_shortname()) + break + elif avm_data_type == 'call_event_incoming' and item() == '': + item('disconnect', self.get_shortname()) + elif avm_data_type == 'is_call_incoming' and item() == '': + item(0, self.get_shortname()) + elif avm_data_type == 'last_caller_outgoing' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['3', '4']: + if 'Name' in element: + item(element['Name'], self.get_shortname()) + else: + item(element['Called'], self.get_shortname()) + break + elif avm_data_type == 'last_number_outgoing' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['3', '4']: + if 'Caller' in element: + item(''.join(filter(lambda x: x.isdigit(), element['Caller'])), self.get_shortname()) + else: + item("", self.get_shortname()) + break + elif avm_data_type == 'last_called_number_outgoing' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['3', '4']: + item(element['Called'], self.get_shortname()) + break + elif avm_data_type == 'last_call_date_outgoing' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['3', '4']: + date = str(element['Date']) + date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] + item(date, self.get_shortname()) + break + elif avm_data_type == 'call_event_outgoing' and item() == '': + item('disconnect', self.get_shortname()) + elif avm_data_type == 'is_call_outgoing' and item() == '': + item(0, self.get_shortname()) + elif avm_data_type == 'call_event' and item() == '': + item('disconnect', self.get_shortname()) + elif avm_data_type == 'call_direction' and item() == '': + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['1', '2']: + item('incoming', self.get_shortname()) + break + if element['Type'] in ['3', '4']: + item('outgoing', self.get_shortname()) + break + if self._call_monitor: + if self._monitoring_service is not None: + self._monitoring_service.register_item(item, avm_data_type) + + # handle items specific to call-duration + elif avm_data_type in _call_duration_attributes: + # initially get data from calllist + if avm_data_type == 'call_duration_incoming' and item() == 0: + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['1', '2']: + duration = element['Duration'] + duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 + item(duration, self.get_shortname()) + break + elif avm_data_type == 'call_duration_outgoing' and item() == 0: + if self._fritz_device.get_calllist_from_cache() is not None: + for element in self._fritz_device.get_calllist_from_cache(): + if element['Type'] in ['3', '4']: + duration = element['Duration'] + duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 + item(duration, self.get_shortname()) + break + if self._call_monitor: + if self._monitoring_service is not None: + self._monitoring_service.register_item(item, avm_data_type) + + # handle smarthome items using aha-interface (old / new) + elif avm_data_type in _aha_attributes: + if self._aha_http_interface: + self._fritz_home.register_item(item, avm_data_type) + else: + self.logger.warning(f"Items with avm attribute found, which needs aha-http-interface. This is not enabled for that plugin; Item will be ignored.") + + # handle items updated by tr-064 interface + elif avm_data_type in _tr064_attributes: + self._fritz_device.register_item(item, avm_data_type) + else: + self.logger.warning(f"avm_data_type={avm_data_type} if item={item.id()} unknown. Item will be ignored.") + + # items which can be changed outside the plugin context and need to be submitted to the FritzDevice + if avm_data_type in (_avm_rw_attributes + _aha_wo_attributes + _aha_rw_attributes + _homeauto_rw_attributes): + return self.update_item + def parse_logic(self, logic): """ Default plugin parse_logic method @@ -156,64 +610,19 @@ def update_item(self, item, caller=None, source=None, dest=None): self.logger.info(f"Update item: {item.property.path}, item has been changed outside this plugin") if self.has_iattr(item.conf, 'foo_itemtag'): - self.logger.debug(f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}") + self.logger.debug( + f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}") pass - def poll_aha(self): - - self.logger.debug(f'Starting AHA update loop for instance {self.get_instance_name()}.') - - # update devices - self._fritz_home.update_devices() - - # get device dict - _device_dict = self._fritz_home.get_devices_as_dict() - - for ain in _device_dict: - if not self.aha_devices.get(ain): - self.aha_devices[ain] = {} - self.aha_devices[ain]['connected_to_item'] = False - self.aha_devices[ain]['switch'] = {} - self.aha_devices[ain]['temperature_sensor'] = {} - self.aha_devices[ain]['thermostat'] = {} - self.aha_devices[ain]['alarm'] = {} - - self.aha_devices[ain]['online'] = bool(_device_dict[ain].present) - self.aha_devices[ain]['name'] = _device_dict[ain].name - self.aha_devices[ain]['productname'] = _device_dict[ain].productname - self.aha_devices[ain]['manufacturer'] = _device_dict[ain].manufacturer - self.aha_devices[ain]['fw_version'] = _device_dict[ain].fw_version - self.aha_devices[ain]['lock'] = bool(_device_dict[ain].lock) - self.aha_devices[ain]['device_lock'] = bool(_device_dict[ain].device_lock) - self.aha_devices[ain]['functions'] = [] - - if _device_dict[ain].has_thermostat: - self.aha_devices[ain]['functions'].append('thermostat') - self.aha_devices[ain]['thermostat']['actual_temperature'] = _device_dict[ain].actual_temperature - self.aha_devices[ain]['thermostat']['target_temperature'] = _device_dict[ain].target_temperature - self.aha_devices[ain]['thermostat']['comfort_temperature'] = _device_dict[ain].comfort_temperature - self.aha_devices[ain]['thermostat']['eco_temperature'] = _device_dict[ain].eco_temperature - self.aha_devices[ain]['thermostat']['battery_low'] = bool(_device_dict[ain].battery_low) - self.aha_devices[ain]['thermostat']['battery_level'] = _device_dict[ain].battery_level - self.aha_devices[ain]['thermostat']['window_open'] = bool(_device_dict[ain].window_open) - self.aha_devices[ain]['thermostat']['summer_active'] = bool(_device_dict[ain].summer_active) - self.aha_devices[ain]['thermostat']['holiday_active'] = bool(_device_dict[ain].holiday_active) + @property + def get_callmonitor(self): + return self._call_monitor - if _device_dict[ain].has_switch: - self.aha_devices[ain]['functions'].append('switch') - self.aha_devices[ain]['switch']['switch_state'] = bool(_device_dict[ain].switch_state) - self.aha_devices[ain]['switch']['power'] = _device_dict[ain].power - self.aha_devices[ain]['switch']['energy'] = _device_dict[ain].energy - self.aha_devices[ain]['switch']['voltage'] = _device_dict[ain].voltage + def monitoring_service_connect(self): + self._monitoring_service.connect() - if _device_dict[ain].has_temperature_sensor: - self.aha_devices[ain]['functions'].append('temperature_sensor') - self.aha_devices[ain]['temperature_sensor']['temperature'] = _device_dict[ain].temperature - self.aha_devices[ain]['temperature_sensor']['offset'] = _device_dict[ain].offset - - if _device_dict[ain].has_alarm: - self.aha_devices[ain]['functions'].append('alarm') - self.aha_devices[ain]['alarm']['alert_state'] = bool(_device_dict[ain].alert_state) + def monitoring_service_disconnect(self): + self._monitoring_service.disconnect() class FritzDevice: @@ -264,7 +673,7 @@ class FritzDevice: WANIPConnection """ - def __init__(self, host, port, ssl, verify, username, password, plugin_instance=None): + def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, plugin_instance=None): self._plugin_instance = plugin_instance self._plugin_instance.logger.debug("Init FritzDevice") @@ -275,16 +684,58 @@ def __init__(self, host, port, ssl, verify, username, password, plugin_instance= self._verify = verify self._username = username self._password = password + self._call_monitor_incoming_filter = call_monitor_incoming_filter self._available = True - self._items = [] - self._smarthome_items = [] - self._smarthome_devices = {} - self._DeviceInfo = {} - self._Hosts = {} + self._data_cache = {} + self._calllist_cache = [] + self._timeout = 10 + self._items = {} + self._session = requests.Session() + + self.items = Items.get_instance() + + # get client objects + self.client = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=FRITZ_TR64_DESC_FILE, plugin_instance=plugin_instance) + self.client_igd = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=FRITZ_IGD_DESC_FILE, plugin_instance=plugin_instance) + + # get GetDefaultConnectionService + self._data_cache['InternetGatewayDevice'] = {'DeviceInfo': {'GetInfo': {}, 'GetSecurityPort': {}}, 'Layer3Forwarding': {}} + self._data_cache['InternetGatewayDevice']['Layer3Forwarding']['GetDefaultConnectionService'] = self.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() + + def register_item(self, item, avm_data_type: str): - self.client = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), plugin_instance=plugin_instance) + # handle wlan items + if avm_data_type in _wlan_config_attributes: + avm_wlan_index = self._get_wlan_index(item) + if avm_wlan_index is not None: + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_wlan_index' found; append to list.") + self._items[item] = (avm_data_type, avm_wlan_index) + else: + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_wlan_index' is not defined; Item will be ignored.") + + # handle network_device related items + elif avm_data_type in (_host_attribute + _host_child_attributes): + avm_mac = self._get_mac(item) + if avm_mac is not None: + if avm_mac is not None: + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_mac' found; append to list.") + self._items[item] = (avm_data_type, avm_mac) + else: + self._plugin_instance.logger.warning("Item {item.id()} with avm attribute found, but 'avm_mac' is not defined; Item will be ignored.") + + # handle tam related items + elif avm_data_type in _tam_attributes: + avm_tam_index = self._get_tam_index(item) + if avm_tam_index is not None: + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") + self._items[item] = (avm_data_type, avm_tam_index) + else: + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") + + else: + self._items[item] = (avm_data_type, None) - def _build_url(self): + def _build_url(self) -> str: """ Builds a request url @@ -298,6 +749,112 @@ def _build_url(self): return url + def _get_wlan_index(self, item) -> int: + """ + return wlan index for given item + """ + + wlan_index = None + for i in range(2): + attribute = 'avm2_wlan_index' + attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" + + wlan_index = self._plugin_instance.get_iattr_value(item.conf, attribute) + if wlan_index: + break + wlan_index = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) + if wlan_index: + break + else: + item = item.return_parent() + + if wlan_index is not None: + wlan_index = int(wlan_index) - 1 + if not 0 <= wlan_index <= 2: + wlan_index = None + self._plugin_instance.logger.warning(f"Attribute 'avm2_wlan_index' for item {item.id()} not in valid range 1-3.") + + return wlan_index + + def _get_tam_index(self, item) -> int: + """ + return tam index for given item + """ + + tam_index = None + for i in range(2): + attribute = 'avm2_tam_index' + attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" + + tam_index = self._plugin_instance.get_iattr_value(item.conf, attribute) + if tam_index: + break + tam_index = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) + if tam_index: + break + else: + item = item.return_parent() + + if tam_index is not None: + tam_index = int(tam_index) - 1 + if not 0 <= tam_index <= 4: + tam_index = None + self._plugin_instance.logger.warning(f"Attribute 'avm_tam_index' for item {item.id()} not in valid range 1-5.") + + return tam_index + + def _get_deflection_index(self, item) -> int: + """ + return deflection index for given item + """ + + deflection_index = None + for i in range(2): + attribute = 'avm2_deflection_index' + attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" + + deflection_index = self._plugin_instance.get_iattr_value(item.conf, attribute) + if deflection_index: + break + deflection_index = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) + if deflection_index: + break + else: + item = item.return_parent() + + if deflection_index is not None: + deflection_index = int(deflection_index) - 1 + if not 0 <= deflection_index <= 31: + deflection_index = None + self._plugin_instance.logger.warning(f"Attribute 'avm_deflection_index' for item {item.id()} not in valid range 1-5.") + + return deflection_index + + def _get_mac(self, item) -> str: + """ + return mac for given item + """ + + mac = None + for i in range(2): + attribute = 'avm_mac' + attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" + + mac = self._plugin_instance.get_iattr_value(item.conf, attribute) + if mac: + break + mac = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) + if mac: + break + else: + item = item.return_parent() + + return mac + + # -------------------------------------- + # Properties of FritzDevice + # -------------------------------------- + def get_host(self): """ Returns the hostname / IP of the FritzDevice @@ -322,22 +879,6 @@ def get_verify(self): """ return self._verify - def get_items(self): - """ - Returns added items - - :return: array of items hold by the device - """ - return self._items - - def get_item_count(self): - """ - Returns number of added items - - :return: number of items hold by the device - """ - return len(self._items) - def is_ssl(self): """ Returns information if SSL is enabled @@ -378,60 +919,489 @@ def get_password(self): """ return self._password + @property + def default_connection_service(self): + return self._data_cache['InternetGatewayDevice']['Layer3Forwarding']['GetDefaultConnectionService']['NewDefaultConnectionService'] + @property def manufacturer_name(self): - if 'GetInfo' not in self._DeviceInfo: - self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self._DeviceInfo['GetInfo'].NewManufacturerName + if not self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']: + self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']['NewManufacturerName'] @property def manufacturer_oui(self): - if 'GetInfo' not in self._DeviceInfo: - self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self._DeviceInfo['GetInfo'].NewManufacturerOUI + if not self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']: + self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']['NewManufacturerOUI'] @property def model_name(self): - if 'GetInfo' not in self._DeviceInfo: - self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self._DeviceInfo['GetInfo'].NewModelName + if not self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']: + self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']['NewModelName'] @property def desciption(self): - if 'GetInfo' not in self._DeviceInfo: - self._DeviceInfo['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() - return self._DeviceInfo['GetInfo'].NewDescription + if not self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']: + self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo'] = self.client.InternetGatewayDevice.DeviceInfo.GetInfo() + return self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetInfo']['NewDescription'] @property def safe_port(self): - if 'GetSecurityPort' not in self._DeviceInfo: - self._DeviceInfo['GetSecurityPort'] = self.client.InternetGatewayDevice.DeviceInfo.GetSecurityPort() - return self._DeviceInfo['GetSecurityPort'].NewSecurityPort - - def GetGenericHostEntry(self, index): - if 'GetGenericHostEntry' not in self._Hosts: - self._Hosts['GetGenericHostEntry'] = {} - if index not in self._Hosts['GetGenericHostEntry']: - self._Hosts['GetGenericHostEntry'][index] = {} - self._Hosts['GetGenericHostEntry'][index].update(self.client.LANDevice.Hosts.GetGenericHostEntry(NewIndex=index)) - return self._Hosts['GetGenericHostEntry'][index] - - def wlan_info(self): - for index in range(3): - info = self.client.LANDevice.WLANConfiguration[index].GetInfo() - self._plugin_instance.logger.debug(f"WLAN info{index} = {info}") + if not self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetSecurityPort']: + self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetSecurityPort'] = self.client.InternetGatewayDevice.DeviceInfo.GetSecurityPort() + return self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetSecurityPort']['NewSecurityPort'] + + # ---------------------------------- + # TBD + # ---------------------------------- + def update_items(self): + + for item in self._items: + avm_data_type = self._items[item][0] + index = self._items[item][1] + self._plugin_instance.logger.debug(f"FritzDevice: _update_items called: item={item} with avm_data_type={avm_data_type} and index={index}") + + data = self._poll_fritz_device(avm_data_type, index) + + if data is not None: + item(data, self._plugin_instance.get_shortname()) + + def _poll_fritz_device(self, avm_data_type: str, index: int): + + link_ppp = { + 'wan_connection_status': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewConnectionStatus'), + 'wan_connection_error': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewLastConnectionError'), + 'wan_is_connected': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewConnectionStatus'), + 'wan_uptime': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewUptime'), + 'wan_ip': ('WANConnectionDevice', 'WANPPPConnection', 'GetExternalIPAddress', 'NewExternalIPAddress'), + } + + link_ip = { + 'wan_connection_status': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewConnectionStatus'), + 'wan_connection_error': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewLastConnectionError'), + 'wan_is_connected': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewConnectionStatus'), + 'wan_uptime': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewUptime'), + 'wan_ip': ('WANConnectionDevice', 'WANIPConnection', 'GetExternalIPAddress', 'NewExternalIPAddress'), + } + + link = { + 'uptime': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewUpTime'), + 'serial_number': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewSerialNumber'), + 'software_version': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewSoftwareVersion'), + 'hardware_version': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewHardwareVersion'), + 'myfritz_status': ('InternetGatewayDevice', 'X_AVM_DE_MyFritz', 'GetInfo', 'NewEnabled'), + 'tam': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'GetInfo', 'NewEnable'), + 'tam_name': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'GetInfo', 'NewName'), + 'wan_upstream': ('WANDevice', 'WANDSLInterfaceConfig', 'GetInfo', 'NewUpstreamCurrRate'), + 'wan_downstream': ('WANDevice', 'WANDSLInterfaceConfig', 'GetInfo', 'NewDownstreamCurrRate'), + 'wan_total_packets_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalPacketsSent', 'NewTotalPacketsSent'), + 'wan_total_packets_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalPacketsReceived', 'NewTotalPacketsReceived'), + 'wan_current_packets_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewPacketSendRate'), + 'wan_current_packets_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewPacketReceiveRate'), + 'wan_total_bytes_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalBytesSent', 'NewTotalBytesSent'), + 'wan_total_bytes_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalBytesReceived', 'NewTotalBytesReceived'), + 'wan_current_bytes_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewByteSendRate'), + 'wan_current_bytes_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewByteReceiveRate'), + 'wan_link': ('WANDevice', 'WANCommonInterfaceConfig', 'GetCommonLinkProperties', 'NewPhysicalLinkStatus'), + 'wlanconfig': ('LANDevice', 'WLANConfiguration', 'GetInfo', 'NewEnable'), + 'wlanconfig_ssid': ('LANDevice', 'WLANConfiguration', 'GetInfo', 'NewSSID'), + 'wlan_guest_time_remaining': ('LANDevice', 'WLANConfiguration', 'X_AVM_DE_GetWLANExtInfo', 'NewX_AVM_DE_TimeRemain'), + # '':: ('', '', '', ''), + } + + # Create link dict depending on connection type + if 'PPP' in self.default_connection_service: + link.update(link_ppp) + else: + link.update(link_ip) + + # define client + client = 'client' + if avm_data_type.startswith('wan_current'): + client = 'client_igd' + + # check if avm_data_type is linked + if avm_data_type not in link: + return + + # gather data + data = self._update_data_cache(client, link[avm_data_type][0], link[avm_data_type][1], link[avm_data_type][2], link[avm_data_type][3], index) + + # correct data + if avm_data_type == 'wan_is_connected': + data = True if data == 'Connected' else False + elif avm_data_type == 'wan_link': + data = True if data == 'Up' else False + + # return result + return data + + def _update_data_cache(self, client: str, device: str, service: str, action: str, argument: str = None, index: int = None) -> dict: + # self._plugin_instance.logger.debug(f"_data_cache called with device={device}, service={service}, action={action}, argument={argument} index={index}") + if device not in self._data_cache: + self._data_cache[device] = {} + if service not in self._data_cache[device]: + self._data_cache[device][service] = {} + if action not in self._data_cache[device][service]: + self._data_cache[device][service][action] = {} + + if index is None: + if not self._data_cache[device][service][action]: + self._data_cache[device][service][action] = eval(f"self.{client}.{device}.{service}.{action}()") + if not argument: + return self._data_cache[device][service][action] + else: + return self._data_cache[device][service][action][argument] + else: + if not self._data_cache[device][service][action]: + self._data_cache[device][service][action] = {} + if index not in self._data_cache[device][service][action]: + self._data_cache[device][service][action][index] = eval(f"self.{client}.{device}.{service}[{index}].{action}()") + if not argument: + return self._data_cache[device][service][action][index] + else: + return self._data_cache[device][service][action][index][argument] + + def _request(self, url: str, timeout: int, verify: bool): + request = requests.get(url, timeout=timeout, verify=verify) + if request.status_code == 200: + return request + + def _request_response_to_xml(self, request): + root = etree.fromstring(request.content) + return root + + # ---------------------------------- + # Fritz Device methods, reboot, wol, reconnect + # ---------------------------------- + + def reboot(self): + """ + Reboots the FritzDevice + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceconfigSCPD.pdf + """ + + self.client.InternetGatewayDevice.DeviceConfig.Reboot() + + def reconnect(self): + """ + Reconnects the FritzDevice to the WAN + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf + http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanpppconnSCPD.pdf + """ + + if 'PPP' in self.default_connection_service: + self.client.WANConnectionDevice.WANPPPConnection.ForceTermination() + else: + self.client.WANConnectionDevice.WANIPPConnection.ForceTermination() + + def wol(self, mac_address: str): + """ + Sends a WOL (WakeOnLAN) command to a MAC address + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf + + :param mac_address: MAC address of the device to wake up + """ + + self.client.LanDevice.Hosts.X_AVM_DE_GetAutoWakeOnLANByMACAddress(NewMACAddress=mac_address) + + # ---------------------------------- + # caller methods + # ---------------------------------- + def get_call_origin(self) -> str: + """ + Gets the phone name, currently set as call_origin. + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf + :return: String phone name + """ + + phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialGetConfig()['NewX_AVM-DE_PhoneName'] + if phone_name is None: + self._plugin_instance.logger.error("No call origin available.") + return phone_name + + def get_phone_name(self, index: int = 1) -> str: + """ + Get the phone name at a specific index. The returned value can be used as phone_name for set_call_origin. + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf + + :param index: Parameter is an INT, starting from 1. In case an index does not exist, an error is logged. + :return: String phone name + """ + + phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetPhonePort()['NewX_AVM-DE_PhoneName'] + if phone_name is None: + self._plugin_instance.logger.error(f"No phone name available at provided index {index}") + return phone_name + + def set_call_origin(self, phone_name: str): + """ + Sets the call origin, e.g. before running 'start_call' + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf + + :param phone_name: full phone identifier, could be e.g. '\*\*610' for an internal device + """ + + self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialSetConfig(NewX_AVM_DE_PhoneName=phone_name.strip()) + + def start_call(self, phone_number: str): + """ + Triggers a call for a given phone number + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf + + :param phone_number: full phone number to call + """ + + self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialNumber(NewX_AVM_DE_PhoneNumber=phone_number.strip()) + + def cancel_call(self): + """ + Cancels an active call + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf + """ + + self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialHangup() + + def get_contact_name_by_phone_number(self, phone_number: str = '', phonebook_id: int = 0) -> str: + """ + """ + + if phone_number.endswith('#'): + phone_number = phone_number.strip('#') + + phonebook_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetPhonebook(NewPhonebookID=phonebook_id)[ + 'NewPhonebookURL'] + phonebooks = self._request_response_to_xml(self._request(phonebook_url, self._timeout, self._verify)) + if phonebooks is not None: + for phonebook in phonebooks.iter('phonebook'): + for contact in phonebook.iter('contact'): + for number in contact.findall('.//number'): + if number.text: + nr = number.text.strip() + if phone_number in nr: + return contact.find('.//realName').text + else: + self._plugin_instance.logger.error("Phonebook not available on the FritzDevice") + + def get_phone_numbers_by_name(self, name: str = '', phonebook_id: int = 0) -> dict: + """ + """ + tel_type = {"mobile": "CELL", "work": "WORK", "home": "HOME"} + result_numbers = {} + phonebook_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetPhonebook(NewPhonebookID=phonebook_id)[ + 'NewPhonebookURL'] + phonebooks = self._request_response_to_xml(self._request(phonebook_url, self._timeout, self._verify)) + if phonebooks is not None: + for phonebook in phonebooks.iter('phonebook'): + for contact in phonebook.iter('contact'): + for real_name in contact.findall('.//realName'): + if name.lower() in real_name.text.lower(): + result_numbers[real_name.text] = [] + for number in contact.findall('.//number'): + if number.text: + result_number_dict = dict() + result_number_dict['number'] = number.text.strip() + result_number_dict['type'] = tel_type[number.attrib["type"]] + result_numbers[real_name.text].append(result_number_dict) + return result_numbers + else: + self._plugin_instance.logger.error("Phonebook not available on the FritzDevice") + + def get_calllist_from_cache(self) -> list: + """ + returns the cached calllist when all items are initialized. The filter set by plugin.conf is applied. + + :return: Array of calllist entries + """ + + if not self._calllist_cache: + self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) + elif len(self._calllist_cache) == 0: + self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) + return self._calllist_cache + + def get_calllist(self, filter_incoming: str = '') -> list: + """ + """ + + calllist_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetCallList()['NewCallListURL'] + calllist = self._request_response_to_xml(self._request(calllist_url, self._timeout, self._verify)) + + if calllist: + result_entries = [] + for calllist_entry in calllist.iter('Call'): + result_entry = {} + progress = True + if len(filter_incoming) > 0: + call_type_element = calllist_entry.find('Type') + call_type = call_type_element.text + if call_type == '1' or call_type == '2': + called_number = calllist_entry.find('CalledNumber').text + if filter_incoming not in called_number: + progress = False + + if progress: + attributes = ['Id', 'Type', 'Caller', 'Called', 'CalledNumber', 'Name', 'Numbertype', 'Device', 'Port', + 'Date', 'Duration'] + for attribute in attributes: + attribute_value = calllist_entry.find(attribute) + if attribute_value is not None: + if attribute != 'Date': + result_entry[attribute] = attribute_value.text + else: + result_entry[attribute] = datetime.datetime.strptime(attribute_value.text, '%d.%m.%y %H:%M') + result_entries.append(result_entry) + return result_entries + else: + self._plugin_instance.logger.error("Calllist not available on the FritzDevice") + + # ---------------------------------- + # get logs methods + # ---------------------------------- + def get_device_log_from_tr064(self) -> str: + """ + uses: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceinfoSCPD.pdf + + Gets the Device Log via TR-064 + :return: Array of Device Log Entries (Strings) + """ + + device_log = self.client.InternetGatewayDevice.DeviceInfo.GetDeviceLog()['NewDeviceLog'] + if device_log is None: + return "" + return device_log.split("\n") + + # ---------------------------------- + # set wlan methods + # ---------------------------------- + def set_wlan_config(self, wlan_index: int, new_enable: bool = False): + """ + Set WLAN Config + + uses: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf + """ + + # set WLAN to ON/OFF + self.client.LANDevice.WLANConfiguration[wlan_index].SetEnable(NewEnable=int(new_enable)) + # check if remaining time is set as item + for item in self._items: # search for guest time remaining item. + if self._items[item][0] == 'wlan_guest_time_remaining' and self._items[item][1] == wlan_index: + data = self._poll_fritz_device('wlan_guest_time_remaining', wlan_index) + if data is not None: + item(data, self._plugin_instance.get_shortname()) + + # ---------------------------------- + # set tam methods + # ---------------------------------- + def set_tam(self, tam_index: int = 0, new_enable: bool = False): + """ + Set TAM + """ + + self.client.InternetGatewayDevice.X_AVM_DE_TAM.SetEnable(NewIndex=tam_index, NewEnable=int(new_enable)) + + # ---------------------------------- + # set home automation switch + # ---------------------------------- + def set_aha_device(self, ain: str = '', set_switch: bool = False): + """ + Set AHA-Device via TR-064 protocol + """ + + # SwitchState: OFF, ON, TOGGLE, UNDEFINED + switch_state = "ON" if set_switch is True else "OFF" + self.client.InternetGatewayDevice.X_AVM_DE_Homeauto.SetSwitch(NewAIN=ain, NewSwitchState=switch_state) + + # ---------------------------------- + # set deflection + # ---------------------------------- + def set_deflection(self, deflection_id: int = 0, new_enable: bool = False): + """ + Enable or disable a deflection. + DeflectionID is in the range of 0 .. NumberOfDeflections-1 + | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf + + :param deflection_id: deflection id (default: 0) + :param new_enable: new enable (default: False) + """ + + self.client.InternetGatewayDevice.X_AVM_DE_OnTel.SetDeflectionEnable(NewDeflectionId=deflection_id, NewEnable=int(new_enable)) + + # ---------------------------------- + # Host + # ---------------------------------- + def is_host_active(self, mac_address: str) -> bool: + """ + Checks if a MAC address is active on the FritzDevice, e.g. the status can be used for simple presence detection + + | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf + | Also reference: https://blog.pregos.info/2015/11/07/anwesenheitserkennung-fuer-smarthome-mit-der-fritzbox-via-tr-064/ + + :param: MAC address of the host + :return: True or False, depending if the host is active on the FritzDevice + """ + + is_active = self.client.InternetGatewayDevice.Hosts.GetSpecificHostEntry(NewMACAddress=mac_address)['NewActive'] + return bool(is_active) + + def get_hosts(self, only_active: bool = False) -> list: + """ + Gets the information (host details) of all hosts as an array of dicts + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf + + :param only_active: bool, if only active hosts shall be returned + :return: Array host dicts (see get_host_details) + """ + + number_of_hosts = self.client.InternetGatewayDevice.Hosts.GetHostNumberOfEntries()['NewHostNumberOfEntries'] + hosts = [] + for i in range(1, number_of_hosts): + host = self.get_host_details(i) + if not only_active or (only_active and host['is_active']): + hosts.append(host) + return hosts + + def get_host_details(self, index: int): + """ + Gets the information of a hosts at a specific index + + Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf + + :param index: index of host in hosts list + :return: Dict host data: name, interface_type, ip_address, address_source, mac_address, is_active, lease_time_remaining + """ + + host_info = self.client.InternetGatewayDevice.Hosts.GetGenericHostEntry(NewIndex=index) + host = { + 'name': host_info['NewHostName'], + 'interface_type': host_info['NewInterfaceType'], + 'ip_address': host_info['NewIPAddress'], + 'address_source': host_info['NewAddressSource'], + 'mac_address': host_info['NewMACAddress'], + 'is_active': bool(host_info['NewActive']), + 'lease_time_remaining': host_info['NewLeaseTimeRemaining'] + } + return host class Client: """TR-064 client. - :param str username: - Username with access to router. - :param str password: - Passwort to access router. - :param str base_url: - URL to router. + :param str username: Username with access to router. + :param str password: Passwort to access router. + :param str base_url: URL to router. """ - def __init__(self, username, password, verify, base_url='https://192.168.178.1:49443', plugin_instance=None): + def __init__(self, username, password, verify, base_url='https://192.168.178.1:49443', description_file=FRITZ_TR64_DESC_FILE, plugin_instance=None): # handle plugin instance self._plugin_instance = plugin_instance @@ -439,51 +1409,65 @@ def __init__(self, username, password, verify, base_url='https://192.168.178.1:4 self.base_url = base_url self.auth = HTTPDigestAuth(username, password) self.verify = verify + self.description_file = description_file self.devices = {} - self._plugin_instance.logger.debug(f"Init Client") + self._plugin_instance.logger.debug(f"Init Client for description file={self.description_file}") def __getattr__(self, name): if name not in self.devices: - self._fetch_devices() + self._fetch_devices(self.description_file) if name in self.devices: return self.devices[name] - def _fetch_devices(self, description_file='/tr64desc.xml'): + def _fetch_devices(self, description_file): """Fetch device description.""" - request = requests.get(f'{self.base_url}{description_file}', verify=self.verify) + + self._plugin_instance.logger.debug(f"_fetch_devices called for description file={description_file}") + + request = requests.get(f'{self.base_url}/{description_file}', verify=self.verify) + + if description_file == 'igddesc.xml': + namespaces = IGD_DEVICE_NAMESPACE + else: + namespaces = TR064_DEVICE_NAMESPACE if request.status_code == 200: xml = etree.parse(BytesIO(request.content)) - for device in xml.findall('.//device', namespaces=TR064_DEVICE_NAMESPACE): - name = device.findtext('deviceType', namespaces=TR064_DEVICE_NAMESPACE).split(':')[-2] + for device in xml.findall('.//device', namespaces=namespaces): + name = device.findtext('deviceType', namespaces=namespaces).split(':')[-2] if name not in self.devices: - self.devices[name] = FritzDevice.Device(device, self.auth, self.verify, self.base_url) + self.devices[name] = FritzDevice.Device(device, self.auth, self.verify, self.base_url, description_file) + + self._plugin_instance.logger.debug(f"Client: {self.description_file} devices={self.devices}") class Device: """TR-064 device. - :param lxml.etree.Element xml: - XML device element - :param HTTPBasicAuthHandler auth: - HTTPBasicAuthHandler object, e.g. HTTPDigestAuth - :param str base_url: - URL to router. + :param lxml.etree.Element xml: XML device element + :param HTTPBasicAuthHandler auth: HTTPBasicAuthHandler object, e.g. HTTPDigestAuth + :param str base_url: URL to router. """ - def __init__(self, xml, auth, verify, base_url): + def __init__(self, xml, auth, verify, base_url, description_file): + # init logger self.logger = logging.getLogger(__name__) self.services = {} self.verify = verify - for service in xml.findall('./serviceList/service', namespaces=TR064_DEVICE_NAMESPACE): - service_type = service.findtext('serviceType', namespaces=TR064_DEVICE_NAMESPACE) - service_id = service.findtext('serviceId', namespaces=TR064_DEVICE_NAMESPACE) - control_url = service.findtext('controlURL', namespaces=TR064_DEVICE_NAMESPACE) - event_sub_url = service.findtext('eventSubURL', namespaces=TR064_DEVICE_NAMESPACE) - scpdurl = service.findtext('SCPDURL', namespaces=TR064_DEVICE_NAMESPACE) + if description_file == 'igddesc.xml': + namespaces = IGD_DEVICE_NAMESPACE + else: + namespaces = TR064_DEVICE_NAMESPACE + + for service in xml.findall('./serviceList/service', namespaces=namespaces): + service_type = service.findtext('serviceType', namespaces=namespaces) + service_id = service.findtext('serviceId', namespaces=namespaces) + control_url = service.findtext('controlURL', namespaces=namespaces) + event_sub_url = service.findtext('eventSubURL', namespaces=namespaces) + scpdurl = service.findtext('SCPDURL', namespaces=namespaces) name = service_type.split(':')[-2].replace('-', '_') if name not in self.services: @@ -498,30 +1482,36 @@ def __init__(self, xml, auth, verify, base_url): service_id, scpdurl, control_url, - event_sub_url + event_sub_url, + description_file ) ) - def __getattr__(self, name): + # self.logger.debug(f"Device: {description_file} services={self.services}") + + def __getattr__(self, name: str): if name in self.services: return self.services[name] class ServiceList(list): """Service list.""" - def __getattr__(self, name): + def __getattr__(self, name: str): """Direct access to first list entry if brackets omit.""" return self[0].__getattr__(name) def __getitem__(self, index): - """Overriden braket operator to return TR-064 exception.""" + """Override bracket operator to return TR-064 exception.""" if len(self) > index: return super().__getitem__(index) class Service: """TR-064 service.""" - def __init__(self, auth, verify, base_url, service_type, service_id, scpdurl, control_url, event_sub_url): + def __init__(self, auth, verify, base_url, service_type, service_id, scpdurl, control_url, event_sub_url, description_file): + # init logger + self.logger = logging.getLogger(__name__) + self.auth = auth self.verify = verify self.base_url = base_url @@ -531,22 +1521,28 @@ def __init__(self, auth, verify, base_url, service_type, service_id, scpdurl, co self.control_url = control_url self.event_sub_url = event_sub_url self.actions = {} + self.description_file = description_file - def __getattr__(self, name): + if description_file == 'igddesc.xml': + self.namespaces = IGD_SERVICE_NAMESPACE + else: + self.namespaces = TR064_SERVICE_NAMESPACE + + def __getattr__(self, name: str): if name not in self.actions: self._fetch_actions(self.scpdurl) if name in self.actions: return self.actions[name] - def _fetch_actions(self, scpdurl): + def _fetch_actions(self, scpdurl: str): """Fetch action description.""" request = requests.get(f'{self.base_url}{scpdurl}', verify=self.verify) if request.status_code == 200: xml = etree.parse(BytesIO(request.content)) - for action in xml.findall('./actionList/action', namespaces=TR064_SERVICE_NAMESPACE): - name = action.findtext('name', namespaces=TR064_SERVICE_NAMESPACE) + for action in xml.findall('./actionList/action', namespaces=self.namespaces): + name = action.findtext('name', namespaces=self.namespaces) canonical_name = name.replace('-', '_') self.actions[canonical_name] = FritzDevice.Action( action, @@ -556,29 +1552,26 @@ def _fetch_actions(self, scpdurl): self.service_type, self.service_id, self.control_url, - self.verify + self.verify, + self.namespaces ) + # self.logger.debug(f"Service: {self.description_file} scpdurl={self.scpdurl} with actions={self.actions}") + class Action: """TR-064 action. - :param lxml.etree.Element xml: - XML action element - :param HTTPBasicAuthHandler auth: - HTTPBasicAuthHandler object, e.g. HTTPDigestAuth - :param str base_url: - URL to router. - :param str name: - Action name - :param str service_type: - Service type - :param str service_id: - Service ID - :param str control_url: - Control URL - """ - - def __init__(self, xml, auth, base_url, name, service_type, service_id, control_url, verify): + :param lxml.etree.Element xml: XML action element + :param HTTPBasicAuthHandler auth: HTTPBasicAuthHandler object, e.g. HTTPDigestAuth + :param str base_url: URL to router. + :param str name: Action name + :param str service_type: Service type + :param str service_id: Service ID + :param str control_url: Control URL + """ + + def __init__(self, xml, auth, base_url, name, service_type, service_id, control_url, verify, namespaces): + # init logger self.logger = logging.getLogger(__name__) self.auth = auth @@ -597,15 +1590,15 @@ def __init__(self, xml, auth, base_url, name, service_type, service_id, control_ '{http://schemas.xmlsoap.org/soap/envelope/}Envelope', attrib={ '{http://schemas.xmlsoap.org/soap/envelope/}encodingStyle': - 'http://schemas.xmlsoap.org/soap/encoding/'}) + 'http://schemas.xmlsoap.org/soap/encoding/'}) self.body = etree.SubElement(self.envelope, '{http://schemas.xmlsoap.org/soap/envelope/}Body') self.in_arguments = {} self.out_arguments = {} - for argument in xml.findall('./argumentList/argument', namespaces=TR064_SERVICE_NAMESPACE): - name = argument.findtext('name', namespaces=TR064_SERVICE_NAMESPACE) - direction = argument.findtext('direction', namespaces=TR064_SERVICE_NAMESPACE) + for argument in xml.findall('./argumentList/argument', namespaces=namespaces): + name = argument.findtext('name', namespaces=namespaces) + direction = argument.findtext('direction', namespaces=namespaces) if direction == 'in': self.in_arguments[name.replace('-', '_')] = name @@ -649,6 +1642,7 @@ def __call__(self, **kwargs): for arg in list(xml.find('.//{{{}}}{}Response'.format(self.service_type, self.name))): name = self.out_arguments[arg.tag] response[name] = arg.text + # self.logger.debug(f"__call__: control_url={self.control_url} response={response}") return response class AttributeDict(dict): @@ -661,6 +1655,11 @@ def __getattr__(self, name): class FritzHome: """Fritzhome object to communicate with the device.""" + _login_route = "/login_sid.lua?version=2" + _event_route = '/query.lua?mq_log=logger:status/log&sid=' + _homeauto_route = '/webservices/homeautoswitch.lua' + _internet_status_route = '/internet/inetstat_monitor.lua?sid=' + def __init__(self, host, ssl, verify, user, password, plugin_instance): self._plugin_instance = plugin_instance @@ -673,55 +1672,254 @@ def __init__(self, host, ssl, verify, user, password, plugin_instance): self._password = password self._sid = None - self._devices: Dict[str, FritzHome.FritzhomeDevice] = None - self._templates: Dict[str, FritzHome.FritzhomeTemplate] = None + self._devices: Dict[str, FritzHome.FritzhomeDevice] = {} + self._templates: Dict[str, FritzHome.FritzhomeTemplate] = {} self._logged_in = False - + self._items = dict() + self._aha_devices = dict() self._session = requests.Session() - def _request(self, url, params=None, timeout=10): - """Send a request with parameters.""" + def register_item(self, item, avm_data_type: str): + + # handle aha items + avm_ain = self._get_item_ain(item) + if avm_ain is not None: + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_ain' found; append to list.") + self._items[item] = (avm_data_type, avm_ain) + else: + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_ain' is not defined; Item will be ignored.") + + def _get_item_ain(self, item) -> str: + """ + Get AIN of device from item.conf + """ + + ain_device = None + + lookup_item = item + for i in range(2): + attribute = 'ain' + attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" + + ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute) + if ain_device is not None: + break + ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute_w_instance) + if ain_device is not None: + break + else: + lookup_item = lookup_item.return_parent() + + if ain_device: + # deprecated warning for attribute 'ain' + self._plugin_instance.logger.warning( + f"Item {item.id()} uses deprecated 'ain' attribute. Please consider to switch to 'avm_ain'.") + else: + lookup_item = item + for i in range(2): + attribute = 'avm2_ain' + attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" + + ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute_w_instance) + if ain_device is not None: + break + ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute) + if ain_device is not None: + break + else: + lookup_item = lookup_item.return_parent() + + if ain_device is not None: + self._plugin_instance.logger.error('Device AIN is not defined or instance not given') + return str(ain_device) + + def _poll_aha(self): + + self._plugin_instance.logger.debug(f'Starting AHA update loop for instance {self._plugin_instance.get_instance_name()}.') + + # update devices + self.update_devices() + + # get device dict + _device_dict = self.get_devices_as_dict() + + for ain in _device_dict: + if not self._aha_devices.get(ain): + self._aha_devices[ain] = {} + self._aha_devices[ain]['connected_to_item'] = False + self._aha_devices[ain]['switch'] = {} + self._aha_devices[ain]['temperature_sensor'] = {} + self._aha_devices[ain]['thermostat'] = {} + self._aha_devices[ain]['alarm'] = {} + + self._aha_devices[ain]['online'] = bool(_device_dict[ain].present) + self._aha_devices[ain]['name'] = _device_dict[ain].name + self._aha_devices[ain]['productname'] = _device_dict[ain].productname + self._aha_devices[ain]['manufacturer'] = _device_dict[ain].manufacturer + self._aha_devices[ain]['fw_version'] = _device_dict[ain].fw_version + self._aha_devices[ain]['lock'] = bool(_device_dict[ain].lock) + self._aha_devices[ain]['device_lock'] = bool(_device_dict[ain].device_lock) + self._aha_devices[ain]['functions'] = [] + + if _device_dict[ain].has_thermostat: + self._aha_devices[ain]['functions'].append('thermostat') + self._aha_devices[ain]['thermostat']['actual_temperature'] = _device_dict[ain].actual_temperature + self._aha_devices[ain]['thermostat']['target_temperature'] = _device_dict[ain].target_temperature + self._aha_devices[ain]['thermostat']['comfort_temperature'] = _device_dict[ain].comfort_temperature + self._aha_devices[ain]['thermostat']['eco_temperature'] = _device_dict[ain].eco_temperature + self._aha_devices[ain]['thermostat']['battery_low'] = bool(_device_dict[ain].battery_low) + self._aha_devices[ain]['thermostat']['battery_level'] = _device_dict[ain].battery_level + self._aha_devices[ain]['thermostat']['window_open'] = bool(_device_dict[ain].window_open) + self._aha_devices[ain]['thermostat']['summer_active'] = bool(_device_dict[ain].summer_active) + self._aha_devices[ain]['thermostat']['holiday_active'] = bool(_device_dict[ain].holiday_active) + + if _device_dict[ain].has_switch: + self._aha_devices[ain]['functions'].append('switch') + self._aha_devices[ain]['switch']['switch_state'] = bool(_device_dict[ain].switch_state) + self._aha_devices[ain]['switch']['power'] = _device_dict[ain].power + self._aha_devices[ain]['switch']['energy'] = _device_dict[ain].energy + self._aha_devices[ain]['switch']['voltage'] = _device_dict[ain].voltage + + if _device_dict[ain].has_temperature_sensor: + self._aha_devices[ain]['functions'].append('temperature_sensor') + self._aha_devices[ain]['temperature_sensor']['temperature'] = _device_dict[ain].temperature + self._aha_devices[ain]['temperature_sensor']['offset'] = _device_dict[ain].offset + + if _device_dict[ain].has_alarm: + self._aha_devices[ain]['functions'].append('alarm') + self._aha_devices[ain]['alarm']['alert_state'] = bool(_device_dict[ain].alert_state) + + def update_items(self): + """ + Update smarthome item values using information from dict '_aha_devices' + """ + + # first poll current data + self._poll_aha() + + for item in self._items: + # get avm_data_type and ain + _avm_data_type = self._items[item][0] + _ain = self._items[item][1] + + # get device sub-dict from dict + device = self._items.get(_ain, None) + + if device is not None: + # Attributes that are write only commands with no corresponding read commands are excluded from status updates via update black list: + update_black_list = ['switch_toggle'] + + if _avm_data_type not in update_black_list: + # Remove "set_" prefix to set corresponding r/o or r/w item to returned value: + if _avm_data_type.startswith('set_'): + _avm_data_type = _avm_data_type[len('set_'):] + # set item + if _avm_data_type in device: + item(device[_avm_data_type], self._plugin_instance.get_shortname()) + else: + self._plugin_instance.logger.warning(f'Attribute <{_avm_data_type}> at device <{_ain}> to be set to Item <{item}> is not available.') + else: + self._plugin_instance.logger.warning(f'No values for item {item.id()} with AIN {_ain} available.') + + def _request(self, url: str, params: dict = None, timeout: int = 10, result: str = 'text') -> Union[str, dict]: + """Send a request with parameters. + :param url URL to be requested + :param params params for request + :param timeout timeout + :param result type of result + :return request response + :type return + """ + rsp = self._session.get(url, params=params, timeout=timeout, verify=self._verify) - rsp.raise_for_status() - return rsp.text.strip() - def _login_request(self, username=None, secret=None): - """Send a login request with paramerters.""" - url = self.get_prefixed_host() + "/login_sid.lua" + status_code = rsp.status_code + if status_code == 200: + self._plugin_instance.logger.debug("Sending HTTP request successful") + if result == 'json': + try: + data = rsp.json() + except JSONDecodeError: + self._plugin_instance.logger.error('Error occurred during parsing request response to json') + else: + self._plugin_instance.logger.error(type(data)) + return data + else: + return rsp.text.strip() + elif status_code == 403: + self._plugin_instance.logger.debug("HTTP access denied. Try to get new Session ID.") + else: + self._plugin_instance.logger.error(f"HTTP request error code: {status_code}") + rsp.raise_for_status() + self._plugin_instance.logger.debug(f"Url: {url}") + self._plugin_instance.logger.debug(f"Params: {params}") + + def _login_request(self, username=None, challenge_response=None): + """Send a login request with parameters.""" + url = self.get_prefixed_host() + self._login_route + # self._plugin_instance.logger.debug(f"_login_request: url={url}") params = {} if username: params["username"] = username - if secret: - params["response"] = secret + if challenge_response: + params["response"] = challenge_response + # self._plugin_instance.logger.debug(f"_login_request: params={params}") plain = self._request(url, params) + # self._plugin_instance.logger.debug(f"_login_request: plain={plain}") dom = ElementTree.fromstring(plain) sid = dom.findtext("SID") challenge = dom.findtext("Challenge") - return sid, challenge + blocktime = int(dom.findtext("BlockTime")) + + # self._plugin_instance.logger.debug(f"_login_request: sid={sid}, challenge={challenge}, blocktime={blocktime}") + return sid, challenge, blocktime def _logout_request(self): """Send a logout request.""" - url = self.get_prefixed_host() + "/login_sid.lua" - params = {"security:command/logout": "1", "sid": self._sid} + url = self.get_prefixed_host() + self._login_route + params = {"logout": "1", "sid": self._sid} self._request(url, params) @staticmethod - def _create_login_secret(challenge, password): - """Create a login secret.""" - to_hash = (challenge + "-" + password).encode("UTF-16LE") - hashed = hashlib.md5(to_hash).hexdigest() - return "{0}-{1}".format(challenge, hashed) + def _calculate_md5_response(challenge: str, password: str) -> str: + """ + Calculate the response for a challenge using legacy MD5 + """ - def _aha_request(self, cmd, ain=None, param=None, rf=str): - """Send an AHA request.""" + response = challenge + "-" + password + # the legacy response needs utf_16_le encoding + response = response.encode("utf_16_le") + md5_sum = hashlib.md5() + md5_sum.update(response) + response = challenge + "-" + md5_sum.hexdigest() + return response - self._plugin_instance.logger.debug(f"_aha_request called with cmd={cmd}, ain={ain}, param={param}, rf={rf} ") + @staticmethod + def _calculate_pbkdf2_response(challenge: str, password: str) -> str: + """ + Calculate the response for a given challenge via PBKDF2 + """ + + challenge_parts = challenge.split("$") + # Extract all necessary values encoded into the challenge + iter1 = int(challenge_parts[1]) + salt1 = bytes.fromhex(challenge_parts[2]) + iter2 = int(challenge_parts[3]) + salt2 = bytes.fromhex(challenge_parts[4]) + # Hash twice, once with static salt... + hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1) + # Once with dynamic salt. + hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2) + return f"{challenge_parts[4]}${hash2.hex()}" + + def _aha_request(self, cmd, ain=None, param=None, rf='str'): + """Send an AHA request.""" if not self._logged_in: self.login() - url = self.get_prefixed_host() + "/webservices/homeautoswitch.lua" + url = self.get_prefixed_host() + self._homeauto_route params = {"switchcmd": cmd, "sid": self._sid} if param: params.update(param) @@ -730,26 +1928,40 @@ def _aha_request(self, cmd, ain=None, param=None, rf=str): plain = self._request(url, params) - self.logout() - if plain == "inval": self._plugin_instance.logger.error("InvalidError") return - if rf == bool: + if rf == 'bool': return bool(int(plain)) - return rf(plain) + elif rf == 'str': + return str(plain) + elif rf == 'int': + return int(plain) + elif rf == 'float': + return float(plain) + else: + return plain def login(self): """Login and get a valid session ID.""" self._plugin_instance.logger.debug("AHA login called") try: - (sid, challenge) = self._login_request() + (sid, challenge, blocktime) = self._login_request() + if blocktime > 0: + self._plugin_instance.logger.debug(f"Waiting for {blocktime} seconds...") + time.sleep(blocktime) + if sid == "0000000000000000": - secret = self._create_login_secret(challenge, self._password) - (sid2, challenge) = self._login_request(username=self._user, secret=secret) + if challenge.startswith('2$'): + self._plugin_instance.logger.debug("PBKDF2 supported") + challenge_response = self._calculate_pbkdf2_response(challenge, self._password) + else: + self._plugin_instance.logger.debug("Falling back to MD5") + challenge_response = self._calculate_md5_response(challenge, self._password) + (sid2, challenge, blocktime) = self._login_request(username=self._user, challenge_response=challenge_response) if sid2 == "0000000000000000": - self._plugin_instance.logger.warning("login failed %s", sid2) + self._plugin_instance.logger.warning(f"login failed {sid2}") self._plugin_instance.logger.error(f"LoginError for {self._user}") return self._sid = sid2 @@ -765,6 +1977,24 @@ def logout(self): self._sid = None self._logged_in = False + def check_sid(self): + """ + Check if knows Session ID is still valid + """ + + self._plugin_instance.logger.debug(f"check_sid called") + url = self.get_prefixed_host() + self._login_route + params = {"sid": self._sid} + plain = self._request(url, params) + dom = ElementTree.fromstring(plain) + sid = dom.findtext("SID") + + if sid == "0000000000000000": + self._plugin_instance.logger.warning(f"Session ID is invalid. Try to generate new one.") + self.login() + else: + self._plugin_instance.logger.info(f"Session ID is still valid.") + def get_prefixed_host(self): """Choose the correct protocol prefix for the host. Supports three input formats: @@ -775,9 +2005,9 @@ def get_prefixed_host(self): host = self._host if not host.startswith("https://") or not host.startswith("http://"): if self._ssl: - host = "https://"+host + host = "https://" + host else: - host = "http://"+host + host = "http://" + host return host def update_devices(self): @@ -798,6 +2028,10 @@ def update_devices(self): def _get_listinfo_elements(self, entity_type): """Get the DOM elements for the entity list.""" plain = self._aha_request("get" + entity_type + "listinfos") + + if plain is None: + return + dom = ElementTree.fromstring(plain) return dom.findall(entity_type) @@ -829,7 +2063,7 @@ def get_device_by_ain(self, ain): def get_device_present(self, ain): """Get the device presence.""" - return self._aha_request("getswitchpresent", ain=ain, rf=bool) + return self._aha_request("getswitchpresent", ain=ain, rf='bool') def get_device_name(self, ain): """Get the device name.""" @@ -837,34 +2071,34 @@ def get_device_name(self, ain): def get_switch_state(self, ain): """Get the switch state.""" - return self._aha_request("getswitchstate", ain=ain, rf=bool) + return self._aha_request("getswitchstate", ain=ain, rf='bool') def set_switch_state_on(self, ain): """Set the switch to on state.""" - return self._aha_request("setswitchon", ain=ain, rf=bool) + return self._aha_request("setswitchon", ain=ain, rf='bool') def set_switch_state_off(self, ain): """Set the switch to off state.""" - return self._aha_request("setswitchoff", ain=ain, rf=bool) + return self._aha_request("setswitchoff", ain=ain, rf='bool') def set_switch_state_toggle(self, ain): """Toggle the switch state.""" - return self._aha_request("setswitchtoggle", ain=ain, rf=bool) + return self._aha_request("setswitchtoggle", ain=ain, rf='bool') def get_switch_power(self, ain): """Get the switch power consumption.""" - return self._aha_request("getswitchpower", ain=ain, rf=int) + return self._aha_request("getswitchpower", ain=ain, rf='int') def get_switch_energy(self, ain): """Get the switch energy.""" - return self._aha_request("getswitchenergy", ain=ain, rf=int) + return self._aha_request("getswitchenergy", ain=ain, rf='int') def get_temperature(self, ain): """Get the device temperature sensor value.""" - return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 + return self._aha_request("gettemperature", ain=ain, rf='float') / 10.0 def _get_temperature(self, ain, name): - plain = self._aha_request(name, ain=ain, rf=float) + plain = self._aha_request(name, ain=ain, rf='float') return (plain - 16) / 2 + 8 def get_target_temperature(self, ain): @@ -1027,6 +2261,29 @@ def apply_template(self, ain): """Applies a template.""" self._aha_request("applytemplate", ain=ain) + def get_device_log_from_lua(self): + """ + Gets the Device Log from the LUA HTTP Interface via LUA Scripts (more complete than the get_device_log TR-064 version. + :return: Array of Device Log Entries (text, type, category, timestamp, date, time) + """ + + if not self._logged_in: + self.login() + + url = self.get_prefixed_host() + self._event_route + params = {"sid": self._sid} + data = self._request(url, params, result='json')['mq_log'] + newlog = [] + for text, typ, cat in data: + l_date = text[:8] + l_time = text[9:17] + l_text = text[18:] + l_cat = int(cat) + l_type = int(typ) + l_ts = int(datetime.datetime.timestamp(datetime.datetime.strptime(text[:17], '%d.%m.%y %H:%M:%S'))) + newlog.append([l_text, l_type, l_cat, l_ts, l_date, l_time]) + return newlog + class FritzhomeDeviceFeatures(IntFlag): ALARM = 0x0010 UNKNOWN = 0x0020 @@ -1050,7 +2307,7 @@ def __init__(self, fritz=None, node=None): self.logger = logging.getLogger(__name__) self._fritz = None - self.ain: str = None + self.ain: str = '' self._functionsbitmask = None if fritz is not None: @@ -1066,7 +2323,7 @@ def _has_feature(self, feature) -> bool: return feature in FritzHome.FritzhomeDeviceFeatures(self._functionsbitmask) def _update_from_node(self, node): - self.logger.debug(ElementTree.tostring(node)) + # self.logger.debug(ElementTree.tostring(node)) self.ain = node.attrib["identifier"] self._functionsbitmask = int(node.attrib["functionbitmask"]) @@ -1235,7 +2492,7 @@ def __init__(self, node=None): self._update_from_node(node) def _update_from_node(self, node): - self.logger.debug(ElementTree.tostring(node)) + # self.logger.debug(ElementTree.tostring(node)) self.ain = node.attrib["identifier"] self.identifier = node.attrib["id"] self.name = node.findtext("name") @@ -1654,3 +2911,403 @@ def __init__(self, fritz=None, node=None): def _update_from_node(self, node): super()._update_from_node(node) + + +class Callmonitor: + def __init__(self, host, port, callback, call_monitor_incoming_filter, plugin_instance): + + self._plugin_instance = plugin_instance + self._plugin_instance.logger.debug("Init Callmonitor") + + self._host = host + self._port = port + + self._call_monitor_incoming_filter = call_monitor_incoming_filter + self._callback = callback + self._items = dict() # more general items for the call monitor + self._trigger_items = dict() # items which can be used to trigger sth, e.g. a logic + self._items_incoming = dict() # items for incoming calls + self._items_outgoing = dict() # items for outgoing calls + self._duration_item_in = dict() # 2 items, one for counting the incoming, one for counting the outgoing call duration + self._duration_item_out = dict() + self._call_active = dict() + self._listen_active = False + self._call_active['incoming'] = False + self._call_active['outgoing'] = False + self._call_incoming_cid = dict() + self._call_outgoing_cid = dict() + self._call_monitor_incoming_filter = call_monitor_incoming_filter + self.conn = None + self._listen_thread = None + + self.connect() + + def connect(self): + """ + Connects to the call monitor of the AVM device + """ + + if self._listen_active: + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("MonitoringService: Connect called while listen active") + return + + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.conn.connect((self._host, self._port)) + _name = f'plugins.{self._plugin_instance.get_fullname()}.Monitoring_Service' + self._listen_thread = threading.Thread(target=self._listen, name=_name).start() + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("MonitoringService: connection established") + except Exception as e: + self.conn = None + self._plugin_instance.logger.error( + f"MonitoringService: Cannot connect to {self._host} on port: {self._port}, CallMonitor activated by #96*5*? - Error: {e}") + return + + def disconnect(self): + """ + Disconnects from the call monitor of the AVM device + """ + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("MonitoringService: disconnecting") + self._listen_active = False + self._stop_counter('incoming') + self._stop_counter('outgoing') + try: + self._listen_thread.join(1) + except Exception: + pass + + try: + self.conn.shutdown(2) + except Exception: + pass + + def reconnect(self): + """ + Reconnects to the call monitor of the AVM device + """ + + self.disconnect() + self.connect() + + def register_item(self, item, avm_data_type: str): + """ + Registers an item to the CallMonitoringService + + :param item: item to register + :param avm_data_type: avm_data_type of item to be registered + """ + + # handle _call_monitor_attributes_in + if avm_data_type in _call_monitor_attributes_in: + self._items_incoming[item] = (avm_data_type, None) + elif avm_data_type in _call_monitor_attributes_out: + self._items_outgoing[item] = (avm_data_type, None) + elif avm_data_type in _trigger_attributes: + avm_incoming_allowed = self._plugin_instance.get_iattr_value(item.conf, 'avm2_incoming_allowed') + avm_target_number = self._plugin_instance.get_iattr_value(item.conf, 'avm2_target_number') + if not avm_incoming_allowed or not avm_target_number: + self._plugin_instance.logger.error(f"For Trigger-item={item.id()} both 'avm2_incoming_allowed' and 'avm2_target_number' must be specified as attributes. Item will be ignored.") + else: + self._trigger_items[item] = (avm_data_type, avm_incoming_allowed, avm_target_number) + elif avm_data_type in _call_duration_attributes: + if 'in' in avm_data_type: + self._duration_item_in[item] = (avm_data_type, None) + else: + self._duration_item_out[item] = (avm_data_type, None) + else: + self._items[item] = (avm_data_type, None) + + def get_items(self): + return list(self._items.keys()) + + def get_trigger_items(self): + return list(self._trigger_items.keys()) + + def get_items_incoming(self): + return list(self._items_incoming.keys()) + + def get_items_outgoing(self): + return list(self._items_outgoing.keys()) + + @property + def get_item_count_total(self): + """ + Returns number of added items (all items of MonitoringService service + + :return: number of items hold by the MonitoringService + """ + + return len(self._items) + len(self._trigger_items) + len(self._items_incoming) + len(self._items_outgoing) + len(self._duration_item_in) + len(self._duration_item_out) + + def _listen(self, recv_buffer: int = 4096): + """ + Function which listens to the established connection. + """ + + self._listen_active = True + buffer = "" + while self._listen_active: + data = self.conn.recv(recv_buffer) + if data == "": + self._plugin_instance.logger.error("CallMonitor connection not open anymore.") + else: + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Data Received from CallMonitor: {data.decode('utf-8')}") + buffer += data.decode("utf-8") + while buffer.find("\n") != -1: + line, buffer = buffer.split("\n", 1) + if line: + self._parse_line(line) + + def _start_counter(self, timestamp: str, direction: str): + """ + Start counter to measure duration of a call + """ + + if direction == 'incoming': + self._call_connect_timestamp = time.mktime(datetime.datetime.strptime(timestamp, "%d.%m.%y %H:%M:%S").timetuple()) + self._duration_counter_thread_incoming = threading.Thread(target=self._count_duration_incoming, + name=f"MonitoringService_Duration_Incoming_{self._plugin_instance.get_instance_name()}").start() + self._plugin_instance.logger.debug('Counter incoming - STARTED') + elif direction == 'outgoing': + self._call_connect_timestamp = time.mktime(datetime.datetime.strptime(timestamp, "%d.%m.%y %H:%M:%S").timetuple()) + self._duration_counter_thread_outgoing = threading.Thread(target=self._count_duration_outgoing, + name=f"MonitoringService_Duration_Outgoing_{self._plugin_instance.get_instance_name()}").start() + self._plugin_instance.logger.debug('Counter outgoing - STARTED') + + def _stop_counter(self, direction: str): + """ + Stop counter to measure duration of a call, but only stop of thread is active + """ + + if self._call_active[direction]: + self._call_active[direction] = False + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f'STOPPING {direction}') + try: + if direction == 'incoming': + self._duration_counter_thread_incoming.join(1) + elif direction == 'outgoing': + self._duration_counter_thread_outgoing.join(1) + except Exception: + pass + + def _count_duration_incoming(self): + """ + Count duration of incoming call and set item value + """ + + self._call_active['incoming'] = True + while self._call_active['incoming']: + if self._duration_item_in is not None: + duration = time.time() - self._call_connect_timestamp + duration_item = list(self._duration_item_in.keys())[0] + duration_item(int(duration), self._plugin_instance.get_shortname()) + time.sleep(1) + + def _count_duration_outgoing(self): + """ + Count duration of outgoing call and set item value + """ + + self._call_active['outgoing'] = True + while self._call_active['outgoing']: + if self._duration_item_out is not None: + duration = time.time() - self._call_connect_timestamp + duration_item = list(self._duration_item_out.keys())[0] + duration_item(int(duration), self._plugin_instance.get_shortname()) + time.sleep(1) + + def _parse_line(self, line: str): + """ + Parses a data set in the form of a line. + + Data Format: + Ausgehende Anrufe: datum;CALL;ConnectionID;Nebenstelle;GenutzteNummer;AngerufeneNummer;SIP+Nummer + Eingehende Anrufe: datum;RING;ConnectionID;Anrufer-Nr;Angerufene-Nummer;SIP+Nummer + Zustandegekommene Verbindung: datum;CONNECT;ConnectionID;Nebenstelle;Nummer; + Ende der Verbindung: datum;DISCONNECT;ConnectionID;dauerInSekunden; + + :param line: data line which is parsed + """ + + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(line) + line = line.split(";") + + try: + if line[1] == "RING": + self._trigger(line[3], line[4], line[0], line[2], line[1], '') + elif line[1] == "CALL": + self._trigger(line[4], line[5], line[0], line[2], line[1], line[3]) + elif line[1] == "CONNECT": + self._trigger('', '', line[0], line[2], line[1], line[3]) + elif line[1] == "DISCONNECT": + self._trigger('', '', '', line[2], line[1], '') + except Exception as e: + self._plugin_instance.logger.error(f"MonitoringService: {type(e).__name__} while handling Callmonitor response: {e}") + + def _trigger(self, call_from: str, call_to: str, time: str, callid: str, event: str, branch: str): + """ + Triggers the event: sets item values and looks up numbers in the phone book. + """ + + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Event: {event}, Call From: {call_from}, Call To: {call_to}, Time: {time}, CallID: {callid}, Branch: {branch}") + + # set generic item value + for item in self._items: + avm_data_type = self._items[item][0] + if avm_data_type == 'call_event': + item(event.lower(), self._plugin_instance.get_shortname()) + if avm_data_type == 'call_direction': + if event == 'RING': + item("incoming", self._plugin_instance.get_shortname()) + else: + item("outgoing", self._plugin_instance.get_shortname()) + + # handle incoming call + if event == 'RING': + # process "trigger items" + for trigger_item in self._trigger_items: + avm_data_type = self._trigger_items[trigger_item][0] + avm_incoming_allowed = self._trigger_items[trigger_item][1] + avm_target_number = self._trigger_items[trigger_item][2] + trigger_item(0, self._plugin_instance.get_shortname()) + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"{avm_data_type} {call_from} {call_to}") + if avm_incoming_allowed == call_from and avm_target_number == call_to: + trigger_item(1, self._plugin_instance.get_shortname()) + + if self._call_monitor_incoming_filter in call_to: + # set call id for incoming call + self._call_incoming_cid = callid + + # reset duration for incoming calls + if self._duration_item_in is not None: + duration_item = list(self._duration_item_in.keys())[0] + duration_item(0, self._plugin_instance.get_shortname()) + + # process items specific to incoming calls + for item in self._items_incoming: # update items for incoming calls + avm_data_type = self._items[item][0] + if avm_data_type == 'is_call_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting is_call_incoming: True") + item(True, self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_caller_incoming': + if call_from != '' and call_from is not None: + name = self._callback(call_from) + if name != '' and name is not None: + item(name, self._plugin_instance.get_shortname()) + else: + item(call_from, self._plugin_instance.get_shortname()) + else: + item("Unbekannt", self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_call_date_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting last_call_date_incoming: {time}") + item(time, self._plugin_instance.get_shortname()) + elif avm_data_type == 'call_event_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") + item(event.lower(), self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_number_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting last_number_incoming: {call_from}") + item(call_from, self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_called_number_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting last_called_number_incoming: {call_to}") + item(call_to, self._plugin_instance.get_shortname()) + + # handle outgoing call + elif event == 'CALL': + # set call id for outgoing call + self._call_outgoing_cid = callid + + # reset duration for outgoing calls + if self._duration_item_out is not None: + duration_item = list(self._duration_item_out.keys())[0] + duration_item(0, self._plugin_instance.get_shortname()) + + # process items specific to outgoing calls + for item in self._items_outgoing: + avm_data_type = self._items[item][0] + if avm_data_type == 'is_call_outgoing': + item(True, self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_caller_outgoing': + name = self._callback(call_to) + if name != '' and name is not None: + item(name, self._plugin_instance.get_shortname()) + else: + item(call_to, self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_call_date_outgoing': + item(time, self._plugin_instance.get_shortname()) + elif avm_data_type == 'call_event_outgoing': + item(event.lower(), self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_number_outgoing': + item(call_from, self._plugin_instance.get_shortname()) + elif avm_data_type == 'last_called_number_outgoing': + item(call_to, self._plugin_instance.get_shortname()) + + # handle established connection + elif event == 'CONNECT': + # handle OUTGOING calls + if callid == self._call_outgoing_cid: + if self._duration_item_out is not None: # start counter thread only if duration item set and call is outgoing + self._stop_counter('outgoing') # stop potential running counter for parallel (older) outgoing call + self._start_counter(time, 'outgoing') + for item in self._items_outgoing: + avm_data_type = self._items[item][0] + if avm_data_type == 'call_event_outgoing': + item(event.lower(), self._plugin_instance.get_shortname()) + + # handle INCOMING calls + elif callid == self._call_incoming_cid: + if self._duration_item_in is not None: # start counter thread only if duration item set and call is incoming + self._stop_counter('incoming') # stop potential running counter for parallel (older) incoming call + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("Starting Counter for Call Time") + self._start_counter(time, 'incoming') + for item in self._items_incoming: + avm_data_type = self._items[item][0] + if avm_data_type == 'call_event_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") + item(event.lower(), self._plugin_instance.get_shortname()) + + # handle ended connection + elif event == 'DISCONNECT': + # handle OUTGOING calls + if callid == self._call_outgoing_cid: + for item in self._items_outgoing: + avm_data_type = self._items[item][0] + if avm_data_type == 'call_event_outgoing': + item(event.lower(), self._plugin_instance.get_shortname()) + elif avm_data_type == 'is_call_outgoing': + item(False, self._plugin_instance.get_shortname()) + if self._duration_item_out is not None: # stop counter threads + self._stop_counter('outgoing') + self._call_outgoing_cid = None + + # handle INCOMING calls + elif callid == self._call_incoming_cid: + for item in self._items_incoming: + avm_data_type = self._items[item][0] + if avm_data_type == 'call_event_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") + item(event.lower(), self._plugin_instance.get_shortname()) + elif avm_data_type == 'is_call_incoming': + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting is_call_incoming: {False}") + item(False, self._plugin_instance.get_shortname()) + if self._duration_item_in is not None: # stop counter threads + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("Stopping Counter for Call Time") + self._stop_counter('incoming') + self._call_incoming_cid = None diff --git a/plugin.yaml b/plugin.yaml index 631937a9b..b50fc98c1 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -66,12 +66,590 @@ parameters: description: de: '(optional) Schaltet die Zertifikate-Prüfung an oder aus. Normalerweise False.' en: '(optional) Turns certificate verification on or off. Typically False' + call_monitor: + type: bool + default: False + description: + de: '(optional) Aktiviert oder deaktiviert den MonitoringService, welcher auf den Call Monitor des FritzDevice verbindet. Der Call Monitor muss über ein verbundenes Telefon via #96*5* aktiviert sein.' + en: '(optional) Activates or deactivates the MonitoringService, which connects to the FritzDevice`s call monitor. The call monitor has to be activated before by a connected telephone via calling #96*5*' + call_monitor_incoming_filter: + type: str + default: '' + description: + de: '(optional) Spezielle Rufnummern ausfiltern, die vom Callmonitor ignoriert werden sollen.' + en: '(optional) Filter only specific numbers to be watched by call monitor' + avm_home_automation: + type: bool + default: False + description: + de: '(optional) Aktiviert oder deaktiviert den Zugriff auf AVM Smarthome Geräte mit dem AHA HTTP Interface.' + en: '(optional) Activates or deactivates access to AVM smarthome devices via AHA HTTP interface' + +item_attributes: + # Definition of item attributes defined by this plugin + avm2_data_type: + type: str + mandatory: True + description: + de: 'AVM Datentyp des jeweiligen Items.' + en: 'AVM Data Type of the respective item.' + valid_list: + # Fritzdevice Attribute + - 'uptime' # r/o + - 'serial_number' # r/o + - 'software_version' # r/o + - 'hardware_version' # r/o + # Myfritz Attribute + - 'myfritz_status' # r/o + # Call Monitor Attribute + - 'monitor_trigger' # r/o + - 'is_call_incoming' # r/o + - 'call_duration_incoming' # r/o + - 'last_caller_incoming' # r/o + - 'last_number_incoming' # r/o + - 'last_called_number_incoming' # r/o + - 'last_call_date_incoming' # r/o + - 'call_event_incoming' # r/o + - 'is_call_outgoing' # r/o + - 'call_duration_outgoing' # r/o + - 'last_caller_outgoing' # r/o + - 'last_number_outgoing' # r/o + - 'last_called_number_outgoing' # r/o + - 'last_call_date_outgoing' # r/o + - 'call_event_outgoing' # r/o + - 'call_direction' # r/o + - 'call_event' # r/o + # TAM Attribute + - 'tam' # r/w + - 'tam_name' # r/o + - 'tam_old_message_number' # r/o not supported / implemented + - 'tam_new_message_number' # r/o + - 'tam_total_message_number' # r/o + # WAN Attribute + - 'wan_connection_status' # r/o + - 'wan_connection_error' # r/o + - 'wan_is_connected' # r/o + - 'wan_uptime' # r/o + - 'wan_ip' # r/o + - 'wan_upstream' # r/o + - 'wan_downstream' # r/o + - 'wan_total_packets_sent' # r/o + - 'wan_total_packets_received' # r/o + - 'wan_current_packets_sent' # r/o + - 'wan_current_packets_received' # r/o + - 'wan_total_bytes_sent' # r/o + - 'wan_total_bytes_received' # r/o + - 'wan_current_bytes_sent' # r/o + - 'wan_current_bytes_received' # r/o + - 'wan_link' # r/o + # WLAN Attribute + - 'wlanconfig' # r/w + - 'wlanconfig_ssid' # r/o + - 'wlan_guest_time_remaining' # r/o + # Host Attribute + - 'network_device' # r/o Defines Network device via MAC-Adresse + - 'device_ip' # r/o Geräte-IP (Muss Child von 'network_device' sein) ipv4 + - 'device_connection_type' # r/o Verbindungstyp (Muss Child von 'network_device' sein) str + - 'device_hostname' # r/o Gerätename (Muss Child von 'network_device' sein) str + - 'connection_status' # r/o Verbindungsstatus (Muss Child von 'network_device' sein) bool + # Smarthome Attribute (Deprecated avm data types. Please use alternative AHA interface type) + - 'aha_device' # r/w Steckdose schalten; siehe "switch_state" + - 'hkr_device' # r/o Status des HKR (OPEN; CLOSED; TEMP) + - 'set_temperature' # r/o siehe "target_temperature" + - 'temperature' # r/o siehe "current_temperature" + - 'set_temperature_reduced' # r/o siehe "temperature_reduced" + - 'set_temperature_comfort' # r/o siehe "temperature_comfort" + - 'firmware_version' # r/o siehe "fw_version" + # Deflections + - 'number_of_deflections' # r/o Anzahl der eingestellten Rufumleitungen + - 'deflection_details' # r/o Details zur Rufumleitung (als dict); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item + - 'deflections_details' # r/o Details zu allen Rufumleitung (als dict) + - 'deflection_enable' # r/w Rufumleitung Status an/aus; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_type' # r/o Type der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_number' # r/o Telefonnummer, die umgeleitet wird; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_to_number' # r/o Zielrufnummer der Umleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_mode' # r/o Modus der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_outgoing' # r/o Outgoing der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_phonebook_id' # r/o Phonebook_ID der Zielrufnummer (Only valid if Type==fromPB); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + # AHA Interface attributes + - 'device_id' # r/o Geräte -ID str + - 'manufacturer' # r/o Hersteller str + - 'product_name' # r/o Produktname str + - 'fw_version' # r/o Firmware Version str + - 'connected' # r/o Verbindungsstatus bool + - 'device_name' # r/o Gerätename str + - 'tx_busy' # r/o Verbindung aktiv bool + - 'device_functions' # r/o Im Gerät verhandene Funktionen list + + - 'set_target_temperature' # w/o Soll-Temperatur Setzen num + - 'target_temperature' # r/w Soll-Temperatur (Status aund Setzen) num + - 'current_temperature' # r/o Ist-Temperatur num + - 'temperature_reduced' # r/o Eingestellte reduzierte Temperatur num + - 'temperature_comfort' # r/o Eingestellte Komfort-Temperatur num + - 'temperature_offset' # r/o Eingestellter Temperatur-Offset num + - 'set_window_open' # w/o "Window Open" Funktionen Setzen bool + - 'window_open' # r/w "Window Open" Funktion (Status aund Setzen) bool + - 'windowopenactiveendtime' # r/o Zeitliches Ende der "Window Open" Funktion num + - 'set_hkr_boost' # w/o "Boost" Funktion Setzen bool + - 'hkr_boost' # r/w "Boost" Funktion (Status aund Setzen) bool + # - 'boost_active' # r/o Status der "Boost" Funktion bool deprecated + - 'boostactiveendtime' # r/o Zeitliches Ende der "Boost" Funktion num + - 'summer_active' # r/o Status der "Sommer" Funktion bool + - 'holiday_active' # r/o Status der "Holiday" Funktion bool + - 'battery_low' # r/o "Battery low" Status bool + - 'battery_level' # r/o Batterie-Status in % num + - 'lock' # r/o Tastensperre über UI/API aktiv bool + - 'device_lock' # r/o Tastensperre direkt am Gerät ein bool + - 'errorcode' # r/o Fehlercodes die der HKR liefert num + + - 'set_simpleonoff' # w/o Gerät/Aktor/Lampe an-/ausschalten bool + - 'simpleonoff' # w/r Gerät/Aktor/Lampe (Status und Setzen) bool neu 1.6.4 + + - 'set_level' # w/o Level/Niveau von 0 bis 255 Setzen num + - 'level' # w/r Level/Niveau von 0 bis 255 (Setzen & Status) num neu 1.6.4 + - 'set_levelpercentage' # w/o Level/Niveau von 0% bis 100% Setzen num + - 'levelpercentage' # w/r Level/Niveau von 0% bis 100% (Setzen & Status) num neu 1.6.4 + - 'set_hue' # w/o Hue Setzen num + - 'hue' # w/r Hue (Status und Setzen) num neu 1.6.4 + - 'set_saturation' # w/o Saturation Setzen num + - 'saturation' # w/r Saturation (Status und Setzen) num neu 1.6.4 + - 'set_colortemperature' # w/o Farbtemperatur Setzten num + - 'colortemperature' # w/r Farbtemperatur (Status und Setzen) num neu 1.6.4 + + - 'switch_state' # r/w Schaltzustand Steckdose (Status und Setzen) bool + - 'switch_mode' # r/o Zeitschaltung oder manuell schalten bool + - 'switch_toggle' # w/o Schaltzustand umschalten (toggle) bool + + - 'power' # r/o Leistung in W (Aktualisierung alle 2 min) num + - 'energy' # r/o absoluter Verbrauch seit Inbetriebnahme num + - 'voltage' # r/o Spannung in V (Aktualisierung alle 2 min) num + + - 'humidity' # r/o Relative Luftfeuchtigkeit in % (FD440) num + - 'alert_state' # r/o letzter übermittelter Alarmzustand bool + + avm2_incoming_allowed: + type: str + mandatory: False + description: + de: '(optional) Definition der erlaubten eingehenden Rufnummer in Items vom avm_data_type `monitor_trigger`.' + en: '(optional) Definition of the allowed incoming number. Only in items of avm_data_type `monitor_trigger`.' + + avm2_target_number: + type: str + mandatory: False + description: + de: '(optional) Definition der erlaubten angerufenen Rufnummer in Items vom avm_data_type `monitor_trigger`.' + en: '(optional) Definition of the allowed called number. Only in items of avm_data_type `monitor_trigger`.' + + avm2_wlan_index: + type: int + description: + de: '(optional) Definition des Wlans ueber index: (1: 2.4Ghz, 2: 5Ghz, 3: Gaeste).' + en: '(optional) Definition of WiFi via index: (1: 2.4GHz, 2: 5GHz, 3: Guest)' + valid_min: 1 + valid_max: 3 + + avm2_mac: + type: mac + mandatory: False + description: + de: 'Definition der MAC Adresse für Items vom avm_data_type `network_device`. Nur für diese Items mandatory!' + en: 'Definition of the MAC address for items of avm_data_type `network_device`. Only mandatory for these items!' + + avm2_ain: + type: str + mandatory: False + description: + de: "Definition der AktorIdentifikationsNummer (AIN) Items vom avm_data_types für `AHA-Interface`. Nur für diese Items mandatory!" + en: "Definition of the ActorIdentificationNumber (AIN) for items of avm_data_types `AHA-Interface`. Only mandatory for these items!" + + avm2_tam_index: + type: int + mandatory: False + description: + de: 'Index für den Anrufbeantworter, normalerweise für den ersten eine "1". Es werden bis zu 5 Anrufbeantworter vom Gerät unterstützt.' + en: 'Index für the answering machine, normally a "1" for the first one. Supported are up to 5 answering machines.' + valid_min: 1 + valid_max: 5 + + avm2_deflection_index: + type: int + mandatory: False + description: + de: 'Index für die Rufumleitung, normalerweise für die erste eine "1".' + en: 'Index deflection, normally a "1" for the first one.' + valid_min: 1 + valid_max: 32 + + avm2_read_after_write: + type: int + description: + de: 'Konfiguriert eine Verzögerung in Sekunden nachdem ein Lesekommando nach einem Schreibkommando gesendet wird.' + en: 'Configures delay in seconds to issue a read command after write command' + +item_structs: + info: + uptime: + type: num + visu_acl: ro + avm2_data_type@instance: uptime + serial_number: + type: str + visu_acl: ro + avm2_data_type@instance: serial_number + firmware: + type: str + visu_acl: ro + avm2_data_type@instance: software_version + hardware_version: + type: str + visu_acl: ro + avm2_data_type@instance: hardware_version + myfritz: + type: bool + avm2_data_type@instance: myfritz_status + + monitor: + trigger: + type: bool + avm2_data_type@instance: monitor_trigger + avm2_incoming_allowed@instance: xxxxxxxx + avm2_target_number@instance: xxxxxxxx + enforce_updates: yes + + incoming: + is_call_incoming: + type: bool + avm2_data_type@instance: is_call_incoming + duration: + type: num + avm2_data_type@instance: call_duration_incoming + last_caller: + type: str + avm2_data_type@instance: last_caller_incoming + last_calling_number: + type: str + avm2_data_type@instance: last_number_incoming + last_called_number: + type: str + avm2_data_type@instance: last_called_number_incoming + last_call_date: + type: str + avm2_data_type@instance: last_call_date_incoming + event: + type: str + avm2_data_type@instance: call_event_incoming + + outgoing: + is_call_outgoing: + type: bool + avm2_data_type@instance: is_call_outgoing + duration: + type: num + avm2_data_type@instance: call_duration_outgoing + last_caller: + type: str + avm2_data_type@instance: last_caller_outgoing + last_calling_number: + type: str + avm2_data_type@instance: last_number_outgoing + last_called_number: + type: str + avm2_data_type@instance: last_called_number_outgoing + last_call_date: + type: str + avm2_data_type@instance: last_call_date_outgoing + event: + type: str + avm2_data_type@instance: call_event_outgoing + + newest: + direction: + type: str + avm2_data_type@instance: call_direction + cache: yes + event: + type: str + avm2_data_type@instance: call_event + cache: yes + + tam: + type: bool + visu_acl: rw + avm_data_type@instance: tam + avm_tam_index@instance: 1 + + name: + type: str + visu_acl: ro + avm_data_type@instance: tam_name + message_number_old: + type: num + visu_acl: ro + avm_data_type@instance: tam_old_message_number + eval: (sh...message_number_total()-sh...message_number_new()) + eval_trigger: + - ..message_number_total + - ..message_number_new + message_number_new: + type: num + visu_acl: ro + avm_data_type@instance: tam_new_message_number + message_number_total: + type: num + visu_acl: ro + avm_data_type@instance: tam_total_message_number + + deflection: + type: bool + visu_acl: rw + avm_data_type@instance: deflection_enable + avm_deflection_index@instance: 1 + + deflection_type: + type: str + visu_acl: ro + avm_data_type@instance: deflection_type + deflection_number: + type: str + visu_acl: ro + avm_data_type@instance: deflection_number + deflection_to_number: + type: str + visu_acl: ro + avm_data_type@instance: deflection_to_number + deflection_mode: + type: str + visu_acl: ro + avm_data_type@instance: deflection_mode + deflection_outgoing: + type: str + visu_acl: ro + avm_data_type@instance: deflection_outgoing + deflection_phonebook_id: + type: num + visu_acl: ro + avm_data_type@instance: deflection_phonebook_id + + wan: + connection_status: + type: str + visu_acl: ro + avm2_data_type@instance: wan_connection_status + connection_error: + type: str + visu_acl: ro + avm2_data_type@instance: wan_connection_error + is_connected: + type: bool + visu_acl: ro + avm2_data_type@instance: wan_is_connected + uptime: + type: num + visu_acl: ro + avm2_data_type@instance: wan_uptime + upstream: + type: num + visu_acl: ro + avm2_data_type@instance: wan_upstream + downstream: + type: num + visu_acl: ro + avm2_data_type@instance: wan_downstream + total_packets_sent: + type: num + visu_acl: ro + avm2_data_type@instance: wan_total_packets_sent + total_packets_received: + type: num + visu_acl: ro + avm2_data_type@instance: wan_total_packets_received + total_bytes_sent: + type: num + visu_acl: ro + avm2_data_type@instance: wan_total_bytes_sent + total_bytes_received: + type: num + visu_acl: ro + avm2_data_type@instance: wan_total_bytes_received + link: + type: bool + visu_acl: ro + avm2_data_type@instance: wan_link + reconnect: + type: bool + visu_acl: rw + enforce_updates: yes + + wlan: + wlan_1: + type: bool + visu_acl: rw + avm2_data_type@instance: wlanconfig # 2,4ghz + avm2_wlan_index@instance: 1 + wlan_1_ssid: + type: str + visu_acl: ro + avm2_data_type@instance: wlanconfig_ssid # 2,4ghz + avm2_wlan_index@instance: 1 + wlan_2: + type: bool + visu_acl: rw + avm2_data_type@instance: wlanconfig # 5 GHz + avm2_wlan_index@instance: 2 + wlan_2_ssid: + type: str + visu_acl: ro + avm2_data_type@instance: wlanconfig_ssid # 5 GHz + avm2_wlan_index@instance: 2 + wlan_gast: + type: bool + visu_acl: rw + avm2_data_type@instance: wlanconfig # Guest + avm2_wlan_index@instance: 3 + wlan_gast_ssid: + type: str + visu_acl: ro + avm2_data_type@instance: wlanconfig_ssid # Guest + avm2_wlan_index@instance: 3 + wlan_gast_tr: + type: num + visu_acl: rw + avm2_data_type@instance: wlan_guest_time_remaining # Guest + avm2_wlan_index@instance: 3 + + device: + avm_data_type@instance: network_device + type: bool + visu_acl: ro + + ip: + type: str + avm_data_type@instance: device_ip + visu_acl: ro + + connection_type: + type: str + avm_data_type@instance: device_connection_type + visu_acl: ro + + hostname: + type: str + avm_data_type@instance: device_hostname + visu_acl: ro + + smarthome_general: + name: + avm_data_type@instance: device_name + type: str + identifier: + avm_data_type@instance: device_id + type: str + productname: + avm_data_type@instance: product_name + type: str + manufacturer: + avm_data_type@instance: manufacturer + type: str + firmware_version: + avm_data_type@instance: fw_version + type: str + present: + avm_data_type@instance: connected + type: bool + functions: + avm_data_type@instance: device_functions + type: list + + smarthome_hkr: + current_temperature: + avm_data_type@instance: current_temperature + type: num + target_temperature: + avm_data_type@instance: target_temperature + avm_read_after_write@instance: 5 + type: num + # set_target_temperature: + # avm_data_type@instance: set_target_temperature + # type: num + comfort_temperature: + avm_data_type@instance: temperature_comfort + type: num + eco_temperature: + avm_data_type@instance: temperature_reduced + type: num + battery_low: + avm_data_type@instance: battery_low + type: bool + battery_level: + avm_data_type@instance: battery_level + type: num + window_open: + avm_data_type@instance: window_open + type: bool + summer_active: + avm_data_type@instance: summer_active + type: bool + holiday_active: + avm_data_type@instance: holiday_active + type: bool + errorcode: + avm_data_type@instance: errorcode + type: num + # set_window_open: + # avm_data_type@instance: set_window_open + # type: bool + windowopenactiveendtime: + avm_data_type@instance: windowopenactiveendtime + type: num + # set_hkr_boost: + # avm_data_type@instance: set_hkr_boost + # type: bool + hkr_boost: + avm_data_type@instance: hkr_boost + type: bool + boostactiveendtime: + avm_data_type@instance: boostactiveendtime + type: num + lock: + avm_data_type@instance: lock + type: bool + device_lock: + avm_data_type@instance: device_lock + type: bool + + smarthome_temperature_sensor: + temperatur: + avm_data_type@instance: current_temperature + type: num + temperature_offset: + avm_data_type@instance: temperature_offset + type: num + + smarthome_alert: + state: + avm_data_type@instance: alert + type: bool -item_attributes: NONE - # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) + smarthome_switch: + switch_state: + avm_data_type@instance: switch_state + type: bool + switch_toggle: + avm_data_type@instance: switch_toggle + type: bool + enforce_updates: yes -item_structs: NONE - # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) + smarthome_powermeter: + power: + avm_data_type@instance: power + type: num + energy: + avm_data_type@instance: energy + type: num + voltage: + avm_data_type@instance: voltage + type: num #item_attribute_prefixes: # Definition of item attributes that only have a common prefix (enter 'item_attribute_prefixes: NONE' or ommit this section, if section should be empty) diff --git a/webif/templates/index.html b/webif/templates/index.html index d2de1d7df..3a85bd147 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -72,60 +72,48 @@ - - - - - - - - - - + + - - + + - - + + - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + +
{{ _('model_name') }}{{ p._fritz_device.model_name }}
{{ _('GetInfo') }}{{ p._fritz_device._DeviceInfo['GetInfo'] }}
{{ _('safe_port') }}{{ p._fritz_device.safe_port }}{{ _('Items@Fritzdevice') }}{{ p._fritz_device._items }}
{{ _('GetSecurityPort') }}{{ p._fritz_device._DeviceInfo['GetSecurityPort'] }}{{ _('model_name') }}{{ p._fritz_device.model_name }}
{{ _('GetGenericHostEntry') }}{{ p._fritz_device.GetGenericHostEntry(1) }}{{ _('test') }}{{ p._fritz_device.client.InternetGatewayDevice.X_AVM_DE_TAM.GetInfo(NewIndex=1) }}
{{ _('WLANConfiguration[0]') }} {{ p._fritz_device.client.LANDevice.WLANConfiguration[0].GetInfo() }}
{{ _('_Hosts') }}{{ p._fritz_device._Hosts }}
{{ _('WANPPP') }}{{ p._fritz_device.client.WANConnectionDevice.WANPPPConnection.GetExternalIPAddress() }}{{ _('LANDevice') }}{{ p._fritz_device._data_cache['LANDevice'] }}
{{ _('WANIP') }}{{ p._fritz_device.client.WANConnectionDevice.WANIPConnection.GetInfo() }}{{ _('GetDefaultConnectionService') }}{{ p._fritz_device.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() }}
{{ _('update_devices') }}{{ p._fritz_home.update_devices() }}{{ _('client_igd.devices') }}{{ p._fritz_device.client_igd.WANDevice.WANCommonInterfaceConfig.GetAddonInfos() }}
{{ _('devices') }}{{ p._fritz_home._devices }}{{ _('GetPhonebook') }}{{ p._fritz_device.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetPhonebook(NewPhonebookID=0) }}
{{ _('aha_devices') }}{{ p.aha_devices }}{{ _('get contact by phonenumber') }}{{ p._fritz_device.get_contact_name_by_phone_number('01735631526#') }}
{{ _('get_device_by_ain') }}{{ p._fritz_home.get_device_by_ain('11963 0521424') }}{{ _('Device Log') }}{{ p._fritz_device.client.InternetGatewayDevice.DeviceInfo.GetInfo()['NewDeviceLog'] }}
{{ _('poll_aha') }}{{ p.poll_aha() }}{{ _('_monitoring_service._items') }}{{ p._monitoring_service._items }}
From fe10b76d8690c0746e4d65d7771ad8b7af504d27 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Fri, 11 Mar 2022 16:26:07 +0100 Subject: [PATCH 005/178] Update --- __init__.py | 969 +++++++++++++++++++++++++------------ plugin.yaml | 118 ++--- webif/templates/index.html | 46 +- 3 files changed, 739 insertions(+), 394 deletions(-) diff --git a/__init__.py b/__init__.py index 0d557eab3..31aba52b2 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### -# Copyright 2022- +# Copyright 2022- Michael Wenzel wenzel_michael@web.de ######################################################################### # This file is part of SmartHomeNG. # https://www.smarthomeNG.de @@ -242,8 +242,7 @@ 'deflection_enable'] _call_monitor_attributes_gen = ['call_event', - 'call_direction', - 'monitor_trigger'] + 'call_direction'] _call_monitor_attributes_in = ['is_call_incoming', 'last_caller_incoming', @@ -259,15 +258,16 @@ 'last_number_outgoing', 'last_called_number_outgoing'] +_call_monitor_attributes_trigger = ['monitor_trigger'] + _call_monitor_attributes = [*_call_monitor_attributes_gen, *_call_monitor_attributes_in, - *_call_monitor_attributes_out] + *_call_monitor_attributes_out, + *_call_monitor_attributes_trigger] _call_duration_attributes = ['call_duration_incoming', 'call_duration_outgoing'] -_trigger_attributes = ['monitor_trigger'] - _wan_connection_attributes = ['wan_connection_status', 'wan_connection_error', 'wan_is_connected', @@ -302,7 +302,7 @@ _host_child_attributes = ['device_ip', 'device_connection_type', - 'device_hostname' + 'device_hostname', 'connection_status'] _host_attributes = [*_host_attribute, @@ -372,6 +372,12 @@ def __init__(self, sh): self.logger.info('Init AVM2 Plugin') + # Enable / Disable debug log generation depending on log level + if self.logger.isEnabledFor(logging.DEBUG): + self.debug_log = True + else: + self.debug_log = False + # Get/Define Properties _host = self.get_parameter_value('host') _port = self.get_parameter_value('port') @@ -388,19 +394,41 @@ def __init__(self, sh): urllib3.disable_warnings() # init FritzDevice - self._fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, _call_monitor_incoming_filter, self) + try: + self._fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, _call_monitor_incoming_filter, self) + except Exception as e: + self.logger.warning(f"Error {e} establishing connection to Fritzdevice via AHA-HTTP-Interface.") + self._fritz_device = None + else: + self.logger.debug(f"Connection to FritzDevice established.") # init FritzHome if self._aha_http_interface: - self._fritz_home = FritzHome(_host, ssl, _verify, _username, _passwort, self) + try: + self._fritz_home = FritzHome(_host, ssl, _verify, _username, _passwort, self) + except Exception as e: + self.logger.warning(f"Error {e} establishing connection to Fritzdevice via AHA-HTTP-Interface.") + self._fritz_home = None + else: + self.logger.debug(f"Connection to FritzDevice via AHA-HTTP-Interface established.") + else: + self._fritz_home = None # init Call Monitor - if self._call_monitor: - self._monitoring_service = Callmonitor(_host, - 1012, - self._fritz_device.get_contact_name_by_phone_number, - _call_monitor_incoming_filter, - self) + if self._call_monitor and self._fritz_device.connected: + try: + self._monitoring_service = Callmonitor(_host, + 1012, + self._fritz_device.get_contact_name_by_phone_number, + _call_monitor_incoming_filter, + self) + except Exception as e: + self.logger.warning(f"Error {e} establishing connection to Fritzdevice CallMonitor.") + self._monitoring_service = None + else: + self.logger.debug(f"Connection to FritzDevice CallMonitor established.") + else: + self._monitoring_service = None # init WebIF self.init_webinterface(WebInterface) @@ -410,13 +438,14 @@ def run(self): Run method for the plugin """ self.logger.debug("Run method called") - self.scheduler_add('poll_tr064', self._fritz_device.update_items, prio=5, cycle=self._cycle, offset=4) + self.scheduler_add('poll_tr064', self._fritz_device.update_item_values, prio=5, cycle=self._cycle, offset=4) if self._aha_http_interface: # add scheduler for updating items self.scheduler_add('poll_aha', self._fritz_home.update_items, prio=5, cycle=self._cycle, offset=2) # add scheduler for checking validity of session id self.scheduler_add('check_sid', self._fritz_home.check_sid, prio=5, cycle=900, offset=30) - + if self._monitoring_service: + self._monitoring_service.set_callmonitor_item_values_initially() self.alive = True def stop(self): @@ -429,9 +458,8 @@ def stop(self): self.scheduler_remove('poll_aha') self.scheduler_remove('check_sid') self._fritz_home.logout() - if self._call_monitor: + if self._monitoring_service: self._monitoring_service.disconnect() - self.alive = False def parse_item(self, item): @@ -447,6 +475,7 @@ def parse_item(self, item): with the item, caller, source and dest as arguments and in case of the knx plugin the value can be sent to the knx with a knx write function within the knx plugin. """ + if self.has_iattr(item.conf, 'avm2_data_type'): self.logger.debug(f"parse item: {item}") @@ -454,132 +483,30 @@ def parse_item(self, item): avm_data_type = self.get_iattr_value(item.conf, 'avm2_data_type') # handle items specific to call monitor - if avm_data_type in _call_monitor_attributes: - # initially - if item empty - get data from calllist - if avm_data_type == 'last_caller_incoming' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - if 'Name' in element: - item(element['Name'], self.get_shortname()) - else: - item(element['Caller'], self.get_shortname()) - break - elif avm_data_type == 'last_number_incoming' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - if 'Caller' in element: - item(element['Caller'], self.get_shortname()) - else: - item("", self.get_shortname()) - break - elif avm_data_type == 'last_called_number_incoming' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - item(element['CalledNumber'], self.get_shortname()) - break - elif avm_data_type == 'last_call_date_incoming' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - date = str(element['Date']) - date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self.get_shortname()) - break - elif avm_data_type == 'call_event_incoming' and item() == '': - item('disconnect', self.get_shortname()) - elif avm_data_type == 'is_call_incoming' and item() == '': - item(0, self.get_shortname()) - elif avm_data_type == 'last_caller_outgoing' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - if 'Name' in element: - item(element['Name'], self.get_shortname()) - else: - item(element['Called'], self.get_shortname()) - break - elif avm_data_type == 'last_number_outgoing' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - if 'Caller' in element: - item(''.join(filter(lambda x: x.isdigit(), element['Caller'])), self.get_shortname()) - else: - item("", self.get_shortname()) - break - elif avm_data_type == 'last_called_number_outgoing' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - item(element['Called'], self.get_shortname()) - break - elif avm_data_type == 'last_call_date_outgoing' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - date = str(element['Date']) - date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self.get_shortname()) - break - elif avm_data_type == 'call_event_outgoing' and item() == '': - item('disconnect', self.get_shortname()) - elif avm_data_type == 'is_call_outgoing' and item() == '': - item(0, self.get_shortname()) - elif avm_data_type == 'call_event' and item() == '': - item('disconnect', self.get_shortname()) - elif avm_data_type == 'call_direction' and item() == '': - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - item('incoming', self.get_shortname()) - break - if element['Type'] in ['3', '4']: - item('outgoing', self.get_shortname()) - break - if self._call_monitor: - if self._monitoring_service is not None: - self._monitoring_service.register_item(item, avm_data_type) - - # handle items specific to call-duration - elif avm_data_type in _call_duration_attributes: - # initially get data from calllist - if avm_data_type == 'call_duration_incoming' and item() == 0: - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - duration = element['Duration'] - duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self.get_shortname()) - break - elif avm_data_type == 'call_duration_outgoing' and item() == 0: - if self._fritz_device.get_calllist_from_cache() is not None: - for element in self._fritz_device.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - duration = element['Duration'] - duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self.get_shortname()) - break - if self._call_monitor: - if self._monitoring_service is not None: - self._monitoring_service.register_item(item, avm_data_type) - - # handle smarthome items using aha-interface (old / new) + if avm_data_type in (_call_monitor_attributes + _call_duration_attributes): + if self._monitoring_service: + self._monitoring_service.register_item(item, avm_data_type) + else: + self.logger.warning(f"Items with avm attribute found, which needs Call-Monitoring-Service. This is not available/enabled for that plugin; Item will be ignored.") + + # handle smarthome items using aha-interface (new) elif avm_data_type in _aha_attributes: - if self._aha_http_interface: + if self._fritz_home: self._fritz_home.register_item(item, avm_data_type) else: - self.logger.warning(f"Items with avm attribute found, which needs aha-http-interface. This is not enabled for that plugin; Item will be ignored.") + self.logger.warning(f"Items with avm attribute found, which needs aha-http-interface. This is not available/enabled for that plugin; Item will be ignored.") # handle items updated by tr-064 interface elif avm_data_type in _tr064_attributes: - self._fritz_device.register_item(item, avm_data_type) + if self._fritz_device: + self._fritz_device.register_item(item, avm_data_type) + else: + self.logger.warning(f"Items with avm attribute found, which needs tr064 interface. This is not available/enabled; Item will be ignored.") + # handle anything else else: - self.logger.warning(f"avm_data_type={avm_data_type} if item={item.id()} unknown. Item will be ignored.") + self.logger.warning(f"Item={item.id()} has unknown avm_data_type={avm_data_type}. Item will be ignored.") - # items which can be changed outside the plugin context and need to be submitted to the FritzDevice + # items which can be changed outside the plugin context if avm_data_type in (_avm_rw_attributes + _aha_wo_attributes + _aha_rw_attributes + _homeauto_rw_attributes): return self.update_item @@ -604,15 +531,30 @@ def update_item(self, item, caller=None, source=None, dest=None): :param source: if given it represents the source :param dest: if given it represents the dest """ - if self.alive and caller != self.get_shortname(): - # code to execute if the plugin is not stopped - # and only, if the item has not been changed by this this plugin: - self.logger.info(f"Update item: {item.property.path}, item has been changed outside this plugin") - if self.has_iattr(item.conf, 'foo_itemtag'): - self.logger.debug( - f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}") - pass + if self.alive and caller != self.get_shortname() and item in self._items: + + avm_data_type = self._items[item][0] + + readafterwrite = None + if self.has_iattr(item.conf, 'avm2_read_after_write'): + readafterwrite = int(self.get_iattr_value(item.conf, 'avm2_read_after_write')) + if self.debug_log: + self.logger.debug(f'Attempting read after write for item: {item.id()}, avm_data_type: {avm_data_type}, delay: {readafterwrite}s') + + self.logger.info(f"Update item: {item.property.path} with avm_data_type={avm_data_type} item has been changed outside this plugin") + + # handle items updated by tr-064 interface + if avm_data_type in _tr064_attributes: + if self.debug_log: + self.logger.debug(f"Updated item={item.id()} with avm_data_type={avm_data_type} identified as part of '_tr064_attributes'") + self._fritz_device.handle_updated_item(item, avm_data_type, readafterwrite) + + # handle items updated by _aha_attributes + elif avm_data_type in _aha_attributes and self._fritz_home: + if self.debug_log: + self.logger.debug(f"Updated item={item.id()} with avm_data_type={avm_data_type} identified as part of '_aha_attributes'") + self._fritz_home.handle_updated_item(item, avm_data_type, readafterwrite) @property def get_callmonitor(self): @@ -624,6 +566,9 @@ def monitoring_service_connect(self): def monitoring_service_disconnect(self): self._monitoring_service.disconnect() + def get_calllist(self): + return self._fritz_device.get_calllist_from_cache() + class FritzDevice: """ @@ -691,16 +636,25 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self._timeout = 10 self._items = {} self._session = requests.Session() - - self.items = Items.get_instance() + self._connected = False # get client objects self.client = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=FRITZ_TR64_DESC_FILE, plugin_instance=plugin_instance) self.client_igd = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=FRITZ_IGD_DESC_FILE, plugin_instance=plugin_instance) # get GetDefaultConnectionService - self._data_cache['InternetGatewayDevice'] = {'DeviceInfo': {'GetInfo': {}, 'GetSecurityPort': {}}, 'Layer3Forwarding': {}} - self._data_cache['InternetGatewayDevice']['Layer3Forwarding']['GetDefaultConnectionService'] = self.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() + _default_connection_service = self.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() + if not isinstance(_default_connection_service, int): + self._data_cache['InternetGatewayDevice'] = {'DeviceInfo': {'GetInfo': {}, 'GetSecurityPort': {}}, 'Layer3Forwarding': {}} + self._data_cache['InternetGatewayDevice']['Layer3Forwarding']['GetDefaultConnectionService'] = _default_connection_service + self._connected = True + self._plugin_instance.logger.debug(f"FritzDevice alive") + else: + self._connected = False + if str(_default_connection_service) == '401': + self._plugin_instance.logger.error(f"Unable to connect to FritzDevice. Check user and password.") + else: + self._plugin_instance.logger.error(f"Unable to determine _default_connection_service. ErrorCode {_default_connection_service}.") def register_item(self, item, avm_data_type: str): @@ -735,6 +689,27 @@ def register_item(self, item, avm_data_type: str): else: self._items[item] = (avm_data_type, None) + def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): + + # ToDo: Implement ReadafterWrite + + index = self._items[item][1] + + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Item {item.id()} with avm_data_type={avm_data_type} has changed for index {index}; New value={bool(item())}") + + # handle wlan items + if avm_data_type == 'wlanconfig': + self.client.LANDevice.WLANConfiguration[index].SetEnable(NewEnable=bool(item())) + + # handle tam items + elif avm_data_type == 'tam': + self.client.InternetGatewayDevice.X_AVM_DE_TAM.SetEnable(NewIndex=index, NewEnable=bool(item())) + + # handle deflection_enable + elif avm_data_type == 'deflection_enable': + self.client.InternetGatewayDevice.X_AVM_DE_OnTel.SetDeflectionEnable(NewDeflectionId=index, NewEnable=bool(item())) + def _build_url(self) -> str: """ Builds a request url @@ -837,7 +812,7 @@ def _get_mac(self, item) -> str: mac = None for i in range(2): - attribute = 'avm_mac' + attribute = 'avm2_mac' attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" mac = self._plugin_instance.get_iattr_value(item.conf, attribute) @@ -849,7 +824,7 @@ def _get_mac(self, item) -> str: else: item = item.return_parent() - return mac + return str(mac) # -------------------------------------- # Properties of FritzDevice @@ -919,6 +894,10 @@ def get_password(self): """ return self._password + @property + def connected(self): + return self._connected + @property def default_connection_service(self): return self._data_cache['InternetGatewayDevice']['Layer3Forwarding']['GetDefaultConnectionService']['NewDefaultConnectionService'] @@ -956,59 +935,65 @@ def safe_port(self): # ---------------------------------- # TBD # ---------------------------------- - def update_items(self): + def update_item_values(self): - for item in self._items: - avm_data_type = self._items[item][0] - index = self._items[item][1] - self._plugin_instance.logger.debug(f"FritzDevice: _update_items called: item={item} with avm_data_type={avm_data_type} and index={index}") - - data = self._poll_fritz_device(avm_data_type, index) - - if data is not None: - item(data, self._plugin_instance.get_shortname()) + if self._connected: + for item in self._items: + avm_data_type = self._items[item][0] + index = self._items[item][1] + self._plugin_instance.logger.debug(f"FritzDevice: _update_items called: item={item} with avm_data_type={avm_data_type} and index={index}") + data = self._poll_fritz_device(avm_data_type, index) + if data is not None: + item(data, self._plugin_instance.get_shortname()) + else: + self._plugin_instance.logger.warning(f"FritzDevice not connected. No update of item values possible.") - def _poll_fritz_device(self, avm_data_type: str, index: int): + def _poll_fritz_device(self, avm_data_type: str, index: Union[int, str]): link_ppp = { - 'wan_connection_status': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewConnectionStatus'), - 'wan_connection_error': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewLastConnectionError'), - 'wan_is_connected': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewConnectionStatus'), - 'wan_uptime': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', 'NewUptime'), - 'wan_ip': ('WANConnectionDevice', 'WANPPPConnection', 'GetExternalIPAddress', 'NewExternalIPAddress'), + 'wan_connection_status': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', None, 'NewConnectionStatus'), + 'wan_connection_error': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', None, 'NewLastConnectionError'), + 'wan_is_connected': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', None, 'NewConnectionStatus'), + 'wan_uptime': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', None, 'NewUptime'), + 'wan_ip': ('WANConnectionDevice', 'WANPPPConnection', 'GetExternalIPAddress', None, 'NewExternalIPAddress'), } link_ip = { - 'wan_connection_status': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewConnectionStatus'), - 'wan_connection_error': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewLastConnectionError'), - 'wan_is_connected': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewConnectionStatus'), - 'wan_uptime': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', 'NewUptime'), - 'wan_ip': ('WANConnectionDevice', 'WANIPConnection', 'GetExternalIPAddress', 'NewExternalIPAddress'), + 'wan_connection_status': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', None, 'NewConnectionStatus'), + 'wan_connection_error': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', None, 'NewLastConnectionError'), + 'wan_is_connected': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', None, 'NewConnectionStatus'), + 'wan_uptime': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', None, 'NewUptime'), + 'wan_ip': ('WANConnectionDevice', 'WANIPConnection', 'GetExternalIPAddress', None, 'NewExternalIPAddress'), } link = { - 'uptime': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewUpTime'), - 'serial_number': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewSerialNumber'), - 'software_version': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewSoftwareVersion'), - 'hardware_version': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', 'NewHardwareVersion'), - 'myfritz_status': ('InternetGatewayDevice', 'X_AVM_DE_MyFritz', 'GetInfo', 'NewEnabled'), - 'tam': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'GetInfo', 'NewEnable'), - 'tam_name': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'GetInfo', 'NewName'), - 'wan_upstream': ('WANDevice', 'WANDSLInterfaceConfig', 'GetInfo', 'NewUpstreamCurrRate'), - 'wan_downstream': ('WANDevice', 'WANDSLInterfaceConfig', 'GetInfo', 'NewDownstreamCurrRate'), - 'wan_total_packets_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalPacketsSent', 'NewTotalPacketsSent'), - 'wan_total_packets_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalPacketsReceived', 'NewTotalPacketsReceived'), - 'wan_current_packets_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewPacketSendRate'), - 'wan_current_packets_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewPacketReceiveRate'), - 'wan_total_bytes_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalBytesSent', 'NewTotalBytesSent'), - 'wan_total_bytes_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalBytesReceived', 'NewTotalBytesReceived'), - 'wan_current_bytes_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewByteSendRate'), - 'wan_current_bytes_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', 'NewByteReceiveRate'), - 'wan_link': ('WANDevice', 'WANCommonInterfaceConfig', 'GetCommonLinkProperties', 'NewPhysicalLinkStatus'), - 'wlanconfig': ('LANDevice', 'WLANConfiguration', 'GetInfo', 'NewEnable'), - 'wlanconfig_ssid': ('LANDevice', 'WLANConfiguration', 'GetInfo', 'NewSSID'), - 'wlan_guest_time_remaining': ('LANDevice', 'WLANConfiguration', 'X_AVM_DE_GetWLANExtInfo', 'NewX_AVM_DE_TimeRemain'), - # '':: ('', '', '', ''), + # 'avm_data_type': ('Device', 'Service', 'Action', 'In_Argument', 'Out_Argument'), + 'uptime': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', None, 'NewUpTime'), + 'serial_number': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', None, 'NewSerialNumber'), + 'software_version': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', None, 'NewSoftwareVersion'), + 'hardware_version': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', None, 'NewHardwareVersion'), + 'myfritz_status': ('InternetGatewayDevice', 'X_AVM_DE_MyFritz', 'GetInfo', None, 'NewEnabled'), + 'tam': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'GetInfo', 'NewIndex', 'NewEnable'), + 'tam_name': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'GetInfo', 'NewIndex', 'NewName'), + 'wan_upstream': ('WANDevice', 'WANDSLInterfaceConfig', 'GetInfo', None, 'NewUpstreamCurrRate'), + 'wan_downstream': ('WANDevice', 'WANDSLInterfaceConfig', 'GetInfo', None, 'NewDownstreamCurrRate'), + 'wan_total_packets_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalPacketsSent', None, 'NewTotalPacketsSent'), + 'wan_total_packets_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalPacketsReceived', None, 'NewTotalPacketsReceived'), + 'wan_current_packets_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', None, 'NewPacketSendRate'), + 'wan_current_packets_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', None, 'NewPacketReceiveRate'), + 'wan_total_bytes_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalBytesSent', None, 'NewTotalBytesSent'), + 'wan_total_bytes_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetTotalBytesReceived', None, 'NewTotalBytesReceived'), + 'wan_current_bytes_sent': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', None, 'NewByteSendRate'), + 'wan_current_bytes_received': ('WANDevice', 'WANCommonInterfaceConfig', 'GetAddonInfos', None, 'NewByteReceiveRate'), + 'wan_link': ('WANDevice', 'WANCommonInterfaceConfig', 'GetCommonLinkProperties', None, 'NewPhysicalLinkStatus'), + 'wlanconfig': ('LANDevice', 'WLANConfiguration', 'GetInfo', 'WLAN', 'NewEnable'), + 'wlanconfig_ssid': ('LANDevice', 'WLANConfiguration', 'GetInfo', 'WLAN', 'NewSSID'), + 'wlan_guest_time_remaining': ('LANDevice', 'WLANConfiguration', 'X_AVM_DE_GetWLANExtInfo', 'WLAN', 'NewX_AVM_DE_TimeRemain'), + 'device_ip': ('LANDevice', 'Hosts', 'GetSpecificHostEntry', 'NewMACAddress', 'NewIPAddress'), + 'device_connection_type': ('LANDevice', 'Hosts', 'GetSpecificHostEntry', 'NewMACAddress', 'NewInterfaceType'), + 'device_hostname': ('LANDevice', 'Hosts', 'GetSpecificHostEntry', 'NewMACAddress', 'NewHostName'), + 'network_device': ('LANDevice', 'Hosts', 'GetSpecificHostEntry', 'NewMACAddress', 'NewActive'), + 'connection_status': ('LANDevice', 'Hosts', 'GetSpecificHostEntry', 'NewMACAddress', 'NewActive'), } # Create link dict depending on connection type @@ -1027,7 +1012,7 @@ def _poll_fritz_device(self, avm_data_type: str, index: int): return # gather data - data = self._update_data_cache(client, link[avm_data_type][0], link[avm_data_type][1], link[avm_data_type][2], link[avm_data_type][3], index) + data = self._update_data_cache(client, link[avm_data_type][0], link[avm_data_type][1], link[avm_data_type][2], link[avm_data_type][3], link[avm_data_type][4], index) # correct data if avm_data_type == 'wan_is_connected': @@ -1038,8 +1023,8 @@ def _poll_fritz_device(self, avm_data_type: str, index: int): # return result return data - def _update_data_cache(self, client: str, device: str, service: str, action: str, argument: str = None, index: int = None) -> dict: - # self._plugin_instance.logger.debug(f"_data_cache called with device={device}, service={service}, action={action}, argument={argument} index={index}") + def _update_data_cache(self, client: str, device: str, service: str, action: str, in_argument: str = None, out_argument: str = None, index: int = None) -> dict: + self._plugin_instance.logger.debug(f"_update_data_cache called with device={device}, service={service}, action={action}, in_argument={in_argument}, out_argument={out_argument}, index={index}") if device not in self._data_cache: self._data_cache[device] = {} if service not in self._data_cache[device]: @@ -1048,21 +1033,23 @@ def _update_data_cache(self, client: str, device: str, service: str, action: str self._data_cache[device][service][action] = {} if index is None: - if not self._data_cache[device][service][action]: - self._data_cache[device][service][action] = eval(f"self.{client}.{device}.{service}.{action}()") - if not argument: + self._data_cache[device][service][action] = eval(f"self.{client}.{device}.{service}.{action}()") + if not out_argument: return self._data_cache[device][service][action] else: - return self._data_cache[device][service][action][argument] + return self._data_cache[device][service][action][out_argument] else: - if not self._data_cache[device][service][action]: - self._data_cache[device][service][action] = {} if index not in self._data_cache[device][service][action]: + self._data_cache[device][service][action][index] = {} + + if in_argument == 'WLAN': self._data_cache[device][service][action][index] = eval(f"self.{client}.{device}.{service}[{index}].{action}()") - if not argument: + else: + self._data_cache[device][service][action][index] = eval(f"self.{client}.{device}.{service}.{action}({in_argument}='{index}')") + if not out_argument: return self._data_cache[device][service][action][index] else: - return self._data_cache[device][service][action][index][argument] + return self._data_cache[device][service][action][index][out_argument] def _request(self, url: str, timeout: int, verify: bool): request = requests.get(url, timeout=timeout, verify=verify) @@ -1234,7 +1221,10 @@ def get_calllist(self, filter_incoming: str = '') -> list: """ """ - calllist_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetCallList()['NewCallListURL'] + calllist_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetCallList() + if isinstance(calllist_url, int): + return [] + calllist_url = calllist_url['NewCallListURL'] calllist = self._request_response_to_xml(self._request(calllist_url, self._timeout, self._verify)) if calllist: @@ -1412,7 +1402,7 @@ def __init__(self, username, password, verify, base_url='https://192.168.178.1:4 self.description_file = description_file self.devices = {} - self._plugin_instance.logger.debug(f"Init Client for description file={self.description_file}") + self._plugin_instance.logger.debug(f"Init Client for description at url={base_url} with description_file={self.description_file} and plugin_instance={plugin_instance}") def __getattr__(self, name): if name not in self.devices: @@ -1678,33 +1668,99 @@ def __init__(self, host, ssl, verify, user, password, plugin_instance): self._items = dict() self._aha_devices = dict() self._session = requests.Session() + self._connected = False + + # Login + self.login() def register_item(self, item, avm_data_type: str): # handle aha items avm_ain = self._get_item_ain(item) if avm_ain is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_ain' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined avm_ain={avm_ain} found; append to list.") self._items[item] = (avm_data_type, avm_ain) else: self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_ain' is not defined; Item will be ignored.") + def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): + + # ToDo: Implement ReadafterWrite + + # get AIN + ain = self._items[item][1] + + # handle hkr window_open + if avm_data_type in ['set_window_open', 'window_open']: + # set windows open for 12h or closed + seconds = 12*60*60 if bool(item()) else 0 + self.set_window_open(ain, seconds) + + # handle target temperature + elif avm_data_type in ['set_target_temperature', 'target_temperature']: + # + self.set_target_temperature(ain, float(item())) + + # handle hkr boost mode + elif avm_data_type in ['set_hkr_boost', 'hkr_boost']: + # set boost for 12h or off + seconds = 12 * 60 * 60 if bool(item()) else 0 + self.set_boost(ain, seconds) + + # handle light on/off + elif avm_data_type in ['set_simpleonoff', 'simpleonoff']: + if bool(item()): + self.set_state_on(ain) + else: + self.set_state_off(ain) + + # handle level + elif avm_data_type in ['set_level', 'level']: + self.set_level(ain, item()) + + # handle level percentage + elif avm_data_type in ['set_levelpercentage', 'levelpercentage']: + self.set_level_percentage(ain, item()) + + # handle socket switch/relais on/off + elif avm_data_type == 'switch_state': + if bool(item()): + self.set_switch_state_on(ain) + else: + self.set_switch_state_off(ain) + + # handle socket switch/relais toggle + elif avm_data_type == 'switch_toggle': + self.set_switch_state_toggle(ain) + + # handle set hue + elif avm_data_type in ['set_hue', 'hue']: + # Full RGB hue will be supported by Fritzbox approximately from Q2 2022 on: + # Currently, only use default RGB colors that are supported by default (getcolordefaults) + # These default colors have given saturation values. + self.set_color_discrete(ain, int(item()), duration=0) + self._plugin_instance.logger.info(f"Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") + + # handle set saturation + elif avm_data_type in ['set_saturation', 'saturation']: + self._plugin_instance.logger.info(f"Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") + + elif avm_data_type in ['set_colortemperature', 'colortemperature']: + self._plugin_instance.set_color_temp(ain, item(), 8) + + else: + self._plugin_instance.logger.error(f"{avm_data_type} is not defined to be updated.") + def _get_item_ain(self, item) -> str: """ Get AIN of device from item.conf """ - ain_device = None lookup_item = item for i in range(2): attribute = 'ain' - attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" - ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute) - if ain_device is not None: - break - ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute_w_instance) if ain_device is not None: break else: @@ -1718,83 +1774,138 @@ def _get_item_ain(self, item) -> str: lookup_item = item for i in range(2): attribute = 'avm2_ain' - attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" - - ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute_w_instance) - if ain_device is not None: - break ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute) if ain_device is not None: break else: lookup_item = lookup_item.return_parent() - if ain_device is not None: + if ain_device is None: self._plugin_instance.logger.error('Device AIN is not defined or instance not given') - return str(ain_device) + return ain_device def _poll_aha(self): self._plugin_instance.logger.debug(f'Starting AHA update loop for instance {self._plugin_instance.get_instance_name()}.') - # update devices - self.update_devices() + _link_base = {'device_id': 'identifier', + 'fw_version': 'fw_version', + 'product_name': 'productname', + 'manufacturer': 'manufacturer', + 'connected': 'present', + 'device_name': 'name'} + + _link_thermostat = {'current_temperature': 'actual_temperature', + 'target_temperature': 'target_temperature', + 'temperature_reduced': 'eco_temperature', + 'temperature_comfort': 'comfort_temperature', + 'device_lock': 'device_lock', + 'lock': 'lock', + 'errorcode': 'error_code', + 'battery_low': 'battery_low', + 'battery_level': 'battery_level', + 'window_open': 'window_open', + 'summer_active': 'summer_active', + 'holiday_active': 'holiday_active', + 'nextchange_endperiod': 'nextchange_endperiod', + 'nextchange_temperature': 'nextchange_temperature', + 'hkr_boost': 'boost_active', + 'windowopenactiveendtime': 'windowopenactiveendtime', + 'boostactiveendtime': 'boostactiveendtime'} + + _link_temperature = {'temperature_offset': 'offset', + 'current_temperature': 'temperature', + 'humidity': 'rel_humidity'} + + _link_button = {'tx_busy': 'txbusy', + 'battery_low': 'batterylow', + 'battery_level': 'battery_level', + 'switch_state': 'state', + 'switch_mode': 'mode', + 'button_id': 'identifier', + 'button_name': 'name', + 'last_pressed': 'last_pressed'} + + _link_alarm = {'alarm': 'alert_state', + 'last_alert_chgtimestamp': 'last_alert_chgtimestamp'} + + # ToDo: split light bulb and level + _link_lightbulb = {'state': 'state', + 'level': 'level', + 'hue': 'hue', + 'saturation': 'saturation', + 'unmapped_hue': 'unmapped_hue', + 'unmapped_saturation': 'unmapped_saturation', + 'colortemperature': 'color_temp', + 'current_mode': 'color_mode', + 'supported_modes': 'supported_color_mode'} + + _link_powermeter = {'power': 'power', + 'energy': 'energy', + 'voltage': 'voltage'} + + _link_switch = {'switch_state': 'switch_state', + 'switch_mode': 'switch_mode', + 'lock': 'lock'} - # get device dict _device_dict = self.get_devices_as_dict() for ain in _device_dict: if not self._aha_devices.get(ain): self._aha_devices[ain] = {} - self._aha_devices[ain]['connected_to_item'] = False - self._aha_devices[ain]['switch'] = {} - self._aha_devices[ain]['temperature_sensor'] = {} - self._aha_devices[ain]['thermostat'] = {} - self._aha_devices[ain]['alarm'] = {} - - self._aha_devices[ain]['online'] = bool(_device_dict[ain].present) - self._aha_devices[ain]['name'] = _device_dict[ain].name - self._aha_devices[ain]['productname'] = _device_dict[ain].productname - self._aha_devices[ain]['manufacturer'] = _device_dict[ain].manufacturer - self._aha_devices[ain]['fw_version'] = _device_dict[ain].fw_version - self._aha_devices[ain]['lock'] = bool(_device_dict[ain].lock) - self._aha_devices[ain]['device_lock'] = bool(_device_dict[ain].device_lock) - self._aha_devices[ain]['functions'] = [] + self._aha_devices[ain]['device_functions'] = [] - if _device_dict[ain].has_thermostat: - self._aha_devices[ain]['functions'].append('thermostat') - self._aha_devices[ain]['thermostat']['actual_temperature'] = _device_dict[ain].actual_temperature - self._aha_devices[ain]['thermostat']['target_temperature'] = _device_dict[ain].target_temperature - self._aha_devices[ain]['thermostat']['comfort_temperature'] = _device_dict[ain].comfort_temperature - self._aha_devices[ain]['thermostat']['eco_temperature'] = _device_dict[ain].eco_temperature - self._aha_devices[ain]['thermostat']['battery_low'] = bool(_device_dict[ain].battery_low) - self._aha_devices[ain]['thermostat']['battery_level'] = _device_dict[ain].battery_level - self._aha_devices[ain]['thermostat']['window_open'] = bool(_device_dict[ain].window_open) - self._aha_devices[ain]['thermostat']['summer_active'] = bool(_device_dict[ain].summer_active) - self._aha_devices[ain]['thermostat']['holiday_active'] = bool(_device_dict[ain].holiday_active) + for entry in _link_base: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_base[entry]}") - if _device_dict[ain].has_switch: - self._aha_devices[ain]['functions'].append('switch') - self._aha_devices[ain]['switch']['switch_state'] = bool(_device_dict[ain].switch_state) - self._aha_devices[ain]['switch']['power'] = _device_dict[ain].power - self._aha_devices[ain]['switch']['energy'] = _device_dict[ain].energy - self._aha_devices[ain]['switch']['voltage'] = _device_dict[ain].voltage + if _device_dict[ain].has_thermostat: + self._aha_devices[ain]['device_functions'].append('thermostat') + for entry in _link_thermostat: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_thermostat[entry]}") if _device_dict[ain].has_temperature_sensor: - self._aha_devices[ain]['functions'].append('temperature_sensor') - self._aha_devices[ain]['temperature_sensor']['temperature'] = _device_dict[ain].temperature - self._aha_devices[ain]['temperature_sensor']['offset'] = _device_dict[ain].offset + self._aha_devices[ain]['device_functions'].append('temperature_sensor') + for entry in _link_temperature: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_temperature[entry]}") + + if _device_dict[ain].has_button: + self._aha_devices[ain]['device_functions'].append('button') + for entry in _link_button: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_button[entry]}") + + if _device_dict[ain].has_button: + self._aha_devices[ain]['device_functions'].append('alarm') + for entry in _link_alarm: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_alarm[entry]}") + + if _device_dict[ain].has_lightbulb: + self._aha_devices[ain]['device_functions'].append('color_device') + for entry in _link_lightbulb: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_lightbulb[entry]}") + + if _device_dict[ain].has_repeater: + self._aha_devices[ain]['device_functions'].append('repeater') + + if _device_dict[ain].has_powermeter: + self._aha_devices[ain]['device_functions'].append('powermeter') + for entry in _link_powermeter: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_powermeter[entry]}") - if _device_dict[ain].has_alarm: - self._aha_devices[ain]['functions'].append('alarm') - self._aha_devices[ain]['alarm']['alert_state'] = bool(_device_dict[ain].alert_state) + if _device_dict[ain].has_switch: + self._aha_devices[ain]['device_functions'].append('powermeter') + for entry in _link_switch: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_switch[entry]}") def update_items(self): """ Update smarthome item values using information from dict '_aha_devices' """ - # first poll current data + if not self._logged_in: + self._plugin_instance.logger.warning(f"No connection to FritzDevice via AHA-HTTP-Interface. No update of item values possible.") + return + + # first update data self._poll_aha() for item in self._items: @@ -1803,7 +1914,7 @@ def update_items(self): _ain = self._items[item][1] # get device sub-dict from dict - device = self._items.get(_ain, None) + device = self._aha_devices.get(_ain, None) if device is not None: # Attributes that are write only commands with no corresponding read commands are excluded from status updates via update black list: @@ -1817,9 +1928,9 @@ def update_items(self): if _avm_data_type in device: item(device[_avm_data_type], self._plugin_instance.get_shortname()) else: - self._plugin_instance.logger.warning(f'Attribute <{_avm_data_type}> at device <{_ain}> to be set to Item <{item}> is not available.') + self._plugin_instance.logger.warning(f'Attribute={_avm_data_type} at device with AIN={_ain} to be set to Item={item.id()} is not available.') else: - self._plugin_instance.logger.warning(f'No values for item {item.id()} with AIN {_ain} available.') + self._plugin_instance.logger.warning(f'No values for item={item.id()} at device with AIN={_ain} available.') def _request(self, url: str, params: dict = None, timeout: int = 10, result: str = 'text') -> Union[str, dict]: """Send a request with parameters. @@ -1916,9 +2027,6 @@ def _calculate_pbkdf2_response(challenge: str, password: str) -> str: def _aha_request(self, cmd, ain=None, param=None, rf='str'): """Send an AHA request.""" - if not self._logged_in: - self.login() - url = self.get_prefixed_host() + self._homeauto_route params = {"switchcmd": cmd, "sid": self._sid} if param: @@ -1962,11 +2070,11 @@ def login(self): (sid2, challenge, blocktime) = self._login_request(username=self._user, challenge_response=challenge_response) if sid2 == "0000000000000000": self._plugin_instance.logger.warning(f"login failed {sid2}") - self._plugin_instance.logger.error(f"LoginError for {self._user}") + self._plugin_instance.logger.error(f"LoginError for User {self._user}") return self._sid = sid2 except Exception as e: - self._plugin_instance.logger.error(f"LoginError {e} occurred for {self._user}") + self._plugin_instance.logger.error(f"LoginError {e} occurred for User {self._user}") else: self._logged_in = True @@ -2053,8 +2161,8 @@ def get_devices(self): def get_devices_as_dict(self): """Get the list of all known devices.""" - if self._devices is None: - self.update_devices() + # if self._devices is None: + self.update_devices() return self._devices def get_device_by_ain(self, ain): @@ -2069,6 +2177,8 @@ def get_device_name(self, ain): """Get the device name.""" return self._aha_request("getswitchname", ain=ain) + # switch-related commands + def get_switch_state(self, ain): """Get the switch state.""" return self._aha_request("getswitchstate", ain=ain, rf='bool') @@ -2093,6 +2203,8 @@ def get_switch_energy(self, ain): """Get the switch energy.""" return self._aha_request("getswitchenergy", ain=ain, rf='int') + # thermostat-related commands + def get_temperature(self, ain): """Get the device temperature sensor value.""" return self._aha_request("gettemperature", ain=ain, rf='float') / 10.0 @@ -2122,6 +2234,12 @@ def set_window_open(self, ain, seconds): self._aha_request("sethkrwindowopen", ain=ain, param={'endtimestamp': endtimestamp}) + def set_boost(self, ain, seconds): + """Set the thermostate target temperature.""" + endtimestamp = int(time.time() + seconds) + + self._aha_request("sethkrboost", ain=ain, param={'endtimestamp': endtimestamp}) + def get_comfort_temperature(self, ain): """Get the thermostate comfort temperature.""" return self._get_temperature(ain, "gethkrkomfort") @@ -2201,6 +2319,50 @@ def set_color(self, ain, hsv, duration=0, mapped=True): # undocumented API method for free color selection self._aha_request("setunmappedcolor", ain=ain, param=params) + def set_color_discrete(self, ain, hue, duration=0): + """ + Set Led color to closest discrete hue value. Currently, only those are supported for FritzDect500 RGB LED bulbs + """ + + if hue <= 20: + # self._plugin_instance.logger.debug(f'setcolor to red (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 358, 'saturation': 180, 'duration': int(duration)}, rf='bool') + elif hue <= 45: + # self._plugin_instance.logger.debug(f'setcolor to orange (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 35, 'saturation': 214, 'duration': int(duration)}, rf='bool') + elif hue <= 55: + # self._plugin_instance.logger.debug(f'setcolor to yellow (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 52, 'saturation': 153, 'duration': int(duration)}, rf='bool') + elif hue <= 100: + # self._plugin_instance.logger.debug(f'setcolor to grasgreen (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 92, 'saturation': 123, 'duration': int(duration)}, rf='bool') + elif hue <= 135: + # self._plugin_instance.logger.debug(f'setcolor to green (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 120, 'saturation': 160, 'duration': int(duration)}, rf='bool') + elif hue <= 175: + # self._plugin_instance.logger.debug(f'setcolor to turquoise (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 160, 'saturation': 145, 'duration': int(duration)}, rf='bool') + elif hue <= 210: + # self._plugin_instance.logger.debug(f'setcolor to cyan (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 195, 'saturation': 179, 'duration': int(duration)}, rf='bool') + elif hue <= 240: + # self._plugin_instance.logger.debug(f'setcolor to blue (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 225, 'saturation': 204, 'duration': int(duration)}, rf='bool') + elif hue <= 280: + # self._plugin_instance.logger.debug(f'setcolor to violett (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 266, 'saturation': 169, 'duration': int(duration)}, rf='bool') + elif hue <= 310: + # self._plugin_instance.logger.debug(f'setcolor to magenta (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 296, 'saturation': 140, 'duration': int(duration)}, rf='bool') + elif hue <= 350: + # self._plugin_instance.logger.debug(f'setcolor to pink (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 335, 'saturation': 180, 'duration': int(duration)}, rf='bool') + elif hue <= 360: + # self._plugin_instance.logger.debug(f'setcolor to red (hue={hue})') + return self._aha_request("setcolor", ain=ain, param={'hue': 358, 'saturation': 180, 'duration': int(duration)}, rf='bool') + else: + self._plugin_instance.logger.error(f'setcolor hue out of range (hue={hue})') + def get_color_temps(self, ain): """Get temperatures supported by this lightbulb.""" colordefaults = self._get_colordefaults(ain) @@ -2261,6 +2423,8 @@ def apply_template(self, ain): """Applies a template.""" self._aha_request("applytemplate", ain=ain) + # Log-related commands + def get_device_log_from_lua(self): """ Gets the Device Log from the LUA HTTP Interface via LUA Scripts (more complete than the get_device_log TR-064 version. @@ -2285,19 +2449,22 @@ def get_device_log_from_lua(self): return newlog class FritzhomeDeviceFeatures(IntFlag): - ALARM = 0x0010 - UNKNOWN = 0x0020 - BUTTON = 0x0020 - THERMOSTAT = 0x0040 - POWER_METER = 0x0080 - TEMPERATURE = 0x0100 - SWITCH = 0x0200 - DECT_REPEATER = 0x0400 - MICROPHONE = 0x0800 - HANFUN = 0x2000 - SWITCHABLE = 0x8000 - DIMMABLE = 0x10000 - LIGHTBULB = 0x20000 + + HANFUN_DEVICE = 0x0001 # Bit 0: HAN-FUN Gerät + LIGHT = 0x0002 # Bit 2: Licht / Lampe + ALARM = 0x0010 # Bit 4: Alarm-Sensor + BUTTON = 0x0020 # Bit 5: AVM-Button + THERMOSTAT = 0x0040 # Bit 6: Heizkörperregler + POWER_METER = 0x0080 # Bit 7: Energie Messgerät + TEMPERATURE = 0x0100 # Bit 8: Temperatursensor + SWITCH = 0x0200 # Bit 9: Schaltsteckdose + DECT_REPEATER = 0x0400 # Bit 10: AVM DECT Repeater + MICROPHONE = 0x0800 # Bit 11: Mikrofon + HANFUN = 0x2000 # Bit 13: HAN-FUN-Unit + SWITCHABLE = 0x8000 # Bit 15: an-/ausschaltbares Gerät/Steckdose/Lampe/Aktor + DIMMABLE = 0x10000 # Bit 16: Gerät mit einstellbarem Dimm-, Höhen- bzw. Niveau-Level + LIGHTBULB = 0x20000 # Bit 17: Lampe mit einstellbarer Farbe/Farbtemperatur + BLIND = 0x40000 # Bit 18: Rollladen(Blind) - hoch, runter, stop und level 0% bis 100 % class FritzhomeEntityBase(ABC): """The Fritzhome Entity class.""" @@ -2386,6 +2553,7 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): manufacturer = None productname = None present = None + name = None def __repr__(self): """Return a string.""" @@ -2408,7 +2576,7 @@ def _update_from_node(self, node): self.fw_version = node.attrib["fwversion"] self.manufacturer = node.attrib["manufacturer"] self.productname = node.attrib["productname"] - + self.name = node.findtext("name") self.present = bool(int(node.findtext("present"))) # General @@ -2420,6 +2588,7 @@ class FritzhomeDeviceAlarm(FritzhomeDeviceBase): """The Fritzhome Device class.""" alert_state = None + last_alert_chgtimestamp = None def _update_from_node(self, node): super()._update_from_node(node) @@ -2439,12 +2608,19 @@ def _update_alarm_from_node(self, node): val = node.find("alert") try: self.alert_state = self.get_node_value_as_int_as_bool(val, "state") + self.last_alert_chgtimestamp = self.get_node_value_as_int(val, "lastalertchgtimestamp") except (Exception, ValueError): pass class FritzhomeDeviceButton(FritzhomeDeviceBase): """The Fritzhome Device class.""" + tx_busy = None + battery_low = None + battery_level = None + state = None + mode = None + def _update_from_node(self, node): super()._update_from_node(node) if self.present is False: @@ -2470,6 +2646,8 @@ def _update_button_from_node(self, node): self.tx_busy = self.get_node_value_as_int_as_bool(node, "txbusy") 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")) + self.state = int(self.get_node_value_as_int(node, "state")) + self.mode = str(self.get_node_value_as_int(node, "mode")) except Exception: pass @@ -2629,6 +2807,7 @@ class FritzhomeDevicePowermeter(FritzhomeDeviceBase): power = None energy = None + voltage = None def _update_from_node(self, node): super()._update_from_node(node) @@ -2785,6 +2964,9 @@ class FritzhomeDeviceThermostat(FritzhomeDeviceBase): holiday_active = None nextchange_endperiod = None nextchange_temperature = None + boost_active = None + windowopenactiveendtime = None + boostactiveendtime = None def _update_from_node(self, node): super()._update_from_node(node) @@ -2814,31 +2996,22 @@ def _update_hkr_from_node(self, node): # optional value try: - self.device_lock = self.get_node_value_as_int_as_bool( - hkr_element, "devicelock" - ) + self.device_lock = self.get_node_value_as_int_as_bool(hkr_element, "devicelock") self.lock = self.get_node_value_as_int_as_bool(hkr_element, "lock") self.error_code = self.get_node_value_as_int(hkr_element, "errorcode") - self.battery_low = self.get_node_value_as_int_as_bool( - hkr_element, "batterylow" - ) + self.battery_low = self.get_node_value_as_int_as_bool(hkr_element, "batterylow") self.battery_level = int(self.get_node_value_as_int(hkr_element, "battery")) - self.window_open = self.get_node_value_as_int_as_bool( - hkr_element, "windowopenactiv" - ) - self.summer_active = self.get_node_value_as_int_as_bool( - hkr_element, "summeractive" - ) - self.holiday_active = self.get_node_value_as_int_as_bool( - hkr_element, "holidayactive" - ) + self.window_open = self.get_node_value_as_int_as_bool(hkr_element, "windowopenactiv") + self.summer_active = self.get_node_value_as_int_as_bool(hkr_element, "summeractive") + self.holiday_active = self.get_node_value_as_int_as_bool(hkr_element, "holidayactive") + self.boost_active = self.get_node_value_as_int_as_bool(hkr_element, "boostactive") + self.windowopenactiveendtime = self.get_node_value_as_int(hkr_element, "windowopenactiveendtime") + self.boostactiveendtime = self.get_node_value_as_int(hkr_element, "boostactiveendtime") + nextchange_element = hkr_element.find("nextchange") - self.nextchange_endperiod = int( - self.get_node_value_as_int(nextchange_element, "endperiod") - ) - self.nextchange_temperature = self.get_temp_from_node( - nextchange_element, "tchange" - ) + self.nextchange_endperiod = int(self.get_node_value_as_int(nextchange_element, "endperiod")) + self.nextchange_temperature = self.get_temp_from_node(nextchange_element, "tchange") + except Exception: pass @@ -2858,6 +3031,10 @@ def set_window_open(self, seconds): """Set the thermostate to window open.""" return self._fritz.set_window_open(self.ain, seconds) + def set_boost(self, seconds): + """Set the thermostate to window open.""" + return self._fritz.set_boost(self.ain, seconds) + def get_comfort_temperature(self): """Get the thermostate comfort temperature.""" return self._fritz.get_comfort_temperature(self.ain) @@ -2894,6 +3071,36 @@ def set_hkr_state(self, state): self.set_target_temperature(value) + # ToDo: Complete Blind + class FritzhomeDeviceBlind(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + # Blind + @property + def has_blind(self): + """Check if the device has temperature function.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.BLIND) + + # ToDo: Complete Switchable + class FritzhomeDeviceSwitchable(FritzhomeDeviceBase): + """The Fritzhome Device class.""" + + def _update_from_node(self, node): + super()._update_from_node(node) + if self.present is False: + return + + # Blind + @property + def has_switchable(self): + """Check if the device has temperature function.""" + return self._has_feature(FritzHome.FritzhomeDeviceFeatures.SWITCHABLE) + class FritzhomeDevice( FritzhomeDeviceAlarm, FritzhomeDeviceButton, @@ -2903,6 +3110,8 @@ class FritzhomeDevice( FritzhomeDeviceTemperature, FritzhomeDeviceThermostat, FritzhomeDeviceLightBulb, + FritzhomeDeviceBlind, + FritzhomeDeviceSwitchable, ): """The Fritzhome Device class.""" @@ -2928,8 +3137,8 @@ def __init__(self, host, port, callback, call_monitor_incoming_filter, plugin_in self._trigger_items = dict() # items which can be used to trigger sth, e.g. a logic self._items_incoming = dict() # items for incoming calls self._items_outgoing = dict() # items for outgoing calls - self._duration_item_in = dict() # 2 items, one for counting the incoming, one for counting the outgoing call duration - self._duration_item_out = dict() + self._duration_item_in = dict() # item counting the incoming call duration + self._duration_item_out = dict() # item counting the outgoing call duration self._call_active = dict() self._listen_active = False self._call_active['incoming'] = False @@ -2957,20 +3166,19 @@ def connect(self): self.conn.connect((self._host, self._port)) _name = f'plugins.{self._plugin_instance.get_fullname()}.Monitoring_Service' self._listen_thread = threading.Thread(target=self._listen, name=_name).start() - if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug("MonitoringService: connection established") except Exception as e: self.conn = None self._plugin_instance.logger.error( f"MonitoringService: Cannot connect to {self._host} on port: {self._port}, CallMonitor activated by #96*5*? - Error: {e}") - return + else: + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("MonitoringService: connection established") def disconnect(self): """ Disconnects from the call monitor of the AVM device """ - if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug("MonitoringService: disconnecting") + self._plugin_instance.logger.debug("MonitoringService: disconnecting") self._listen_active = False self._stop_counter('incoming') self._stop_counter('outgoing') @@ -3003,23 +3211,154 @@ def register_item(self, item, avm_data_type: str): # handle _call_monitor_attributes_in if avm_data_type in _call_monitor_attributes_in: self._items_incoming[item] = (avm_data_type, None) + elif avm_data_type in _call_monitor_attributes_out: self._items_outgoing[item] = (avm_data_type, None) - elif avm_data_type in _trigger_attributes: + + elif avm_data_type in _call_monitor_attributes_gen: + self._items[item] = (avm_data_type, None) + + elif avm_data_type in _call_monitor_attributes_trigger: avm_incoming_allowed = self._plugin_instance.get_iattr_value(item.conf, 'avm2_incoming_allowed') avm_target_number = self._plugin_instance.get_iattr_value(item.conf, 'avm2_target_number') if not avm_incoming_allowed or not avm_target_number: self._plugin_instance.logger.error(f"For Trigger-item={item.id()} both 'avm2_incoming_allowed' and 'avm2_target_number' must be specified as attributes. Item will be ignored.") else: self._trigger_items[item] = (avm_data_type, avm_incoming_allowed, avm_target_number) + elif avm_data_type in _call_duration_attributes: if 'in' in avm_data_type: self._duration_item_in[item] = (avm_data_type, None) else: self._duration_item_out[item] = (avm_data_type, None) + else: self._items[item] = (avm_data_type, None) + def set_callmonitor_item_values_initially(self): + + _calllist = self._plugin_instance.get_calllist() + + if not _calllist: + return + + for item in self._items_incoming: + avm_data_type = self._items_incoming[item][0] + + if avm_data_type == 'last_caller_incoming': + for element in _calllist: + if element['Type'] in ['1', '2']: + if 'Name' in element: + item(element['Name'], self._plugin_instance.get_shortname()) + else: + item(element['Caller'], self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'last_number_incoming': + for element in _calllist: + if element['Type'] in ['1', '2']: + if 'Caller' in element: + item(element['Caller'], self._plugin_instance.get_shortname()) + else: + item("", self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'last_called_number_incoming': + for element in _calllist: + if element['Type'] in ['1', '2']: + item(element['CalledNumber'], self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'last_call_date_incoming': + for element in _calllist: + if element['Type'] in ['1', '2']: + date = str(element['Date']) + date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] + item(date, self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'call_event_incoming': + item('disconnect', self._plugin_instance.get_shortname()) + + elif avm_data_type == 'is_call_incoming': + item(0, self._plugin_instance.get_shortname()) + + for item in self._items_outgoing: + avm_data_type = self._items_outgoing[item][0] + + if avm_data_type == 'last_caller_outgoing': + for element in _calllist: + if element['Type'] in ['3', '4']: + if 'Name' in element: + item(element['Name'], self._plugin_instance.get_shortname()) + else: + item(element['Called'], self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'last_number_outgoing': + for element in _calllist: + if element['Type'] in ['3', '4']: + if 'Caller' in element: + item(''.join(filter(lambda x: x.isdigit(), element['Caller'])), self._plugin_instance.get_shortname()) + else: + item("", self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'last_called_number_outgoing': + for element in _calllist: + if element['Type'] in ['3', '4']: + item(element['Called'], self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'last_call_date_outgoing': + for element in _calllist: + if element['Type'] in ['3', '4']: + date = str(element['Date']) + date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] + item(date, self._plugin_instance.get_shortname()) + break + + elif avm_data_type == 'call_event_outgoing': + item('disconnect', self._plugin_instance.get_shortname()) + + elif avm_data_type == 'is_call_outgoing': + item(0, self._plugin_instance.get_shortname()) + + for item in self._items: + avm_data_type = self._items[item][0] + + if avm_data_type == 'call_event': + item('disconnect', self._plugin_instance.get_shortname()) + + elif avm_data_type == 'call_direction': + for element in _calllist: + if element['Type'] in ['1', '2']: + item('incoming', self._plugin_instance.get_shortname()) + break + if element['Type'] in ['3', '4']: + item('outgoing', self._plugin_instance.get_shortname()) + break + + for item in self._duration_item_in: + avm_data_type = self._duration_item_in[item][0] + if avm_data_type == 'call_duration_incoming': + for element in _calllist: + if element['Type'] in ['1', '2']: + duration = element['Duration'] + duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 + item(duration, self._plugin_instance.get_shortname()) + break + + for item in self._duration_item_out: + avm_data_type = self._duration_item_out[item][0] + if avm_data_type == 'call_duration_outgoing': + for element in _calllist: + if element['Type'] in ['3', '4']: + duration = element['Duration'] + duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 + item(duration, self._plugin_instance.get_shortname()) + break + def get_items(self): return list(self._items.keys()) diff --git a/plugin.yaml b/plugin.yaml index b50fc98c1..4185070de 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,12 +1,12 @@ # Metadata for the plugin plugin: # Global plugin attributes - type: unknown # plugin type (gateway, interface, protocol, system, web) + type: interface # plugin type (gateway, interface, protocol, system, web) description: - de: 'Beispiel Plugin für SmartHomeNG v1.5 und höher' - en: 'Sample plugin for SmartHomeNG v1.5 and up' - maintainer: -# tester: # Who tests this plugin? + de: 'Ansteuerung von AVM FRITZ!Boxen, WLAN-Repeatern, DECT Steckdosen, etc.' + en: 'Get and send data from/to AVM devices such as the FRITZ!Box, Wifi Repeaters or DECT sockets.' + maintainer: sisamiwe + tester: psilo, schuma, aschwith, bmx state: develop # change to ready when done with development # keywords: iot xyz # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page @@ -17,7 +17,7 @@ plugin: # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) # py_minversion: 3.6 # minimum Python version to use for this plugin # py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) - multi_instance: false # plugin supports multi instance + multi_instance: true # plugin supports multi instance restartable: unknown classname: AVM2 # class containing the plugin @@ -147,7 +147,7 @@ item_attributes: - 'wlanconfig_ssid' # r/o - 'wlan_guest_time_remaining' # r/o # Host Attribute - - 'network_device' # r/o Defines Network device via MAC-Adresse + - 'network_device' # r/o Verbindungsstatus // Defines Network device via MAC-Adresse - 'device_ip' # r/o Geräte-IP (Muss Child von 'network_device' sein) ipv4 - 'device_connection_type' # r/o Verbindungstyp (Muss Child von 'network_device' sein) str - 'device_hostname' # r/o Gerätename (Muss Child von 'network_device' sein) str @@ -376,17 +376,17 @@ item_structs: tam: type: bool visu_acl: rw - avm_data_type@instance: tam - avm_tam_index@instance: 1 + avm2_data_type@instance: tam + avm2_tam_index@instance: 1 name: type: str visu_acl: ro - avm_data_type@instance: tam_name + avm2_data_type@instance: tam_name message_number_old: type: num visu_acl: ro - avm_data_type@instance: tam_old_message_number + avm2_data_type@instance: tam_old_message_number eval: (sh...message_number_total()-sh...message_number_new()) eval_trigger: - ..message_number_total @@ -394,42 +394,42 @@ item_structs: message_number_new: type: num visu_acl: ro - avm_data_type@instance: tam_new_message_number + avm2_data_type@instance: tam_new_message_number message_number_total: type: num visu_acl: ro - avm_data_type@instance: tam_total_message_number + avm2_data_type@instance: tam_total_message_number deflection: type: bool visu_acl: rw - avm_data_type@instance: deflection_enable - avm_deflection_index@instance: 1 + avm2_data_type@instance: deflection_enable + avm2_deflection_index@instance: 1 deflection_type: type: str visu_acl: ro - avm_data_type@instance: deflection_type + avm2_data_type@instance: deflection_type deflection_number: type: str visu_acl: ro - avm_data_type@instance: deflection_number + avm2_data_type@instance: deflection_number deflection_to_number: type: str visu_acl: ro - avm_data_type@instance: deflection_to_number + avm2_data_type@instance: deflection_to_number deflection_mode: type: str visu_acl: ro - avm_data_type@instance: deflection_mode + avm2_data_type@instance: deflection_mode deflection_outgoing: type: str visu_acl: ro - avm_data_type@instance: deflection_outgoing + avm2_data_type@instance: deflection_outgoing deflection_phonebook_id: type: num visu_acl: ro - avm_data_type@instance: deflection_phonebook_id + avm2_data_type@instance: deflection_phonebook_id wan: connection_status: @@ -519,136 +519,136 @@ item_structs: avm2_wlan_index@instance: 3 device: - avm_data_type@instance: network_device + avm2_data_type@instance: network_device type: bool visu_acl: ro ip: type: str - avm_data_type@instance: device_ip + avm2_data_type@instance: device_ip visu_acl: ro connection_type: type: str - avm_data_type@instance: device_connection_type + avm2_data_type@instance: device_connection_type visu_acl: ro hostname: type: str - avm_data_type@instance: device_hostname + avm2_data_type@instance: device_hostname visu_acl: ro smarthome_general: name: - avm_data_type@instance: device_name + avm2_data_type@instance: device_name type: str identifier: - avm_data_type@instance: device_id + avm2_data_type@instance: device_id type: str productname: - avm_data_type@instance: product_name + avm2_data_type@instance: product_name type: str manufacturer: - avm_data_type@instance: manufacturer + avm2_data_type@instance: manufacturer type: str firmware_version: - avm_data_type@instance: fw_version + avm2_data_type@instance: fw_version type: str present: - avm_data_type@instance: connected + avm2_data_type@instance: connected type: bool functions: - avm_data_type@instance: device_functions + avm2_data_type@instance: device_functions type: list smarthome_hkr: current_temperature: - avm_data_type@instance: current_temperature + avm2_data_type@instance: current_temperature type: num target_temperature: - avm_data_type@instance: target_temperature - avm_read_after_write@instance: 5 + avm2_data_type@instance: target_temperature + avm2_read_after_write@instance: 5 type: num # set_target_temperature: - # avm_data_type@instance: set_target_temperature + # avm2_data_type@instance: set_target_temperature # type: num comfort_temperature: - avm_data_type@instance: temperature_comfort + avm2_data_type@instance: temperature_comfort type: num eco_temperature: - avm_data_type@instance: temperature_reduced + avm2_data_type@instance: temperature_reduced type: num battery_low: - avm_data_type@instance: battery_low + avm2_data_type@instance: battery_low type: bool battery_level: - avm_data_type@instance: battery_level + avm2_data_type@instance: battery_level type: num window_open: - avm_data_type@instance: window_open + avm2_data_type@instance: window_open type: bool summer_active: - avm_data_type@instance: summer_active + avm2_data_type@instance: summer_active type: bool holiday_active: - avm_data_type@instance: holiday_active + avm2_data_type@instance: holiday_active type: bool errorcode: - avm_data_type@instance: errorcode + avm2_data_type@instance: errorcode type: num # set_window_open: - # avm_data_type@instance: set_window_open + # avm2_data_type@instance: set_window_open # type: bool windowopenactiveendtime: - avm_data_type@instance: windowopenactiveendtime + avm2_data_type@instance: windowopenactiveendtime type: num # set_hkr_boost: - # avm_data_type@instance: set_hkr_boost + # avm2_data_type@instance: set_hkr_boost # type: bool hkr_boost: - avm_data_type@instance: hkr_boost + avm2_data_type@instance: hkr_boost type: bool boostactiveendtime: - avm_data_type@instance: boostactiveendtime + avm2_data_type@instance: boostactiveendtime type: num lock: - avm_data_type@instance: lock + avm2_data_type@instance: lock type: bool device_lock: - avm_data_type@instance: device_lock + avm2_data_type@instance: device_lock type: bool smarthome_temperature_sensor: temperatur: - avm_data_type@instance: current_temperature + avm2_data_type@instance: current_temperature type: num temperature_offset: - avm_data_type@instance: temperature_offset + avm2_data_type@instance: temperature_offset type: num smarthome_alert: state: - avm_data_type@instance: alert + avm2_data_type@instance: alert type: bool smarthome_switch: switch_state: - avm_data_type@instance: switch_state + avm2_data_type@instance: switch_state type: bool switch_toggle: - avm_data_type@instance: switch_toggle + avm2_data_type@instance: switch_toggle type: bool enforce_updates: yes smarthome_powermeter: power: - avm_data_type@instance: power + avm2_data_type@instance: power type: num energy: - avm_data_type@instance: energy + avm2_data_type@instance: energy type: num voltage: - avm_data_type@instance: voltage + avm2_data_type@instance: voltage type: num #item_attribute_prefixes: diff --git a/webif/templates/index.html b/webif/templates/index.html index 3a85bd147..3f64e8bd1 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -76,44 +76,50 @@ {{ p._fritz_device._items }} - {{ _('model_name') }} - {{ p._fritz_device.model_name }} + {{ _('tam@Fritzdevice') }} + {{ p._fritz_device.client.InternetGatewayDevice.X_AVM_DE_TAM.GetInfo(NewIndex=0) }} - {{ _('test') }} - {{ p._fritz_device.client.InternetGatewayDevice.X_AVM_DE_TAM.GetInfo(NewIndex=1) }} + {{ _('Host@Fritzdevice') }} + {{ p._fritz_device.client.LANDevice.Hosts.GetSpecificHostEntry(NewMACAddress='14:3C:C3:7D:26:D6') }} + {% if p._fritz_home %} - {{ _('WLANConfiguration[0]') }} - {{ p._fritz_device.client.LANDevice.WLANConfiguration[0].GetInfo() }} + {{ _('Items@FritzHome') }} + {{ p._fritz_home._items }} - {{ _('LANDevice') }} - {{ p._fritz_device._data_cache['LANDevice'] }} + {{ _('get_devices_as_dict@FritzHome') }} + {{ p._fritz_home.get_devices_as_dict() }} - {{ _('GetDefaultConnectionService') }} - {{ p._fritz_device.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() }} + {{ _('_poll_aha@FritzHome') }} + {{ p._fritz_home._poll_aha() }} - {{ _('client_igd.devices') }} - {{ p._fritz_device.client_igd.WANDevice.WANCommonInterfaceConfig.GetAddonInfos() }} + {{ _('_aha_devices@FritzHome') }} + {{ p._fritz_home._aha_devices }} + + {% endif %} + + {% if p._monitoring_service %} - {{ _('GetPhonebook') }} - {{ p._fritz_device.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetPhonebook(NewPhonebookID=0) }} + {{ _('Items@Callmonitor') }} + {{ p._monitoring_service._items }} + {% endif %} - {{ _('get contact by phonenumber') }} - {{ p._fritz_device.get_contact_name_by_phone_number('01735631526#') }} + {{ _('model_name') }} + {{ p._fritz_device.client.InternetGatewayDevice.DeviceInfo.GetInfo() }} - {{ _('Device Log') }} - {{ p._fritz_device.client.InternetGatewayDevice.DeviceInfo.GetInfo()['NewDeviceLog'] }} + {{ _('GetDefaultConnectionService') }} + {{ p._fritz_device.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() }} - {{ _('_monitoring_service._items') }} - {{ p._monitoring_service._items }} + {{ _('GetAddonInfos') }} + {{ p._fritz_device.client_igd.WANDevice.WANCommonInterfaceConfig.GetAddonInfos() }} From 6b1f83f550147277a69a16daa86c814b3d36ab17 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 16 Mar 2022 10:21:09 +0100 Subject: [PATCH 006/178] Update --- __init__.py | 956 ++++++++++++++++++++++--------------- plugin.yaml | 289 +++++++++-- webif/__init__.py | 108 +++-- webif/templates/index.html | 440 +++++++++++++---- 4 files changed, 1264 insertions(+), 529 deletions(-) diff --git a/__init__.py b/__init__.py index 31aba52b2..a2a1cf226 100644 --- a/__init__.py +++ b/__init__.py @@ -64,122 +64,6 @@ TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} -""" -Definition DeviceGroupDicts -""" - -InternetGatewayDevice = { - 'DeviceInfo': { - 'GetInfo': {}, - 'GetSecurityPort': {}, - }, - 'DeviceConfig': { - 'GetPersistentData': {}, - }, - 'X_AVM_DE_MyFritz': { - 'GetInfo': {}, - 'GetNumberOfServices': {}, - }, - 'X_VoIP': { - 'GetInfoEx': {}, - }, - 'X_AVM_DE_OnTel': { - 'GetInfo': {}, - 'GetInfoByIndex': {}, - 'GetNumberOfEntries': {}, - 'GetCallList': {}, - 'GetPhonebookList': {}, - 'GetPhonebook': {}, - 'GetNumberOfDeflections': {}, - 'GetDeflection': {}, - 'GetDeflections': {}, - 'GetDECTHandsetList': {}, - }, - 'X_AVM_DE_Dect': { - 'GetNumberOfDectEntries': {}, - 'GetGenericDectEntry': {}, - 'GetSpecificDectEntry': {}, - }, - 'X_AVM_DE_TAM': { - 'GetInfo': {}, - 'GetMessageList': {}, - 'GetList': {}, - }, - 'X_AVM_DE_Homeauto': { - 'GetInfo': {}, - 'GetGenericDeviceInfos': {}, - 'GetSpecificDeviceInfos': {}, - }, -} - -LANDevice = { - 'WLANConfiguration': { - 'GetInfo': {}, - 'GetGenericAssociatedDeviceInfo': {}, - 'X_AVM_DE_GetSpecificAssociatedDeviceInfoByIp': {}, - 'GetStatistics': {}, - 'GetPacketStatistics': {}, - 'X_AVM_DE_GetWLANHybridMode': {}, - 'X_AVM_DE_GetWLANExtInfo': {}, - }, - 'Hosts': { - 'GetHostNumberOfEntries': {}, - 'GetSpecificHostEntry': {}, - 'GetGenericHostEntry': {}, - 'X_AVM_DE_GetSpecificHostEntryByIp': {}, - }, - 'LANEthernetInterfaceConfig': { - 'GetInfo': {}, - 'GetStatistics': {}, - }, - 'LANHostConfigManagement': { - 'GetInfo': {}, - }, -} - -WANDevice = { - 'WANCommonInterfaceConfig': { - 'GetCommonLinkProperties': {}, - 'GetTotalBytesSent': {}, - 'GetTotalBytesReceived': {}, - 'GetTotalPacketsSent': {}, - 'GetTotalPacketsReceived': {}, - 'X_AVM_DE_GetOnlineMonitor': {}, - }, - 'WANDSLInterfaceConfig': { - 'GetInfo': {}, - 'GetStatisticsTotal': {}, - 'X_AVM_DE_GetDSLDiagnoseInfo': {}, - }, -} - -WANConnectionDevice = { - 'WANDSLLinkConfig': { - 'GetInfo': {}, - 'GetDSLLinkInfo': {}, - 'GetDestinationAddress': {} - }, - 'WANEthernetLinkConfig': { - 'GetEthernetLinkStatus': {} - }, - 'WANPPPConnection': { - 'GetInfo': {}, - 'GetConnectionTypeInfo': {}, - 'GetStatusInfo': {}, - 'GetGenericPortMappingEntry': {}, - 'GetSpecificPortMappingEntry': {}, - 'GetExternalIPAddress': {} - }, - 'WANIPConnection': { - 'GetInfo': {}, - 'GetConnectionTypeInfo': {}, - 'GetStatusInfo': {}, - 'GetGenericPortMappingEntry': {}, - 'GetSpecificPortMappingEntry': {}, - 'GetExternalIPAddress': {} - }, -} - """ Definition of attribute value groups of avm_data_type """ @@ -388,6 +272,7 @@ def __init__(self, sh): self._call_monitor = self.get_parameter_value('call_monitor') self._aha_http_interface = self.get_parameter_value('avm_home_automation') self._cycle = self.get_parameter_value('cycle') + self.webif_pagelength = self.get_parameter_value('webif_pagelength') self.alive = False ssl = self.get_parameter_value('ssl') if ssl and not _verify: @@ -417,11 +302,9 @@ def __init__(self, sh): # init Call Monitor if self._call_monitor and self._fritz_device.connected: try: - self._monitoring_service = Callmonitor(_host, - 1012, + self._monitoring_service = Callmonitor(_host, 1012, self._fritz_device.get_contact_name_by_phone_number, - _call_monitor_incoming_filter, - self) + _call_monitor_incoming_filter, self) except Exception as e: self.logger.warning(f"Error {e} establishing connection to Fritzdevice CallMonitor.") self._monitoring_service = None @@ -569,56 +452,23 @@ def monitoring_service_disconnect(self): def get_calllist(self): return self._fritz_device.get_calllist_from_cache() + def get_monitoring_service(self): + return self._monitoring_service + + def get_fritz_device(self): + return self._fritz_device + + def get_fritz_home(self): + return self._fritz_home + class FritzDevice: """ This class encapsulates information related to a specific FritzDevice - - # Devices / Services - InternetGatewayDevice - DeviceInfo - GetInfo - SetProvisioningCode - GetDeviceLog - GetSecurityPort - DeviceConfig - Layer3Forwarding - LANConfigSecurity - ManagementServer - Time - UserInterface - X_AVM-DE_Storage - X_AVM-DE_WebDAVClient - X_AVM-DE_UPnP - X_AVM-DE_Speedtest - X_AVM-DE_RemoteAccess - X_AVM-DE_MyFritz - X_VoIP - X_AVM-DE_OnTel - X_AVM-DE_Dect - X_AVM-DE_TAM - X_AVM-DE_AppSetup - X_AVM-DE_Homeauto - X_AVM-DE_Homeplug - X_AVM-DE_Filelinks - X_AVM-DE_Auth - X_AVM-DE_HostFilter - LANDevice - WLANConfiguration - Hosts - LANEthernetInterfaceConfig - LANHostConfigManagement - WANDevice - WANCommonInterfaceConfig - WANDSLInterfaceConfig - WANConnectionDevice - WANDSLLinkConfig - WANEthernetLinkConfig - WANPPPConnection - WANIPConnection """ def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, plugin_instance=None): + """Init class FritzDevice""" self._plugin_instance = plugin_instance self._plugin_instance.logger.debug("Init FritzDevice") @@ -657,6 +507,9 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self._plugin_instance.logger.error(f"Unable to determine _default_connection_service. ErrorCode {_default_connection_service}.") def register_item(self, item, avm_data_type: str): + """ + Parsed items valid fpr that class will be registered + """ # handle wlan items if avm_data_type in _wlan_config_attributes: @@ -690,25 +543,44 @@ def register_item(self, item, avm_data_type: str): self._items[item] = (avm_data_type, None) def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): + """ + Updated Item will be processed and value communicated to AVM Device + """ - # ToDo: Implement ReadafterWrite + # get index + _index = self._items[item][1] - index = self._items[item][1] + # to be set value + to_be_set_value = item() - if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm_data_type={avm_data_type} has changed for index {index}; New value={bool(item())}") + # define command per avm_data_type + _dispatcher = {'wlanconfig': (self.set_wlan_config, self.get_wlan_config), + 'tam': (self.set_tam, self.get_tam), + 'deflection_enable': (self.set_deflection, self.get_deflection), + } - # handle wlan items - if avm_data_type == 'wlanconfig': - self.client.LANDevice.WLANConfiguration[index].SetEnable(NewEnable=bool(item())) + # do logging + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Item {item.id()} with avm_data_type={avm_data_type} has changed for index {_index}; New value={bool(item())}") - # handle tam items - elif avm_data_type == 'tam': - self.client.InternetGatewayDevice.X_AVM_DE_TAM.SetEnable(NewIndex=index, NewEnable=bool(item())) + # call setting method + _dispatcher[avm_data_type][0](_index, bool(to_be_set_value)) - # handle deflection_enable - elif avm_data_type == 'deflection_enable': - self.client.InternetGatewayDevice.X_AVM_DE_OnTel.SetDeflectionEnable(NewDeflectionId=index, NewEnable=bool(item())) + # handle readafterwrite + if readafterwrite: + wait = float(readafterwrite) + time.sleep(wait) + try: + set_value = _dispatcher[avm_data_type][1](_index) + except Exception: + self._plugin_instance.logger.error(f"{avm_data_type} is not defined to be read.") + else: + item(set_value, self._plugin_instance.get_instance_name()) + if set_value != to_be_set_value: + self._plugin_instance.logger.warning(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") + else: + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") def _build_url(self) -> str: """ @@ -716,6 +588,7 @@ def _build_url(self) -> str: :return: string of the url, dependent on settings of the FritzDevice """ + if self.is_ssl(): url_prefix = "https" else: @@ -894,6 +767,12 @@ def get_password(self): """ return self._password + def get_item_dict(self): + return self._items + + def get_item_list(self): + return list(self._items.keys()) + @property def connected(self): return self._connected @@ -933,9 +812,12 @@ def safe_port(self): return self._data_cache['InternetGatewayDevice']['DeviceInfo']['GetSecurityPort']['NewSecurityPort'] # ---------------------------------- - # TBD + # Update methods # ---------------------------------- def update_item_values(self): + """ + Updates Item Values + """ if self._connected: for item in self._items: @@ -949,6 +831,9 @@ def update_item_values(self): self._plugin_instance.logger.warning(f"FritzDevice not connected. No update of item values possible.") def _poll_fritz_device(self, avm_data_type: str, index: Union[int, str]): + """ + Poll Fritz Device and feed dictonary + """ link_ppp = { 'wan_connection_status': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', None, 'NewConnectionStatus'), @@ -967,7 +852,7 @@ def _poll_fritz_device(self, avm_data_type: str, index: Union[int, str]): } link = { - # 'avm_data_type': ('Device', 'Service', 'Action', 'In_Argument', 'Out_Argument'), + # 'avm_data_type': ('Device', 'Service', 'Action', 'In_Argument', 'Out_Argument'), 'uptime': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', None, 'NewUpTime'), 'serial_number': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', None, 'NewSerialNumber'), 'software_version': ('InternetGatewayDevice', 'DeviceInfo', 'GetInfo', None, 'NewSoftwareVersion'), @@ -1024,6 +909,10 @@ def _poll_fritz_device(self, avm_data_type: str, index: Union[int, str]): return data def _update_data_cache(self, client: str, device: str, service: str, action: str, in_argument: str = None, out_argument: str = None, index: int = None) -> dict: + """ + Update cache dict with poll data + """ + self._plugin_instance.logger.debug(f"_update_data_cache called with device={device}, service={service}, action={action}, in_argument={in_argument}, out_argument={out_argument}, index={index}") if device not in self._data_cache: self._data_cache[device] = {} @@ -1052,11 +941,23 @@ def _update_data_cache(self, client: str, device: str, service: str, action: str return self._data_cache[device][service][action][index][out_argument] def _request(self, url: str, timeout: int, verify: bool): + """ + Do get request and return response + """ + request = requests.get(url, timeout=timeout, verify=verify) if request.status_code == 200: return request + else: + self._plugin_instance.logger.error(f"Request to URL0{url} failed with {request.status_code}") + request.raise_for_status() + + @staticmethod + def _request_response_to_xml(request): + """ + Parse request response to element object + """ - def _request_response_to_xml(self, request): root = etree.fromstring(request.content) return root @@ -1161,6 +1062,7 @@ def cancel_call(self): def get_contact_name_by_phone_number(self, phone_number: str = '', phonebook_id: int = 0) -> str: """ + Get contact from phone book by phone number """ if phone_number.endswith('#'): @@ -1182,7 +1084,9 @@ def get_contact_name_by_phone_number(self, phone_number: str = '', phonebook_id: def get_phone_numbers_by_name(self, name: str = '', phonebook_id: int = 0) -> dict: """ + Get phone number from phone book by contact """ + tel_type = {"mobile": "CELL", "work": "WORK", "home": "HOME"} result_numbers = {} phonebook_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetPhonebook(NewPhonebookID=phonebook_id)[ @@ -1219,6 +1123,7 @@ def get_calllist_from_cache(self) -> list: def get_calllist(self, filter_incoming: str = '') -> list: """ + request calllist from Fritz Device """ calllist_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetCallList() @@ -1256,7 +1161,7 @@ def get_calllist(self, filter_incoming: str = '') -> list: self._plugin_instance.logger.error("Calllist not available on the FritzDevice") # ---------------------------------- - # get logs methods + # logs methods # ---------------------------------- def get_device_log_from_tr064(self) -> str: """ @@ -1272,7 +1177,7 @@ def get_device_log_from_tr064(self) -> str: return device_log.split("\n") # ---------------------------------- - # set wlan methods + # wlan methods # ---------------------------------- def set_wlan_config(self, wlan_index: int, new_enable: bool = False): """ @@ -1290,8 +1195,12 @@ def set_wlan_config(self, wlan_index: int, new_enable: bool = False): if data is not None: item(data, self._plugin_instance.get_shortname()) + def get_wlan_config(self, wlan_index: int): + """Get WLAN ON/OFF State""" + return bool(self.client.LANDevice.WLANConfiguration[wlan_index].GetInfo()['NewEnable']) + # ---------------------------------- - # set tam methods + # tam methods # ---------------------------------- def set_tam(self, tam_index: int = 0, new_enable: bool = False): """ @@ -1300,6 +1209,9 @@ def set_tam(self, tam_index: int = 0, new_enable: bool = False): self.client.InternetGatewayDevice.X_AVM_DE_TAM.SetEnable(NewIndex=tam_index, NewEnable=int(new_enable)) + def get_tam(self, tam_index: int = 0): + return bool(self.client.InternetGatewayDevice.X_AVM_DE_TAM.GetInfo(NewIndex=tam_index)['NewEnable']) + # ---------------------------------- # set home automation switch # ---------------------------------- @@ -1313,7 +1225,7 @@ def set_aha_device(self, ain: str = '', set_switch: bool = False): self.client.InternetGatewayDevice.X_AVM_DE_Homeauto.SetSwitch(NewAIN=ain, NewSwitchState=switch_state) # ---------------------------------- - # set deflection + # deflection # ---------------------------------- def set_deflection(self, deflection_id: int = 0, new_enable: bool = False): """ @@ -1327,6 +1239,10 @@ def set_deflection(self, deflection_id: int = 0, new_enable: bool = False): self.client.InternetGatewayDevice.X_AVM_DE_OnTel.SetDeflectionEnable(NewDeflectionId=deflection_id, NewEnable=int(new_enable)) + def get_deflection(self, deflection_id: int = 0): + """Get Deflection state of deflection_id""" + return bool(self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetDeflection(NewDeflectionId=deflection_id)['NewEnable']) + # ---------------------------------- # Host # ---------------------------------- @@ -1386,9 +1302,64 @@ def get_host_details(self, index: int): class Client: """TR-064 client. - :param str username: Username with access to router. - :param str password: Passwort to access router. - :param str base_url: URL to router. + + Description file provided by FritzDevice will be read. All DEVICES, SERVICES, ACTIONS definied in that desciption file can be used. + Format is: Client.DeviceName.ServiceName.ActionName(Arguments, ...) like Client.InternetGatewayDevice.Hosts.GetGenericHostEntry + Capital and lowercase letters must match with + + If a service is offered multiple times, actions can be accessed by the zero-based square bracket operator: + Client.DeviceName.ServiceName[1].ActionName(Arguments, ...) + + If services, actions or arguments contain a minus sign -, it must be replaced with an underscore _. + + # Devices / Services / Actions + InternetGatewayDevice + DeviceInfo + GetInfo + SetProvisioningCode + GetDeviceLog + GetSecurityPort + DeviceConfig + Layer3Forwarding + LANConfigSecurity + ManagementServer + Time + UserInterface + X_AVM-DE_Storage + X_AVM-DE_WebDAVClient + X_AVM-DE_UPnP + X_AVM-DE_Speedtest + X_AVM-DE_RemoteAccess + X_AVM-DE_MyFritz + X_VoIP + X_AVM-DE_OnTel + X_AVM-DE_Dect + X_AVM-DE_TAM + X_AVM-DE_AppSetup + X_AVM-DE_Homeauto + X_AVM-DE_Homeplug + X_AVM-DE_Filelinks + X_AVM-DE_Auth + X_AVM-DE_HostFilter + LANDevice + WLANConfiguration + Hosts + LANEthernetInterfaceConfig + LANHostConfigManagement + WANDevice + WANCommonInterfaceConfig + WANDSLInterfaceConfig + WANConnectionDevice + WANDSLLinkConfig + WANEthernetLinkConfig + WANPPPConnection + WANIPConnection + + :param str username: Username with access to router. + :param str password: Passwort to access router. + :param str base_url: URL to router. + :param str description_file: Description File to be read + :param plugin_instance: instance of Plugin """ def __init__(self, username, password, verify, base_url='https://192.168.178.1:49443', description_file=FRITZ_TR64_DESC_FILE, plugin_instance=None): @@ -1412,8 +1383,9 @@ def __getattr__(self, name): return self.devices[name] def _fetch_devices(self, description_file): - - """Fetch device description.""" + """ + Fetch device description. + """ self._plugin_instance.logger.debug(f"_fetch_devices called for description file={description_file}") @@ -1435,7 +1407,9 @@ def _fetch_devices(self, description_file): self._plugin_instance.logger.debug(f"Client: {self.description_file} devices={self.devices}") class Device: - """TR-064 device. + """ + TR-064 device. + :param lxml.etree.Element xml: XML device element :param HTTPBasicAuthHandler auth: HTTPBasicAuthHandler object, e.g. HTTPDigestAuth :param str base_url: URL to router. @@ -1484,7 +1458,9 @@ def __getattr__(self, name: str): return self.services[name] class ServiceList(list): - """Service list.""" + """ + Service list. + """ def __getattr__(self, name: str): """Direct access to first list entry if brackets omit.""" @@ -1496,7 +1472,9 @@ def __getitem__(self, index): return super().__getitem__(index) class Service: - """TR-064 service.""" + """ + TR-064 service. + """ def __init__(self, auth, verify, base_url, service_type, service_id, scpdurl, control_url, event_sub_url, description_file): # init logger @@ -1526,7 +1504,10 @@ def __getattr__(self, name: str): return self.actions[name] def _fetch_actions(self, scpdurl: str): - """Fetch action description.""" + """ + Fetch action Actions. + """ + request = requests.get(f'{self.base_url}{scpdurl}', verify=self.verify) if request.status_code == 200: xml = etree.parse(BytesIO(request.content)) @@ -1549,7 +1530,9 @@ def _fetch_actions(self, scpdurl: str): # self.logger.debug(f"Service: {self.description_file} scpdurl={self.scpdurl} with actions={self.actions}") class Action: - """TR-064 action. + """ + TR-064 action. + :param lxml.etree.Element xml: XML action element :param HTTPBasicAuthHandler auth: HTTPBasicAuthHandler object, e.g. HTTPDigestAuth :param str base_url: URL to router. @@ -1636,14 +1619,18 @@ def __call__(self, **kwargs): return response class AttributeDict(dict): - """Direct access dict entries like attributes.""" + """ + Direct access dict entries like attributes. + """ def __getattr__(self, name): return self[name] class FritzHome: - """Fritzhome object to communicate with the device.""" + """ + Fritzhome object to communicate with the device via AHA-HTTP Interface. + """ _login_route = "/login_sid.lua?version=2" _event_route = '/query.lua?mq_log=logger:status/log&sid=' @@ -1651,6 +1638,9 @@ class FritzHome: _internet_status_route = '/internet/inetstat_monitor.lua?sid=' def __init__(self, host, ssl, verify, user, password, plugin_instance): + """ + Init the Class FritzHome + """ self._plugin_instance = plugin_instance self._plugin_instance.logger.debug("Init Fritzhome") @@ -1674,6 +1664,9 @@ def __init__(self, host, ssl, verify, user, password, plugin_instance): self.login() def register_item(self, item, avm_data_type: str): + """ + Parsed items valid fpr that class will be registered + """ # handle aha items avm_ain = self._get_item_ain(item) @@ -1683,78 +1676,11 @@ def register_item(self, item, avm_data_type: str): else: self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_ain' is not defined; Item will be ignored.") - def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): - - # ToDo: Implement ReadafterWrite - - # get AIN - ain = self._items[item][1] - - # handle hkr window_open - if avm_data_type in ['set_window_open', 'window_open']: - # set windows open for 12h or closed - seconds = 12*60*60 if bool(item()) else 0 - self.set_window_open(ain, seconds) - - # handle target temperature - elif avm_data_type in ['set_target_temperature', 'target_temperature']: - # - self.set_target_temperature(ain, float(item())) - - # handle hkr boost mode - elif avm_data_type in ['set_hkr_boost', 'hkr_boost']: - # set boost for 12h or off - seconds = 12 * 60 * 60 if bool(item()) else 0 - self.set_boost(ain, seconds) - - # handle light on/off - elif avm_data_type in ['set_simpleonoff', 'simpleonoff']: - if bool(item()): - self.set_state_on(ain) - else: - self.set_state_off(ain) - - # handle level - elif avm_data_type in ['set_level', 'level']: - self.set_level(ain, item()) - - # handle level percentage - elif avm_data_type in ['set_levelpercentage', 'levelpercentage']: - self.set_level_percentage(ain, item()) - - # handle socket switch/relais on/off - elif avm_data_type == 'switch_state': - if bool(item()): - self.set_switch_state_on(ain) - else: - self.set_switch_state_off(ain) - - # handle socket switch/relais toggle - elif avm_data_type == 'switch_toggle': - self.set_switch_state_toggle(ain) - - # handle set hue - elif avm_data_type in ['set_hue', 'hue']: - # Full RGB hue will be supported by Fritzbox approximately from Q2 2022 on: - # Currently, only use default RGB colors that are supported by default (getcolordefaults) - # These default colors have given saturation values. - self.set_color_discrete(ain, int(item()), duration=0) - self._plugin_instance.logger.info(f"Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") - - # handle set saturation - elif avm_data_type in ['set_saturation', 'saturation']: - self._plugin_instance.logger.info(f"Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") - - elif avm_data_type in ['set_colortemperature', 'colortemperature']: - self._plugin_instance.set_color_temp(ain, item(), 8) - - else: - self._plugin_instance.logger.error(f"{avm_data_type} is not defined to be updated.") - def _get_item_ain(self, item) -> str: """ Get AIN of device from item.conf """ + ain_device = None lookup_item = item @@ -1785,6 +1711,9 @@ def _get_item_ain(self, item) -> str: return ain_device def _poll_aha(self): + """ + Poll all AHA Devices for updates + """ self._plugin_instance.logger.debug(f'Starting AHA update loop for instance {self._plugin_instance.get_instance_name()}.') @@ -1851,9 +1780,9 @@ def _poll_aha(self): _device_dict = self.get_devices_as_dict() for ain in _device_dict: - if not self._aha_devices.get(ain): - self._aha_devices[ain] = {} - self._aha_devices[ain]['device_functions'] = [] + self._plugin_instance.logger.debug(f"_poll_aha: handle AIN={ain} with _device_dict[ain]={_device_dict[ain]}") + self._aha_devices[ain] = {} + self._aha_devices[ain]['device_functions'] = [] for entry in _link_base: self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_base[entry]}") @@ -1873,7 +1802,7 @@ def _poll_aha(self): for entry in _link_button: self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_button[entry]}") - if _device_dict[ain].has_button: + if _device_dict[ain].has_alarm: self._aha_devices[ain]['device_functions'].append('alarm') for entry in _link_alarm: self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_alarm[entry]}") @@ -1932,8 +1861,16 @@ def update_items(self): else: self._plugin_instance.logger.warning(f'No values for item={item.id()} at device with AIN={_ain} available.') + def get_item_dict(self): + return self._items + + def get_item_list(self): + return list(self._items.keys()) + def _request(self, url: str, params: dict = None, timeout: int = 10, result: str = 'text') -> Union[str, dict]: - """Send a request with parameters. + """ + Send a request with parameters. + :param url URL to be requested :param params params for request :param timeout timeout @@ -1942,51 +1879,58 @@ def _request(self, url: str, params: dict = None, timeout: int = 10, result: str :type return """ - rsp = self._session.get(url, params=params, timeout=timeout, verify=self._verify) - - status_code = rsp.status_code - if status_code == 200: - self._plugin_instance.logger.debug("Sending HTTP request successful") - if result == 'json': - try: - data = rsp.json() - except JSONDecodeError: - self._plugin_instance.logger.error('Error occurred during parsing request response to json') + try: + rsp = self._session.get(url, params=params, timeout=timeout, verify=self._verify) + except Exception as e: + self._plugin_instance.logger.error(f"Error during GET request {e} occurred.") + else: + status_code = rsp.status_code + if status_code == 200: + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("Sending HTTP request successful") + if result == 'json': + try: + data = rsp.json() + except JSONDecodeError: + self._plugin_instance.logger.error('Error occurred during parsing request response to json') + else: + return data else: - self._plugin_instance.logger.error(type(data)) - return data + return rsp.text.strip() + elif status_code == 403: + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug("HTTP access denied. Try to get new Session ID.") else: - return rsp.text.strip() - elif status_code == 403: - self._plugin_instance.logger.debug("HTTP access denied. Try to get new Session ID.") - else: - self._plugin_instance.logger.error(f"HTTP request error code: {status_code}") - rsp.raise_for_status() - self._plugin_instance.logger.debug(f"Url: {url}") - self._plugin_instance.logger.debug(f"Params: {params}") + self._plugin_instance.logger.error(f"HTTP request error code: {status_code}") + rsp.raise_for_status() + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Url: {url}") + self._plugin_instance.logger.debug(f"Params: {params}") def _login_request(self, username=None, challenge_response=None): - """Send a login request with parameters.""" + """ + Send a login request with parameters. + """ + url = self.get_prefixed_host() + self._login_route - # self._plugin_instance.logger.debug(f"_login_request: url={url}") params = {} if username: params["username"] = username if challenge_response: params["response"] = challenge_response - # self._plugin_instance.logger.debug(f"_login_request: params={params}") plain = self._request(url, params) - # self._plugin_instance.logger.debug(f"_login_request: plain={plain}") dom = ElementTree.fromstring(plain) sid = dom.findtext("SID") challenge = dom.findtext("Challenge") blocktime = int(dom.findtext("BlockTime")) - # self._plugin_instance.logger.debug(f"_login_request: sid={sid}, challenge={challenge}, blocktime={blocktime}") return sid, challenge, blocktime def _logout_request(self): - """Send a logout request.""" + """ + Send a logout request. + """ + url = self.get_prefixed_host() + self._login_route params = {"logout": "1", "sid": self._sid} @@ -2025,7 +1969,9 @@ def _calculate_pbkdf2_response(challenge: str, password: str) -> str: return f"{challenge_parts[4]}${hash2.hex()}" def _aha_request(self, cmd, ain=None, param=None, rf='str'): - """Send an AHA request.""" + """ + Send an AHA request. + """ url = self.get_prefixed_host() + self._homeauto_route params = {"switchcmd": cmd, "sid": self._sid} @@ -2079,7 +2025,10 @@ def login(self): self._logged_in = True def logout(self): - """Logout.""" + """ + Logout. + """ + self._plugin_instance.logger.debug("AHA logout called") self._logout_request() self._sid = None @@ -2104,12 +2053,15 @@ def check_sid(self): self._plugin_instance.logger.info(f"Session ID is still valid.") def get_prefixed_host(self): - """Choose the correct protocol prefix for the host. + """ + Choose the correct protocol prefix for the host. + Supports three input formats: - https://(requests use strict certificate validation by default) - http:// (unecrypted) - (unencrypted) """ + host = self._host if not host.startswith("https://") or not host.startswith("http://"): if self._ssl: @@ -2118,7 +2070,13 @@ def get_prefixed_host(self): host = "http://" + host return host + # device-related commands + def update_devices(self): + """ + Updating AHA Devices respective dictonary + """ + self._plugin_instance.logger.info("Updating Devices ...") if self._devices is None: self._devices = {} @@ -2134,7 +2092,10 @@ def update_devices(self): return True def _get_listinfo_elements(self, entity_type): - """Get the DOM elements for the entity list.""" + """ + Get the DOM elements for the entity list. + """ + plain = self._aha_request("get" + entity_type + "listinfos") if plain is None: @@ -2144,11 +2105,17 @@ def _get_listinfo_elements(self, entity_type): return dom.findall(entity_type) def get_device_elements(self): - """Get the DOM elements for the device list.""" + """ + 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.""" + """ + Get the DOM element for the specified device. + """ + elements = self.get_device_elements() for element in elements: if element.attrib["identifier"] == ain: @@ -2156,69 +2123,125 @@ def get_device_element(self, ain): return None def get_devices(self): - """Get the list of all known devices.""" + """ + Get the list of all known devices. + """ + return list(self.get_devices_as_dict().values()) def get_devices_as_dict(self): - """Get the list of all known devices.""" + """ + Get the list of all known devices. + """ + # if self._devices is None: self.update_devices() return self._devices def get_device_by_ain(self, ain): - """Return a device specified by the AIN.""" + """ + Return a device specified by the AIN. + """ + return self.get_devices_as_dict()[ain] def get_device_present(self, ain): - """Get the device presence.""" + """ + Get the device presence. + """ + return self._aha_request("getswitchpresent", ain=ain, rf='bool') def get_device_name(self, ain): - """Get the device name.""" + """ + Get the device name. + """ + return self._aha_request("getswitchname", ain=ain) # switch-related commands def get_switch_state(self, ain): - """Get the switch state.""" + """ + Get the switch state. + """ + return self._aha_request("getswitchstate", ain=ain, rf='bool') def set_switch_state_on(self, ain): - """Set the switch to on state.""" + """ + Set the switch to on state. + """ + return self._aha_request("setswitchon", ain=ain, rf='bool') def set_switch_state_off(self, ain): - """Set the switch to off state.""" + """ + Set the switch to off state. + """ + return self._aha_request("setswitchoff", ain=ain, rf='bool') def set_switch_state_toggle(self, ain): - """Toggle the switch state.""" + """ + Toggle the switch state. + """ + return self._aha_request("setswitchtoggle", ain=ain, rf='bool') + def set_switch_state(self, ain, state): + """ + Set the switch to on state. + """ + + if bool(state): + return self.set_switch_state_on(ain) + else: + return self.set_switch_state_off(ain) + def get_switch_power(self, ain): - """Get the switch power consumption.""" + """ + Get the switch power consumption. + """ + return self._aha_request("getswitchpower", ain=ain, rf='int') def get_switch_energy(self, ain): - """Get the switch energy.""" + """ + Get the switch energy. + """ + return self._aha_request("getswitchenergy", ain=ain, rf='int') # thermostat-related commands def get_temperature(self, ain): - """Get the device temperature sensor value.""" + """ + Get the device temperature sensor value. + """ + return self._aha_request("gettemperature", ain=ain, rf='float') / 10.0 def _get_temperature(self, ain, name): + """ + Get temperature raw value + """ + plain = self._aha_request(name, ain=ain, rf='float') return (plain - 16) / 2 + 8 def get_target_temperature(self, ain): - """Get the thermostate target temperature.""" + """ + Get the thermostate target temperature. + """ + return self._get_temperature(ain, "gethkrtsoll") def set_target_temperature(self, ain, temperature): - """Set the thermostate target temperature.""" + """ + Set the thermostate target temperature. + """ + temp = int(16 + ((float(temperature) - 8) * 2)) if temp < min(range(16, 56)): @@ -2229,46 +2252,108 @@ def set_target_temperature(self, ain, temperature): self._aha_request("sethkrtsoll", ain=ain, param={'param': temp}) def set_window_open(self, ain, seconds): - """Set the thermostate target temperature.""" + """ + Set windows open. + """ + + if isinstance(seconds, bool): + seconds = 12*60*60 if seconds else 0 endtimestamp = int(time.time() + seconds) self._aha_request("sethkrwindowopen", ain=ain, param={'endtimestamp': endtimestamp}) + def get_window_open(self, ain): + """ + Get windows open. + """ + + return self.get_devices_as_dict()[ain].window_open + def set_boost(self, ain, seconds): - """Set the thermostate target temperature.""" + """ + Set the thermostate to boost. + """ + + if isinstance(seconds, bool): + seconds = 12*60*60 if seconds else 0 endtimestamp = int(time.time() + seconds) self._aha_request("sethkrboost", ain=ain, param={'endtimestamp': endtimestamp}) + def get_boost(self, ain): + """ + Get boost status. + """ + + return self.get_devices_as_dict()[ain].boost_active + def get_comfort_temperature(self, ain): - """Get the thermostate comfort temperature.""" + """ + Get the thermostate comfort temperature. + """ + return self._get_temperature(ain, "gethkrkomfort") def get_eco_temperature(self, ain): - """Get the thermostate eco temperature.""" + """ + Get the thermostate eco temperature. + """ + return self._get_temperature(ain, "gethkrabsenk") def get_device_statistics(self, ain): - """Get device statistics.""" + """ + Get device statistics. + """ + plain = self._aha_request("getbasicdevicestats", ain=ain) return plain # Lightbulb-related commands def set_state_off(self, ain): - """Set the switch/actuator/lightbulb to on state.""" + """ + Set the switch/actuator/lightbulb to on state. + """ + self._aha_request("setsimpleonoff", ain=ain, param={'onoff': 0}) def set_state_on(self, ain): - """Set the switch/actuator/lightbulb to on state.""" + """ + Set the switch/actuator/lightbulb to on state. + """ + self._aha_request("setsimpleonoff", ain=ain, param={'onoff': 1}) def set_state_toggle(self, ain): - """Toggle the switch/actuator/lightbulb state.""" + """ + Toggle the switch/actuator/lightbulb state. + """ + self._aha_request("setsimpleonoff", ain=ain, param={'onoff': 2}) + def set_state(self, ain, state): + """ + Set the switch/actuator/lightbulb to a state. + """ + + if bool(state): + self.set_state_on(ain) + else: + self.set_state_off(ain) + + def get_state(self, ain): + """ + Get the switch/actuator/lightbulb to a state. + """ + + return self.get_devices_as_dict()[ain].state + def set_level(self, ain, level): - """Set level/brightness/height in interval [0,255].""" + """ + Set level/brightness/height in interval [0,255]. + """ + if level < 0: level = 0 # 0% elif level > 255: @@ -2276,17 +2361,41 @@ def set_level(self, ain, level): self._aha_request("setlevel", ain=ain, param={'level': int(level)}) + def get_level(self, ain): + """ + get level/brightness/height in interval [0,255]. + """ + + return self.get_devices_as_dict()[ain].level + def set_level_percentage(self, ain, level): - """Set level/brightness/height in interval [0,100].""" + """ + Set level/brightness/height in interval [0,100]. + """ + # Scale percentage to [0,255] interval self.set_level(ain, int(level * 2.55)) + def get_level_percentage(self, ain): + """ + get level/brightness/height in interval [0,100]. + """ + + return self.get_devices_as_dict()[ain].level_percentage + def _get_colordefaults(self, ain): + """ + Get colour defaults + """ + plain = self._aha_request("getcolordefaults", ain=ain) return ElementTree.fromstring(plain) def get_colors(self, ain): - """Get colors (HSV-space) supported by this lightbulb.""" + """ + Get colors (HSV-space) supported by this lightbulb. + """ + colordefaults = self._get_colordefaults(ain) colors = {} for hs in colordefaults.iter('hs'): @@ -2304,10 +2413,12 @@ def get_colors(self, ain): return colors def set_color(self, ain, hsv, duration=0, mapped=True): - """Set hue and saturation. + """ + Set hue and saturation. hsv: HUE colorspace element obtained from get_colors() duration: Speed of change in seconds, 0 = instant """ + params = { 'hue': int(hsv[0]), 'saturation': int(hsv[1]), @@ -2363,28 +2474,50 @@ def set_color_discrete(self, ain, hue, duration=0): else: self._plugin_instance.logger.error(f'setcolor hue out of range (hue={hue})') + def get_hue(self, ain): + """ + Get Hue value. + """ + return self.get_devices_as_dict()[ain].hue + def get_color_temps(self, ain): - """Get temperatures supported by this lightbulb.""" + """ + Get temperatures supported by this lightbulb. + """ + colordefaults = self._get_colordefaults(ain) temperatures = [] for temp in colordefaults.iter('temp'): temperatures.append(temp.get("value")) return temperatures - def set_color_temp(self, ain, temperature, duration=0): - """Set color temperature. + def set_color_temp(self, ain, temperature, duration=1): + """ + Set color temperature. 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) + def get_color_temp(self, ain): + """ + Get color temperature. + """ + + return self.get_devices_as_dict()[ain].color_temp + # Template-related commands def update_templates(self): + """ + Update templates + """ + self._plugin_instance.logger.info("Updating Templates ...") if self._templates is None: self._templates = {} @@ -2402,25 +2535,40 @@ def update_templates(self): return True def get_template_elements(self): - """Get the DOM elements for the template list.""" + """ + Get the DOM elements for the template list. + """ + return self._get_listinfo_elements("template") def get_templates(self): - """Get the list of all known templates.""" + """ + Get the list of all known templates. + """ + return list(self.get_templates_as_dict().values()) def get_templates_as_dict(self): - """Get the list of all known templates.""" + """ + Get the list of all known templates. + """ + if self._templates is None: self.update_templates() return self._templates def get_template_by_ain(self, ain): - """Return a template specified by the AIN.""" + """ + Return a template specified by the AIN. + """ + return self.get_templates_as_dict()[ain] def apply_template(self, ain): - """Applies a template.""" + """ + Applies a template. + """ + self._aha_request("applytemplate", ain=ain) # Log-related commands @@ -2428,6 +2576,7 @@ def apply_template(self, ain): def get_device_log_from_lua(self): """ Gets the Device Log from the LUA HTTP Interface via LUA Scripts (more complete than the get_device_log TR-064 version. + :return: Array of Device Log Entries (text, type, category, timestamp, date, time) """ @@ -2448,6 +2597,67 @@ def get_device_log_from_lua(self): newlog.append([l_text, l_type, l_cat, l_ts, l_date, l_time]) return newlog + # Handling of updated items + + def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): + """ + Updated Item will be processed and value communicated to AVM Device + """ + + # define set method per avm_data_type + _dispatcher = {'window_open': (self.set_window_open, self.get_window_open), + 'target_temperature': (self.set_target_temperature, self.get_target_temperature), + 'hkr_boost': (self.set_boost, self.get_boost), + 'simpleonoff': (self.set_state, self.get_state), + 'level': (self.set_level, self.get_level), + 'levelpercentage': (self.set_level_percentage, self.get_level_percentage), + 'switch_state': (self.set_switch_state, self.get_switch_state), + 'switch_toggle': (self.set_switch_state_toggle, self.get_switch_state), + 'colortemperature': (self.set_color_temp, self.get_color_temp), + 'hue': (self.set_color_discrete, self.get_hue), + } + + # get AIN + _ain = self._items[item][1] + + # adapt avm_data_type by removing 'set_' + if avm_data_type.startswith('set_'): + avm_data_type = avm_data_type[len('set_'):] + + # logs message for upcoming/limited functionality + if avm_data_type == 'hue': + # Full RGB hue will be supported by Fritzbox approximately from Q2 2022 on: + # Currently, only use default RGB colors that are supported by default (getcolordefaults) + # These default colors have given saturation values. + self._plugin_instance.logger.info( + f"Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") + elif avm_data_type == 'saturation': + self._plugin_instance.logger.info( + f"Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") + + # Call set method per avm_data_type + to_be_set_value = item() + try: + _dispatcher[avm_data_type][0](_ain, to_be_set_value) + except Exception: + self._plugin_instance.logger.error(f"{avm_data_type} is not defined to be updated.") + + # handle readafterwrite + if readafterwrite: + wait = float(readafterwrite) + time.sleep(wait) + try: + set_value = _dispatcher[avm_data_type][1](_ain) + except Exception: + self._plugin_instance.logger.error(f"{avm_data_type} is not defined to be read.") + else: + item(set_value, self._plugin_instance.get_instance_name()) + if set_value != to_be_set_value: + self._plugin_instance.logger.warning(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") + else: + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") + class FritzhomeDeviceFeatures(IntFlag): HANFUN_DEVICE = 0x0001 # Bit 0: HAN-FUN Gerät @@ -2498,7 +2708,8 @@ def _update_from_node(self, node): # XML Helpers - def get_node_value(self, elem, node): + @staticmethod + def get_node_value(elem, node): return elem.findtext(node) def get_node_value_as_int(self, elem, node) -> int: @@ -2679,7 +2890,8 @@ def _update_from_node(self, node): except ValueError: pass - def get_node_value(self, elem, node): + @staticmethod + def get_node_value(elem, node): return elem.findtext(node) def get_node_value_as_int(self, elem, node) -> int: @@ -2732,8 +2944,7 @@ def _update_lightbulb_from_node(self, node): try: self.color_mode = colorcontrol_element.attrib.get("current_mode") - self.supported_color_mode = colorcontrol_element.attrib.get( - "supported_modes") + self.supported_color_mode = colorcontrol_element.attrib.get("supported_modes") except ValueError: pass @@ -2741,13 +2952,11 @@ def _update_lightbulb_from_node(self, node): try: self.hue = self.get_node_value_as_int(colorcontrol_element, "hue") - self.saturation = self.get_node_value_as_int(colorcontrol_element, - "saturation") + self.saturation = self.get_node_value_as_int(colorcontrol_element, "saturation") self.unmapped_hue = self.get_node_value_as_int(colorcontrol_element, "unmapped_hue") - self.unmapped_saturation = self.get_node_value_as_int(colorcontrol_element, - "unmapped_saturation") + self.unmapped_saturation = self.get_node_value_as_int(colorcontrol_element, "unmapped_saturation") except ValueError: # reset values after color mode changed self.hue = None @@ -2756,8 +2965,7 @@ def _update_lightbulb_from_node(self, node): self.unmapped_saturation = None try: - self.color_temp = self.get_node_value_as_int(colorcontrol_element, - "temperature") + self.color_temp = self.get_node_value_as_int(colorcontrol_element, "temperature") except ValueError: # reset values after color mode changed @@ -2926,24 +3134,19 @@ def has_temperature_sensor(self): def _update_temperature_from_node(self, node): temperature_element = node.find("temperature") try: - self.offset = ( - self.get_node_value_as_int(temperature_element, "offset") / 10.0 - ) + 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 - ) + self.temperature = (self.get_node_value_as_int(temperature_element, "celsius") / 10.0) except ValueError: pass humidity_element = node.find("humidity") if humidity_element is not None: try: - self.rel_humidity = self.get_node_value_as_int(humidity_element, - "rel_humidity") + self.rel_humidity = self.get_node_value_as_int(humidity_element, "rel_humidity") except ValueError: pass @@ -3046,12 +3249,11 @@ def get_eco_temperature(self): def get_hkr_state(self): """Get the thermostate state.""" try: - return { - 126.5: "off", - 127.0: "on", - self.eco_temperature: "eco", - self.comfort_temperature: "comfort", - }[self.target_temperature] + return {126.5: "off", + 127.0: "on", + self.eco_temperature: "eco", + self.comfort_temperature: "comfort", + }[self.target_temperature] except KeyError: return "manual" @@ -3060,12 +3262,11 @@ def set_hkr_state(self, state): Possible values for state are: 'on', 'off', 'comfort', 'eco'. """ try: - value = { - "off": 0, - "on": 100, - "eco": self.eco_temperature, - "comfort": self.comfort_temperature, - }[state] + value = {"off": 0, + "on": 100, + "eco": self.eco_temperature, + "comfort": self.comfort_temperature, + }[state] except KeyError: return @@ -3123,7 +3324,11 @@ def _update_from_node(self, node): class Callmonitor: + def __init__(self, host, port, callback, call_monitor_incoming_filter, plugin_instance): + """ + Inits the Callmonitor class + """ self._plugin_instance = plugin_instance self._plugin_instance.logger.debug("Init Callmonitor") @@ -3178,6 +3383,7 @@ def disconnect(self): """ Disconnects from the call monitor of the AVM device """ + self._plugin_instance.logger.debug("MonitoringService: disconnecting") self._listen_active = False self._stop_counter('incoming') @@ -3236,6 +3442,9 @@ def register_item(self, item, avm_data_type: str): self._items[item] = (avm_data_type, None) def set_callmonitor_item_values_initially(self): + """ + Set callmonitor related item values after startup + """ _calllist = self._plugin_instance.get_calllist() @@ -3359,18 +3568,21 @@ def set_callmonitor_item_values_initially(self): item(duration, self._plugin_instance.get_shortname()) break - def get_items(self): + def get_item_list(self): return list(self._items.keys()) - def get_trigger_items(self): + def get_trigger_item_list(self): return list(self._trigger_items.keys()) - def get_items_incoming(self): + def get_item_incoming_list(self): return list(self._items_incoming.keys()) - def get_items_outgoing(self): + def get_item_outgoing_list(self): return list(self._items_outgoing.keys()) + def get_item_all_list(self): + return self.get_item_list() + self.get_trigger_item_list() + self.get_item_incoming_list() + self.get_item_outgoing_list() + @property def get_item_count_total(self): """ @@ -3489,13 +3701,13 @@ def _parse_line(self, line: str): except Exception as e: self._plugin_instance.logger.error(f"MonitoringService: {type(e).__name__} while handling Callmonitor response: {e}") - def _trigger(self, call_from: str, call_to: str, time: str, callid: str, event: str, branch: str): + def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: str, branch: str): """ Triggers the event: sets item values and looks up numbers in the phone book. """ if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"Event: {event}, Call From: {call_from}, Call To: {call_to}, Time: {time}, CallID: {callid}, Branch: {branch}") + self._plugin_instance.logger.debug(f"Event={event}, Call from={call_from}, Call to={call_to}, Time={dt}, CallID={callid}, Branch={branch}") # set generic item value for item in self._items: @@ -3599,7 +3811,7 @@ def _trigger(self, call_from: str, call_to: str, time: str, callid: str, event: if callid == self._call_outgoing_cid: if self._duration_item_out is not None: # start counter thread only if duration item set and call is outgoing self._stop_counter('outgoing') # stop potential running counter for parallel (older) outgoing call - self._start_counter(time, 'outgoing') + self._start_counter(dt, 'outgoing') for item in self._items_outgoing: avm_data_type = self._items[item][0] if avm_data_type == 'call_event_outgoing': @@ -3611,7 +3823,7 @@ def _trigger(self, call_from: str, call_to: str, time: str, callid: str, event: self._stop_counter('incoming') # stop potential running counter for parallel (older) incoming call if self._plugin_instance.debug_log: self._plugin_instance.logger.debug("Starting Counter for Call Time") - self._start_counter(time, 'incoming') + self._start_counter(dt, 'incoming') for item in self._items_incoming: avm_data_type = self._items[item][0] if avm_data_type == 'call_event_incoming': diff --git a/plugin.yaml b/plugin.yaml index 4185070de..173351786 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -84,6 +84,17 @@ parameters: description: de: '(optional) Aktiviert oder deaktiviert den Zugriff auf AVM Smarthome Geräte mit dem AHA HTTP Interface.' en: '(optional) Activates or deactivates access to AVM smarthome devices via AHA HTTP interface' + webif_pagelength: + type: int + default: 100 + valid_list: + - -1 + - 25 + - 50 + - 100 + description: + de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden' + en: 'Amount of items being listed in a web interface table per page by default' item_attributes: # Definition of item attributes defined by this plugin @@ -308,7 +319,6 @@ item_structs: myfritz: type: bool avm2_data_type@instance: myfritz_status - monitor: trigger: type: bool @@ -316,7 +326,6 @@ item_structs: avm2_incoming_allowed@instance: xxxxxxxx avm2_target_number@instance: xxxxxxxx enforce_updates: yes - incoming: is_call_incoming: type: bool @@ -339,7 +348,6 @@ item_structs: event: type: str avm2_data_type@instance: call_event_incoming - outgoing: is_call_outgoing: type: bool @@ -362,7 +370,6 @@ item_structs: event: type: str avm2_data_type@instance: call_event_outgoing - newest: direction: type: str @@ -372,7 +379,6 @@ item_structs: type: str avm2_data_type@instance: call_event cache: yes - tam: type: bool visu_acl: rw @@ -399,7 +405,6 @@ item_structs: type: num visu_acl: ro avm2_data_type@instance: tam_total_message_number - deflection: type: bool visu_acl: rw @@ -430,7 +435,6 @@ item_structs: type: num visu_acl: ro avm2_data_type@instance: deflection_phonebook_id - wan: connection_status: type: str @@ -480,7 +484,6 @@ item_structs: type: bool visu_acl: rw enforce_updates: yes - wlan: wlan_1: type: bool @@ -517,27 +520,22 @@ item_structs: visu_acl: rw avm2_data_type@instance: wlan_guest_time_remaining # Guest avm2_wlan_index@instance: 3 - device: avm2_data_type@instance: network_device type: bool visu_acl: ro - ip: type: str avm2_data_type@instance: device_ip visu_acl: ro - connection_type: type: str avm2_data_type@instance: device_connection_type visu_acl: ro - hostname: type: str avm2_data_type@instance: device_hostname visu_acl: ro - smarthome_general: name: avm2_data_type@instance: device_name @@ -560,18 +558,17 @@ item_structs: functions: avm2_data_type@instance: device_functions type: list - smarthome_hkr: current_temperature: - avm2_data_type@instance: current_temperature - type: num + avm2_data_type@instance: current_temperature + type: num target_temperature: avm2_data_type@instance: target_temperature avm2_read_after_write@instance: 5 type: num - # set_target_temperature: - # avm2_data_type@instance: set_target_temperature - # type: num + set_target_temperature: + avm2_data_type@instance: set_target_temperature + type: num comfort_temperature: avm2_data_type@instance: temperature_comfort type: num @@ -596,15 +593,15 @@ item_structs: errorcode: avm2_data_type@instance: errorcode type: num - # set_window_open: - # avm2_data_type@instance: set_window_open - # type: bool + set_window_open: + avm2_data_type@instance: set_window_open + type: bool windowopenactiveendtime: avm2_data_type@instance: windowopenactiveendtime type: num - # set_hkr_boost: - # avm2_data_type@instance: set_hkr_boost - # type: bool + set_hkr_boost: + avm2_data_type@instance: set_hkr_boost + type: bool hkr_boost: avm2_data_type@instance: hkr_boost type: bool @@ -617,20 +614,17 @@ item_structs: device_lock: avm2_data_type@instance: device_lock type: bool - smarthome_temperature_sensor: - temperatur: + current_temperature: avm2_data_type@instance: current_temperature type: num temperature_offset: avm2_data_type@instance: temperature_offset type: num - smarthome_alert: state: avm2_data_type@instance: alert type: bool - smarthome_switch: switch_state: avm2_data_type@instance: switch_state @@ -639,7 +633,6 @@ item_structs: avm2_data_type@instance: switch_toggle type: bool enforce_updates: yes - smarthome_powermeter: power: avm2_data_type@instance: power @@ -655,8 +648,240 @@ item_structs: # Definition of item attributes that only have a common prefix (enter 'item_attribute_prefixes: NONE' or ommit this section, if section should be empty) # NOTE: This section should only be used, if really nessesary (e.g. for the stateengine plugin) -plugin_functions: NONE - # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) +plugin_functions: + # Definition of function interface of the plugin + + cancel_call: + type: void + description: + de: "Beendet einen aktiven Anruf." + en: "Cancels an active call." + parameters: + # This function has no parameters + + get_call_origin: + type: str + description: + de: "Gib den Namen des Telefons zurück, das aktuell als 'call origin' gesetzt ist." + en: "Gets the phone name, currently set as 'call origin'." + parameters: + # This function has no parameters + + get_calllist: + type: list(dict(str)) + description: + de: "Ermittelt ein Array mit dicts aller Einträge der Anrufliste (Attribute 'Id', 'Type', 'Caller', 'Called', 'CalledNumber', 'Name', 'Numbertype', 'Device', 'Port', 'Date',' Duration' (einige optional))." + en: "Returns an array of dicts with all calllist entries (attributes 'Id', 'Type', 'Caller', 'Called', 'CalledNumber', 'Name', 'Numbertype', 'Device', 'Port', 'Date', 'Duration' (some optional))." + parameters: + filter_incoming: + type: str + default: '' + description: + de: "Filter, um nur die Anrufe zu erhalten, die zu einer bestimmten angerufenen Nummer gehören." + en: "Filter to filter calls to a specific destination phone number." + phonebook_id: + type: int + default: 0 + description: + de: "ID des Telefonbuchs, in dem nachgeschlagen werden soll." + en: "ID of the phone book, in which numbers should be looked up." + + get_contact_name_by_phone_number: + type: str + description: + de: "Durchsucht das Telefonbuch mit einer (vollständigen) Telefonnummer nach Kontakten. Falls kein Name gefunden wird, wird die Telefonnummer zurückgeliefert." + en: "Searches the phonebook for a contact by a given (complete) phone number. In case no name is found, the phone number is returned." + parameters: + phone_number: + type: str + description: + de: "Vollständige Telefonnummer" + en: "Complete phone number" + phonebook_id: + type: int + default: 0 + description: + de: "ID des Telefonbuchs, in dem nachgeschlagen werden soll." + en: "ID of the phone book, in which numbers should be looked up." + + get_device_log_from_lua: + type: list(list(str)) + description: + de: "Ermittelt die Logeinträge auf dem Gerät über die LUA Schnittstelle /query.lua?mq_log=logger:status/log." + en: "Gets the log entries on the device via the LUA interface /query.lua?mq_log=logger:status/log." + parameters: + # This function has no parameters + + get_device_log_from_tr064: + type: list(str) + description: + de: "Ermittelt die Logeinträge auf dem Gerät über die TR-064 Schnittstelle." + en: "Gets the log entries on the device via the TR-064 interface." + parameters: + # This function has no parameters + + get_host_details: + type: dict(str) + description: + de: "Ermittelt die Informationen zu einem Host an einem angegebenen Index." + en: "Gets the information of a hosts at a specific index." + parameters: + index: + type: int + description: + de: "Index" + en: "Index" + + get_hosts: + type: list(dict(str)) + description: + de: "Ermittelt ein Array mit den Namen aller verbundener Hosts." + en: "Gets the name of all connected hosts as an array." + parameters: + only_active: + type: bool + description: + de: "True, wenn nur aktuell aktive Hosts zurückgegeben werden sollen." + en: "True, if only active hosts shall be returned." + + get_phone_name: + type: str + description: + de: "Gibt den Namen eines Telefons an einem Index zurück. Der zurückgegebene Wert kann in 'set_call_origin' verwendet werden." + en: "Get the phone name at a specific index. The returend value can be used as phone_name for set_call_origin." + parameters: + index: + type: int + description: + de: "Index" + en: "Index" + + get_phone_numbers_by_name: + type: dict(dict(str)) + description: + de: "Durchsucht das Telefonbuch mit einem Namen nach nach Kontakten und liefert die zugehörigen Telefonnummern." + en: "Searches the phonebook for a contact by a given name and returns the corresponding phone numbers." + parameters: + name: + type: str + description: + de: "Anteiliger oder vollständiger Name des Kontakts." + en: "Partial or full name of the contact." + phonebook_id: + type: int + default: 0 + description: + de: "ID des Telefonbuchs, in dem nachgeschlagen werden soll." + en: "ID of the phone book, in which numbers should be looked up." + + is_host_active: + type: bool + description: + de: "Prüft, ob eine MAC Adresse auf dem Gerät aktiv ist. Das kann bspw. für die Umsetzung einer Präsenzerkennung genutzt werden." + en: "Checks if a MAC address is active on the FritzDevice, e.g. the status can be used for simple presence detection." + parameters: + mac_address: + type: mac + description: + de: "MAC Adresse" + en: "MAC address" + + reboot: + type: void + description: + de: "Startet das Gerät neu." + en: "Reboots the device." + parameters: + # This function has no parameters + + reconnect: + type: void + description: + de: "Verbindet das Gerät neu mit dem WAN (Wide Area Network)." + en: "Reconnects the device to the WAN (Wide Area Network)." + parameters: + # This function has no parameters + + set_call_origin: + type: void + description: + de: "Setzt den 'call origin', bspw. vor dem Aufruf von 'start_call'." + en: "Sets the 'call origin', e.g. before running 'start_call'." + parameters: + phone_name: + type: mac + description: + de: "Identifikator des Telefons, dass als 'call origin' gesetzt werden soll. Bspw. zwei Sterne gefolgt von '610' für ein internes Gerät." + en: "Full phone identifier, could be e.g. two asterix followed by '610' for an internal device." + + start_call: + type: void + description: + de: "Startet einen Anruf an eine übergebene Telefonnummer (intern oder extern)." + en: "Starts a call for a given phone number (internal or external)." + parameters: + phone_number: + type: str + description: + de: "Vollständige Telefonnummer, die angerufen werden soll." + en: "Full phone number to call" + + wol: + type: void + description: + de: "Sendet einen WOL (WakeOnLAN) Befehl an eine MAC Adresse." + en: "Sends a WOL (WakeOnLAN) command to a MAC address." + parameters: + mac_address: + type: mac + description: + de: "MAC Adresse" + en: "MAC address" + + get_number_of_deflections: + type: bool + description: + de: "Liefert die Anzahl der Rufumleitungen zurück." + en: "Returns Number of set deflections." + parameters: + # This function has no parameters + + get_deflection: + type: bool + description: + de: "Liefert die Details der Rufumleitung der angegebenen ID zurück (Default-ID = 0)" + en: "Returns details of deflection with given deflection_id (default id = 0)" + parameters: + deflection_id: + type: int + description: + de: "Identifikator der abzufragenden Rufumleitung." + en: "Identifier of deflection." + + get_deflections: + type: bool + description: + de: "Liefert die Details aller Rufumleitungen zurück." + en: "Returns details of all deflections." + parameters: + # This function has no parameters + + set_deflection_enable: + type: bool + description: + de: "Schaltet die Rufumleitung mit angegebener ID an oder aus." + en: "Enables or disables deflection with given ID." + parameters: + deflection_id: + type: int + description: + de: "Identifikator der abzufragenden Rufumleitung." + en: "identifier of deflection." + enable: + type: bool + description: + de: "An / Aus" + en: "Enable / Disable" logic_parameters: NONE # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) diff --git a/webif/__init__.py b/webif/__init__.py index cfe41ab94..5fc5cf7a5 100644 --- a/webif/__init__.py +++ b/webif/__init__.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### -# Copyright 2020- +# Copyright 2022- Michael Wenzel wenzel_michael@web.de ######################################################################### # This file is part of SmartHomeNG. # https://www.smarthomeNG.de # https://knx-user-forum.de/forum/supportforen/smarthome-py # -# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and -# upwards. +# Part of AVM2 Plugin # # SmartHomeNG is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,20 +24,15 @@ # ######################################################################### -import datetime -import time -import os - +import json from lib.item import Items from lib.model.smartplugin import SmartPluginWebIf - # ------------------------------------------ # Webinterface of the plugin # ------------------------------------------ import cherrypy -import csv from jinja2 import Environment, FileSystemLoader @@ -53,29 +47,62 @@ def __init__(self, webif_dir, plugin): :type webif_dir: str :type plugin: object """ + self.logger = plugin.logger self.webif_dir = webif_dir self.plugin = plugin self.items = Items.get_instance() - self.tplenv = self.init_template_environment() + self.logger.debug(f"Init WebIF of {self.plugin.get_shortname()}") @cherrypy.expose - def index(self, reload=None): + def index(self, reload=None, action=None): """ Build index.html for cherrypy - Render the template and return the html file to be delivered to the browser :return: contents of the template after beeing rendered """ + + if self.plugin.get_fritz_device(): + raw_items = self.plugin.get_fritz_device().get_item_list() + tr064_items = sorted(raw_items, key=lambda k: str.lower(k['_path'])) + tr064_item_count = len(raw_items) + else: + tr064_items = None + tr064_item_count = None + + if self.plugin.get_fritz_home(): + raw_items = self.plugin.get_fritz_home().get_item_list() + aha_items = sorted(raw_items, key=lambda k: str.lower(k['_path'])) + aha_item_count = len(raw_items) + else: + aha_items = None + aha_item_count = None + + if self.plugin.get_monitoring_service(): + raw_items = self.plugin.get_monitoring_service().get_item_all_list() + call_monitor_items = sorted(raw_items, key=lambda k: str.lower(k['_path'])) + call_monitor_item_count = len(raw_items) + else: + call_monitor_items = None + call_monitor_item_count = None + tmpl = self.tplenv.get_template('index.html') - # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) - return tmpl.render(p=self.plugin, - items=sorted(self.items.return_items(), key=lambda k: str.lower(k['_path'])), - item_count=0) + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), + plugin_version=self.plugin.get_version(), + plugin_info=self.plugin.get_info(), + tr064_items=tr064_items, + tr064_item_count=tr064_item_count, + call_monitor_items=call_monitor_items, + call_monitor_item_count=call_monitor_item_count, + aha_items=aha_items, + aha_item_count=aha_item_count, + p=self.plugin, + webif_pagelength=self.plugin.webif_pagelength, + ) @cherrypy.expose def get_data_html(self, dataSet=None): @@ -87,18 +114,43 @@ def get_data_html(self, dataSet=None): :param dataSet: Dataset for which the data should be returned (standard: None) :return: dict with the data needed to update the web page. """ + if dataSet is None: - # get the new data - data = {} - - # data['item'] = {} - # for i in self.plugin.items: - # data['item'][i]['value'] = self.plugin.getitemvalue(i) - # - # return it as json the the web page - # try: - # return json.dumps(data) - # except Exception as e: - # self.logger.error("get_data_html exception: {}".format(e)) + data = dict() + if self.plugin.get_monitoring_service(): + data['call_monitor'] = {} + for item in self.plugin.get_monitoring_service().get_item_all_list(): + data['call_monitor'][item.id()] = {} + data['call_monitor'][item.id()]['value'] = item() + data['call_monitor'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') + data['call_monitor'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + + if self.plugin.get_fritz_device(): + data['tr064_items'] = {} + for item in self.plugin.get_fritz_device().get_item_list(): + data['tr064_items'][item.id()] = {} + data['tr064_items'][item.id()]['value'] = item() + data['tr064_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') + data['tr064_items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + + if self.plugin.get_fritz_home(): + data['aha_items'] = {} + for item in self.plugin.get_fritz_home().get_item_list(): + data['aha_items'][item.id()] = {} + data['aha_items'][item.id()]['value'] = item() + data['aha_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') + data['aha_items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + + try: + return json.dumps(data, default=str) + except Exception as e: + self.logger.error(f"get_data_html exception: {e}") return {} + @cherrypy.expose + def reboot(self): + self.plugin.get_fritz_device().reboot() + + @cherrypy.expose + def reconnect(self): + self.plugin.get_fritz_device().reconnect() diff --git a/webif/templates/index.html b/webif/templates/index.html index 3f64e8bd1..ad0ca466f 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -1,127 +1,373 @@ {% extends "base_plugin.html" %} - {% set logo_frame = false %} +{% set update_interval = 5000 %} + +{% block pluginstyles %} + +{% endblock pluginstyles %} - -{% set update_interval = 0 %} +{% block pluginscripts %} + + +{% endblock pluginscripts %} +{% set tabcount = 6 %} + +{% set tab1title = _(""'AVM2 TR-064 Items'" (" ~ tr064_item_count ~ ") ") %} +{% set tab2title = _(""'AVM2 AHA Items'" (" ~ aha_item_count ~ ") ") %} +{% set tab3title = _(""'Plugin-API'"") %} +{% set tab4title = _(""'Log-Einträge'"") %} +{% set tab5title = _(""'Call Monitor Items'" (" ~ call_monitor_item_count ~ ") ") %} +{% set tab6title = _(""'AVM2 AHA Devices'"") %} + +{% set language = p.get_sh().get_defaultlanguage() %} +{% if language not in ['en','de'] %} + {% set language = 'en' %} +{% endif %} {% block headtable %} - - - - - - + + + + - - - - - - + + + + - - - - - - + + + +
Prompt 1{% if 1 == 2 %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}Prompt 4{{ _('Wert 4') }} + {% if p.get_fritz_device() %} + {{ _('Gerät verfügbar') }} + {% else %} + {{ _('Gerät nicht verfügbar') }} + {% endif %} + {{ _('Verbunden') }} + + {% if p.get_fritz_device() %} + {{ _('Ja') }}{% if p.get_fritz_device().is_ssl() %}, SSL{% endif %} + {% else %} + {{ _('Nein') }} + {% endif %} + {{ _('Benutzer') }}{{ p.get_parameter_value_for_display('username') }}
Prompt 2{{ _('Wert 2') }}Prompt 5- + {% if p.get_monitoring_service() and p._monitoring_service._listen_active %} + {{ _('Call Monitor verbunden') }} + {% else %} + {{ _('Call Monitor nicht verbunden') }} + {% endif %} + {{ _('Call Monitor') }} + + {% if p.get_monitoring_service() %}{{ _('Ja') }}{% if not p.get_monitoring_service()._listen_active %}, {{ _('nicht verbunden') }}{% endif %}{% else %}{{ _('Nein') }}{% endif %}{{ _('Passwort') }}{{ p.get_parameter_value_for_display('password') }}
Prompt 3-Prompt 6-{{ _('Host') }}{{ p._fritz_device.get_host() }}{{ _('Port') }}{{ p.get_fritz_device().get_port() }} {% if p.get_fritz_device().is_ssl() %}(HTTPS){% endif %}
-{% endblock headtable %} +{% endblock %} - {% block buttons %} -{% if 1==2 %} -
- + + +{% endblock buttons %} + +{% block bodytab1 %} +
+
+ +
+
+ + + + + + + + + + + + + + {% if tr064_items %} + {% for item in tr064_items %} + {% set item_id = item.id() %} + {% if p.get_instance_name() %} + {% set instance_key = "avm2_data_type@"+p.get_instance_name() %} + {% else %} + {% set instance_key = "avm2_data_type" %} + {% endif %} + + + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}.{{ item() }}.{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}.{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-{% endif %} +
{% endblock %} - -{% set tabcount = 4 %} +{% block bodytab2 %} +
+
+ +
+
+ + + + + + + + + + + + + {% if aha_items %} + {% for item in aha_items %} + {% set item_id = item.id() %} + {% if p.get_instance_name() %} + {% set instance_key = "avm2_data_type@"+p.get_instance_name() %} + {% else %} + {% set instance_key = "avm2_data_type" %} + {% endif %} + + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}.{{ item() }}.{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}.{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
+
+
+{% endblock %} - -{% if item_count==0 %} - {% set start_tab = 1 %} +{% block bodytab3 %} +
+
+ {% for function, dict in p.metadata.plugin_functions.items() %} +
+
+ {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) +
+
+ {{ dict['description'][language] }}
+ {% if dict['parameters'] is not none %} +
+
+ {{ _('Parameter') }}: +
+
+
    + {% for name, paramdict in dict['parameters'].items() %} +
  • + {{ name }}: {{ paramdict['type'] }}
    + {{ paramdict['description'][language] }} +
  • + {% endfor %} +
+
+
+ {% endif %} +
+
+ {% endfor %} +
+
+{% endblock %} + + +{% block bodytab4 %} +{% if p._fritz_home %} +{% set logentries = p._fritz_home.get_device_log_from_lua() %} +
+
+ + + + + + + + + + + + {% if logentries %} + {% for logentry in logentries %} + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Datum')}}{{ _('Uhrzeit')}}{{ _('Meldung')}}{{ _('Nachrichtennummer')}}{{ _('Kategorie')}}
{{ logentry[4] }}{{ logentry[5] }}{{ logentry[0] }} + + {{ logentry[1] }} + + {{ _('cat_'+logentry[2]|string) }}
+
+
{% endif %} +{% endblock %} - -{% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} -{% block bodytab1 %} -
- - - - - - - - - - - - - - - {% if p._fritz_home %} - - - - - - - - - - - - - - - - +{% block bodytab5 %} +
+
+ +
+
+
{{ _('Items@Fritzdevice') }}{{ p._fritz_device._items }}
{{ _('tam@Fritzdevice') }}{{ p._fritz_device.client.InternetGatewayDevice.X_AVM_DE_TAM.GetInfo(NewIndex=0) }}
{{ _('Host@Fritzdevice') }}{{ p._fritz_device.client.LANDevice.Hosts.GetSpecificHostEntry(NewMACAddress='14:3C:C3:7D:26:D6') }}
{{ _('Items@FritzHome') }}{{ p._fritz_home._items }}
{{ _('get_devices_as_dict@FritzHome') }}{{ p._fritz_home.get_devices_as_dict() }}
{{ _('_poll_aha@FritzHome') }}{{ p._fritz_home._poll_aha() }}
{{ _('_aha_devices@FritzHome') }}{{ p._fritz_home._aha_devices }}
+ + + + + + + + + + + + {% if call_monitor_items %} + {% for item in call_monitor_items %} + {% set item_id = item.id() %} + {% if p.get_instance_name() %} + {% set instance_key = "avm2_data_type@"+p.get_instance_name() %} + {% else %} + {% set instance_key = "avm2_data_type" %} + {% endif %} + + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}.{{ item() }}.{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}.{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
+
+
+{% endblock %} - {% endif %} +{% block bodytab6 %} - {% if p._monitoring_service %} - - {{ _('Items@Callmonitor') }} - {{ p._monitoring_service._items }} - - {% endif %} - - {{ _('model_name') }} - {{ p._fritz_device.client.InternetGatewayDevice.DeviceInfo.GetInfo() }} - - - {{ _('GetDefaultConnectionService') }} - {{ p._fritz_device.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() }} - - - {{ _('GetAddonInfos') }} - {{ p._fritz_device.client_igd.WANDevice.WANCommonInterfaceConfig.GetAddonInfos() }} - - - +
+
+ +
+
+ + + + + + + + + + {% if p._fritz_home %} + {% for ain in p._fritz_home._aha_devices %} + + + + + + {% endfor %} + {% endif %} + +
{{ _('No') }}{{ _('Device ain') }}{{ _('Device Details (dict)') }}
{{ loop.index }}{{ ain }}{{ p._fritz_home._aha_devices[ain] }}
+
-{% endblock bodytab1 %} + +{% endblock %} \ No newline at end of file From 68cce65e1f98b622de5a918c6f0afea4b405f049 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Thu, 17 Mar 2022 17:08:20 +0100 Subject: [PATCH 007/178] Update --- __init__.py | 31 ++++++++++++++++++------------- webif/templates/index.html | 23 ++++++++++++++++++----- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/__init__.py b/__init__.py index a2a1cf226..d4f490da7 100644 --- a/__init__.py +++ b/__init__.py @@ -2081,7 +2081,12 @@ def update_devices(self): if self._devices is None: self._devices = {} - for element in self.get_device_elements(): + elements = self.get_device_elements() + + if not elements: + return False + + for element in elements: if element.attrib["identifier"] in self._devices.keys(): self._plugin_instance.logger.info("Updating already existing Device " + element.attrib["identifier"]) self._devices[element.attrib["identifier"]]._update_from_node(element) @@ -2134,9 +2139,8 @@ def get_devices_as_dict(self): Get the list of all known devices. """ - # if self._devices is None: - self.update_devices() - return self._devices + if self.update_devices(): + return self._devices def get_device_by_ain(self, ain): """ @@ -3700,6 +3704,7 @@ def _parse_line(self, line: str): self._trigger('', '', '', line[2], line[1], '') except Exception as e: self._plugin_instance.logger.error(f"MonitoringService: {type(e).__name__} while handling Callmonitor response: {e}") + self._plugin_instance.logger.error(f"Callmonitor response: {line}") def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: str, branch: str): """ @@ -3743,8 +3748,8 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st duration_item(0, self._plugin_instance.get_shortname()) # process items specific to incoming calls - for item in self._items_incoming: # update items for incoming calls - avm_data_type = self._items[item][0] + for item in self._items_incoming: + avm_data_type = self._items_incoming[item][0] if avm_data_type == 'is_call_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting is_call_incoming: True") @@ -3761,7 +3766,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st elif avm_data_type == 'last_call_date_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting last_call_date_incoming: {time}") - item(time, self._plugin_instance.get_shortname()) + item(str(time), self._plugin_instance.get_shortname()) elif avm_data_type == 'call_event_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") @@ -3787,7 +3792,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st # process items specific to outgoing calls for item in self._items_outgoing: - avm_data_type = self._items[item][0] + avm_data_type = self._items_outgoing[item][0] if avm_data_type == 'is_call_outgoing': item(True, self._plugin_instance.get_shortname()) elif avm_data_type == 'last_caller_outgoing': @@ -3813,7 +3818,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st self._stop_counter('outgoing') # stop potential running counter for parallel (older) outgoing call self._start_counter(dt, 'outgoing') for item in self._items_outgoing: - avm_data_type = self._items[item][0] + avm_data_type = self._items_outgoing[item][0] if avm_data_type == 'call_event_outgoing': item(event.lower(), self._plugin_instance.get_shortname()) @@ -3825,7 +3830,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st self._plugin_instance.logger.debug("Starting Counter for Call Time") self._start_counter(dt, 'incoming') for item in self._items_incoming: - avm_data_type = self._items[item][0] + avm_data_type = self._items_incoming[item][0] if avm_data_type == 'call_event_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") @@ -3836,7 +3841,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st # handle OUTGOING calls if callid == self._call_outgoing_cid: for item in self._items_outgoing: - avm_data_type = self._items[item][0] + avm_data_type = self._items_outgoing[item][0] if avm_data_type == 'call_event_outgoing': item(event.lower(), self._plugin_instance.get_shortname()) elif avm_data_type == 'is_call_outgoing': @@ -3848,14 +3853,14 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st # handle INCOMING calls elif callid == self._call_incoming_cid: for item in self._items_incoming: - avm_data_type = self._items[item][0] + avm_data_type = self._items_incoming[item][0] if avm_data_type == 'call_event_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") item(event.lower(), self._plugin_instance.get_shortname()) elif avm_data_type == 'is_call_incoming': if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"Setting is_call_incoming: {False}") + self._plugin_instance.logger.debug(f"Setting is_call_incoming: False") item(False, self._plugin_instance.get_shortname()) if self._duration_item_in is not None: # stop counter threads if self._plugin_instance.debug_log: diff --git a/webif/templates/index.html b/webif/templates/index.html index ad0ca466f..135f4eb1a 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -223,7 +223,6 @@ {% endblock %} - {% block bodytab3 %}
@@ -258,7 +257,6 @@
{% endblock %} - {% block bodytab4 %} {% if p._fritz_home %} {% set logentries = p._fritz_home.get_device_log_from_lua() %} @@ -297,7 +295,6 @@ {% endif %} {% endblock %} - {% block bodytab5 %}
@@ -341,7 +338,6 @@ {% endblock %} {% block bodytab6 %} -
@@ -369,5 +365,22 @@
- +
+
+ + + + + + + + + + + + + +
{{ _('1') }}{{ _('2') }}
{{ _('Call Items') }}{{ p._monitoring_service._items_incoming }}
+
+
{% endblock %} \ No newline at end of file From 8066c27b2f8540c22d6c324b0f2bb0e6412638c2 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sun, 3 Apr 2022 16:56:55 +0200 Subject: [PATCH 008/178] Update --- __init__.py | 134 +++++----- plugin.yaml | 16 +- webif/__init__.py | 14 +- webif/templates/index.html | 495 +++++++++++++++++++------------------ 4 files changed, 354 insertions(+), 305 deletions(-) diff --git a/__init__.py b/__init__.py index d4f490da7..db0a8b753 100644 --- a/__init__.py +++ b/__init__.py @@ -1132,7 +1132,7 @@ def get_calllist(self, filter_incoming: str = '') -> list: calllist_url = calllist_url['NewCallListURL'] calllist = self._request_response_to_xml(self._request(calllist_url, self._timeout, self._verify)) - if calllist: + if calllist is not None: result_entries = [] for calllist_entry in calllist.iter('Call'): result_entry = {} @@ -1633,7 +1633,8 @@ class FritzHome: """ _login_route = "/login_sid.lua?version=2" - _event_route = '/query.lua?mq_log=logger:status/log&sid=' + _log_route = '/query.lua?mq_log=logger:status/log' + _log_separate_route = '/query.lua?mq_log=logger:status/log_separate' _homeauto_route = '/webservices/homeautoswitch.lua' _internet_status_route = '/internet/inetstat_monitor.lua?sid=' @@ -1659,6 +1660,7 @@ def __init__(self, host, ssl, verify, user, password, plugin_instance): self._aha_devices = dict() self._session = requests.Session() self._connected = False + self._log_entry_count = 200 # Login self.login() @@ -1777,53 +1779,56 @@ def _poll_aha(self): 'switch_mode': 'switch_mode', 'lock': 'lock'} - _device_dict = self.get_devices_as_dict() - - for ain in _device_dict: - self._plugin_instance.logger.debug(f"_poll_aha: handle AIN={ain} with _device_dict[ain]={_device_dict[ain]}") - self._aha_devices[ain] = {} - self._aha_devices[ain]['device_functions'] = [] - - for entry in _link_base: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_base[entry]}") - - if _device_dict[ain].has_thermostat: - self._aha_devices[ain]['device_functions'].append('thermostat') - for entry in _link_thermostat: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_thermostat[entry]}") - - if _device_dict[ain].has_temperature_sensor: - self._aha_devices[ain]['device_functions'].append('temperature_sensor') - for entry in _link_temperature: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_temperature[entry]}") - - if _device_dict[ain].has_button: - self._aha_devices[ain]['device_functions'].append('button') - for entry in _link_button: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_button[entry]}") - - if _device_dict[ain].has_alarm: - self._aha_devices[ain]['device_functions'].append('alarm') - for entry in _link_alarm: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_alarm[entry]}") - - if _device_dict[ain].has_lightbulb: - self._aha_devices[ain]['device_functions'].append('color_device') - for entry in _link_lightbulb: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_lightbulb[entry]}") - - if _device_dict[ain].has_repeater: - self._aha_devices[ain]['device_functions'].append('repeater') - - if _device_dict[ain].has_powermeter: - self._aha_devices[ain]['device_functions'].append('powermeter') - for entry in _link_powermeter: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_powermeter[entry]}") - - if _device_dict[ain].has_switch: - self._aha_devices[ain]['device_functions'].append('powermeter') - for entry in _link_switch: - self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_switch[entry]}") + try: + _device_dict = self.get_devices_as_dict() + except Exception as e: + self._plugin_instance.logger.warning(f"Error {e} eccurred during method _poll_aha.") + else: + for ain in _device_dict: + self._plugin_instance.logger.debug(f"_poll_aha: handle AIN={ain} with _device_dict[ain]={_device_dict[ain]}") + self._aha_devices[ain] = {} + self._aha_devices[ain]['device_functions'] = [] + + for entry in _link_base: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_base[entry]}") + + if _device_dict[ain].has_thermostat: + self._aha_devices[ain]['device_functions'].append('thermostat') + for entry in _link_thermostat: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_thermostat[entry]}") + + if _device_dict[ain].has_temperature_sensor: + self._aha_devices[ain]['device_functions'].append('temperature_sensor') + for entry in _link_temperature: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_temperature[entry]}") + + if _device_dict[ain].has_button: + self._aha_devices[ain]['device_functions'].append('button') + for entry in _link_button: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_button[entry]}") + + if _device_dict[ain].has_alarm: + self._aha_devices[ain]['device_functions'].append('alarm') + for entry in _link_alarm: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_alarm[entry]}") + + if _device_dict[ain].has_lightbulb: + self._aha_devices[ain]['device_functions'].append('color_device') + for entry in _link_lightbulb: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_lightbulb[entry]}") + + if _device_dict[ain].has_repeater: + self._aha_devices[ain]['device_functions'].append('repeater') + + if _device_dict[ain].has_powermeter: + self._aha_devices[ain]['device_functions'].append('powermeter') + for entry in _link_powermeter: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_powermeter[entry]}") + + if _device_dict[ain].has_switch: + self._aha_devices[ain]['device_functions'].append('powermeter') + for entry in _link_switch: + self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_switch[entry]}") def update_items(self): """ @@ -2587,10 +2592,11 @@ def get_device_log_from_lua(self): if not self._logged_in: self.login() - url = self.get_prefixed_host() + self._event_route + url = self.get_prefixed_host() + self._log_route params = {"sid": self._sid} data = self._request(url, params, result='json')['mq_log'] newlog = [] + idx = 0 for text, typ, cat in data: l_date = text[:8] l_time = text[9:17] @@ -2599,8 +2605,26 @@ def get_device_log_from_lua(self): l_type = int(typ) l_ts = int(datetime.datetime.timestamp(datetime.datetime.strptime(text[:17], '%d.%m.%y %H:%M:%S'))) newlog.append([l_text, l_type, l_cat, l_ts, l_date, l_time]) + if idx == self._log_entry_count: + break + idx += 1 return newlog + def get_device_log_from_lua_separated(self): + """ + Gets the Device Log from the LUA HTTP Interface via LUA Scripts (more complete than the get_device_log TR-064 version). + + :return: list of device logs list (date, time, log, type, category) + """ + + if not self._logged_in: + self.login() + + url = self.get_prefixed_host() + self._log_separate_route + params = {"sid": self._sid} + data = self._request(url, params, result='json')['mq_log'] + return data + # Handling of updated items def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): @@ -3437,7 +3461,7 @@ def register_item(self, item, avm_data_type: str): self._trigger_items[item] = (avm_data_type, avm_incoming_allowed, avm_target_number) elif avm_data_type in _call_duration_attributes: - if 'in' in avm_data_type: + if avm_data_type == 'call_duration_incoming': self._duration_item_in[item] = (avm_data_type, None) else: self._duration_item_out[item] = (avm_data_type, None) @@ -3487,7 +3511,7 @@ def set_callmonitor_item_values_initially(self): if element['Type'] in ['1', '2']: date = str(element['Date']) date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self._plugin_instance.get_shortname()) + item(str(time), self._plugin_instance.get_shortname()) break elif avm_data_type == 'call_event_incoming': @@ -3528,7 +3552,7 @@ def set_callmonitor_item_values_initially(self): if element['Type'] in ['3', '4']: date = str(element['Date']) date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self._plugin_instance.get_shortname()) + item(str(time), self._plugin_instance.get_shortname()) break elif avm_data_type == 'call_event_outgoing': @@ -3689,9 +3713,9 @@ def _parse_line(self, line: str): :param line: data line which is parsed """ + line = line.split(";") if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(line) - line = line.split(";") try: if line[1] == "RING": @@ -3802,7 +3826,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st else: item(call_to, self._plugin_instance.get_shortname()) elif avm_data_type == 'last_call_date_outgoing': - item(time, self._plugin_instance.get_shortname()) + item(str(time), self._plugin_instance.get_shortname()) elif avm_data_type == 'call_event_outgoing': item(event.lower(), self._plugin_instance.get_shortname()) elif avm_data_type == 'last_number_outgoing': diff --git a/plugin.yaml b/plugin.yaml index 173351786..40c57299a 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -86,15 +86,25 @@ parameters: en: '(optional) Activates or deactivates access to AVM smarthome devices via AHA HTTP interface' webif_pagelength: type: int - default: 100 + default: 0 valid_list: - -1 + - 0 - 25 - 50 - 100 description: - de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden' - en: 'Amount of items being listed in a web interface table per page by default' + de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden. + 0 = automatisch, -1 = alle' + en: 'Amount of items being listed in a web interface table per page by default. + 0 = automatic, -1 = all' + description_long: + de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden.\n + Bei 0 wird die Tabelle automatisch an die Höhe des Browserfensters angepasst.\n + Bei -1 werden alle Tabelleneinträge auf einer Seite angezeigt.' + en: 'Amount of items being listed in a web interface table per page by default.\n + 0 adjusts the table height automatically based on the height of the browser windows.\n + -1 shows all table entries on one page.' item_attributes: # Definition of item attributes defined by this plugin diff --git a/webif/__init__.py b/webif/__init__.py index 5fc5cf7a5..0651dd702 100644 --- a/webif/__init__.py +++ b/webif/__init__.py @@ -25,14 +25,9 @@ ######################################################################### import json +import cherrypy from lib.item import Items from lib.model.smartplugin import SmartPluginWebIf - -# ------------------------------------------ -# Webinterface of the plugin -# ------------------------------------------ - -import cherrypy from jinja2 import Environment, FileSystemLoader @@ -91,6 +86,11 @@ def index(self, reload=None, action=None): tmpl = self.tplenv.get_template('index.html') + try: + pagelength = self.plugin.webif_pagelength + except Exception: + pagelength = 100 + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), plugin_info=self.plugin.get_info(), @@ -101,7 +101,7 @@ def index(self, reload=None, action=None): aha_items=aha_items, aha_item_count=aha_item_count, p=self.plugin, - webif_pagelength=self.plugin.webif_pagelength, + webif_pagelength=pagelength, ) @cherrypy.expose diff --git a/webif/templates/index.html b/webif/templates/index.html index 135f4eb1a..1dbd81529 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -13,37 +13,71 @@ table th.datatype { width: 250px; } - } table th.value { width: 250px; } #itemtable { - display: none; - table-layout: fixed; + display: none; + table-layout: fixed; } #smarthomeitemtable { - display: none; - table-layout: fixed; + display: none; + table-layout: fixed; + } + #smarthome_devicetable { + display: none; + table-layout: fixed; + } + #logtable { + display: none; } {% endblock pluginstyles %} {% block pluginscripts %} {% endblock pluginscripts %} + {% set tabcount = 6 %} {% set tab1title = _(""'AVM2 TR-064 Items'" (" ~ tr064_item_count ~ ") ") %} -{% set tab2title = _(""'AVM2 AHA Items'" (" ~ aha_item_count ~ ") ") %} + +{% if p._fritz_home and aha_item_count > 0 %} + {% set tab2title = _(""'AVM2 AHA Items'" (" ~ aha_item_count ~ ") ") %} +{% else %} + {% set tab2title = "hidden" %} +{% endif %} + {% set tab3title = _(""'Plugin-API'"") %} + {% set tab4title = _(""'Log-Einträge'"") %} -{% set tab5title = _(""'Call Monitor Items'" (" ~ call_monitor_item_count ~ ") ") %} -{% set tab6title = _(""'AVM2 AHA Devices'" (" ~ len(p._fritz_home._aha_devices) ~ ") ") %} +{% if p._call_monitor and call_monitor_item_count > 0 %} + {% set tab5title = _(""'Call Monitor Items'" (" ~ call_monitor_item_count ~ ") ") %} +{% else %} + {% set tab5title = "hidden" %} +{% endif %} + +{% if p._fritz_home and len(p._fritz_home._aha_devices) > 0%} + {% set tab6title = _(""'AVM2 AHA Devices'" (" ~ len(p._fritz_home._aha_devices) ~ ") ") %} +{% else %} + {% set tab6title = "hidden" %} +{% endif %} + + {% set language = p.get_sh().get_defaultlanguage() %} {% if language not in ['en','de'] %} {% set language = 'en' %} From 1d6f1bbb16ed4212813e57cde8af027e28e7bf15 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 19 Apr 2022 15:06:23 +0200 Subject: [PATCH 021/178] - Update WebIF for hidden tabs --- webif/templates/index.html | 141 +++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/webif/templates/index.html b/webif/templates/index.html index ad9ebb0d9..8af83f37e 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -112,22 +112,22 @@ {% set tab2title = "hidden" %} {% endif %} -{% set tab3title = _(""'Plugin-API'"") %} - -{% set tab4title = _(""'Log-Einträge'"") %} - -{% if p._call_monitor and call_monitor_item_count > 0 %} - {% set tab5title = _(""'Call Monitor Items'" (" ~ call_monitor_item_count ~ ") ") %} +{% if p._fritz_home and len(p._fritz_home._aha_devices) > 0%} + {% set tab3title = _(""'AVM2 AHA Devices'" (" ~ len(p._fritz_home._aha_devices) ~ ") ") %} {% else %} - {% set tab5title = "hidden" %} + {% set tab3title = "hidden" %} {% endif %} -{% if p._fritz_home and len(p._fritz_home._aha_devices) > 0%} - {% set tab6title = _(""'AVM2 AHA Devices'" (" ~ len(p._fritz_home._aha_devices) ~ ") ") %} +{% if p._call_monitor and call_monitor_item_count > 0 %} + {% set tab4title = _(""'Call Monitor Items'" (" ~ call_monitor_item_count ~ ") ") %} {% else %} - {% set tab6title = "hidden" %} + {% set tab4title = "hidden" %} {% endif %} +{% set tab5title = _(""'Log-Einträge'"") %} + +{% set tab6title = _(""'Plugin-API'"") %} + {% set language = p.get_sh().get_defaultlanguage() %} {% if language not in ['en','de'] %} @@ -183,6 +183,7 @@ {% endblock %} + {% block buttons %} @@ -269,63 +270,21 @@ {% block bodytab3 %}
- {% for function, dict in p.metadata.plugin_functions.items() %} -
-
- {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) -
-
- {{ dict['description'][language] }}
- {% if dict['parameters'] is not none %} -
-
- {{ _('Parameter') }}: -
-
-
    - {% for name, paramdict in dict['parameters'].items() %} -
  • - {{ name }}: {{ paramdict['type'] }}
    - {{ paramdict['description'][language] }} -
  • - {% endfor %} -
-
-
- {% endif %} -
-
- {% endfor %} -
-{% endblock %} - - -{% block bodytab4 %} -
- +
- - - - + + + {% if p._fritz_home %} - {% set logentries = p._fritz_home.get_device_log_from_lua_separated() %} - {% endif %} - {% if logentries %} - {% for logentry in logentries%} + {% for ain in p._fritz_home._aha_devices %} - - - - + + + {% endfor %} {% endif %} @@ -335,7 +294,7 @@ {% endblock %} -{% block bodytab5 %} +{% block bodytab4 %}
{{ 'Datum/Uhrzeit' }}{{ 'Meldung' }}{{ 'Typ' }}{{ 'Kategorie' }}{{ 'No' }}{{ 'Device AIN' }}{{ 'Device Details (dict)' }}
{{ logentry[0] }}{{ logentry[1] }} - - {{ logentry[2] }} - - {{ _('cat_'+logentry[3]|string) }}{{ loop.index }}{{ ain }}{{ p._fritz_home._aha_devices[ain] }}
@@ -373,23 +332,32 @@ {% endblock %} -{% block bodytab6 %} +{% block bodytab5 %}
-
+
- - - + + + + {% if p._fritz_home %} - {% for ain in p._fritz_home._aha_devices %} + {% set logentries = p._fritz_home.get_device_log_from_lua_separated() %} + {% endif %} + {% if logentries %} + {% for logentry in logentries%} - - - + + + + {% endfor %} {% endif %} @@ -397,3 +365,36 @@
{{ 'No' }}{{ 'Device AIN' }}{{ 'Device Details (dict)' }}{{ 'Datum/Uhrzeit' }}{{ 'Meldung' }}{{ 'Typ' }}{{ 'Kategorie' }}
{{ loop.index }}{{ ain }}{{ p._fritz_home._aha_devices[ain] }}{{ logentry[0] }}{{ logentry[1] }} + + {{ logentry[2] }} + + {{ _('cat_'+logentry[3]|string) }}
{% endblock %} + + +{% block bodytab6 %} +
+ {% for function, dict in p.metadata.plugin_functions.items() %} +
+
+ {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) +
+
+ {{ dict['description'][language] }}
+ {% if dict['parameters'] is not none %} +
+
+ {{ _('Parameter') }}: +
+
+
    + {% for name, paramdict in dict['parameters'].items() %} +
  • + {{ name }}: {{ paramdict['type'] }}
    + {{ paramdict['description'][language] }} +
  • + {% endfor %} +
+
+
+ {% endif %} +
+
+ {% endfor %} +
+{% endblock %} From debb27ffb73d402301a3005d30e9be54fc818f05 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 20 Apr 2022 13:03:41 +0200 Subject: [PATCH 022/178] - Bugfixing for getting index and readafterwrite --- __init__.py | 188 +++++++++++++++++-------------------- plugin.yaml | 5 + webif/templates/index.html | 1 - 3 files changed, 93 insertions(+), 101 deletions(-) diff --git a/__init__.py b/__init__.py index f2b1d3549..df129282e 100644 --- a/__init__.py +++ b/__init__.py @@ -416,12 +416,15 @@ def update_item(self, item, caller=None, source=None, dest=None): :param item: item to be updated towards the plugin :param caller: if given it represents the callers name :param source: if given it represents the source - :param dest: if given it represents the dest + :param dest: if given it represents the destination """ - if self.alive and caller != self.get_shortname() and item in self._items: + if self.alive and caller != self.get_fullname(): - avm_data_type = self._items[item][0] + # get avm_data_type + avm_data_type = self.get_iattr_value(item.conf, 'avm2_data_type') + + self.logger.info(f"Updated item: {item.property.path} with avm_data_type={avm_data_type} item has been changed outside this plugin from caller={caller}") readafterwrite = None if self.has_iattr(item.conf, 'avm2_read_after_write'): @@ -429,8 +432,6 @@ def update_item(self, item, caller=None, source=None, dest=None): if self.debug_log: self.logger.debug(f'Attempting read after write for item: {item.id()}, avm_data_type: {avm_data_type}, delay: {readafterwrite}s') - self.logger.info(f"Update item: {item.property.path} with avm_data_type={avm_data_type} item has been changed outside this plugin") - # handle items updated by tr-064 interface if avm_data_type in _tr064_attributes: if self.debug_log: @@ -564,12 +565,10 @@ def register_item(self, item, avm_data_type: str): elif avm_data_type in _deflection_attributes: avm_deflection_index = self._get_deflection_index(item) if avm_deflection_index is not None: - self._plugin_instance.logger.debug( - f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") self._items[item] = (avm_data_type, avm_deflection_index) else: - self._plugin_instance.logger.warning( - f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") # handle all other items else: @@ -587,8 +586,8 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): to_be_set_value = item() # define command per avm_data_type - _dispatcher = {'wlanconfig': (self.set_wlan_config, self.get_wlan_config), - 'tam': (self.set_tam, self.get_tam), + _dispatcher = {'wlanconfig': (self.set_wlan, self.get_wlan), + 'tam': (self.set_tam, self.get_tam), 'deflection_enable': (self.set_deflection, self.get_deflection), } @@ -601,19 +600,14 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): # handle readafterwrite if readafterwrite: - wait = float(readafterwrite) - time.sleep(wait) - try: - set_value = _dispatcher[avm_data_type][1](_index) - except Exception: - self._plugin_instance.logger.error(f"{avm_data_type} is not defined to be read.") + time.sleep(readafterwrite) + set_value = _dispatcher[avm_data_type][1](_index) + item(set_value, self._plugin_instance.get_fullname()) + if set_value != to_be_set_value: + self._plugin_instance.logger.warning(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") else: - item(set_value, self._plugin_instance.get_instance_name()) - if set_value != to_be_set_value: - self._plugin_instance.logger.warning(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") - else: - if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") def _build_url(self) -> str: """ @@ -638,12 +632,8 @@ def _get_wlan_index(self, item) -> int: wlan_index = None for i in range(2): attribute = 'avm2_wlan_index' - attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" wlan_index = self._plugin_instance.get_iattr_value(item.conf, attribute) - if wlan_index: - break - wlan_index = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) if wlan_index: break else: @@ -665,12 +655,8 @@ def _get_tam_index(self, item) -> int: tam_index = None for i in range(2): attribute = 'avm2_tam_index' - attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" tam_index = self._plugin_instance.get_iattr_value(item.conf, attribute) - if tam_index: - break - tam_index = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) if tam_index: break else: @@ -692,12 +678,8 @@ def _get_deflection_index(self, item) -> int: deflection_index = None for i in range(2): attribute = 'avm2_deflection_index' - attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" deflection_index = self._plugin_instance.get_iattr_value(item.conf, attribute) - if deflection_index: - break - deflection_index = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) if deflection_index: break else: @@ -719,12 +701,8 @@ def _get_mac(self, item) -> str: mac = None for i in range(2): attribute = 'avm2_mac' - attribute_w_instance = f"{attribute}@{self._plugin_instance.get_instance_name()}" mac = self._plugin_instance.get_iattr_value(item.conf, attribute) - if mac: - break - mac = self._plugin_instance.get_iattr_value(item.conf, attribute_w_instance) if mac: break else: @@ -884,7 +862,7 @@ def update_item_values(self): self._plugin_instance.logger.error(f"Error {data} '{self.errorcodes.get(data, None)}' occurred during update of item={item}. Check item configuration regarding supported/activated function of AVM device. ") _item_errorlist.append(item) else: - item(data, self._plugin_instance.get_shortname()) + item(data, self._plugin_instance.get_fullname()) else: self._plugin_instance.logger.warning(f"FritzDevice not connected. No update of item values possible.") @@ -1323,7 +1301,7 @@ def get_device_log_from_tr064(self) -> str: # ---------------------------------- # wlan methods # ---------------------------------- - def set_wlan_config(self, wlan_index: int, new_enable: bool = False): + def set_wlan(self, wlan_index: int, new_enable: bool = False): """ Set WLAN Config @@ -1331,17 +1309,27 @@ def set_wlan_config(self, wlan_index: int, new_enable: bool = False): """ # set WLAN to ON/OFF + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"set_wlan called: wlan_index={wlan_index}, new_enable={new_enable}") self.client.LANDevice.WLANConfiguration[wlan_index].SetEnable(NewEnable=int(new_enable)) + # check if remaining time is set as item for item in self._items: # search for guest time remaining item. if self._items[item][0] == 'wlan_guest_time_remaining' and self._items[item][1] == wlan_index: data = self._poll_fritz_device('wlan_guest_time_remaining', wlan_index) if data is not None: - item(data, self._plugin_instance.get_shortname()) + item(data, self._plugin_instance.get_fullname()) + + def get_wlan(self, wlan_index: int): + """ + Get WLAN ON/OFF State + + uses: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf + """ + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"get_wlan called: wlan_index={wlan_index}") - def get_wlan_config(self, wlan_index: int): - """Get WLAN ON/OFF State""" - return bool(self.client.LANDevice.WLANConfiguration[wlan_index].GetInfo()['NewEnable']) + return self.client.LANDevice.WLANConfiguration[wlan_index].GetInfo()['NewEnable'] # ---------------------------------- # tam methods @@ -1362,7 +1350,7 @@ def get_tam(self, tam_index: int = 0): uses: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_tam.pdf """ - return bool(self.client.InternetGatewayDevice.X_AVM_DE_TAM.GetInfo(NewIndex=tam_index)['NewEnable']) + return self.client.InternetGatewayDevice.X_AVM_DE_TAM.GetInfo(NewIndex=tam_index)['NewEnable'] def get_tam_list(self, tam_index: int = 0): """ @@ -1429,7 +1417,7 @@ def set_deflection(self, deflection_id: int = 0, new_enable: bool = False): def get_deflection(self, deflection_id: int = 0): """Get Deflection state of deflection_id""" - return bool(self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetDeflection(NewDeflectionId=deflection_id)['NewEnable']) + return self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetDeflection(NewDeflectionId=deflection_id)['NewEnable'] # ---------------------------------- # Host @@ -2052,7 +2040,7 @@ def update_items(self): _avm_data_type = _avm_data_type[len('set_'):] # set item if _avm_data_type in device: - item(device[_avm_data_type], self._plugin_instance.get_shortname()) + item(device[_avm_data_type], self._plugin_instance.get_fullname()) else: self._plugin_instance.logger.warning(f'Attribute={_avm_data_type} at device with AIN={_ain} to be set to Item={item.id()} is not available.') else: @@ -2886,7 +2874,7 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): except Exception: self._plugin_instance.logger.error(f"{avm_data_type} is not defined to be read.") else: - item(set_value, self._plugin_instance.get_instance_name()) + item(set_value, self._plugin_instance.get_fullname()) if set_value != to_be_set_value: self._plugin_instance.logger.warning(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") else: @@ -3693,24 +3681,24 @@ def set_callmonitor_item_values_initially(self): for element in _calllist: if element['Type'] in ['1', '2']: if 'Name' in element: - item(element['Name'], self._plugin_instance.get_shortname()) + item(element['Name'], self._plugin_instance.get_fullname()) else: - item(element['Caller'], self._plugin_instance.get_shortname()) + item(element['Caller'], self._plugin_instance.get_fullname()) break elif avm_data_type == 'last_number_incoming': for element in _calllist: if element['Type'] in ['1', '2']: if 'Caller' in element: - item(element['Caller'], self._plugin_instance.get_shortname()) + item(element['Caller'], self._plugin_instance.get_fullname()) else: - item("", self._plugin_instance.get_shortname()) + item("", self._plugin_instance.get_fullname()) break elif avm_data_type == 'last_called_number_incoming': for element in _calllist: if element['Type'] in ['1', '2']: - item(element['CalledNumber'], self._plugin_instance.get_shortname()) + item(element['CalledNumber'], self._plugin_instance.get_fullname()) break elif avm_data_type == 'last_call_date_incoming': @@ -3718,14 +3706,14 @@ def set_callmonitor_item_values_initially(self): if element['Type'] in ['1', '2']: date = str(element['Date']) times = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(str(times), self._plugin_instance.get_shortname()) + item(str(times), self._plugin_instance.get_fullname()) break elif avm_data_type == 'call_event_incoming': - item('disconnect', self._plugin_instance.get_shortname()) + item('disconnect', self._plugin_instance.get_fullname()) elif avm_data_type == 'is_call_incoming': - item(0, self._plugin_instance.get_shortname()) + item(0, self._plugin_instance.get_fullname()) for item in self._items_outgoing: avm_data_type = self._items_outgoing[item][0] @@ -3734,24 +3722,24 @@ def set_callmonitor_item_values_initially(self): for element in _calllist: if element['Type'] in ['3', '4']: if 'Name' in element: - item(element['Name'], self._plugin_instance.get_shortname()) + item(element['Name'], self._plugin_instance.get_fullname()) else: - item(element['Called'], self._plugin_instance.get_shortname()) + item(element['Called'], self._plugin_instance.get_fullname()) break elif avm_data_type == 'last_number_outgoing': for element in _calllist: if element['Type'] in ['3', '4']: if 'Caller' in element: - item(''.join(filter(lambda x: x.isdigit(), element['Caller'])), self._plugin_instance.get_shortname()) + item(''.join(filter(lambda x: x.isdigit(), element['Caller'])), self._plugin_instance.get_fullname()) else: - item("", self._plugin_instance.get_shortname()) + item("", self._plugin_instance.get_fullname()) break elif avm_data_type == 'last_called_number_outgoing': for element in _calllist: if element['Type'] in ['3', '4']: - item(element['Called'], self._plugin_instance.get_shortname()) + item(element['Called'], self._plugin_instance.get_fullname()) break elif avm_data_type == 'last_call_date_outgoing': @@ -3759,28 +3747,28 @@ def set_callmonitor_item_values_initially(self): if element['Type'] in ['3', '4']: date = str(element['Date']) times = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(str(times), self._plugin_instance.get_shortname()) + item(str(times), self._plugin_instance.get_fullname()) break elif avm_data_type == 'call_event_outgoing': - item('disconnect', self._plugin_instance.get_shortname()) + item('disconnect', self._plugin_instance.get_fullname()) elif avm_data_type == 'is_call_outgoing': - item(0, self._plugin_instance.get_shortname()) + item(0, self._plugin_instance.get_fullname()) for item in self._items: avm_data_type = self._items[item][0] if avm_data_type == 'call_event': - item('disconnect', self._plugin_instance.get_shortname()) + item('disconnect', self._plugin_instance.get_fullname()) elif avm_data_type == 'call_direction': for element in _calllist: if element['Type'] in ['1', '2']: - item('incoming', self._plugin_instance.get_shortname()) + item('incoming', self._plugin_instance.get_fullname()) break if element['Type'] in ['3', '4']: - item('outgoing', self._plugin_instance.get_shortname()) + item('outgoing', self._plugin_instance.get_fullname()) break for item in self._duration_item_in: @@ -3790,7 +3778,7 @@ def set_callmonitor_item_values_initially(self): if element['Type'] in ['1', '2']: duration = element['Duration'] duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self._plugin_instance.get_shortname()) + item(duration, self._plugin_instance.get_fullname()) break for item in self._duration_item_out: @@ -3800,7 +3788,7 @@ def set_callmonitor_item_values_initially(self): if element['Type'] in ['3', '4']: duration = element['Duration'] duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self._plugin_instance.get_shortname()) + item(duration, self._plugin_instance.get_fullname()) break def get_item_list(self): @@ -3891,7 +3879,7 @@ def _count_duration_incoming(self): if self._duration_item_in is not None: duration = time.time() - self._call_connect_timestamp duration_item = list(self._duration_item_in.keys())[0] - duration_item(int(duration), self._plugin_instance.get_shortname()) + duration_item(int(duration), self._plugin_instance.get_fullname()) time.sleep(1) def _count_duration_outgoing(self): @@ -3904,7 +3892,7 @@ def _count_duration_outgoing(self): if self._duration_item_out is not None: duration = time.time() - self._call_connect_timestamp duration_item = list(self._duration_item_out.keys())[0] - duration_item(int(duration), self._plugin_instance.get_shortname()) + duration_item(int(duration), self._plugin_instance.get_fullname()) time.sleep(1) def _parse_line(self, line: str): @@ -3949,12 +3937,12 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st for item in self._items: avm_data_type = self._items[item][0] if avm_data_type == 'call_event': - item(event.lower(), self._plugin_instance.get_shortname()) + item(event.lower(), self._plugin_instance.get_fullname()) if avm_data_type == 'call_direction': if event == 'RING': - item("incoming", self._plugin_instance.get_shortname()) + item("incoming", self._plugin_instance.get_fullname()) else: - item("outgoing", self._plugin_instance.get_shortname()) + item("outgoing", self._plugin_instance.get_fullname()) # handle incoming call if event == 'RING': @@ -3963,11 +3951,11 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st avm_data_type = self._trigger_items[trigger_item][0] avm_incoming_allowed = self._trigger_items[trigger_item][1] avm_target_number = self._trigger_items[trigger_item][2] - trigger_item(0, self._plugin_instance.get_shortname()) + trigger_item(0, self._plugin_instance.get_fullname()) if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"{avm_data_type} {call_from} {call_to}") if avm_incoming_allowed == call_from and avm_target_number == call_to: - trigger_item(1, self._plugin_instance.get_shortname()) + trigger_item(1, self._plugin_instance.get_fullname()) if self._call_monitor_incoming_filter in call_to: # set call id for incoming call @@ -3976,7 +3964,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st # reset duration for incoming calls if self._duration_item_in is not None: duration_item = list(self._duration_item_in.keys())[0] - duration_item(0, self._plugin_instance.get_shortname()) + duration_item(0, self._plugin_instance.get_fullname()) # process items specific to incoming calls for item in self._items_incoming: @@ -3984,32 +3972,32 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st if avm_data_type == 'is_call_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting is_call_incoming: True") - item(True, self._plugin_instance.get_shortname()) + item(True, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_caller_incoming': if call_from != '' and call_from is not None: name = self._callback(call_from) if name != '' and name is not None: - item(name, self._plugin_instance.get_shortname()) + item(name, self._plugin_instance.get_fullname()) else: - item(call_from, self._plugin_instance.get_shortname()) + item(call_from, self._plugin_instance.get_fullname()) else: - item("Unbekannt", self._plugin_instance.get_shortname()) + item("Unbekannt", self._plugin_instance.get_fullname()) elif avm_data_type == 'last_call_date_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting last_call_date_incoming: {time}") - item(str(time), self._plugin_instance.get_shortname()) + item(str(time), self._plugin_instance.get_fullname()) elif avm_data_type == 'call_event_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") - item(event.lower(), self._plugin_instance.get_shortname()) + item(event.lower(), self._plugin_instance.get_fullname()) elif avm_data_type == 'last_number_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting last_number_incoming: {call_from}") - item(call_from, self._plugin_instance.get_shortname()) + item(call_from, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_called_number_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting last_called_number_incoming: {call_to}") - item(call_to, self._plugin_instance.get_shortname()) + item(call_to, self._plugin_instance.get_fullname()) # handle outgoing call elif event == 'CALL': @@ -4019,27 +4007,27 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st # reset duration for outgoing calls if self._duration_item_out is not None: duration_item = list(self._duration_item_out.keys())[0] - duration_item(0, self._plugin_instance.get_shortname()) + duration_item(0, self._plugin_instance.get_fullname()) # process items specific to outgoing calls for item in self._items_outgoing: avm_data_type = self._items_outgoing[item][0] if avm_data_type == 'is_call_outgoing': - item(True, self._plugin_instance.get_shortname()) + item(True, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_caller_outgoing': name = self._callback(call_to) if name != '' and name is not None: - item(name, self._plugin_instance.get_shortname()) + item(name, self._plugin_instance.get_fullname()) else: - item(call_to, self._plugin_instance.get_shortname()) + item(call_to, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_call_date_outgoing': - item(str(time), self._plugin_instance.get_shortname()) + item(str(time), self._plugin_instance.get_fullname()) elif avm_data_type == 'call_event_outgoing': - item(event.lower(), self._plugin_instance.get_shortname()) + item(event.lower(), self._plugin_instance.get_fullname()) elif avm_data_type == 'last_number_outgoing': - item(call_from, self._plugin_instance.get_shortname()) + item(call_from, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_called_number_outgoing': - item(call_to, self._plugin_instance.get_shortname()) + item(call_to, self._plugin_instance.get_fullname()) # handle established connection elif event == 'CONNECT': @@ -4051,7 +4039,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st for item in self._items_outgoing: avm_data_type = self._items_outgoing[item][0] if avm_data_type == 'call_event_outgoing': - item(event.lower(), self._plugin_instance.get_shortname()) + item(event.lower(), self._plugin_instance.get_fullname()) # handle INCOMING calls elif callid == self._call_incoming_cid: @@ -4065,7 +4053,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st if avm_data_type == 'call_event_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") - item(event.lower(), self._plugin_instance.get_shortname()) + item(event.lower(), self._plugin_instance.get_fullname()) # handle ended connection elif event == 'DISCONNECT': @@ -4074,9 +4062,9 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st for item in self._items_outgoing: avm_data_type = self._items_outgoing[item][0] if avm_data_type == 'call_event_outgoing': - item(event.lower(), self._plugin_instance.get_shortname()) + item(event.lower(), self._plugin_instance.get_fullname()) elif avm_data_type == 'is_call_outgoing': - item(False, self._plugin_instance.get_shortname()) + item(False, self._plugin_instance.get_fullname()) if self._duration_item_out is not None: # stop counter threads self._stop_counter('outgoing') self._call_outgoing_cid = None @@ -4088,11 +4076,11 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st if avm_data_type == 'call_event_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") - item(event.lower(), self._plugin_instance.get_shortname()) + item(event.lower(), self._plugin_instance.get_fullname()) elif avm_data_type == 'is_call_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting is_call_incoming: False") - item(False, self._plugin_instance.get_shortname()) + item(False, self._plugin_instance.get_fullname()) if self._duration_item_in is not None: # stop counter threads if self._plugin_instance.debug_log: self._plugin_instance.logger.debug("Stopping Counter for Call Time") diff --git a/plugin.yaml b/plugin.yaml index e67d76004..d8ebfd7b3 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -400,6 +400,7 @@ item_structs: visu_acl: rw avm2_data_type@instance: tam avm2_tam_index@instance: 1 + avm2_read_after_write@instance: 5 name: type: str @@ -422,6 +423,7 @@ item_structs: visu_acl: rw avm2_data_type@instance: deflection_enable avm2_deflection_index@instance: 1 + avm2_read_after_write@instance: 5 deflection_type: type: str @@ -502,6 +504,7 @@ item_structs: visu_acl: rw avm2_data_type@instance: wlanconfig # 2,4ghz avm2_wlan_index@instance: 1 + avm2_read_after_write@instance: 5 wlan_1_ssid: type: str visu_acl: ro @@ -512,6 +515,7 @@ item_structs: visu_acl: rw avm2_data_type@instance: wlanconfig # 5 GHz avm2_wlan_index@instance: 2 + avm2_read_after_write@instance: 5 wlan_2_ssid: type: str visu_acl: ro @@ -522,6 +526,7 @@ item_structs: visu_acl: rw avm2_data_type@instance: wlanconfig # Guest avm2_wlan_index@instance: 3 + avm2_read_after_write@instance: 5 wlan_gast_ssid: type: str visu_acl: ro diff --git a/webif/templates/index.html b/webif/templates/index.html index 8af83f37e..70a953728 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -225,7 +225,6 @@ {% endif %} - {{ p._fritz_device.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetDeflections }}
{% endblock %} From bd60385b4b103ca871bb9103f318fe0a932f7808 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sun, 18 Sep 2022 11:30:19 +0200 Subject: [PATCH 023/178] - Bugfixing --- __init__.py | 70 ++++++++++++++++++++------------ plugin.yaml | 34 ++++++---------- user_doc.rst | 81 +++++++++++++++++++++++++++++--------- webif/__init__.py | 24 +++++------ webif/templates/index.html | 73 ++++++++++++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 80 deletions(-) diff --git a/__init__.py b/__init__.py index df129282e..3e21eb53e 100644 --- a/__init__.py +++ b/__init__.py @@ -7,9 +7,6 @@ # https://www.smarthomeNG.de # https://knx-user-forum.de/forum/supportforen/smarthome-py # -# Sample plugin for new plugins to run with SmartHomeNG version 1.8 and -# upwards. -# # SmartHomeNG is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -445,7 +442,7 @@ def update_item(self, item, caller=None, source=None, dest=None): self.logger.debug(f"Updated item={item.id()} with avm_data_type={avm_data_type} identified as part of '_aha_attributes'") self._fritz_home.handle_updated_item(item, avm_data_type, readafterwrite) else: - self.logger.warning(f"Smarthome Interface not activated or not available. Update for {avm_data_type} will not be executed.") + self.logger.warning(f"AVM Homeautomation Interface not activated or not available. Update for {avm_data_type} will not be executed.") @property def get_callmonitor(self): @@ -469,6 +466,9 @@ def get_fritz_device(self): def get_fritz_home(self): return self._fritz_home + @property + def get_log_level(self): + return self.logger.getEffectiveLevel() class FritzDevice: """ @@ -537,38 +537,38 @@ def register_item(self, item, avm_data_type: str): if avm_data_type in _wlan_config_attributes: avm_wlan_index = self._get_wlan_index(item) if avm_wlan_index is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_wlan_index' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_wlan_index' found; append to list.") self._items[item] = (avm_data_type, avm_wlan_index) else: - self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_wlan_index' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm2_wlan_index' is not defined; Item will be ignored.") # handle network_device related items elif avm_data_type in (_host_attribute + _host_child_attributes): avm_mac = self._get_mac(item) if avm_mac is not None: if avm_mac is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_mac' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_mac' found; append to list.") self._items[item] = (avm_data_type, avm_mac) else: - self._plugin_instance.logger.warning("Item {item.id()} with avm attribute found, but 'avm_mac' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning("Item {item.id()} with avm attribute found, but 'avm2_mac' is not defined; Item will be ignored.") # handle tam related items elif avm_data_type in _tam_attributes: avm_tam_index = self._get_tam_index(item) if avm_tam_index is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_tam_index' found; append to list.") self._items[item] = (avm_data_type, avm_tam_index) else: - self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm2_tam_index' is not defined; Item will be ignored.") # handle deflection related items elif avm_data_type in _deflection_attributes: avm_deflection_index = self._get_deflection_index(item) if avm_deflection_index is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_tam_index' found; append to list.") self._items[item] = (avm_data_type, avm_deflection_index) else: - self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm2_tam_index' is not defined; Item will be ignored.") # handle all other items else: @@ -778,9 +778,11 @@ def get_password(self): """ return self._password + @property def get_item_dict(self): return self._items + @property def get_item_list(self): return list(self._items.keys()) @@ -1132,7 +1134,7 @@ def get_call_origin(self) -> str: :return: String phone name """ - phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialGetConfig()['NewX_AVM-DE_PhoneName'] + phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialGetConfig()['NewX_AVM_DE_PhoneName'] if phone_name is None: self._plugin_instance.logger.error("No call origin available.") return phone_name @@ -1147,7 +1149,7 @@ def get_phone_name(self, index: int = 1) -> str: :return: String phone name """ - phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetPhonePort()['NewX_AVM-DE_PhoneName'] + phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetPhonePort()['NewX_AVM_DE_PhoneName'] if phone_name is None: self._plugin_instance.logger.error(f"No phone name available at provided index {index}") return phone_name @@ -1958,9 +1960,12 @@ def _poll_aha(self): try: _device_dict = self.get_devices_as_dict() except Exception as e: - self._plugin_instance.logger.warning(f"Error {e} occurred during method _poll_aha.") + self._plugin_instance.logger.warning(f"Error {e} occurred while getting device infos.") else: if isinstance(_device_dict, dict): + + initial_run = True if not self._aha_devices else False + for ain in _device_dict: self._plugin_instance.logger.debug(f"_poll_aha: handle AIN={ain} with _device_dict[ain]={_device_dict[ain]}") self._aha_devices[ain] = {} @@ -2003,11 +2008,17 @@ def _poll_aha(self): self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_powermeter[entry]}") if _device_dict[ain].has_switch: - self._aha_devices[ain]['device_functions'].append('powermeter') + self._aha_devices[ain]['device_functions'].append('switch') for entry in _link_switch: self._aha_devices[ain][entry] = eval(f"_device_dict[ain].{_link_switch[entry]}") + + if initial_run: + self._plugin_instance.logger.info(f"The following AVM Homeautomation Devices were discovered:") + for ain in self._aha_devices: + self._plugin_instance.logger.info(f" - AIN: {ain}, Product Name: {self._aha_devices[ain]['product_name']}, Device Name: {self._aha_devices[ain]['device_name']}, Functions: {', '.join(self._aha_devices[ain]['device_functions'])}") + else: - self._plugin_instance.logger.info(f"No AVM Smarthome Devices detected. Check Plugin settings or smarthome devices.") + self._plugin_instance.logger.info(f"No AVM Homeautomation Device detected. Check Plugin settings for AVM Homeautomation devices.") def update_items(self): """ @@ -2046,9 +2057,11 @@ def update_items(self): else: self._plugin_instance.logger.warning(f'No values for item={item.id()} at device with AIN={_ain} available.') + @property def get_item_dict(self): return self._items + @property def get_item_list(self): return list(self._items.keys()) @@ -2097,7 +2110,7 @@ def _login_request(self, username=None, challenge_response=None): Send a login request with parameters. """ - url = self.get_prefixed_host() + self._login_route + url = self._get_prefixed_host() + self._login_route params = {} if username: params["username"] = username @@ -2116,7 +2129,7 @@ def _logout_request(self): Send a logout request. """ - url = self.get_prefixed_host() + self._login_route + url = self._get_prefixed_host() + self._login_route params = {"logout": "1", "sid": self._sid} self._request(url, params) @@ -2158,7 +2171,7 @@ def _aha_request(self, cmd, ain=None, param=None, rf='str'): Send an AHA request. """ - url = self.get_prefixed_host() + self._homeauto_route + url = self._get_prefixed_host() + self._homeauto_route params = {"switchcmd": cmd, "sid": self._sid} if param: params.update(param) @@ -2225,7 +2238,7 @@ def check_sid(self): """ self._plugin_instance.logger.debug(f"check_sid called") - url = self.get_prefixed_host() + self._login_route + url = self._get_prefixed_host() + self._login_route params = {"sid": self._sid} plain = self._request(url, params) dom = ElementTree.fromstring(plain) @@ -2237,7 +2250,7 @@ def check_sid(self): else: self._plugin_instance.logger.info(f"Session ID is still valid.") - def get_prefixed_host(self): + def _get_prefixed_host(self): """ Choose the correct protocol prefix for the host. @@ -2259,7 +2272,7 @@ def get_prefixed_host(self): def update_devices(self): """ - Updating AHA Devices respective dictonary + Updating AHA Devices respective dictionary """ self._plugin_instance.logger.info("Updating Devices ...") @@ -2772,7 +2785,7 @@ def get_device_log_from_lua(self): if not self._logged_in: self.login() - url = self.get_prefixed_host() + self._log_route + url = self._get_prefixed_host() + self._log_route params = {"sid": self._sid} # get data @@ -2804,7 +2817,7 @@ def get_device_log_from_lua_separated(self): if not self._logged_in: self.login() - url = self.get_prefixed_host() + self._log_separate_route + url = self._get_prefixed_host() + self._log_separate_route params = {"sid": self._sid} data = self._request(url, params, result='json') @@ -3791,20 +3804,25 @@ def set_callmonitor_item_values_initially(self): item(duration, self._plugin_instance.get_fullname()) break + @property def get_item_list(self): return list(self._items.keys()) + @property def get_trigger_item_list(self): return list(self._trigger_items.keys()) + @property def get_item_incoming_list(self): return list(self._items_incoming.keys()) + @property def get_item_outgoing_list(self): return list(self._items_outgoing.keys()) + @property def get_item_all_list(self): - return self.get_item_list() + self.get_trigger_item_list() + self.get_item_incoming_list() + self.get_item_outgoing_list() + return self.get_item_list + self.get_trigger_item_list + self.get_item_incoming_list + self.get_item_outgoing_list @property def get_item_count_total(self): diff --git a/plugin.yaml b/plugin.yaml index d8ebfd7b3..fa548caaf 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -9,8 +9,8 @@ plugin: tester: psilo, schuma, aschwith, bmx state: develop # change to ready when done with development # keywords: iot xyz -# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page -# support: https://knx-user-forum.de/forum/supportforen/smarthome-py + documentation: http://smarthomeng.de/user/plugins/avm/user_doc.html + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin version: 1.0.0 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.8 # minimum shNG version to use this plugin @@ -553,7 +553,7 @@ item_structs: type: str avm2_data_type@instance: device_hostname visu_acl: ro - smarthome_general: + aha_general: name: avm2_data_type@instance: device_name type: str @@ -575,17 +575,11 @@ item_structs: functions: avm2_data_type@instance: device_functions type: list - smarthome_hkr: - current_temperature: - avm2_data_type@instance: current_temperature - type: num + aha_thermostat: target_temperature: avm2_data_type@instance: target_temperature avm2_read_after_write@instance: 5 type: num - set_target_temperature: - avm2_data_type@instance: set_target_temperature - type: num comfort_temperature: avm2_data_type@instance: temperature_comfort type: num @@ -601,6 +595,9 @@ item_structs: window_open: avm2_data_type@instance: window_open type: bool + windowopenactiveendtime: + avm2_data_type@instance: windowopenactiveendtime + type: num summer_active: avm2_data_type@instance: summer_active type: bool @@ -610,15 +607,6 @@ item_structs: errorcode: avm2_data_type@instance: errorcode type: num - set_window_open: - avm2_data_type@instance: set_window_open - type: bool - windowopenactiveendtime: - avm2_data_type@instance: windowopenactiveendtime - type: num - set_hkr_boost: - avm2_data_type@instance: set_hkr_boost - type: bool hkr_boost: avm2_data_type@instance: hkr_boost type: bool @@ -631,18 +619,18 @@ item_structs: device_lock: avm2_data_type@instance: device_lock type: bool - smarthome_temperature_sensor: + aha_temperature_sensor: current_temperature: avm2_data_type@instance: current_temperature type: num temperature_offset: avm2_data_type@instance: temperature_offset type: num - smarthome_alert: + aha_alert: state: avm2_data_type@instance: alert type: bool - smarthome_switch: + aha_switch: switch_state: avm2_data_type@instance: switch_state type: bool @@ -650,7 +638,7 @@ item_structs: avm2_data_type@instance: switch_toggle type: bool enforce_updates: yes - smarthome_powermeter: + aha_powermeter: power: avm2_data_type@instance: power type: num diff --git a/user_doc.rst b/user_doc.rst index 2dacdb837..40b0bc055 100644 --- a/user_doc.rst +++ b/user_doc.rst @@ -153,12 +153,12 @@ Zur Vereinfachung der Einrichtung von Items sind für folgende Item-structs vord - ``wan`` - WAN Items - ``wlan`` - Wireless Lan Items - ``device`` - Items eines verbundenen Gerätes -- ``smarthome_general`` - Allgemeine Informationen eines AVM HomeAutomation Devices -- ``smarthome_hkr`` - spezifische Informationen eines AVM HomeAutomation Thermostat Devices -- ``smarthome_temperatur_sensor`` - spezifische Informationen eines AVM HomeAutomation Devices mit Temperatursensor -- ``smarthome_alert`` - spezifische Informationen eines AVM HomeAutomation Devices mit Alarmfunktion -- ``smarthome_switch`` - spezifische Informationen eines AVM HomeAutomation Devices mit Schalter -- ``smarthome_powermeter`` - spezifische Informationen eines AVM HomeAutomation Devices mit Strommessung +- ``aha_general`` - Allgemeine Informationen eines AVM HomeAutomation Devices +- ``aha_hkr`` - spezifische Informationen eines AVM HomeAutomation Thermostat Devices +- ``aha_temperatur_sensor`` - spezifische Informationen eines AVM HomeAutomation Devices mit Temperatursensor +- ``aha_alert`` - spezifische Informationen eines AVM HomeAutomation Devices mit Alarmfunktion +- ``aha_switch`` - spezifische Informationen eines AVM HomeAutomation Devices mit Schalter +- ``aha_powermeter`` - spezifische Informationen eines AVM HomeAutomation Devices mit Strommessung Item Beispiel mit Verwendung der structs ohne Instanz @@ -209,9 +209,9 @@ Item Beispiel mit Verwendung der structs ohne Instanz type: foo avm_ain: 'xxxxx xxxxxxx' struct: - - avm.smarthome_general - - avm.smarthome_hkr - - avm.smarthome_temperatur_sensor + - avm.aha_general + - avm.aha_thermostat + - avm.aha_temperatur_sensor Item Beispiel mit Verwendung der structs mit Instanz @@ -225,10 +225,10 @@ Item Beispiel mit Verwendung der structs mit Instanz ain@fritzbox_1: 'xxxxx xxxxxxx' instance: fritzbox_1 struct: - - avm.smarthome_general - - avm.smarthome_switch - - avm.smarthome_powermeter - - avm.smarthome_temperature_sensor + - avm.aha_general + - avm.aha_switch + - avm.aha_powermeter + - avm.aha_temperature_sensor temperature: database: 'yes' power: @@ -426,10 +426,53 @@ Aufruf des Webinterfaces Das Plugin kann aus dem Admin-IF aufgerufen werden. Dazu auf der Seite Plugins in der entsprechenden Zeile das Icon in der Spalte **Web Interface** anklicken. +Es werden nur die Tabs angezeigt, deren Funktionen im Plugin aktiviert sind bzw. die von Fritzdevice unterstützt werden. + Im WebIF stehen folgende Reiter zur Verfügung: - - AVM Items - Tabellarische Auflistung aller Items, die mit dem TR-064 Protokoll ausgelesen werden - - AVM Smarthome Items - Tabellarische Auflistung aller Items, die mit dem AHA Protokoll ausgelesen werden (Items der Smarthome Geräte) - - Plugin-API - Beschreibung der Plugin-API - - Log-Einträge - Listung der Logeinträge der Fritzbox - - Call Monitor Items - Tabellarische Auflistung des Anrufmonitors (nur wenn dieser konfiguriert ist) - - AVM Smarthome Devices - Auflistung der mit der Fritzbox verbundenen Geräte + +AVM Items +~~~~~~~~~ + +Tabellarische Auflistung aller Items, die mit dem TR-064 Protokoll ausgelesen werden + +.. image:: user_doc/assets/webif_tab1.jpg + :class: screenshot + +AVM Smarthome Items +~~~~~~~~~~~~~~~~~~~ +Tabellarische Auflistung aller Items, die mit dem AHA Protokoll ausgelesen werden (Items der AVM HomeAutomation Geräte) + +.. image:: user_doc/assets/webif_tab2.jpg + :class: screenshot + +AVM Smarthome Devices +~~~~~~~~~~~~~~~~~~~~~ + +Auflistung der mit der Fritzbox verbundenen AVM HomeAutomation Geräte + +.. image:: user_doc/assets/webif_tab3.jpg + :class: screenshot + +Call Monitor Items +~~~~~~~~~~~~~~~~~~ + +Tabellarische Auflistung des Anrufmonitors (nur wenn dieser konfiguriert ist) + +.. image:: user_doc/assets/webif_tab4.jpg + :class: screenshot + +Log-Einträge +~~~~~~~~~~~~ + +Listung der Logeinträge der Fritzbox + +.. image:: user_doc/assets/webif_tab5.jpg + :class: screenshot + +Plugin-API +~~~~~~~~~~ + +Beschreibung der Plugin-API + +.. image:: user_doc/assets/webif_tab6.jpg + :class: screenshot diff --git a/webif/__init__.py b/webif/__init__.py index 980a2218e..a9912f9b4 100644 --- a/webif/__init__.py +++ b/webif/__init__.py @@ -61,29 +61,28 @@ def index(self, reload=None, action=None): """ if self.plugin.get_fritz_device(): - raw_items = self.plugin.get_fritz_device().get_item_list() - tr064_items = sorted(raw_items, key=lambda k: str.lower(k['_path'])) - tr064_item_count = len(raw_items) + tr064_items = sorted(self.plugin.get_fritz_device().get_item_list, key=lambda k: str.lower(k['_path'])) + tr064_item_count = len(tr064_items) else: tr064_items = None tr064_item_count = None if self.plugin.get_fritz_home(): - raw_items = self.plugin.get_fritz_home().get_item_list() - aha_items = sorted(raw_items, key=lambda k: str.lower(k['_path'])) - aha_item_count = len(raw_items) + aha_items = sorted(self.plugin.get_fritz_home().get_item_list, key=lambda k: str.lower(k['_path'])) + aha_item_count = len(aha_items) else: aha_items = None aha_item_count = None if self.plugin.get_monitoring_service(): - raw_items = self.plugin.get_monitoring_service().get_item_all_list() - call_monitor_items = sorted(raw_items, key=lambda k: str.lower(k['_path'])) - call_monitor_item_count = len(raw_items) + call_monitor_items = sorted(self.plugin.get_monitoring_service().get_item_all_list, key=lambda k: str.lower(k['_path'])) + call_monitor_item_count = len(call_monitor_items) else: call_monitor_items = None call_monitor_item_count = None + maintenance = True if self.plugin.get_log_level <= 20 else False + tmpl = self.tplenv.get_template('index.html') try: @@ -102,6 +101,7 @@ def index(self, reload=None, action=None): aha_item_count=aha_item_count, p=self.plugin, webif_pagelength=pagelength, + maintenance=maintenance, ) @cherrypy.expose @@ -119,7 +119,7 @@ def get_data_html(self, dataSet=None): data = dict() if self.plugin.get_monitoring_service(): data['call_monitor'] = {} - for item in self.plugin.get_monitoring_service().get_item_all_list(): + for item in self.plugin.get_monitoring_service().get_item_all_list: data['call_monitor'][item.id()] = {} data['call_monitor'][item.id()]['value'] = item() data['call_monitor'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') @@ -127,7 +127,7 @@ def get_data_html(self, dataSet=None): if self.plugin.get_fritz_device(): data['tr064_items'] = {} - for item in self.plugin.get_fritz_device().get_item_list(): + for item in self.plugin.get_fritz_device().get_item_list: data['tr064_items'][item.id()] = {} data['tr064_items'][item.id()]['value'] = item() data['tr064_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') @@ -135,7 +135,7 @@ def get_data_html(self, dataSet=None): if self.plugin.get_fritz_home(): data['aha_items'] = {} - for item in self.plugin.get_fritz_home().get_item_list(): + for item in self.plugin.get_fritz_home().get_item_list: data['aha_items'][item.id()] = {} data['aha_items'][item.id()]['value'] = item() data['aha_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') diff --git a/webif/templates/index.html b/webif/templates/index.html index 70a953728..72bc0e197 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -101,6 +101,10 @@ {% endblock pluginscripts %} + +{% set start_tab = 5 %} + + {% set tabcount = 6 %} @@ -332,7 +336,72 @@ {% block bodytab5 %} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'Befehl' }}{{ 'Ergebnis' }}
{{ 'p._fritz_device.get_call_origin()' }}{{ p._fritz_device.get_call_origin() }}
{{ "X_AVM_DE_NumberOfClients" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetNumberOfClients()['NewX_AVM_DE_NumberOfClients'] }}
{{ "NewX_AVM_DE_ClientIndex=0" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=0) }}
{{ "NewX_AVM_DE_ClientIndex=1" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=1) }}
{{ "NewX_AVM_DE_ClientIndex=2" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=2) }}
{{ "NewX_AVM_DE_ClientIndex=3" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=3) }}
{{ "NewX_AVM_DE_ClientIndex=0" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=0) }}
{{ "NewX_AVM_DE_ClientIndex=1" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=1) }}
{{ "NewX_AVM_DE_ClientIndex=2" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=2) }}
{{ "NewX_AVM_DE_ClientIndex=3" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=3) }}
{{ "X_AVM_DE_GetClients" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClients() }}
{{ "X_AVM_DE_DialSetConfig" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialSetConfig(NewX_AVM_DE_PhoneName='Torklingel') }}
{{ "p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialGetConfig()" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialGetConfig() }}
+ + +
{% endblock %} From fe94d9b12d9f2d6b9530c8ee07a58a03b35b65e3 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 17 Oct 2022 16:33:39 +0200 Subject: [PATCH 024/178] Update WebIF --- __init__.py | 16 ++++++++-------- plugin.yaml | 2 +- webif/__init__.py | 7 ++----- webif/templates/index.html | 1 + 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/__init__.py b/__init__.py index 3e21eb53e..f91c189a0 100644 --- a/__init__.py +++ b/__init__.py @@ -242,7 +242,7 @@ class AVM2(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.0.0' + PLUGIN_VERSION = '2.0.0' def __init__(self, sh): """ @@ -467,9 +467,10 @@ def get_fritz_home(self): return self._fritz_home @property - def get_log_level(self): + def log_level(self): return self.logger.getEffectiveLevel() + class FritzDevice: """ This class encapsulates information related to a specific FritzDevice using TR-064 @@ -2069,12 +2070,11 @@ def _request(self, url: str, params: dict = None, timeout: int = 10, result: str """ Send a request with parameters. - :param url URL to be requested - :param params params for request - :param timeout timeout - :param result type of result - :return request response - :type return + :param url: URL to be requested + :param params: params for request + :param timeout: timeout + :param result: type of result + :return: request response """ try: diff --git a/plugin.yaml b/plugin.yaml index fa548caaf..2d3f1afe4 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: http://smarthomeng.de/user/plugins/avm/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin - version: 1.0.0 # Plugin version (must match the version specified in __init__.py) + version: 2.0.0 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.8 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) # py_minversion: 3.6 # minimum Python version to use for this plugin diff --git a/webif/__init__.py b/webif/__init__.py index a9912f9b4..bff54327d 100644 --- a/webif/__init__.py +++ b/webif/__init__.py @@ -81,8 +81,6 @@ def index(self, reload=None, action=None): call_monitor_items = None call_monitor_item_count = None - maintenance = True if self.plugin.get_log_level <= 20 else False - tmpl = self.tplenv.get_template('index.html') try: @@ -101,7 +99,7 @@ def index(self, reload=None, action=None): aha_item_count=aha_item_count, p=self.plugin, webif_pagelength=pagelength, - maintenance=maintenance, + maintenance=True if self.plugin.log_level <= 20 else False, ) @cherrypy.expose @@ -145,7 +143,6 @@ def get_data_html(self, dataSet=None): return json.dumps(data, default=str) except Exception as e: self.logger.error(f"get_data_html exception: {e}") - return {} @cherrypy.expose def reboot(self): @@ -157,4 +154,4 @@ def reconnect(self): @cherrypy.expose def reset_item_blacklist(self): - self.plugin.get_fritz_device().reset_item_blacklist() \ No newline at end of file + self.plugin.get_fritz_device().reset_item_blacklist() diff --git a/webif/templates/index.html b/webif/templates/index.html index 72bc0e197..4fd90b6b2 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -141,6 +141,7 @@ {% block headtable %} + From bc989a261d9f73ae218c6acf2f8ce8b0a200ebdd Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 17 Oct 2022 17:23:37 +0200 Subject: [PATCH 025/178] Update WebIF --- TR-064links.yaml | 975 ++++++++++++++++++++++++++++++++ webif/__init__.py | 2 + webif/static/img/lamp_green.png | Bin 0 -> 3757 bytes webif/static/img/lamp_red.png | Bin 0 -> 3740 bytes webif/templates/index.html | 155 ++--- 5 files changed, 1057 insertions(+), 75 deletions(-) create mode 100644 TR-064links.yaml create mode 100644 webif/static/img/lamp_green.png create mode 100644 webif/static/img/lamp_red.png diff --git a/TR-064links.yaml b/TR-064links.yaml new file mode 100644 index 000000000..f1c9a13e7 --- /dev/null +++ b/TR-064links.yaml @@ -0,0 +1,975 @@ +InternetGatewayDevice: + DeviceInfo: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceinfoSCPD.pdf + GetInfo: + NewManufacturerName out ManufacturerName + NewManufacturerOUI out ManufacturerOUI + NewModelName out ModelName + NewDescription out Description + NewProductClass out ProductClass + NewSerialNumber out SerialNumber + NewSoftwareVersion out SoftwareVersion + NewHardwareVersion out HardwareVersion + NewSpecVersion out SpecVersion + NewProvisioningCode out ProvisioningCode + NewUpTime out UpTime + NewDeviceLog out DeviceLog + SetProvisioningCode: + NewProvisioningCode + GetDeviceLog: + NewDeviceLog + GetSecurityPort: + NewSecurityPort + DeviceConfig: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceconfigSCPD.pdf + GetPersistentData: + NewPersistentData + Layer3Forwarding: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/layer3forwardingSCPD.pdf + LANConfigSecurity: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/lanconfigsecuritySCPD.pdf + GetInfo: + NewMaxCharsPassword out MaxCharsPassword + NewMinCharsPassword out MinCharsPassword + NewAllowedCharsPassword out AllowedCharsPassword + ManagementServer: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/mgmsrvSCPD.pdf + GetInfo: + NewURL out URL + NewUsername out Username + NewPeriodicInformEnable out PeriodicInformEnable + NewPeriodicInformInterval out PeriodicInformInterval + NewPeriodicInformTime out PeriodicInformTime + NewParameterKey out ParameterKey + NewParameterHash out ParameterHash + NewConnectionRequestURL out ConnectionRequestURL + NewConnectionRequestUsername out ConnectionRequestUsername + NewUpgradesManaged out UpgradesManaged + Time: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/timeSCPD.pdf + GetInfo: + NewNTPServer1 out NTPServer1 + NewNTPServer2 out NTPServer2 + NewCurrentLocalTime out CurrentLocalTime + NewLocalTimeZone out LocalTimeZone not supported + NewLocalTimeZoneName out LocalTimeZoneName not supported + NewDaylightSavingsUsed out DaylightSavingsUsed not supported + NewDaylightSavingsStart out DaylightSavingsStart not supported + NewDaylightSavingsEnd out DaylightSavingsEnd not supported + UserInterface: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/userifSCPD.pdf + GetInfo: + NewUpgradeAvailable out UpgradeAvailable + NewPasswordRequired out PasswordRequired + NewPasswordUserSelectable out PasswordUserSelectable + NewWarrantyDate out WarrantyDate Not supported. Returns + Default string. + NewX_AVM-DE_Version out X_AVM-DE_Version + NewX_AVM-DE_DownloadURL out X_AVM-DE_DownloadURL + NewX_AVM-DE_InfoURL out X_AVM-DE_InfoURL + NewX_AVM-DE_UpdateState out X_AVM-DE_UpdateState + NewX_AVM-DE_LaborVersion out X_AVM-DE_LaborVersion + X_AVM-DE_Storage: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_storageSCPD.pdf + X_AVM-DE_WebDAVClient: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_webdavSCPD.pdf + X_AVM-DE_UPnP: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_upnp.pdf + X_AVM-DE_Speedtest: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_speedtestSCPD.pdf + GetInfo: + NewEnableTcp out EnableTcp + NewEnableUdp out EnableUdp + NewEnableUdpBidirect out EnableUdpBidirect + NewWANEnableTcp out WANEnableTcp + NewWANEnableUdp out WANEnableUdp + NewPortTcp out PortTcp + NewPortUdp out PortUdp + NewPortUdpBidirect out PortUdpBidirect + SetConfig: + NewEnableTcp in EnableTcp + NewEnableUdp in EnableUdp + NewEnableUdpBidirect in EnableUdpBidirect + NewWANEnableTcp in WANEnableTcp + NewWANEnableUdp in WANEnableUdp + X_AVM-DE_RemoteAccess: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_remoteSCPD.pdf + X_AVM-DE_MyFritz: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_myfritzSCPD.pdf + GetInfo: + NewEnabled out Enabled + NewDynDNSName out DynDNSName + NewPort out Port + NewDeviceRegistered out DeviceRegistered + GetNumberOfServices: + NewNumberOfServices + GetServiceByIndex: + X_VoIP: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voip-avm.pdf + GetInfo: + NewFaxT38Enable out FaxT38Enable + NewVoiceCoding out VoiceCoding + GetInfoEx: + NewVoIPNumberMinChars out VoIPNumberMinChars + NewVoIPNumberMaxChars out VoIPNumberMaxChars + NewVoIPNumberAllowedChars out VoIPNumberAllowedChars + NewVoIPUsernameMinChars out VoIPUsernameMinChars + NewVoIPUsernameMaxChars out VoIPUsernameMaxChars + NewVoIPUsernameAllowedChars out VoIPUsernameAllowedChars + NewVoIPPasswordMinChars out VoIPPasswordMinChars + NewVoIPPasswordMaxChars out VoIPPasswordMaxChars + NewVoIPPasswordAllowedChars out VoIPPasswordAllowedChars + NewVoIPRegistrarMinChars out VoIPRegistrarMinChars + NewVoIPRegistrarMaxChars out VoIPRegistrarMaxChars + NewVoIPRegistrarAllowedChars out VoIPRegistrarAllowedChars + NewVoIPSTUNServerMinChars out VoIPSTUNServerMinChars + NewVoIPSTUNServerMaxChars out VoIPSTUNServerMaxChars + NewVoIPSTUNServerAllowedChars out VoIPSTUNServerAllowedChars + NewX_AVM-DE_ClientUsernameMinChars out X_AVM-DE_ClientUsernameMinChars + NewX_AVM-DE_ClientUsernameMaxChars out X_AVM-DE_ClientUsernameMaxChars + NewX_AVM-DE_ClientUsernameAllowedChars out X_AVM-DE_ClientUsernameAllowedChars + NewX_AVM-DE_ClientPasswordMinChars out X_AVM-DE_ClientPasswordMinChars + NewX_AVM-DE_ClientPasswordMaxChars out X_AVM-DE_ClientPasswordMaxChars + NewX_AVM-DE_ClientPasswordAllowedChars out X_AVM-DE_ClientPasswordAllowedChars + GetExistingVoIPNumbers: + NewExistingVoIPNumbers + GetMaxVoIPNumbers: + GetVoIPCommonAreaCode: + GetVoIPEnableAreaCode: + SetVoIPEnableAreaCode: + SetVoIPCommonAreaCode: + GetVoIPCommonCountryCode: + GetVoIPEnableCountryCode: + SetConfig: + SetVoIPCommonCountryCode: + SetVoIPEnableCountryCode: + X_AVM-DE_AddVoIPAccount: + X_AVM-DE_DeleteClient: + X_AVM-DE_DelVoIPAccount: + X_AVM-DE_DialGetConfig: + X_AVM-DE_DialHangup: + X_AVM-DE_DialNumber: + NewX_AVM-DE_PhoneNumber + X_AVM-DE_DialSetConfig: + X_AVM-DE_GetNumberOfClients: + NewX_AVM-DE_NumberOfClients + X_AVM-DE_GetClient: + X_AVM-DE_GetClient2: + X_AVM-DE_GetClient3: + X_AVM-DE_GetClientByClientId: + X_AVM-DE_GetClients: + X_AVM-DE_GetNumberOfNumbers: + X_AVM-DE_GetNumbers: + X_AVM-DE_GetPhonePort: + X_AVM-DE_GetVoIPAccount: + X_AVM-DE_SetClient: + X_AVM-DE_SetClient2: + X_AVM-DE_SetClient3: + X_AVM-DE_SetClient4: + X_AVM-DE_SetDelayedCallNotification: + X_AVM-DE_SetVoIPCommonCountryCode: + X_AVM-DE_GetVoIPCommonCountryCode: + X_AVM-DE_SetVoIPCommonAreaCode: + X_AVM-DE_GetVoIPCommonAreaCode: + X_AVM-DE_GetAlarmClock: + NewIndex in Index + NewX_AVM-DE_AlarmClockEnable out X_AVM-DE_AlarmClockEnable + NewX_AVM-DE_AlarmClockName out X_AVM-DE_AlarmClockName + NewX_AVM-DE_AlarmClockTime out X_AVM-DE_AlarmClockTime + NewX_AVM-DE_AlarmClockWeekdays out X_AVM-DE_AlarmClockWeekdays + NewX_AVM-DE_AlarmClockPhoneName out X_AVM-DE_AlarmClockPhoneName + X_AVM-DE_GetNumberOfAlarmClocks: + NewX_AVM-DE_NumberOfAlarmClocks + X_AVM-DE_SetAlarmClockEnable: + NewIndex in Index + NewX_AVM-DE_AlarmClockEnable in X_AVM-DE_AlarmClockEnable + + X_AVM-DE_OnTel: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf + GetInfo: + NewEnable out Enable + NewStatus out Status + NewLastConnect out LastConnect + NewUrl out Url + NewServiceId out ServiceId + NewUsername out Username + NewName out Name + SetEnable: + NewEnable + SetConfig: + NewEnable in Enable + NewUrl in Url + NewServiceId in ServiceId + NewUsername in Username + NewPassword in Password + NewName in Name Telephone book name + GetInfoByIndex: + NewIndex in Index + NewEnable out Enable + NewStatus out Status + NewLastConnect out LastConnect + NewUrl out Url + NewServiceId out ServiceId + NewUsername out Username + NewName out Name + SetEnableByIndex: + NewIndex in Index + NewEnable in Enable + SetConfigByIndex: + NewIndex in Index + NewEnable in Enable + NewUrl in Url + NewServiceId in ServiceId + NewUsername in Username + NewPassword in Password + NewName in Name Telephone book name + GetNumberOfEntries: + NewOntelNumberOfEntries + GetCallList: + NewCallListURL + GetPhonebookList: + NewPhonebookList + GetPhonebook: + NewPhonebookID in PhonebookID + NewPhonebookURL out PhonebookURL + NewPhonebookName out PhonebookName + NewPhonebookExtraID out PhonebookExtraID + GetPhonebookEntry: + NewPhonebookID in PhonebookID + NewPhonebookEntryID in PhonebookEntryID + NewPhonebookEntryData out PhonebookEntryData + GetNumberOfDeflections: + NewNumberOfDeflections + GetDeflection: + NewDeflectionId in DeflectionId + NewEnable out Enable + NewType out Type + NewNumber out Number + NewDeflectionToNumber out DeflectionToNumber + NewMode out Mode + NewOutgoing out Outgoing + NewPhonebookID out PhonebookID + GetDeflections: + NewDeflectionList + SetDeflectionEnable: + NewDeflectionId in DeflectionId + NewEnable in Enable + GetDECTHandsetList: + NewDectIDList + X_AVM-DE_Dect: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_dectSCPD.pdf + GetNumberOfDectEntries: + NewNumberOfEntries + GetGenericDectEntry: + NewIndex in NumberOfEntries + NewID out ID + NewActive out Active + NewName out Name + NewModel out Model + NewUpdateAvailable out UpdateAvailable + NewUpdateSuccessful out UpdateSuccessful + NewUpdateInfo out UpdateInfo + GetSpecificDectEntry: + NewID in ID + NewActive out Active + NewName out Name + NewModel out Model + NewUpdateAvailable out UpdateAvailable + NewUpdateSuccessful out UpdateSuccessful + NewUpdateInfo out UpdateInfo + DectDoUpdate: + NewID + X_AVM-DE_TAM: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_tam.pdf + GetInfo: + NewIndex in Index + NewEnable out Enable + NewName out Name + NewTAMRunning out TAMRunning + NewStick out Stick + NewStatus out Status + NewCapacity out Capacity Remaining minutes. + NewMode out Mode + NewRingSeconds out RingSeconds + NewPhoneNumbers out PhoneNumbers + SetEnable: + NewIndex in Index + NewEnable in Enable + GetMessageList: + NewIndex in Index + NewURL out URL + GetList: + NewTAMList + X_AVM-DE_AppSetup: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_appsetup.pdf + X_AVM-DE_Homeauto: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_homeauto.pdf + GetInfo: + NewAllowedCharsAIN out String with all allowed chars for state + variable AIN + string + MaxCharsAIN out ui2 + MinCharsAIN out ui2 + MaxCharsDeviceName out ui2 + MinCharsDeviceName out ui2 + GetGenericDeviceInfos: + NewIndex in Index homeautomation device list ui2 + NewAIN out Device identifier string + NewDeviceId out Device ID ui2 + NewFunctionBitMask out Device function information ui2 + NewFirmwareVersion out FRITZ!OS version string + NewManufacturer out Manufacturer information string + NewProductName out Devicetype, eg. “Group”, “Template”, “HAN-FUN”, “AVM DECT Telefon C3”, “Comet DECT”, “FRITZ!DECT 200”, “FRITZ!DECT Repeater 100”, “unknown” string + NewDeviceName out Devicename string + NewPresent out Connection status PresentEnum + NewMultimeterIsEnabled out Feature is supported EnabledEnum + NewMultimeterIsValid out Value is valid ValidEnum + NewMultimeterPower out Power value [1/100 W] ui4 + NewMultimeterEnergy out Energy value [Wh] ui4 + NewTemperatureIsEnabled out Feature is supported EnabledEnum + NewTemperatureIsValid out Value is valid ValidEnum + NewTemperatureCelsius out Temperature [1/10°C] i4 + NewTemperatureOffset out Temperature offset [1/10°C] i4 + NewSwitchIsEnabled out Feature is supported EnabledEnum + NewSwitchIsValid out Value is valid ValidEnum + NewSwitchState out Switch status SwStateEnum + NewSwitchMode out Switch timer control SwModeEnum + NewSwitchLock out Switch keylock bool + NewHkrIsEnabled out HKR feature is supported EnabledEnum + NewHkrIsValid out HKR values are valid ValidEnum + NewHkrIsTemperature out Value is temperature [1/10 °C] i4 + NewHkrSetVentilStatus out HKR set valve status VentilEnum + NewHkrSetTemperature out Value set temperature [1/10 °C] i4 + NewHkrReduceVentilStatus out HKR reduce valve status VentilEnum + NewHkrReduceTemperature out Value reduce temperature [1/10 °C] i4 + NewHkrComfortVentilStatus out HKR comfort valve status VentilEnum + NewHkrComfortTemperature out Value comfort temperature [1/10 °C] i4 + GetSpecificDeviceInfos: + NewAIN in Device identifier string + NewDeviceId out Device ID ui2 + NewFunctionBitMask out Device function information ui2 + NewFirmwareVersion out FRITZ!OS version string + NewManufacturer out Manufacturer information string + NewProductName out Devicetype, eg. “Group”, “Template”, “HAN-FUN”, “AVM DECT Telefon C3”, “Comet DECT”, “FRITZ!DECT 200”, “FRITZ!DECT Repeater 100”, “unknown” string + NewDeviceName out Devicename string + NewPresent out Connection status PresentEnum + NewMultimeterIsEnabled out Feature is supported EnabledEnum + NewMultimeterIsValid out Value is valid ValidEnum + NewMultimeterPower out Power value [1/100 W] ui4 + NewMultimeterEnergy out Energy value [Wh] ui4 + NewTemperatureIsEnabled out Feature is supported EnabledEnum + NewTemperatureIsValid out Value is valid ValidEnum + NewTemperatureCelsius out Temperature [1/10°C] i4 + NewTemperatureOffset out Temperature offset [1/10°C] i4 + NewSwitchIsEnabled out Feature is supported EnabledEnum + NewSwitchIsValid out Value is valid ValidEnum + NewSwitchState out Switch status SwStateEnum + NewSwitchMode out Switch timer control SwModeEnum + NewSwitchLock out Switch keylock bool + NewHkrIsEnabled out HKR feature is supported EnabledEnum + NewHkrIsValid out HKR values are valid ValidEnum + NewHkrIsTemperature out Value is temperature [1/10 °C] i4 + NewHkrSetVentilStatus out HKR set valve status VentilEnum + NewHkrSetTemperature out Value set temperature [1/10 °C] i4 + NewHkrReduceVentilStatus out HKR reduce valve status VentilEnum + NewHkrReduceTemperature out Value reduce temperature [1/10 °C] i4 + NewHkrComfortVentilStatus out HKR comfort valve status VentilEnum + NewHkrComfortTemperature out Value comfort temperature [1/10 °C] i4 + SetDeviceName: + NewAIN + NewDeviceName + SetSwitch: + NewAIN + NewSwitchState + X_AVM-DE_Homeplug: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_homeplugSCPD.pdf + GetNumberOfDeviceEntries: + NewNumberOfEntries + GetGenericDeviceEntry: + NewIndex in NumberOfEntries + NewMACAddress out MACAddress + NewActive out Active + NewName out Name + NewModel out Model + NewUpdateAvailable out UpdateAvailable + NewUpdateSuccessful out UpdateSuccessful + GetSpecificDeviceEntry: + NewMACAddress out MACAddress + NewActive out Active + NewName out Name + NewModel out Model + NewUpdateAvailable out UpdateAvailable + NewUpdateSuccessful out UpdateSuccessful + DeviceDoUpdate: + NewMACAddress + + X_AVM-DE_Filelinks: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_filelinksSCPD.pdf + X_AVM-DE_Auth: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_auth.pdf + X_AVM-DE_HostFilter: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_hostfilterSCPD.pdf + GetAddonInfos: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/IGD1.pdf + NewByteSendRate out ByteSendRate + NewByteReceiveRate out ByteReceiveRate + NewPacketSendRate out PacketSendRate + NewPacketReceiveRate out PacketReceiveRate + NewTotalBytesSent out TotalBytesSent + NewTotalBytesReceived out TotalBytesReceived + NewAutoDisconnectTime out AutoDisconnectTime + NewIdleDisconnectTime out IdleDisconnectTime + NewDNSServer1 out DNSServer1 + NewDNSServer2 out DNSServer2 + NewVoipDNSServer1 out VoipDNSServer1 + NewVoipDNSServer2 out VoipDNSServer2 + NewUpnpControlEnabled out UpnpControlEnabled True, if portmappings allowed + NewRoutedBridgedModeBoth out RoutedBridgedModeBoth + NeWX_AVM_DE_TotalBytesSent64 out X_AVM_DE_TotalBytesSent64 + NeWX_AVM_DE_TotalBytesReceived64 out X_AVM_DE_TotalBytesReceived64 + NeWX_AVM_DE_WANAccessType out X_AVM_DE_WANAccessType +LANDevice: + WLANConfiguration: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf + SetEnable: + NewEnable + GetInfo: + NewEnable out Enable + NewStatus out Status + NewMaxBitRate out MaxBitRate Not supported. Returns + default string. + NewChannel out Channel + NewSSID out SSID + NewBeaconType out BeaconType + NewX_AVMDE_PossibleBeaconTypes + out X_AVMDE_PossibleBeaconTypes + NewMACAddressControlEnabled out MACAddressControlEnabled + NewStandard out Standard Only the highest of the active + modes is returned. + NewBSSID out BSSID + NewBasicEncryptionModes out BasicEncryptionModes + NewBasicAuthenticationMode out BasicAuthenticationMode Returns fixed string “none”. + NewMaxCharsSSID out MaxCharsSSID + NewMinCharsSSID out MinCharsSSID + NewAllowedCharsSSID out AllowedCharsSSID + NewMinCharsPSK out MinCharsPSK + NewMaxCharsPSK out MaxCharsPSK + NewAllowedCharsPSK out AllowedCharsPSK + SetConfig: + NewMaxBitRate in MaxBitRate + NewChannel in Channel + NewSSID in SSID + NewBeaconType in BeaconType Determines WLANencryption to be used. + NewMacAddressControlEnabled in MacAddressControlEnabled + NewBasicEncryptionModes in BasicEncryptionModes + NewBasicAuthenticationMode + GetGenericAssociatedDeviceInfo: + NewAssociatedDeviceMACAddress in AssociatedDeviceMACAddress + NewAssociatedDeviceIPAddress out AssociatedDeviceIPAddress + NewAssociatedDeviceAuthState out AssociatedDeviceAuthState + NewX_AVM-DE_Speed out X_AVM-DE_Speed 19.09.11 + NewX_AVM-DE_SignalStrength out X_AVM-DE_SignalStrength 19.09.11 + X_AVM-DE_GetSpecificAssociatedDeviceInfoByIp: + NewAssociatedDeviceIPAddress in AssociatedDeviceIPAddress + NewAssociatedDeviceMACAddress out AssociatedDeviceMACAddress + NewAssociatedDeviceAuthState out AssociatedDeviceAuthState + NewX_AVM-DE_Speed out X_AVM-DE_Speed + NewX_AVM-DE_SignalStrength out X_AVM-DE_SignalStrength + GetStatistics: + NewTotalPacketsSent + NewTotalPacketsReceived + GetPacketStatistics: + NewTotalPacketsSent + NewTotalPacketsReceived + X_AVM-DE_GetWLANHybridMode: + NewEnable out Enable + NewBeaconType out BeaconType + NewKeyPassphrase out KeyPassphrase + NewSSID out SSID + NewBSSID out BSSID + NewTrafficMode out TrafficMode + NewManualSpeed out ManualSpeed + NewMaxSpeedDS out MaxSpeedDS + NewMaxSpeedUS out MaxSpeedUS + X_AVM-DE_GetWLANExtInfo: + NewX_AVM-DE_APEnabled out X_AVM-DE_APEnabled + NewX_AVM-DE_APType out X_AVM-DE_APType + NewX_AVM-DE_TimeoutActive out X_AVM-DE_TimeoutActive + NewX_AVM-DE_Timeout out X_AVM-DE_Timeout + NewX_AVM-DE_TimeRemain out X_AVM-DE_TimeRemain + NewX_AVM-DE_NoForcedOff out X_AVM-DE_NoForcedOff + NewX_AVM-DE_UserIsolation out X_AVM-DE_UserIsolation + NewX_AVM-DE_EncryptionMode out X_AVM-DE_EncryptionMode + NewX_AVM-DE_LastChangedStamp out X_AVM-DE_LastChangedStam + X_AVM-DE_GetWLANConnectionInfo: + NewAssociatedDeviceMACAddress out AssociatedDeviceMACAddress + NewSSID out SSID + NewBSSID out BSSID + NewBeaconType out BeaconType + NewChannel out Channel + NewStandard out Standard + NewX_AVM-DE_SignalStrength out X_AVM-DE_SignalStrength + NewX_AVM-DE_Speed out X_AVM-DE_Speed + NewX_AVM-DE_SpeedRX out X_AVM-DE_SpeedRX + NewX_AVM-DE_SpeedMax out X_AVM-DE_SpeedMax + NewX_AVM-DE_SpeedRXMax out X_AVM-DE_SpeedRXMax + Hosts: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf + GetHostNumberOfEntries: + NewHostNumberOfEntries + GetSpecificHostEntry: + NewMACAddress in MACAddress + NewIPAddress out IPAddress + NewAddressSource out AddressSource + NewLeaseTimeRemaining out LeaseTimeRemaining + NewInterfaceType out InterfaceType + NewActive out Active + NewHostName out HostName + GetGenericHostEntry: + NewIndex in HostNumberOfEntries + NewMACAddress out MACAddress + NewIPAddress out IPAddress + NewAddressSource out AddressSource + NewLeaseTimeRemaining out LeaseTimeRemaining + NewInterfaceType out InterfaceType + NewActive out Active + NewHostName out HostName + X_AVM-DE_GetSpecificHostEntryByIp: + NewIPAddress in IPAddress + NewMACAddress out MACAddress + NewActive out Active + NewHostName out HostName + NewInterfaceType out InterfaceType + NewX_AVM-DE_Port out X_AVM-DE_Port + NewX_AVM-DE_Speed out X_AVM-DE_Speed + NewX_AVM-DE_UpdateAvailable out X_AVM-DE_UpdateAvailable + NewX_AVM-DE_UpdateSuccessful out X_AVMDE_UpdateSuccessful + NewX_AVM-DE_InfoURL out X_AVM-DE_InfoURL + NewX_AVM-DE_Model out X_AVM-DE_Model + NewX_AVM-DE_URL out X_AVM-DE_URL + LANEthernetInterfaceConfig: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/lanifconfigSCPD.pdf + SetEnable: + NewEnable + GetInfo: + NewEnable out Enable Writeable but internally ignored. + NewStatus out Status + NewMACAddress out MACAddress + NewMaxBitRate out MaxBitRate + NewDuplexMode out DuplexMode + GetStatistics: + NewBytesSent out Stats.BytesSent + NewBytesReceived out Stats.BytesReceived + NewPacketsSent out Stats.PacketsSent + NewPacketsReceived out Stats.PacketsReceived + LANHostConfigManagement: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/lanhostconfigmgmSCPD.pdf + GetInfo: + NewDHCPServerConfigurable out DHCPServerConfigurable + NewDHCPRelay out DHCPRelay + NewMinAddress out MinAddress + NewMaxAddress out MaxAddress + NewReservedAddresses out ReservedAddresses + NewDHCPServerEnable out DHCPServerEnable + NewDNSServers out DNSServers + NewDomainName out DomainName 2009-03-06 + NewIPRouters out IPRouters 2009-03-06 + NewSubnetMask out SubnetMask 2009-03-06 +WANDevice: + WANCommonInterfaceConfig: + GetCommonLinkProperties: + NewWANAccessType + NewLayer1UpstreamMaxBitRate + NewLayer1DownstreamMaxBitRate + NewPhysicalLinkStatus + GetTotalBytesSent: + NewTotalBytesSent + GetTotalBytesReceived: + NewTotalBytesReceived + GetTotalPacketsSent: + NewTotalPacketsSent + GetTotalPacketsReceived: + NewTotalPacketsReceived + X_AVM-DE_GetOnlineMonitor: + NewSyncGroupIndex + NewTotalNumberSyncGroups + NewSyncGroupName + NewSyncGroupMode out SyncGroupMode + Newmax_ds + Newmax_us + Newds_current_bps + Newmc_current_bps + Newus_current_bps + Newprio_realtime_bps + Newprio_high_bps out + Newprio_default_bps + Newprio_low_bps + WANDSLInterfaceConfig: + GetInfo: + NewEnable + NewStatus + NewDataPath + NewUpstreamCurrRate + NewDownstreamCurrRate + NewUpstreamMaxRate + NewDownstreamMaxRate + NewUpstreamNoiseMargin + NewDownstreamNoiseMargin + NewUpstreamAttenuation + NewDownstreamAttenuation + NewATURVendor + NewATURCountry + NewUpstreamPower + NewDownstreamPower + GetStatisticsTotal: + NewReceiveBlocks + NewTransmitBlocks + NewCellDelin + NewLinkRetrain + NewInitErrors + NewInitTimeouts + NewLossOfFraming + NewErroredSecs + NewSeverelyErroredSecs + NewFECErrors + NewATUCFECErrors + NewHECErrors + NewATUCHECErrors + NewATUCCRCErrors + NewCRCErrors + X_AVM-DE_GetDSLDiagnoseInfo: + NewX_AVM-DE_DSLDigagnoseState + NewX_AVM-DE_CableNokDistance + NewX_AVM-DE_DSLLastDiagnoseTime + NewX_AVM-DE_DSLSignalLossTime + NewX_AVM-DE_DSLActive + NewX_AVM-DE_DSLSync +WANConnectionDevice: + WANDSLLinkConfig: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wandslifconfigSCPD.pdf + GetInfo: + NewEnable + NewLinkStatus + NewLinkType + NewDestinationAddress + NewATMEncapsulation + NewAutoConfig + NewATMQoS + NewATMPeakCellRate + NewATMSustainableCellRate + SetEnable: + NewEnable + GetDSLLinkInfo: + NewLinkType + NewLinkStatus + GetDestinationAddress: + NewDestinationAddress + GetStatistics: + NewATMTransmittedBlocks + NewATMReceivedBlocks + NewAAL5CRCErrors + NewATMCRCErrors + WANEthernetLinkConfig: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanethlinkconfigSCPD.pdf + GetEthernetLinkStatus: + NewEthernetLinkStatus + WANPPPConnection: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanpppconnSCPD.pdf + GetInfo: + NewEnable + NewConnectionStatus + NewPossibleConnectionTypes + NewConnectionType + NewName + NewUptime + NewUpstreamMaxBitRate + NewDownstreamMaxBitRate + NewLastConnectionError + NewIdleDisconnectTime + NewRSIPAvailable + NewUserName + NewNATEnabled + NewExternalIPAddress + NewDNSServers + NewMACAddress + NewLastAuthErrorInfo + NewConnectionTrigger + NewMaxCharsUsername + NewMinCharsUsername + NewAllowedCharsUsername + NewMaxCharsPassword + NewMinCharsPassword + NewAllowedCharsPassword + NewTransportType + NewRouteProtocolRx + NewPPPoEServiceName + NewRemoteIPAddress + NewPPPoEACName + NewDNSEnabled + NewDNSOverrideAllowed + GetConnectionTypeInfo: + NewConnectionType + NewPossibleConnectionTypes + SetConnectionType: + NewConnectionType + GetStatusInfo: + NewConnectionStatus + NewLastConnectionError + NewUptime + GetGenericPortMappingEntry: + NewPortMappingIndex + NewRemoteHost + NewExternalPort + NewProtocol + NewInternalPort + NewInternalClient + NewEnabled + NewPortMappingDescription + NewLeaseDuration + GetSpecificPortMappingEntry: + NewExternalPort + NewProtocol + NewInternalPort + NewInternalClient + NewEnabled + NewPortMappingDescription + NewLeaseDuration + GetExternalIPAddress: + NewExternalIPAddress + WANIPConnection: + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf + GetInfo: + NewEnable + NewConnectionStatus + NewPossibleConnectionTypes + NewConnectionType + NewName + NewUptime + NewLastConnectionError + NewRSIPAvailable + NewNATEnabled + NewExternalIPAddress + NewDNSServers + NewMACAddress + NewConnectionTrigger + NewRouteProtocolRx + NewDNSEnabled + NewDNSOverrideAllowed + GetConnectionTypeInfo: + NewConnectionType + NewPossibleConnectionTypes + GetStatusInfo: + NewConnectionStatus + NewLastConnectionError + NewUptime + GetGenericPortMappingEntry: + NewPortMappingIndex + NewRemoteHost + NewExternalPort + NewProtocol + NewInternalPort + NewInternalClient + NewEnabled + NewPortMappingDescription + NewLeaseDuration + GetSpecificPortMappingEntry: + NewExternalPort + NewProtocol + NewInternalPort + NewInternalClient + NewEnabled + NewPortMappingDescription + NewLeaseDuration + GetExternalIPAddress: + NewExternalIPAddress + + +Fritzdevice Attribute: + - 'uptime' # r/o InternetGatewayDevice, DeviceInfo, GetInfo, NewUpTime + - 'serial_number' # r/o InternetGatewayDevice, DeviceInfo, GetInfo, NewSerialNumber + - 'software_version' # r/o InternetGatewayDevice, DeviceInfo, GetInfo, NewSoftwareVersion + - 'hardware_version' # r/o InternetGatewayDevice, DeviceInfo, GetInfo, NewHardwareVersion +Myfritz Attribute: + - 'myfritz_status' # r/o InternetGatewayDevice, X_AVM-DE_MyFritz, GetInfo, NewEnabled +Call Monitor Attribute: + - 'monitor_trigger' # r/o + - 'is_call_incoming' # r/o + - 'call_duration_incoming' # r/o + - 'last_caller_incoming' # r/o + - 'last_number_incoming' # r/o + - 'last_called_number_incoming' # r/o + - 'last_call_date_incoming' # r/o + - 'call_event_incoming' # r/o + - 'is_call_outgoing' # r/o + - 'call_duration_outgoing' # r/o + - 'last_caller_outgoing' # r/o + - 'last_number_outgoing' # r/o + - 'last_called_number_outgoing' # r/o + - 'last_call_date_outgoing' # r/o + - 'call_event_outgoing' # r/o + - 'call_direction' # r/o + - 'call_event' # r/o +TAM Attribute: + - 'tam' # r/w InternetGatewayDevice, X_AVM-DE_TAM, GetInfo, NewEnabled + - 'tam_name' # r/o InternetGatewayDevice, X_AVM-DE_TAM, GetInfo, NewName + - 'tam_old_message_number' # r/o + - 'tam_new_message_number' # r/o + - 'tam_total_message_number' # r/o InternetGatewayDevice, X_AVM-DE_TAM, GetInfo, NewName +WAN Attribute: + - 'wan_connection_status' # r/o WANConnectionDevice, WANIPConnection, GetInfo, NewConnectionStatus + - 'wan_connection_error' # r/o WANConnectionDevice, WANIPConnection, GetInfo, NewLastConnectionError + - 'wan_is_connected' # r/o + - 'wan_uptime' # r/o WANConnectionDevice, WANIPConnection, GetInfo, NewUptime + - 'wan_ip' # r/o WANConnectionDevice, WANIPConnection, GetExternalIPAddress, NewExternalIPAddress + - 'wan_upstream' # r/o + - 'wan_downstream' # r/o + - 'wan_total_packets_sent' # r/o + - 'wan_total_packets_received' # r/o + - 'wan_current_packets_sent' # r/o + - 'wan_current_packets_received' # r/o + - 'wan_total_bytes_sent' # r/o + - 'wan_total_bytes_received' # r/o + - 'wan_current_bytes_sent' # r/o + - 'wan_current_bytes_received' # r/o + - 'wan_link' # r/o +WLAN Attribute: + - 'wlanconfig' # r/w + - 'wlanconfig_ssid' # r/o + - 'wlan_guest_time_remaining' # r/o +Host Attribute: + - 'network_device' # r/o + - 'device_ip' # r/o + - 'device_connection_type' # r/o + - 'device_hostname' # r/o + - 'connection_status' # r/o +Smarthome Attribute via TR-064: + - 'aha_device' # r/w + - 'hkr_device' # r/o + - 'set_temperature' # r/o + - 'temperature' # r/o + - 'set_temperature_reduced' # r/o + - 'set_temperature_comfort' # r/o + - 'firmware_version' # r/o +Deflections: + - 'number_of_deflections' # r/o + - 'deflection_details' # r/o + - 'deflections_details' # r/o + - 'deflection_enable' # r/w + - 'deflection_type' # r/o + - 'deflection_number' # r/o + - 'deflection_to_number' # r/o + - 'deflection_mode' # r/o + - 'deflection_outgoing' # r/o + - 'deflection_phonebook_id' # r/o + +InternetGatewayDevice = { + 'DeviceInfo': { + 'GetInfo': {}, + 'GetSecurityPort': {}, + }, + 'DeviceConfig': { + 'GetPersistentData': {}, + }, + 'X_AVM-DE_MyFritz': { + 'GetInfo': {}, + 'GetNumberOfServices': {}, + }, + 'X_VoIP': { + 'GetInfoEx': {}, + }, + 'X_AVM-DE_OnTel': { + 'GetInfo': {}, + 'GetInfoByIndex': {}, + 'GetNumberOfEntries': {}, + 'GetCallList': {}, + 'GetPhonebookList': {}, + 'GetPhonebook': {}, + 'GetNumberOfDeflections': {}, + 'GetDeflection': {}, + 'GetDeflections': {}, + 'GetDECTHandsetList': {}, + }, + 'X_AVM-DE_Dect': { + 'GetNumberOfDectEntries': {}, + 'GetGenericDectEntry': {}, + 'GetSpecificDectEntry': {}, + }, + 'X_AVM-DE_TAM': { + 'GetInfo': {}, + 'GetMessageList': {}, + 'GetList': {}, + }, + 'X_AVM-DE_Homeauto': { + 'GetInfo': {}, + 'GetGenericDeviceInfos': {}, + 'GetSpecificDeviceInfos': {}, + }, + } + +LANDevice = { + 'WLANConfiguration': { + 'GetInfo': {}, + 'GetGenericAssociatedDeviceInfo': {}, + 'X_AVM-DE_GetSpecificAssociatedDeviceInfoByIp': {}, + 'GetStatistics': {}, + 'GetPacketStatistics': {}, + 'X_AVM-DE_GetWLANHybridMode': {}, + 'X_AVM-DE_GetWLANExtInfo': {}, + }, + 'Hosts': { + 'GetHostNumberOfEntries': {}, + 'GetSpecificHostEntry': {}, + 'GetGenericHostEntry': {}, + 'X_AVM-DE_GetSpecificHostEntryByIp': {}, + }, + 'LANEthernetInterfaceConfig': { + 'GetInfo': {}, + 'GetStatistics': {}, + }, + 'LANHostConfigManagement': { + 'GetInfo': {}, + }, + } + +WANDevice = { + 'WANCommonInterfaceConfig': { + 'GetCommonLinkProperties': {}, + 'GetTotalBytesSent': {}, + 'GetTotalBytesReceived': {}, + 'GetTotalPacketsSent': {}, + 'GetTotalPacketsReceived': {}, + 'X_AVM-DE_GetOnlineMonitor': {}, + }, + 'WANDSLInterfaceConfig': { + 'GetInfo': {}, + 'GetStatisticsTotal': {}, + 'X_AVM-DE_GetDSLDiagnoseInfo': {}, + }, + } + +WANConnectionDevice = { + 'WANDSLLinkConfig': { + 'GetInfo': {}, + 'GetDSLLinkInfo': {}, + 'GetDestinationAddress': {} + }, + 'WANEthernetLinkConfig': { + 'GetEthernetLinkStatus': {} + }, + 'WANPPPConnection': { + 'GetInfo': {}, + 'GetConnectionTypeInfo': {}, + 'GetStatusInfo': {}, + 'GetGenericPortMappingEntry': {}, + 'GetSpecificPortMappingEntry': {}, + 'GetExternalIPAddress': {} + }, + 'WANIPConnection': { + 'GetInfo': {}, + 'GetConnectionTypeInfo': {}, + 'GetStatusInfo': {}, + 'GetGenericPortMappingEntry': {}, + 'GetSpecificPortMappingEntry': {}, + 'GetExternalIPAddress': {} + }, + } diff --git a/webif/__init__.py b/webif/__init__.py index bff54327d..10ac02a3c 100644 --- a/webif/__init__.py +++ b/webif/__init__.py @@ -139,6 +139,8 @@ def get_data_html(self, dataSet=None): data['aha_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') data['aha_items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + data['maintenance'] = True if self.plugin.log_level <= 20 else False + try: return json.dumps(data, default=str) except Exception as e: diff --git a/webif/static/img/lamp_green.png b/webif/static/img/lamp_green.png new file mode 100644 index 0000000000000000000000000000000000000000..fb130568b7d788da9a89b16c97f60d30798bf0ad GIT binary patch literal 3757 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 -{% set start_tab = 5 %} - +{% if tr064_item_count > 0 %} + {% set start_tab = 1 %} +{% endif %} - + {% set tabcount = 6 %} + {% set tab1title = _(""'AVM2 TR-064 Items'" (" ~ tr064_item_count ~ ") ") %} -{% if p._fritz_home and aha_item_count > 0 %} +{% if p._fritz_home and aha_item_count > 0 %} {% set tab2title = _(""'AVM2 AHA Items'" (" ~ aha_item_count ~ ") ") %} {% else %} {% set tab2title = "hidden" %} {% endif %} -{% if p._fritz_home and len(p._fritz_home._aha_devices) > 0%} +{% if p._fritz_home and len(p._fritz_home._aha_devices) > 0%} {% set tab3title = _(""'AVM2 AHA Devices'" (" ~ len(p._fritz_home._aha_devices) ~ ") ") %} {% else %} {% set tab3title = "hidden" %} @@ -130,7 +132,11 @@ {% set tab5title = _(""'Log-Einträge'"") %} -{% set tab6title = _(""'Plugin-API'"") %} +{% if not maintenance %} + {% set tab6title = _(""'Plugin-API'"") %} +{% else %} + {% set tab5title = _(""'Maintenance'"") %} +{% endif %} {% set language = p.get_sh().get_defaultlanguage() %} @@ -337,9 +343,74 @@ {% block bodytab5 %} +
+
+ + + + + + + + + + {% if p._fritz_home %} + {% set logentries = p._fritz_home.get_device_log_from_lua_separated() %} + {% endif %} + {% if logentries %} + {% for logentry in logentries%} + + + + + + + {% endfor %} + {% endif %} + +
{{ 'Datum/Uhrzeit' }}{{ 'Meldung' }}{{ 'Typ' }}{{ 'Kategorie' }}
{{ logentry[0] }}{{ logentry[1] }} + + {{ logentry[2] }} + + {{ _('cat_'+logentry[3]|string) }}
+ +
+{% endblock %} + +{% block bodytab6 %}
+ {% if not maintenance %} + {% for function, dict in p.metadata.plugin_functions.items() %} +
+
+ {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) +
+
+ {{ dict['description'][language] }}
+ {% if dict['parameters'] is not none %} +
+
+ {{ _('Parameter') }}: +
+
+
    + {% for name, paramdict in dict['parameters'].items() %} +
  • + {{ name }}: {{ paramdict['type'] }}
    + {{ paramdict['description'][language] }} +
  • + {% endfor %} +
+
+
+ {% endif %} +
+
+ {% endfor %} + {% endif %} + {% if maintenance %} @@ -402,72 +473,6 @@
- - - -
-{% endblock %} - - -{% block bodytab6 %} -
- {% for function, dict in p.metadata.plugin_functions.items() %} -
-
- {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) -
-
- {{ dict['description'][language] }}
- {% if dict['parameters'] is not none %} -
-
- {{ _('Parameter') }}: -
-
-
    - {% for name, paramdict in dict['parameters'].items() %} -
  • - {{ name }}: {{ paramdict['type'] }}
    - {{ paramdict['description'][language] }} -
  • - {% endfor %} -
-
-
- {% endif %} -
-
- {% endfor %} + {% endif %}
{% endblock %} From a242449ec5c26dbee01b14bb94a7b575bd8a8d06 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 17 Oct 2022 17:55:13 +0200 Subject: [PATCH 026/178] Update plugin.yaml --- plugin.yaml | 294 ++++++++++++++++++++++++++-------------------------- 1 file changed, 145 insertions(+), 149 deletions(-) diff --git a/plugin.yaml b/plugin.yaml index 2d3f1afe4..552e6639b 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -52,7 +52,7 @@ parameters: type: int default: 300 description: - de: '(optional) Zeit zwischen zwei Updateläufen. Default ist 300 Sekunden.' + de: '(optional) Zeit zwischen zwei Runs. Default ist 300 Sekunden.' en: '(optional) Time period between two update cycles. Default is 300 seconds.' ssl: type: bool @@ -122,138 +122,138 @@ item_attributes: en: 'AVM Data Type of the respective item.' valid_list: # Fritzdevice Attribute - - 'uptime' # r/o - - 'serial_number' # r/o - - 'software_version' # r/o - - 'hardware_version' # r/o - # Myfritz Attribute - - 'myfritz_status' # r/o - # Call Monitor Attribute - - 'monitor_trigger' # r/o - - 'is_call_incoming' # r/o - - 'call_duration_incoming' # r/o - - 'last_caller_incoming' # r/o - - 'last_number_incoming' # r/o - - 'last_called_number_incoming' # r/o - - 'last_call_date_incoming' # r/o - - 'call_event_incoming' # r/o - - 'is_call_outgoing' # r/o - - 'call_duration_outgoing' # r/o - - 'last_caller_outgoing' # r/o - - 'last_number_outgoing' # r/o - - 'last_called_number_outgoing' # r/o - - 'last_call_date_outgoing' # r/o - - 'call_event_outgoing' # r/o - - 'call_direction' # r/o - - 'call_event' # r/o - # TAM Attribute alle Attribute benötigen zusätzlich das Attribut 'avm2_tam_index' - - 'tam' # r/w TAM an/aus; benötig - - 'tam_name' # r/o Name des TAM - - 'tam_old_message_number' # r/o Anzahl der alten Nachrichten - - 'tam_new_message_number' # r/o Anzahl der neuen Nachrichten - - 'tam_total_message_number' # r/o Gesamtanzahl der Nachrichten - # WAN Attribute - - 'wan_connection_status' # r/o - - 'wan_connection_error' # r/o - - 'wan_is_connected' # r/o - - 'wan_uptime' # r/o - - 'wan_ip' # r/o - - 'wan_upstream' # r/o - - 'wan_downstream' # r/o - - 'wan_total_packets_sent' # r/o - - 'wan_total_packets_received' # r/o - - 'wan_current_packets_sent' # r/o - - 'wan_current_packets_received' # r/o - - 'wan_total_bytes_sent' # r/o - - 'wan_total_bytes_received' # r/o - - 'wan_current_bytes_sent' # r/o - - 'wan_current_bytes_received' # r/o - - 'wan_link' # r/o - # WLAN Attribute - - 'wlanconfig' # r/w - - 'wlanconfig_ssid' # r/o - - 'wlan_guest_time_remaining' # r/o - # Host Attribute - - 'network_device' # r/o Verbindungsstatus // Defines Network device via MAC-Adresse - - 'device_ip' # r/o Geräte-IP (Muss Child von 'network_device' sein) ipv4 - - 'device_connection_type' # r/o Verbindungstyp (Muss Child von 'network_device' sein) str - - 'device_hostname' # r/o Gerätename (Muss Child von 'network_device' sein) str - - 'connection_status' # r/o Verbindungsstatus (Muss Child von 'network_device' sein) bool + - 'uptime' # r/o num Laufzeit des Fritzdevice in Sekunden + - 'serial_number' # r/o str Serialnummer des Fritzdevice + - 'software_version' # r/o str Software Version + - 'hardware_version' # r/o str Hardware Version + # Myfritz Attribute + - 'myfritz_status' # r/o bool MyFritz Status + # Call Monitor Attribute + - 'monitor_trigger' # r/o bool Monitortrigger + - 'is_call_incoming' # r/o bool Eingehender Anruf erkannt + - 'call_duration_incoming' # r/o num Dauer des eingehenden Anrufs + - 'last_caller_incoming' # r/o str Letzter Anrufer + - 'last_number_incoming' # r/o str Nummer des letzten eingehenden Anrufes + - 'last_called_number_incoming' # r/o str Angerufene Nummer des letzten eingehenden Anrufs + - 'last_call_date_incoming' # r/o str Zeitpunkt des letzten eingehenden Anrufs + - 'call_event_incoming' # r/o str Status des letzten eingehenden Anrufs + - 'is_call_outgoing' # r/o bool Ausgehender Anruf erkannt + - 'call_duration_outgoing' # r/o num Dauer des ausgehenden Anrufs + - 'last_caller_outgoing' # r/o str Letzter angerufener Kontakt + - 'last_number_outgoing' # r/o str Letzte angerufene Nummer + - 'last_called_number_outgoing' # r/o str Letzter verwendete Telefonnummer für ausgehenden Anruf + - 'last_call_date_outgoing' # r/o str Zeitpunkt des letzten ausgehenden Anrufs + - 'call_event_outgoing' # r/o str Status des letzten ausgehenden Anrufs + - 'call_direction' # r/o str Richtung des letzten Anrufes + - 'call_event' # r/o str Status des letzten Anrufes + # TAM Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_tam_index' + - 'tam' # r/w bool TAM an/aus + - 'tam_name' # r/o str Name des TAM + - 'tam_old_message_number' # r/o num Anzahl der alten Nachrichten + - 'tam_new_message_number' # r/o num Anzahl der neuen Nachrichten + - 'tam_total_message_number' # r/o num Gesamtanzahl der Nachrichten + # WAN Attribute + - 'wan_connection_status' # r/o str WAN Verbindungsstatus + - 'wan_connection_error' # r/o str WAN Verbindungsfehler + - 'wan_is_connected' # r/o bool WAN Verbindung aktiv + - 'wan_uptime' # r/o str WAN Verbindungszeit + - 'wan_ip' # r/o str WAN IP Adresse + - 'wan_upstream' # r/o num WAN Upstream Datenmenge + - 'wan_downstream' # r/o num WAN Downstream Datenmenge + - 'wan_total_packets_sent' # r/o num WAN Verbindung-Anzahl insgesamt versendeter Pakete + - 'wan_total_packets_received' # r/o num WAN Verbindung-Anzahl insgesamt empfangener Pakete + - 'wan_current_packets_sent' # r/o num WAN Verbindung-Anzahl aktuell versendeter Pakete + - 'wan_current_packets_received' # r/o num WAN Verbindung-Anzahl aktuell empfangener Pakete + - 'wan_total_bytes_sent' # r/o num WAN Verbindung-Anzahl insgesamt versendeter Bytes + - 'wan_total_bytes_received' # r/o num WAN Verbindung-Anzahl insgesamt empfangener Bytes + - 'wan_current_bytes_sent' # r/o num WAN Verbindung-Anzahl insgesamt versendeter Bytes + - 'wan_current_bytes_received' # r/o num WAN Verbindung-Anzahl insgesamt empfangener Bytes + - 'wan_link' # r/o bool WAN Link + # WLAN Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_wlan_index' + - 'wlanconfig' # r/w bool WLAN An/Aus + - 'wlanconfig_ssid' # r/o str WLAN SSID + - 'wlan_guest_time_remaining' # r/o num Verbleibende Zeit, bis zum automatischen Abschalten des Gäste-WLAN + # Host Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_mac' + - 'network_device' # r/o bool Verbindungsstatus // Defines Network device via MAC-Adresse + - 'device_ip' # r/o str Geräte-IP (Muss Child von 'network_device' sein) + - 'device_connection_type' # r/o str Verbindungstyp (Muss Child von 'network_device' sein) + - 'device_hostname' # r/o str Gerätename (Muss Child von 'network_device' sein) + - 'connection_status' # r/o bool Verbindungsstatus (Muss Child von 'network_device' sein) # Smarthome Attribute (Deprecated avm data types. Please use alternative AHA interface type) - - 'aha_device' # r/w Steckdose schalten; siehe "switch_state" - - 'hkr_device' # r/o Status des HKR (OPEN; CLOSED; TEMP) - - 'set_temperature' # r/o siehe "target_temperature" - - 'temperature' # r/o siehe "current_temperature" - - 'set_temperature_reduced' # r/o siehe "temperature_reduced" - - 'set_temperature_comfort' # r/o siehe "temperature_comfort" - - 'firmware_version' # r/o siehe "fw_version" - # Deflections - - 'number_of_deflections' # r/o Anzahl der eingestellten Rufumleitungen - - 'deflection_details' # r/o Details zur Rufumleitung (als dict); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item - - 'deflections_details' # r/o Details zu allen Rufumleitung (als dict) - - 'deflection_enable' # r/w Rufumleitung Status an/aus; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - - 'deflection_type' # r/o Type der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - - 'deflection_number' # r/o Telefonnummer, die umgeleitet wird; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - - 'deflection_to_number' # r/o Zielrufnummer der Umleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - - 'deflection_mode' # r/o Modus der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - - 'deflection_outgoing' # r/o Outgoing der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - - 'deflection_phonebook_id' # r/o Phonebook_ID der Zielrufnummer (Only valid if Type==fromPB); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - # AHA Interface attributes alle Attribute benötigen zusätzlich das Attribut 'avm2_tam_ain' - - 'device_id' # r/o Geräte -ID str - - 'manufacturer' # r/o Hersteller str - - 'product_name' # r/o Produktname str - - 'fw_version' # r/o Firmware Version str - - 'connected' # r/o Verbindungsstatus bool - - 'device_name' # r/o Gerätename str - - 'tx_busy' # r/o Verbindung aktiv bool - - 'device_functions' # r/o Im Gerät vorhandene Funktionen list - - - 'set_target_temperature' # w/o Soll-Temperatur Setzen num - - 'target_temperature' # r/w Soll-Temperatur (Status und Setzen) num - - 'current_temperature' # r/o Ist-Temperatur num - - 'temperature_reduced' # r/o Eingestellte reduzierte Temperatur num - - 'temperature_comfort' # r/o Eingestellte Komfort-Temperatur num - - 'temperature_offset' # r/o Eingestellter Temperatur-Offset num - - 'set_window_open' # w/o "Window Open" Funktionen Setzen bool - - 'window_open' # r/w "Window Open" Funktion (Status und Setzen) bool - - 'windowopenactiveendtime' # r/o Zeitliches Ende der "Window Open" Funktion num - - 'set_hkr_boost' # w/o "Boost" Funktion Setzen bool - - 'hkr_boost' # r/w "Boost" Funktion (Status aund Setzen) bool - # - 'boost_active' # r/o Status der "Boost" Funktion bool deprecated - - 'boostactiveendtime' # r/o Zeitliches Ende der "Boost" Funktion num - - 'summer_active' # r/o Status der "Sommer" Funktion bool - - 'holiday_active' # r/o Status der "Holiday" Funktion bool - - 'battery_low' # r/o "Battery low" Status bool - - 'battery_level' # r/o Batterie-Status in % num - - 'lock' # r/o Tastensperre über UI/API aktiv bool - - 'device_lock' # r/o Tastensperre direkt am Gerät ein bool - - 'errorcode' # r/o Fehlercodes die der HKR liefert num - - - 'set_simpleonoff' # w/o Gerät/Aktor/Lampe an-/ausschalten bool - - 'simpleonoff' # w/r Gerät/Aktor/Lampe (Status und Setzen) bool neu 1.6.4 - - - 'set_level' # w/o Level/Niveau von 0 bis 255 Setzen num - - 'level' # w/r Level/Niveau von 0 bis 255 (Setzen & Status) num neu 1.6.4 - - 'set_levelpercentage' # w/o Level/Niveau von 0% bis 100% Setzen num - - 'levelpercentage' # w/r Level/Niveau von 0% bis 100% (Setzen & Status) num neu 1.6.4 - - 'set_hue' # w/o Hue Setzen num - - 'hue' # w/r Hue (Status und Setzen) num neu 1.6.4 - - 'set_saturation' # w/o Saturation Setzen num - - 'saturation' # w/r Saturation (Status und Setzen) num neu 1.6.4 - - 'set_colortemperature' # w/o Farbtemperatur Setzen num - - 'colortemperature' # w/r Farbtemperatur (Status und Setzen) num neu 1.6.4 - - - 'switch_state' # r/w Schaltzustand Steckdose (Status und Setzen) bool - - 'switch_mode' # r/o Zeitschaltung oder manuell schalten bool - - 'switch_toggle' # w/o Schaltzustand umschalten (toggle) bool - - - 'power' # r/o Leistung in W (Aktualisierung alle 2 min) num - - 'energy' # r/o absoluter Verbrauch seit Inbetriebnahme num - - 'voltage' # r/o Spannung in V (Aktualisierung alle 2 min) num - - - 'humidity' # r/o Relative Luftfeuchtigkeit in % (FD440) num - - 'alert_state' # r/o letzter übermittelter Alarmzustand bool - + - 'aha_device' # r/w bool Steckdose schalten; siehe "switch_state" + - 'hkr_device' # r/o str Status des HKR (OPEN; CLOSED; TEMP) + - 'set_temperature' # r/o num siehe "target_temperature" + - 'temperature' # r/o num siehe "current_temperature" + - 'set_temperature_reduced' # r/o num siehe "temperature_reduced" + - 'set_temperature_comfort' # r/o num siehe "temperature_comfort" + - 'firmware_version' # r/o str siehe "fw_version" + # Deflections Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_deflection_index' + - 'number_of_deflections' # r/o num Anzahl der eingestellten Rufumleitungen + - 'deflection_details' # r/o dict Details zur Rufumleitung (als dict); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item + - 'deflections_details' # r/o dict Details zu allen Rufumleitung (als dict) + - 'deflection_enable' # r/w bool Rufumleitung Status an/aus; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_type' # r/o str Type der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_number' # r/o str Telefonnummer, die umgeleitet wird; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_to_number' # r/o str Zielrufnummer der Umleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_mode' # r/o str Modus der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_outgoing' # r/o str Outgoing der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + - 'deflection_phonebook_id' # r/o str Phonebook_ID der Zielrufnummer (Only valid if Type==fromPB); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item + # AHA Interface attributes Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_tam_ain' + - 'device_id' # r/o str Geräte -ID + - 'manufacturer' # r/o str Hersteller + - 'product_name' # r/o str Produktname + - 'fw_version' # r/o str Firmware Version + - 'connected' # r/o bool Verbindungsstatus + - 'device_name' # r/o str Gerätename + - 'tx_busy' # r/o bool Verbindung aktiv + - 'device_functions' # r/o list Im Gerät vorhandene Funktionen + + - 'set_target_temperature' # w/o num Soll-Temperatur Setzen + - 'target_temperature' # r/w num Soll-Temperatur (Status und Setzen) + - 'current_temperature' # r/o num Ist-Temperatur + - 'temperature_reduced' # r/o num Eingestellte reduzierte Temperatur + - 'temperature_comfort' # r/o num Eingestellte Komfort-Temperatur + - 'temperature_offset' # r/o num Eingestellter Temperatur-Offset + - 'set_window_open' # w/o bool "Window Open" Funktionen Setzen + - 'window_open' # r/w bool "Window Open" Funktion (Status und Setzen) + - 'windowopenactiveendtime' # r/o num Zeitliches Ende der "Window Open" Funktion + - 'set_hkr_boost' # w/o bool "Boost" Funktion Setzen + - 'hkr_boost' # r/w bool "Boost" Funktion (Status aund Setzen) + - 'boost_active' # r/o bool Status der "Boost" Funktion deprecated + - 'boostactiveendtime' # r/o num Zeitliches Ende der "Boost" Funktion + - 'summer_active' # r/o bool Status der "Sommer" Funktion + - 'holiday_active' # r/o bool Status der "Holiday" Funktion + - 'battery_low' # r/o bool "Battery low" Status + - 'battery_level' # r/o num Batterie-Status in % + - 'lock' # r/o bool Tastensperre über UI/API aktiv + - 'device_lock' # r/o bool Tastensperre direkt am Gerät ein + - 'errorcode' # r/o num Fehlercodes die der HKR liefert + + - 'set_simpleonoff' # w/o bool Gerät/Aktor/Lampe an-/ausschalten + - 'simpleonoff' # w/r bool Gerät/Aktor/Lampe (Status und Setzen) neu 1.6.4 + + - 'set_level' # w/o num Level/Niveau von 0 bis 255 Setzen + - 'level' # w/r num Level/Niveau von 0 bis 255 (Setzen & Status) neu 1.6.4 + - 'set_levelpercentage' # w/o num Level/Niveau von 0% bis 100% Setzen + - 'levelpercentage' # w/r num Level/Niveau von 0% bis 100% (Setzen & Status) neu 1.6.4 + - 'set_hue' # w/o num Hue Setzen + - 'hue' # w/r num Hue (Status und Setzen) neu 1.6.4 + - 'set_saturation' # w/o num Saturation Setzen + - 'saturation' # w/r num Saturation (Status und Setzen) neu 1.6.4 + - 'set_colortemperature' # w/o num Farbtemperatur Setzen + - 'colortemperature' # w/r num Farbtemperatur (Status und Setzen) neu 1.6.4 + + - 'switch_state' # r/w bool Schaltzustand Steckdose (Status und Setzen) + - 'switch_mode' # r/o bool Zeitschaltung oder manuell schalten + - 'switch_toggle' # w/o bool Schaltzustand umschalten (toggle) + + - 'power' # r/o num Leistung in W (Aktualisierung alle 2 min) + - 'energy' # r/o num absoluter Verbrauch seit Inbetriebnahme + - 'voltage' # r/o num Spannung in V (Aktualisierung alle 2 min) + + - 'humidity' # r/o num Relative Luftfeuchtigkeit in % (FD440) + - 'alert_state' # r/o bool letzter übermittelter Alarmzustand + avm2_incoming_allowed: type: str mandatory: False @@ -280,22 +280,22 @@ item_attributes: type: mac mandatory: False description: - de: 'Definition der MAC Adresse für Items vom avm_data_type `network_device`. Nur für diese Items mandatory!' - en: 'Definition of the MAC address for items of avm_data_type `network_device`. Only mandatory for these items!' + de: '(optional) Definition der MAC Adresse für Items vom avm_data_type `network_device`. Nur für diese Items mandatory!' + en: '(optional) Definition of the MAC address for items of avm_data_type `network_device`. Only mandatory for these items!' avm2_ain: type: str mandatory: False description: - de: "Definition der AktorIdentifikationsNummer (AIN) Items vom avm_data_types für `AHA-Interface`. Nur für diese Items mandatory!" - en: "Definition of the ActorIdentificationNumber (AIN) for items of avm_data_types `AHA-Interface`. Only mandatory for these items!" + de: "(optional) Definition der AktorIdentifikationsNummer (AIN) Items vom avm_data_types für `AHA-Interface`. Nur für diese Items mandatory!" + en: "(optional) Definition of the ActorIdentificationNumber (AIN) for items of avm_data_types `AHA-Interface`. Only mandatory for these items!" avm2_tam_index: type: int mandatory: False description: - de: 'Index für den Anrufbeantworter, normalerweise für den ersten eine "1". Es werden bis zu 5 Anrufbeantworter vom Gerät unterstützt.' - en: 'Index für the answering machine, normally a "1" for the first one. Supported are up to 5 answering machines.' + de: '(optional) Index für den Anrufbeantworter, normalerweise für den ersten eine "1". Es werden bis zu 5 Anrufbeantworter vom Gerät unterstützt.' + en: '(optional) Index für the answering machine, normally a "1" for the first one. Supported are up to 5 answering machines.' valid_min: 1 valid_max: 5 @@ -303,16 +303,16 @@ item_attributes: type: int mandatory: False description: - de: 'Index für die Rufumleitung, normalerweise für die erste eine "1".' - en: 'Index deflection, normally a "1" for the first one.' + de: '(optional) Index für die Rufumleitung, normalerweise für die erste eine "1".' + en: '(optional) Index deflection, normally a "1" for the first one.' valid_min: 1 valid_max: 32 avm2_read_after_write: type: int description: - de: 'Konfiguriert eine Verzögerung in Sekunden nachdem ein Lesekommando nach einem Schreibkommando gesendet wird.' - en: 'Configures delay in seconds to issue a read command after write command' + de: '(optional) Konfiguriert eine Verzögerung in Sekunden nachdem ein Lesekommando nach einem Schreibkommando gesendet wird.' + en: '(optional) Configures delay in seconds to issue a read command after write command' item_structs: info: @@ -651,7 +651,7 @@ item_structs: #item_attribute_prefixes: # Definition of item attributes that only have a common prefix (enter 'item_attribute_prefixes: NONE' or ommit this section, if section should be empty) - # NOTE: This section should only be used, if really nessesary (e.g. for the stateengine plugin) + # NOTE: This section should only be used, if really necessary (e.g. for the stateengine plugin) plugin_functions: # Definition of function interface of the plugin @@ -796,16 +796,12 @@ plugin_functions: description: de: "Startet das Gerät neu." en: "Reboots the device." - parameters: - # This function has no parameters reconnect: type: void description: de: "Verbindet das Gerät neu mit dem WAN (Wide Area Network)." en: "Reconnects the device to the WAN (Wide Area Network)." - parameters: - # This function has no parameters set_call_origin: type: void @@ -816,8 +812,8 @@ plugin_functions: phone_name: type: mac description: - de: "Identifikator des Telefons, dass als 'call origin' gesetzt werden soll. Bspw. zwei Sterne gefolgt von '610' für ein internes Gerät." - en: "Full phone identifier, could be e.g. two asterix followed by '610' for an internal device." + de: "Identifikator des Telefons, dass als 'call origin' gesetzt werden soll. bspw. zwei Sterne gefolgt von '610' für ein internes Gerät." + en: "Full phone identifier, could be e.g. two asterisk followed by '610' for an internal device." start_call: type: void From 452223d1c0ae881ff383a7ec06598a61883f0bcc Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 13 Dec 2022 11:51:11 +0100 Subject: [PATCH 027/178] Update WebIF to latest http-Module --- webif/templates/index.html | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/webif/templates/index.html b/webif/templates/index.html index 2ce34a503..9abcb790f 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -1,6 +1,6 @@ {% extends "base_plugin.html" %} {% set logo_frame = false %} -{% set update_interval = 5000 %} +{% set update_interval = ((50 * len(tr064_items) / 1000) | round | int) * 1000 %} {% block pluginstyles %} {% endblock pluginstyles %} @@ -40,6 +60,7 @@ try { webif_pagelength = parseInt(document.getElementById('webif_pagelength').innerHTML); + console.log("webif_pagelength " + webif_pagelength) if (isNaN(parseFloat(webif_pagelength)) || webif_pagelength == 0) { resize = true; webif_pagelength = -1; @@ -56,23 +77,33 @@ } try { - table = $('#itemtable').DataTable( { + table0 = $('#itemtable').DataTable( { pageLength: webif_pagelength, - pageResize: resize}); + pageResize: resize + } ); + console.log("Init table0 for page length " + webif_pagelength + ", pageResize: " + resize); table1 = $('#smarthomeitemtable').DataTable( { pageLength: webif_pagelength, - pageResize: resize}); + pageResize: resize + } ); + console.log("Init table1 for page length " + webif_pagelength + ", pageResize: " + resize); table2 = $('#logtable').DataTable( { pageLength: webif_pagelength, order: [[ 0, 'desc' ]], columnDefs: [{ "targets": [2], "className": "truncate logentry"}].concat($.fn.dataTable.defaults.columnDefs), - pageResize: resize}); + pageResize: resize + } ); + console.log("Init table2 for page length " + webif_pagelength + ", pageResize: " + resize); table3 = $('#call_monitortable').DataTable( { pageLength: webif_pagelength, - pageResize: resize}); + pageResize: resize + } ); + console.log("Init table3 for page length " + webif_pagelength + ", pageResize: " + resize); table4 = $('#smarthome_devicetable').DataTable( { - pageLength: webif_pagelength, - pageResize: resize}); + pageLength: -1, + pageResize: resize + } ); + console.log("Init table4 for page length -1, pageResize: " + resize); } catch (e) { console.log("Datatable JS not loaded, showing standard table without reorder option " +e); @@ -115,7 +146,11 @@ {% set tabcount = 6 %} -{% set tab1title = _(""'AVM2 TR-064 Items'" (" ~ tr064_item_count ~ ") ") %} +{% if p._fritz_device and tr064_item_count > 0 %} + {% set tab1title = _(""'AVM2 TR-064 Items'" (" ~ tr064_item_count ~ ") ") %} +{% else %} + {% set tab2title = "hidden" %} +{% endif %} {% if p._fritz_home and aha_item_count > 0 %} {% set tab2title = _(""'AVM2 AHA Items'" (" ~ aha_item_count ~ ") ") %} @@ -149,7 +184,7 @@ {% set language = 'en' %} {% endif %} - + {% block headtable %} @@ -199,14 +234,14 @@ {% endblock %} - + {% block buttons %} {% endblock buttons %} - + {% block bodytab1 %}
@@ -216,28 +251,24 @@ + - - + + {% if tr064_items %} {% for item in tr064_items %} - {% set item_id = item.id() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm2_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm2_data_type" %} - {% endif %} - + - - - - + + + + + {% endfor %} {% endif %} @@ -256,28 +287,24 @@ + - - + + {% if aha_items %} {% for item in aha_items %} - {% set item_id = item.id() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm2_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm2_data_type" %} - {% endif %} - + - - - - + + + + + {% endfor %} {% endif %} @@ -293,9 +320,10 @@ - - + + + @@ -303,9 +331,10 @@ {% for ain in p._fritz_home._aha_devices %} - - + + {% endfor %} {% endif %} @@ -342,8 +371,8 @@ - - + + @@ -361,9 +390,9 @@ - + - + @@ -436,6 +465,18 @@ + + + + + + + + + + + + From 9c7e677a9de0a63a6391bfcbb3e35c8e364fa624 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 10 Jan 2023 09:11:08 +0100 Subject: [PATCH 041/178] Bugfix get_hosts --- __init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 709a1112c..f799b596d 100644 --- a/__init__.py +++ b/__init__.py @@ -1773,9 +1773,9 @@ def get_hosts(self, only_active: bool = False) -> list: hosts = [] for i in range(1, number_of_hosts): host = self.get_host_details(i) - - if not only_active or (only_active and host['is_active']): - hosts.append(host) + if host is not None: + if not only_active or (only_active and host['is_active']): + hosts.append(host) return hosts def get_host_details(self, index: int): @@ -1794,7 +1794,7 @@ def get_host_details(self, index: int): if not host_info: return elif isinstance(host_info, int): - self._plugin_instance.logger.error(f"Error {host_info} '{self.errorcodes.get(host_info)}' occurred during {index}.") + self._plugin_instance.logger.error(f"Error {host_info} '{self.errorcodes.get(host_info)}' occurred during getting details of host #{index}.") return elif len(host_info) == 7: host = { From ca0010507d1d4e7dd448fdb80ac259bde64e9a18 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 10 Jan 2023 09:15:42 +0100 Subject: [PATCH 042/178] Bugfix get_hosts --- __init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index f799b596d..5bb11dc8a 100644 --- a/__init__.py +++ b/__init__.py @@ -1773,9 +1773,10 @@ def get_hosts(self, only_active: bool = False) -> list: hosts = [] for i in range(1, number_of_hosts): host = self.get_host_details(i) - if host is not None: - if not only_active or (only_active and host['is_active']): - hosts.append(host) + if host is None: + continue + if not only_active or (only_active and host['is_active']): + hosts.append(host) return hosts def get_host_details(self, index: int): From 1b593a40958936dd2a9bf58caebdf864d9e632bc Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 10 Jan 2023 11:02:32 +0100 Subject: [PATCH 043/178] Bugfix get_hosts --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 5bb11dc8a..78285a609 100644 --- a/__init__.py +++ b/__init__.py @@ -1795,7 +1795,7 @@ def get_host_details(self, index: int): if not host_info: return elif isinstance(host_info, int): - self._plugin_instance.logger.error(f"Error {host_info} '{self.errorcodes.get(host_info)}' occurred during getting details of host #{index}.") + self._plugin_instance.logger.error(f"Error {host_info} '{self.errorcodes.get(host_info)}' occurred during getting details of host #{index}.") return elif len(host_info) == 7: host = { From 3455a2ed8c11cc1b46aed1b5a5d69933d1b79670 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Fri, 20 Jan 2023 15:23:13 +0100 Subject: [PATCH 044/178] WebIF Maintenance Page clean up catch error on get_device_log_from_lua_separated --- __init__.py | 23 ++++++++++++----------- webif/templates/index.html | 38 ++------------------------------------ 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/__init__.py b/__init__.py index 78285a609..2bb00ed34 100644 --- a/__init__.py +++ b/__init__.py @@ -1795,7 +1795,7 @@ def get_host_details(self, index: int): if not host_info: return elif isinstance(host_info, int): - self._plugin_instance.logger.error(f"Error {host_info} '{self.errorcodes.get(host_info)}' occurred during getting details of host #{index}.") + self._plugin_instance.logger.info(f"Error {host_info} '{self.errorcodes.get(host_info)}' occurred during getting details of host #{index}.") return elif len(host_info) == 7: host = { @@ -3239,16 +3239,17 @@ def get_device_log_from_lua_separated(self): data = self._request(url, params, result='json') - if data: - data = data['mq_log'] - if self._log_entry_count: - data = data[:self._log_entry_count] - - data_formated = [] - for entry in data: - dt = datetime.datetime.strptime(f"{entry[0]} {entry[1]}", '%d.%m.%y %H:%M:%S').strftime('%d.%m.%Y %H:%M:%S') - data_formated.append([dt, entry[2], entry[3], entry[4]]) - return data_formated + if isinstance(data, dict): + data = data.get('mq_log') + if data is not None: + if self._log_entry_count: + data = data[:self._log_entry_count] + + data_formated = [] + for entry in data: + dt = datetime.datetime.strptime(f"{entry[0]} {entry[1]}", '%d.%m.%y %H:%M:%S').strftime('%d.%m.%Y %H:%M:%S') + data_formated.append([dt, entry[2], entry[3], entry[4]]) + return data_formated # Handling of updated items diff --git a/webif/templates/index.html b/webif/templates/index.html index ec10be8da..9bad16eee 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -477,46 +477,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
{{ _('Pfad') }} {{ _('Typ') }} {{ _('AVM Datentyp') }}{{ _('Cycle') }} {{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.id() }} {{ item.property.type }}{{ item.conf[instance_key] }}.{{ item() }}.{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}.{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}{{ p.fritz_device.item_dict[item][0] }}{{ p.fritz_device.item_dict[item][2] }}.{{ item.property.value }}.{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}.{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{{ _('Pfad') }} {{ _('Typ') }} {{ _('AVM Datentyp') }}{{ _('Cycle') }} {{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.id() }} {{ item.property.type }}{{ item.conf[instance_key] }}.{{ item() }}.{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}.{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}{{ p.fritz_home.item_dict[item][0] }}{{ p.fritz_home.item_dict[item][2] }}.{{ item.property.value }}.{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}.{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{{ 'No' }}{{ 'Device AIN' }}{{ 'Device AIN' }}{{ '' }} {{ 'Device Details (dict)' }}
{{ loop.index }}{{ ain }}{{ ain }} + {{ p._fritz_home._aha_devices[ain] }}
{{ item_id }} {{ item.property.type }}{{ instance_key }}.{{ item() }}{{ item.conf[instance_key]}}.{{ item.property.value }} .{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }} .{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{{ 'Datum/Uhrzeit' }}{{ 'Datum/Uhrzeit' }} {{ 'Meldung' }}{{ 'Typ' }}{{ 'Typ' }} {{ 'Kategorie' }}
{{ "fritz_device._items" }}{{ p.fritz_device._items }}
{{ "fritz_device._item_blacklist" }}{{ p.fritz_device._item_blacklist }}
{{ "_fritz_home._items" }}{{ p.fritz_home._items }}
{{ "X_AVM_DE_NumberOfClients" }} {{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetNumberOfClients()['NewX_AVM_DE_NumberOfClients'] }}{{ "_fritz_home._items" }} {{ p.fritz_home._items }}
{{ "X_AVM_DE_NumberOfClients" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetNumberOfClients()['NewX_AVM_DE_NumberOfClients'] }}
{{ "self._data_cache" }} {{ p._fritz_device._data_cache }}
{{ "NewX_AVM_DE_ClientIndex=0" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=0) }}
{{ "NewX_AVM_DE_ClientIndex=1" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=1) }}
{{ "NewX_AVM_DE_ClientIndex=2" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=2) }}
{{ "NewX_AVM_DE_ClientIndex=3" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient2(NewX_AVM_DE_ClientIndex=3) }}
{{ "NewX_AVM_DE_ClientIndex=0" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=0) }}
{{ "NewX_AVM_DE_ClientIndex=1" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=1) }}
{{ "NewX_AVM_DE_ClientIndex=2" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=2) }}
{{ "NewX_AVM_DE_ClientIndex=3" }}{{ p._fritz_device.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetClient3(NewX_AVM_DE_ClientIndex=3) }}
{% endif %} From a307735ac95aba932153af38e5f308b50854b7d9 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Thu, 26 Jan 2023 17:05:05 +0100 Subject: [PATCH 045/178] BugFix in CallMonitor Time --- __init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 2bb00ed34..0e0aed427 100644 --- a/__init__.py +++ b/__init__.py @@ -4351,7 +4351,7 @@ def _parse_line(self, line: str): line = line.split(";") if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(line) + self._plugin_instance.logger.debug(f"_parse_line: line={line} to be parsed") try: if line[1] == "RING": @@ -4372,7 +4372,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st """ if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"Event={event}, Call from={call_from}, Call to={call_to}, Time={dt}, CallID={callid}, Branch={branch}") + self._plugin_instance.logger.debug(f"_trigger: Event={event}, Call from={call_from}, Call to={call_to}, Time={dt}, CallID={callid}, Branch={branch}") # set generic item value for item in self._items: @@ -4426,7 +4426,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st elif avm_data_type == 'last_call_date_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting last_call_date_incoming: {time}") - item(str(time), self._plugin_instance.get_fullname()) + item(str(dt), self._plugin_instance.get_fullname()) elif avm_data_type == 'call_event_incoming': if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") @@ -4462,7 +4462,7 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st else: item(call_to, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_call_date_outgoing': - item(str(time), self._plugin_instance.get_fullname()) + item(str(dt), self._plugin_instance.get_fullname()) elif avm_data_type == 'call_event_outgoing': item(event.lower(), self._plugin_instance.get_fullname()) elif avm_data_type == 'last_number_outgoing': From 1bef8c7e2a3f7189b1bf6faded442e0e4b3b56e1 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 30 Jan 2023 12:48:22 +0100 Subject: [PATCH 046/178] BugFix in ErrorCode handling Bugfix in InArgumentValue --- __init__.py | 109 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/__init__.py b/__init__.py index 0e0aed427..202a330cb 100644 --- a/__init__.py +++ b/__init__.py @@ -60,6 +60,7 @@ IGD_SERVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:service-1-0'} TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} +TR064_CONTROL_NAMESPACE = {'': 'urn:dslforum-org:control-1-0'} """ @@ -560,6 +561,9 @@ def get_deflections(self): def set_deflection_enable(self, deflection_id: int = 0, new_enable: bool = False): return self._fritz_device.set_deflection(deflection_id, new_enable) + def set_tam(self, tam_index: int = 0, new_enable: bool = False): + return self._fritz_device.set_tam(tam_index, new_enable) + class FritzDevice: """ @@ -574,6 +578,17 @@ class FritzDevice: 713: 'Invalid array index', 714: 'No such array entry in array', 820: 'Internal Error', + 403: 'SIP_FORBIDDEN, Beschreibung steht in der Hilfe (Webinterface)', + 404: 'SIP_NOT_FOUND, Gegenstelle nicht erreichbar (local part der SIP-URL nicht erreichbar (Host schon))', + 405: 'SIP_METHOD_NOT_ALLOWED', + 406: 'SIP_NOT_ACCEPTED', + 408: 'SIP_NO_ANSWER', + 484: 'SIP_ADDRESS_INCOMPLETE, Beschreibung steht in der Hilfe (Webinterface)', + 485: 'SIP_AMBIGUOUS, Beschreibung steht in der Hilfe (Webinterface)', + 486: 'SIP_BUSY_HERE, Ziel besetzt (vermutlich auch andere Gründe bei der Gegenstelle)', + 487: 'SIP_REQUEST_TERMINATED, Anrufversuch beendet (Gegenstelle nahm nach ca. 30 Sek. nicht ab)', + 866: 'second factor authentication required', + 867: 'second factor authentication blocked', } def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, plugin_instance=None): @@ -682,11 +697,10 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): self._plugin_instance.logger.debug(f"Item {item.id()} with avm_data_type={avm_data_type} has changed for index {_index}; New value={to_be_set_value}") # call setting method - # _dispatcher[avm_data_type][0](_index, bool(to_be_set_value)) cmd, args, index = _dispatcher[avm_data_type][1] - self._set_fritz_device(cmd, args, index) + response = self._set_fritz_device(cmd, args, index) if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"Setting AVM Device with successful.") + self._plugin_instance.logger.debug(f"Setting AVM Device with successful with response={response}.") # handle readafterwrite if readafterwrite: @@ -997,7 +1011,7 @@ def _update_item_value(self, item, avm_data_type: str, index: str) -> bool: try: data = self._poll_fritz_device(avm_data_type, index) except Exception as e: - self._plugin_instance.logger.error(f"Error {e} occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") + self._plugin_instance.logger.error(f"Error '{e}' occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") return False else: if data is None: @@ -1197,7 +1211,7 @@ def _get_update_data(self, client: str, device: str, service: str, action: str, else: return data if not out_argument else data[out_argument] - def _set_fritz_device(self, avm_data_type: str, args: str = None, wlan_index=None) -> None: + def _set_fritz_device(self, avm_data_type: str, args: str = None, wlan_index=None): """Set AVM Device based on avm_data_type and args""" if self._plugin_instance.debug_log: @@ -1222,15 +1236,26 @@ def _set_fritz_device(self, avm_data_type: str, args: str = None, wlan_index=Non return device, service, action = link[avm_data_type] - self._plugin_instance.logger.debug(device, service, action, ) + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"avm_data_type={avm_data_type} -> {device}.{service}.{action}") if service.lower().startswith('wlan'): wlan_index = "" if not wlan_index else wlan_index - eval(f"self.client.{device}.{service}[{wlan_index}].{action}({args})") + response = eval(f"self.client.{device}.{service}[{wlan_index}].{action}({args})") elif args is None: - eval(f"self.client.{device}.{service}.{action}()") + response = eval(f"self.client.{device}.{service}.{action}()") else: - eval(f"self.client.{device}.{service}.{action}({args})") + response = eval(f"self.client.{device}.{service}.{action}({args})") + + if self._plugin_instance.debug_log: + self._plugin_instance.logger.debug(f"response={response} for {device}.{service}.{action} with args={args}") + + # return response + if response is None: + self._plugin_instance.logger.info(f"No response for 'self.client.{device}.{service}.{action}({args})' received.") + elif isinstance(response, int) and 99 < response < 1000: + self._plugin_instance.logger.info(f"Response was ErrorCode: {response} '{self.errorcodes.get(response, None)}' for self.client.{device}.{service}.{action}({args})") + return response def _clear_data_cache(self): """ @@ -1337,7 +1362,7 @@ def reboot(self): """ # self.client.InternetGatewayDevice.DeviceConfig.Reboot() - self._set_fritz_device('reboot') + return self._set_fritz_device('reboot') def reconnect(self): """ @@ -1349,10 +1374,10 @@ def reconnect(self): if 'PPP' in self.default_connection_service: # self.client.WANConnectionDevice.WANPPPConnection.ForceTermination() - self._set_fritz_device('reconnect_ppp') + return self._set_fritz_device('reconnect_ppp') else: # self.client.WANConnectionDevice.WANIPPConnection.ForceTermination() - self._set_fritz_device('reconnect_ipp') + return self._set_fritz_device('reconnect_ipp') def wol(self, mac_address: str): """ @@ -1364,7 +1389,7 @@ def wol(self, mac_address: str): """ # self.client.LanDevice.Hosts.X_AVM_DE_GetAutoWakeOnLANByMACAddress(NewMACAddress=mac_address) - self._set_fritz_device('wol', f"NewMACAddress={mac_address}") + return self._set_fritz_device('wol', f"NewMACAddress={mac_address}") # ---------------------------------- # caller methods @@ -1407,11 +1432,11 @@ def set_call_origin(self, phone_name: str): Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - :param phone_name: full phone identifier, could be e.g. '\*\*610' for an internal device + :param phone_name: full phone identifier, could be e.g. '**610' for an internal device """ # self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialSetConfig(NewX_AVM_DE_PhoneName=phone_name.strip()) - self._set_fritz_device('set_call_origin', f"NewX_AVM_DE_PhoneName={phone_name.strip()}") + return self._set_fritz_device('set_call_origin', f"NewX_AVM_DE_PhoneName='{phone_name.strip()}'") def start_call(self, phone_number: str): """ @@ -1423,7 +1448,7 @@ def start_call(self, phone_number: str): """ # self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialNumber(NewX_AVM_DE_PhoneNumber=phone_number.strip()) - self._set_fritz_device('start_call', f"NewX_AVM_DE_PhoneNumber={phone_number.strip()}") + return self._set_fritz_device('start_call', f"NewX_AVM_DE_PhoneNumber={phone_number.strip()}") def cancel_call(self): """ @@ -1433,7 +1458,7 @@ def cancel_call(self): """ # self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialHangup() - self._set_fritz_device('cancel_call') + return self._set_fritz_device('cancel_call') def get_contact_name_by_phone_number(self, phone_number: str = '', phonebook_id: int = 0) -> str: """Get contact from phone book by phone number""" @@ -1569,11 +1594,13 @@ def set_wlan(self, wlan_index: int, new_enable: bool = False): self._plugin_instance.logger.debug(f"set_wlan called: wlan_index={wlan_index}, new_enable={new_enable}") # self.client.LANDevice.WLANConfiguration[wlan_index].SetEnable(NewEnable=int(new_enable)) - self._set_fritz_device('set_wlan', f"NewEnable={int(new_enable)}", wlan_index) + response = self._set_fritz_device('set_wlan', f"NewEnable={int(new_enable)}", wlan_index) # check if remaining time is set as item self.set_wlan_time_remaining(wlan_index) + return response + def set_wlan_time_remaining(self, wlan_index: int): """look for item and set time remaining""" @@ -1621,7 +1648,7 @@ def set_wps(self, wlan_index: int, wps_enable: bool = False): self._plugin_instance.logger.debug(f"set_wps called: wlan_index={wlan_index}, wps_enable={wps_enable}") # self.client.LANDevice.WLANConfiguration[wlan_index].X_AVM_DE_SetWPSEnable(NewX_AVM_DE_WPSEnable=int(wps_enable)) - self._set_fritz_device('set_wps', f"NewX_AVM_DE_WPSEnable={int(wps_enable)}", wlan_index) + return self._set_fritz_device('set_wps', f"NewX_AVM_DE_WPSEnable={int(wps_enable)}", wlan_index) def get_wps(self, wlan_index: int): """ @@ -1647,7 +1674,7 @@ def set_tam(self, tam_index: int = 0, new_enable: bool = False): """ # self.client.InternetGatewayDevice.X_AVM_DE_TAM.SetEnable(NewIndex=tam_index, NewEnable=int(new_enable)) - self._set_fritz_device('set_tam', f"NewIndex={tam_index}, NewEnable={int(new_enable)}") + return self._set_fritz_device('set_tam', f"NewIndex={tam_index}, NewEnable={int(new_enable)}") def get_tam(self, tam_index: int = 0): """ @@ -1707,7 +1734,7 @@ def set_aha_device(self, ain: str = '', set_switch: bool = False): switch_state = "ON" if set_switch is True else "OFF" # self.client.InternetGatewayDevice.X_AVM_DE_Homeauto.SetSwitch(NewAIN=ain, NewSwitchState=switch_state) - self._set_fritz_device('set_aha_device', f"NewAIN={ain}, NewSwitchState={switch_state}") + return self._set_fritz_device('set_aha_device', f"NewAIN={ain}, NewSwitchState={switch_state}") # ---------------------------------- # deflection @@ -1723,7 +1750,7 @@ def set_deflection(self, deflection_id: int = 0, new_enable: bool = False): """ # self.client.InternetGatewayDevice.X_AVM_DE_OnTel.SetDeflectionEnable(NewDeflectionId=deflection_id, NewEnable=int(new_enable)) - self._set_fritz_device('set_deflection', f"NewDeflectionId={deflection_id}, NewEnable={int(new_enable)}") + return self._set_fritz_device('set_deflection', f"NewDeflectionId={deflection_id}, NewEnable={int(new_enable)}") def get_deflection(self, deflection_id: int = 0): """Get Deflection state of deflection_id""" @@ -2097,8 +2124,6 @@ def _fetch_actions(self, scpdurl: str): self.namespaces ) - # self.logger.debug(f"Service: {self.description_file} scpdurl={self.scpdurl} with actions={self.actions}") - class Action: """ TR-064 action. @@ -2159,33 +2184,37 @@ def __call__(self, **kwargs): self.logger.warning('Unknown argument(s) \'' + "', '".join(unknown_arguments) + '\'') # Add SOAP action to header - self.headers['soapaction'] = '"{}#{}"'.format(self.service_type, self.name) + self.headers['soapaction'] = f'"{self.service_type}#{self.name}"' etree.register_namespace('u', self.service_type) # Prepare body for request self.body.clear() - action = etree.SubElement(self.body, '{{{}}}{}'.format(self.service_type, self.name)) + action = etree.SubElement(self.body, f'{{{self.service_type}}}{self.name}') for key in kwargs: arg = etree.SubElement(action, self.in_arguments[key]) arg.text = str(kwargs[key]) # soap._InitChallenge(header) data = etree.tostring(self.envelope, encoding='utf-8', xml_declaration=True).decode() - request = requests.post(f'{self.base_url}{self.control_url}', - headers=self.headers, - auth=self.auth, - data=data, - verify=self.verify) - - # self.logger.warning(f"Action call request={request.content}") + request = requests.post(f'{self.base_url}{self.control_url}', headers=self.headers, auth=self.auth, data=data, verify=self.verify) + # handle response of failed action if request.status_code != 200: - return request.status_code + xml = etree.parse(BytesIO(request.content)) + try: + # error_code = int(xml.find('.//{urn:dslforum-org:control-1-0}errorCode').text) + error_code = int(xml.find(f".//{{{TR064_CONTROL_NAMESPACE['']}}}errorCode").text) + except Exception: + error_code = None + pass + # self.logger.debug(f"status_code={request.status_code}, error_code={error_code.text}") + return error_code if error_code is not None else request.status_code # Translate response and prepare dict xml = etree.parse(BytesIO(request.content)) response = FritzDevice.AttributeDict() - for arg in list(xml.find('.//{{{}}}{}Response'.format(self.service_type, self.name))): + #for arg in list(xml.find('.//{{{}}}{}Response'.format(self.service_type, self.name))): + for arg in list(xml.find(f".//{{{self.service_type}}}{self.name}Response")): name = self.out_arguments[arg.tag] response[name] = arg.text return response @@ -3422,13 +3451,7 @@ class FritzhomeDeviceBase(FritzhomeEntityBase): def __repr__(self): """Return a string.""" - return "{ain} {identifier} {manuf} {prod} {name}".format( - ain=self.ain, - identifier=self.identifier, - manuf=self.manufacturer, - prod=self.productname, - name=self.name, - ) + return f"{self.ain} {self.identifier} {self.manufacturer} {self.productname} {self.name}" def update(self): """Update the device values.""" @@ -4270,7 +4293,7 @@ def _listen(self, recv_buffer: int = 4096): self._plugin_instance.logger.error("CallMonitor connection not open anymore.") else: if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"Data Received from CallMonitor: {data.decode('utf-8')}") + self._plugin_instance.logger.debug(f"Data Received from CallMonitor: {data.decode('utf-8').strip()}") buffer += data.decode("utf-8") while buffer.find("\n") != -1: line, buffer = buffer.split("\n", 1) From ca0108fb4caf89a934aad6dffa25a414df911050 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 30 Jan 2023 12:53:00 +0100 Subject: [PATCH 047/178] BugFix in ErrorCode handling Bugfix in InArgumentValue --- __init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 202a330cb..e3f0d9347 100644 --- a/__init__.py +++ b/__init__.py @@ -3148,8 +3148,8 @@ def set_color_temp(self, ain, temperature, duration=1): params = { 'temperature': int(temperature), - "duration": int(duration) * 10 - } + 'duration': int(duration) * 10 + } self._aha_request("setcolortemperature", ain=ain, param=params) def get_color_temp(self, ain): @@ -3172,9 +3172,7 @@ def update_templates(self): for element in self.get_template_elements(): if element.attrib["identifier"] in self._templates.keys(): - self._plugin_instance.logger.info( - "Updating already existing Template " + element.attrib["identifier"] - ) + self._plugin_instance.logger.info(f"Updating already existing Template {element.attrib['identifier']}") self._templates[element.attrib["identifier"]]._update_from_node(element) else: self._plugin_instance.logger.info("Adding new Template " + element.attrib["identifier"]) From 098c0a7253507b73fd758bf738522c673452b0cc Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 30 Jan 2023 13:28:31 +0100 Subject: [PATCH 048/178] Code clean-up --- __init__.py | 193 +++++++++++++++++++++++++++------------------------- 1 file changed, 100 insertions(+), 93 deletions(-) diff --git a/__init__.py b/__init__.py index e3f0d9347..29ae92a3f 100644 --- a/__init__.py +++ b/__init__.py @@ -50,17 +50,17 @@ Definition of TR-064 details """ -FRITZ_TR64_DESC_FILE = "tr64desc.xml" -FRITZ_IGD_DESC_FILE = "igddesc.xml" -FRITZ_IGD2_DESC_FILE = "igd2desc.xml" -FRITZ_L2TPV3_FILE = "l2tpv3.xml" -FRITZ_FBOX_DESC_FILE = "fboxdesc.xml" +#FRITZ_TR64_DESC_FILE = "tr64desc.xml" +#FRITZ_IGD_DESC_FILE = "igddesc.xml" +#FRITZ_IGD2_DESC_FILE = "igd2desc.xml" +#FRITZ_L2TPV3_FILE = "l2tpv3.xml" +#FRITZ_FBOX_DESC_FILE = "fboxdesc.xml" -IGD_DEVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:device-1-0'} -IGD_SERVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:service-1-0'} -TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} -TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} -TR064_CONTROL_NAMESPACE = {'': 'urn:dslforum-org:control-1-0'} +#IGD_DEVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:device-1-0'} +#IGD_SERVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:service-1-0'} +#TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} +#TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} +#TR064_CONTROL_NAMESPACE = {'': 'urn:dslforum-org:control-1-0'} """ @@ -570,26 +570,44 @@ class FritzDevice: This class encapsulates information related to a specific FritzDevice using TR-064 """ - errorcodes = {401: 'Unable to connect to FritzDevice. Invalid user and/or password.', - 402: 'Invalid arguments', - 500: 'Internal Server Error', - 501: 'Action failed', - 600: 'Argument invalid', - 713: 'Invalid array index', - 714: 'No such array entry in array', - 820: 'Internal Error', - 403: 'SIP_FORBIDDEN, Beschreibung steht in der Hilfe (Webinterface)', - 404: 'SIP_NOT_FOUND, Gegenstelle nicht erreichbar (local part der SIP-URL nicht erreichbar (Host schon))', - 405: 'SIP_METHOD_NOT_ALLOWED', - 406: 'SIP_NOT_ACCEPTED', - 408: 'SIP_NO_ANSWER', - 484: 'SIP_ADDRESS_INCOMPLETE, Beschreibung steht in der Hilfe (Webinterface)', - 485: 'SIP_AMBIGUOUS, Beschreibung steht in der Hilfe (Webinterface)', - 486: 'SIP_BUSY_HERE, Ziel besetzt (vermutlich auch andere Gründe bei der Gegenstelle)', - 487: 'SIP_REQUEST_TERMINATED, Anrufversuch beendet (Gegenstelle nahm nach ca. 30 Sek. nicht ab)', - 866: 'second factor authentication required', - 867: 'second factor authentication blocked', - } + """ + Definition of TR-064 Error Codes + """ + ERROR_CODES = {401: 'Unable to connect to FritzDevice. Invalid user and/or password.', + 402: 'Invalid arguments', + 500: 'Internal Server Error', + 501: 'Action failed', + 600: 'Argument invalid', + 713: 'Invalid array index', + 714: 'No such array entry in array', + 820: 'Internal Error', + 403: 'SIP_FORBIDDEN, Beschreibung steht in der Hilfe (Webinterface)', + 404: 'SIP_NOT_FOUND, Gegenstelle nicht erreichbar (local part der SIP-URL nicht erreichbar (Host schon))', + 405: 'SIP_METHOD_NOT_ALLOWED', + 406: 'SIP_NOT_ACCEPTED', + 408: 'SIP_NO_ANSWER', + 484: 'SIP_ADDRESS_INCOMPLETE, Beschreibung steht in der Hilfe (Webinterface)', + 485: 'SIP_AMBIGUOUS, Beschreibung steht in der Hilfe (Webinterface)', + 486: 'SIP_BUSY_HERE, Ziel besetzt (vermutlich auch andere Gründe bei der Gegenstelle)', + 487: 'SIP_REQUEST_TERMINATED, Anrufversuch beendet (Gegenstelle nahm nach ca. 30 Sek. nicht ab)', + 866: 'second factor authentication required', + 867: 'second factor authentication blocked', + } + + """ + Definition of TR-064 details + """ + FRITZ_TR64_DESC_FILE = "tr64desc.xml" + FRITZ_IGD_DESC_FILE = "igddesc.xml" + FRITZ_IGD2_DESC_FILE = "igd2desc.xml" + FRITZ_L2TPV3_FILE = "l2tpv3.xml" + FRITZ_FBOX_DESC_FILE = "fboxdesc.xml" + + IGD_DEVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:device-1-0'} + IGD_SERVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:service-1-0'} + TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} + TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} + TR064_CONTROL_NAMESPACE = {'': 'urn:dslforum-org:control-1-0'} def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, plugin_instance=None): """ @@ -617,8 +635,8 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self._connected = False # get client objects - self.client = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=FRITZ_TR64_DESC_FILE, plugin_instance=plugin_instance) - self.client_igd = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=FRITZ_IGD_DESC_FILE, plugin_instance=plugin_instance) + self.client = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=self.FRITZ_TR64_DESC_FILE, plugin_instance=plugin_instance) + self.client_igd = FritzDevice.Client(self._username, self._password, self._verify, base_url=self._build_url(), description_file=self.FRITZ_IGD_DESC_FILE, plugin_instance=plugin_instance) # get GetDefaultConnectionService _default_connection_service = self.client.InternetGatewayDevice.Layer3Forwarding.GetDefaultConnectionService() @@ -629,8 +647,8 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self._plugin_instance.logger.debug(f"FritzDevice alive") else: self._connected = False - if _default_connection_service in self.errorcodes: - self._plugin_instance.logger.error(f"Error {_default_connection_service}-'{self.errorcodes[_default_connection_service]}'") + if _default_connection_service in self.ERROR_CODES: + self._plugin_instance.logger.error(f"Error {_default_connection_service}-'{self.ERROR_CODES[_default_connection_service]}'") else: self._plugin_instance.logger.error(f"Unable to determine _default_connection_service. ErrorCode {_default_connection_service}.") @@ -1017,8 +1035,8 @@ def _update_item_value(self, item, avm_data_type: str, index: str) -> bool: if data is None: self._plugin_instance.logger.info(f"Value for item={item} is None.") return False - elif isinstance(data, int) and data in self.errorcodes: - self._plugin_instance.logger.error(f"Error {data} '{self.errorcodes.get(data, None)}' occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") + elif isinstance(data, int) and data in self.ERROR_CODES: + self._plugin_instance.logger.error(f"Error {data} '{self.ERROR_CODES.get(data, None)}' occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") return False else: item(data, self._plugin_instance.get_fullname()) @@ -1039,9 +1057,9 @@ def _poll_fritz_device(self, avm_data_type: str, index=None, enforce_read: bool 'wan_is_connected': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', None, 'NewConnectionStatus'), 'wan_uptime': ('WANConnectionDevice', 'WANPPPConnection', 'GetInfo', None, 'NewUptime'), 'wan_ip': ('WANConnectionDevice', 'WANPPPConnection', 'GetExternalIPAddress', None, 'NewExternalIPAddress'), - } - - link_ip = { + } + + link_ip = { 'wan_connection_status': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', None, 'NewConnectionStatus'), 'wan_connection_error': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', None, 'NewLastConnectionError'), 'wan_is_connected': ('WANConnectionDevice', 'WANIPConnection', 'GetInfo', None, 'NewConnectionStatus'), @@ -1206,7 +1224,7 @@ def _get_update_data(self, client: str, device: str, service: str, action: str, self._plugin_instance.logger.info(f"No data for 'self.{client}.{device}.{service}.{action}()' received.") return elif isinstance(data, int) and 99 < data < 1000: - self._plugin_instance.logger.info(f"Response was ErrorCode: {data} '{self.errorcodes.get(data, None)}' for self.{client}.{device}.{service}.{action}()") + self._plugin_instance.logger.info(f"Response was ErrorCode: {data} '{self.ERROR_CODES.get(data, None)}' for self.{client}.{device}.{service}.{action}()") return data else: return data if not out_argument else data[out_argument] @@ -1254,7 +1272,7 @@ def _set_fritz_device(self, avm_data_type: str, args: str = None, wlan_index=Non if response is None: self._plugin_instance.logger.info(f"No response for 'self.client.{device}.{service}.{action}({args})' received.") elif isinstance(response, int) and 99 < response < 1000: - self._plugin_instance.logger.info(f"Response was ErrorCode: {response} '{self.errorcodes.get(response, None)}' for self.client.{device}.{service}.{action}({args})") + self._plugin_instance.logger.info(f"Response was ErrorCode: {response} '{self.ERROR_CODES.get(response, None)}' for self.client.{device}.{service}.{action}({args})") return response def _clear_data_cache(self): @@ -1822,7 +1840,7 @@ def get_host_details(self, index: int): if not host_info: return elif isinstance(host_info, int): - self._plugin_instance.logger.info(f"Error {host_info} '{self.errorcodes.get(host_info)}' occurred during getting details of host #{index}.") + self._plugin_instance.logger.info(f"Error {host_info} '{self.ERROR_CODES.get(host_info)}' occurred during getting details of host #{index}.") return elif len(host_info) == 7: host = { @@ -1958,7 +1976,7 @@ class Client: :param plugin_instance: instance of Plugin """ - def __init__(self, username, password, verify, base_url='https://192.168.178.1:49443', description_file=FRITZ_TR64_DESC_FILE, plugin_instance=None): + def __init__(self, username, password, verify, base_url='https://192.168.178.1:49443', description_file='tr64desc.xml', plugin_instance=None): # handle plugin instance self._plugin_instance = plugin_instance @@ -1988,9 +2006,9 @@ def _fetch_devices(self, description_file): request = requests.get(f'{self.base_url}/{description_file}', verify=self.verify) if description_file == 'igddesc.xml': - namespaces = IGD_DEVICE_NAMESPACE + namespaces = FritzDevice.IGD_DEVICE_NAMESPACE else: - namespaces = TR064_DEVICE_NAMESPACE + namespaces = FritzDevice.TR064_DEVICE_NAMESPACE if request.status_code == 200: xml = etree.parse(BytesIO(request.content)) @@ -2018,9 +2036,9 @@ def __init__(self, xml, auth, verify, base_url, description_file): self.verify = verify if description_file == 'igddesc.xml': - namespaces = IGD_DEVICE_NAMESPACE + namespaces = FritzDevice.IGD_DEVICE_NAMESPACE else: - namespaces = TR064_DEVICE_NAMESPACE + namespaces = FritzDevice.TR064_DEVICE_NAMESPACE for service in xml.findall('./serviceList/service', namespaces=namespaces): service_type = service.findtext('serviceType', namespaces=namespaces) @@ -2086,9 +2104,9 @@ def __init__(self, auth, verify, base_url, service_type, service_id, scpdurl, co self.description_file = description_file if description_file == 'igddesc.xml': - self.namespaces = IGD_SERVICE_NAMESPACE + self.namespaces = FritzDevice.IGD_SERVICE_NAMESPACE else: - self.namespaces = TR064_SERVICE_NAMESPACE + self.namespaces = FritzDevice.TR064_SERVICE_NAMESPACE def __getattr__(self, name: str): if name not in self.actions: @@ -2154,10 +2172,8 @@ def __init__(self, xml, auth, base_url, name, service_type, service_id, control_ etree.register_namespace('h', 'http://soap-authentication.org/digest/2001/10/') self.headers = {'content-type': 'text/xml; charset="utf-8"'} - self.envelope = etree.Element( - '{http://schemas.xmlsoap.org/soap/envelope/}Envelope', - attrib={ - '{http://schemas.xmlsoap.org/soap/envelope/}encodingStyle': + self.envelope = etree.Element('{http://schemas.xmlsoap.org/soap/envelope/}Envelope', + attrib={'{http://schemas.xmlsoap.org/soap/envelope/}encodingStyle': 'http://schemas.xmlsoap.org/soap/encoding/'}) self.body = etree.SubElement(self.envelope, '{http://schemas.xmlsoap.org/soap/envelope/}Body') @@ -2202,8 +2218,7 @@ def __call__(self, **kwargs): if request.status_code != 200: xml = etree.parse(BytesIO(request.content)) try: - # error_code = int(xml.find('.//{urn:dslforum-org:control-1-0}errorCode').text) - error_code = int(xml.find(f".//{{{TR064_CONTROL_NAMESPACE['']}}}errorCode").text) + error_code = int(xml.find(f".//{{{FritzDevice.TR064_CONTROL_NAMESPACE['']}}}errorCode").text) except Exception: error_code = None pass @@ -2213,7 +2228,6 @@ def __call__(self, **kwargs): # Translate response and prepare dict xml = etree.parse(BytesIO(request.content)) response = FritzDevice.AttributeDict() - #for arg in list(xml.find('.//{{{}}}{}Response'.format(self.service_type, self.name))): for arg in list(xml.find(f".//{{{self.service_type}}}{self.name}Response")): name = self.out_arguments[arg.tag] response[name] = arg.text @@ -2233,11 +2247,14 @@ class FritzHome: Fritzhome object to communicate with the device via AHA-HTTP Interface. """ - _login_route = '/login_sid.lua?version=2' - _log_route = '/query.lua?mq_log=logger:status/log' - _log_separate_route = '/query.lua?mq_log=logger:status/log_separate' - _homeauto_route = '/webservices/homeautoswitch.lua' - _internet_status_route = '/internet/inetstat_monitor.lua?sid=' + """ + Definition of AHA Routes + """ + LOGIN_ROUTE = '/login_sid.lua?version=2' + LOG_ROUTE = '/query.lua?mq_log=logger:status/log' + LOG_SEPARATE_ROUTE = '/query.lua?mq_log=logger:status/log_separate' + HOMEAUTO_ROUTE = '/webservices/homeautoswitch.lua' + INTERNET_STATUS_ROUTE = '/internet/inetstat_monitor.lua?sid=' def __init__(self, host, ssl, verify, user, password, _log_entry_count, plugin_instance): """ @@ -2556,7 +2573,7 @@ def _login_request(self, username=None, challenge_response=None): Send a login request with parameters. """ - url = self._get_prefixed_host() + self._login_route + url = self._get_prefixed_host() + self.LOGIN_ROUTE params = {} if username: params["username"] = username @@ -2575,7 +2592,7 @@ def _logout_request(self): Send a logout request. """ - url = self._get_prefixed_host() + self._login_route + url = self._get_prefixed_host() + self.LOGIN_ROUTE params = {"logout": "1", "sid": self._sid} self._request(url, params) @@ -2617,7 +2634,7 @@ def _aha_request(self, cmd, ain=None, param=None, rf='str'): Send an AHA request. """ - url = self._get_prefixed_host() + self._homeauto_route + url = self._get_prefixed_host() + self.HOMEAUTO_ROUTE params = {"switchcmd": cmd, "sid": self._sid} if param: params.update(param) @@ -2684,7 +2701,7 @@ def check_sid(self): """ self._plugin_instance.logger.debug(f"check_sid called") - url = self._get_prefixed_host() + self._login_route + url = self._get_prefixed_host() + self.LOGIN_ROUTE params = {"sid": self._sid} plain = self._request(url, params) dom = ElementTree.fromstring(plain) @@ -2757,14 +2774,12 @@ 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: @@ -2775,14 +2790,12 @@ def get_devices(self): """ Get the list of all known devices. """ - return list(self.get_devices_as_dict().values()) def get_devices_as_dict(self): """ Get the list of all known devices. """ - if self.update_devices(): return self._devices @@ -2790,21 +2803,18 @@ def get_device_by_ain(self, ain): """ Return a device specified by the AIN. """ - return self.get_devices_as_dict()[ain] def get_device_present(self, ain): """ Get the device presence. """ - return self._aha_request("getswitchpresent", ain=ain, rf='bool') def get_device_name(self, ain): """ Get the device name. """ - return self._aha_request("getswitchname", ain=ain) # switch-related commands @@ -2813,28 +2823,24 @@ def get_switch_state(self, ain): """ Get the switch state. """ - return self._aha_request("getswitchstate", ain=ain, rf='bool') def set_switch_state_on(self, ain): """ Set the switch to on state. """ - return self._aha_request("setswitchon", ain=ain, rf='bool') def set_switch_state_off(self, ain): """ Set the switch to off state. """ - return self._aha_request("setswitchoff", ain=ain, rf='bool') def set_switch_state_toggle(self, ain): """ Toggle the switch state. """ - return self._aha_request("setswitchtoggle", ain=ain, rf='bool') def set_switch_state(self, ain, state): @@ -2858,7 +2864,6 @@ def get_switch_energy(self, ain): """ Get the switch energy. """ - return self._aha_request("getswitchenergy", ain=ain, rf='int') # thermostat-related commands @@ -2867,7 +2872,6 @@ def get_temperature(self, ain): """ Get the device temperature sensor value. """ - return self._aha_request("gettemperature", ain=ain, rf='float') / 10.0 def _get_temperature(self, ain, name): @@ -3080,47 +3084,50 @@ def set_color(self, ain, hsv, duration=0, mapped=True): def set_color_discrete(self, ain, hue, duration=0): """ - Set Led color to closest discrete hue value. Currently, only those are supported for FritzDect500 RGB LED bulbs + Set Led color to the closest discrete hue value. Currently, only those are supported for FritzDect500 RGB LED bulbs """ if hue <= 20: # self._plugin_instance.logger.debug(f'setcolor to red (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 358, 'saturation': 180, 'duration': int(duration)}, rf='bool') + param = {'hue': 358, 'saturation': 180, 'duration': int(duration)} elif hue <= 45: # self._plugin_instance.logger.debug(f'setcolor to orange (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 35, 'saturation': 214, 'duration': int(duration)}, rf='bool') + param = {'hue': 35, 'saturation': 214, 'duration': int(duration)} elif hue <= 55: # self._plugin_instance.logger.debug(f'setcolor to yellow (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 52, 'saturation': 153, 'duration': int(duration)}, rf='bool') + param = {'hue': 52, 'saturation': 153, 'duration': int(duration)} elif hue <= 100: # self._plugin_instance.logger.debug(f'setcolor to grasgreen (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 92, 'saturation': 123, 'duration': int(duration)}, rf='bool') + param = {'hue': 92, 'saturation': 123, 'duration': int(duration)} elif hue <= 135: # self._plugin_instance.logger.debug(f'setcolor to green (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 120, 'saturation': 160, 'duration': int(duration)}, rf='bool') + param = {'hue': 120, 'saturation': 160, 'duration': int(duration)} elif hue <= 175: # self._plugin_instance.logger.debug(f'setcolor to turquoise (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 160, 'saturation': 145, 'duration': int(duration)}, rf='bool') + param = {'hue': 160, 'saturation': 145, 'duration': int(duration)} elif hue <= 210: # self._plugin_instance.logger.debug(f'setcolor to cyan (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 195, 'saturation': 179, 'duration': int(duration)}, rf='bool') + param = {'hue': 195, 'saturation': 179, 'duration': int(duration)} elif hue <= 240: # self._plugin_instance.logger.debug(f'setcolor to blue (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 225, 'saturation': 204, 'duration': int(duration)}, rf='bool') + param = {'hue': 225, 'saturation': 204, 'duration': int(duration)} elif hue <= 280: # self._plugin_instance.logger.debug(f'setcolor to violett (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 266, 'saturation': 169, 'duration': int(duration)}, rf='bool') + param = {'hue': 266, 'saturation': 169, 'duration': int(duration)} elif hue <= 310: # self._plugin_instance.logger.debug(f'setcolor to magenta (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 296, 'saturation': 140, 'duration': int(duration)}, rf='bool') + param = {'hue': 296, 'saturation': 140, 'duration': int(duration)} elif hue <= 350: # self._plugin_instance.logger.debug(f'setcolor to pink (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 335, 'saturation': 180, 'duration': int(duration)}, rf='bool') + param = {'hue': 335, 'saturation': 180, 'duration': int(duration)} elif hue <= 360: # self._plugin_instance.logger.debug(f'setcolor to red (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 358, 'saturation': 180, 'duration': int(duration)}, rf='bool') + param = {'hue': 358, 'saturation': 180, 'duration': int(duration)} else: self._plugin_instance.logger.error(f'setcolor hue out of range (hue={hue})') + return + + return self._aha_request("setcolor", ain=ain, param=param, rf='bool') def get_hue(self, ain): """ @@ -3229,7 +3236,7 @@ def get_device_log_from_lua(self): if not self._logged_in: self.login() - url = self._get_prefixed_host() + self._log_route + url = self._get_prefixed_host() + self.LOG_ROUTE params = {"sid": self._sid} # get data @@ -3261,7 +3268,7 @@ def get_device_log_from_lua_separated(self): if not self._logged_in: self.login() - url = self._get_prefixed_host() + self._log_separate_route + url = self._get_prefixed_host() + self.LOG_SEPARATE_ROUTE params = {"sid": self._sid} data = self._request(url, params, result='json') From 7d226488abf29b5b41f5f60e91b82ec96e8fab45 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 31 Jan 2023 21:49:07 +0100 Subject: [PATCH 049/178] Bugfix _set_fritz_device --- __init__.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/__init__.py b/__init__.py index 29ae92a3f..67e87608a 100644 --- a/__init__.py +++ b/__init__.py @@ -1259,20 +1259,22 @@ def _set_fritz_device(self, avm_data_type: str, args: str = None, wlan_index=Non if service.lower().startswith('wlan'): wlan_index = "" if not wlan_index else wlan_index - response = eval(f"self.client.{device}.{service}[{wlan_index}].{action}({args})") + cmd = f"self.client.{device}.{service}[{wlan_index}].{action}({args})" elif args is None: - response = eval(f"self.client.{device}.{service}.{action}()") + cmd = f"self.client.{device}.{service}.{action}()" else: - response = eval(f"self.client.{device}.{service}.{action}({args})") + cmd = f"self.client.{device}.{service}.{action}({args})" + response = eval(cmd) + if self._plugin_instance.debug_log: - self._plugin_instance.logger.debug(f"response={response} for {device}.{service}.{action} with args={args}") + self._plugin_instance.logger.debug(f"response={response} for cmd={cmd}.") # return response if response is None: - self._plugin_instance.logger.info(f"No response for 'self.client.{device}.{service}.{action}({args})' received.") + self._plugin_instance.logger.info(f"No response for cmd={cmd} received.") elif isinstance(response, int) and 99 < response < 1000: - self._plugin_instance.logger.info(f"Response was ErrorCode: {response} '{self.ERROR_CODES.get(response, None)}' for self.client.{device}.{service}.{action}({args})") + self._plugin_instance.logger.info(f"Response was ErrorCode: {response} '{self.ERROR_CODES.get(response, None)}' for cmd={cmd}") return response def _clear_data_cache(self): @@ -1407,7 +1409,7 @@ def wol(self, mac_address: str): """ # self.client.LanDevice.Hosts.X_AVM_DE_GetAutoWakeOnLANByMACAddress(NewMACAddress=mac_address) - return self._set_fritz_device('wol', f"NewMACAddress={mac_address}") + return self._set_fritz_device('wol', f"NewMACAddress='{mac_address}'") # ---------------------------------- # caller methods @@ -1466,7 +1468,7 @@ def start_call(self, phone_number: str): """ # self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialNumber(NewX_AVM_DE_PhoneNumber=phone_number.strip()) - return self._set_fritz_device('start_call', f"NewX_AVM_DE_PhoneNumber={phone_number.strip()}") + return self._set_fritz_device('start_call', f"NewX_AVM_DE_PhoneNumber='{phone_number.strip()}'") def cancel_call(self): """ @@ -1612,7 +1614,7 @@ def set_wlan(self, wlan_index: int, new_enable: bool = False): self._plugin_instance.logger.debug(f"set_wlan called: wlan_index={wlan_index}, new_enable={new_enable}") # self.client.LANDevice.WLANConfiguration[wlan_index].SetEnable(NewEnable=int(new_enable)) - response = self._set_fritz_device('set_wlan', f"NewEnable={int(new_enable)}", wlan_index) + response = self._set_fritz_device('set_wlan', f"NewEnable='{int(new_enable)}'", wlan_index) # check if remaining time is set as item self.set_wlan_time_remaining(wlan_index) @@ -1666,7 +1668,7 @@ def set_wps(self, wlan_index: int, wps_enable: bool = False): self._plugin_instance.logger.debug(f"set_wps called: wlan_index={wlan_index}, wps_enable={wps_enable}") # self.client.LANDevice.WLANConfiguration[wlan_index].X_AVM_DE_SetWPSEnable(NewX_AVM_DE_WPSEnable=int(wps_enable)) - return self._set_fritz_device('set_wps', f"NewX_AVM_DE_WPSEnable={int(wps_enable)}", wlan_index) + return self._set_fritz_device('set_wps', f"NewX_AVM_DE_WPSEnable='{int(wps_enable)}'", wlan_index) def get_wps(self, wlan_index: int): """ @@ -1692,7 +1694,7 @@ def set_tam(self, tam_index: int = 0, new_enable: bool = False): """ # self.client.InternetGatewayDevice.X_AVM_DE_TAM.SetEnable(NewIndex=tam_index, NewEnable=int(new_enable)) - return self._set_fritz_device('set_tam', f"NewIndex={tam_index}, NewEnable={int(new_enable)}") + return self._set_fritz_device('set_tam', f"NewIndex={tam_index}, NewEnable='{int(new_enable)}'") def get_tam(self, tam_index: int = 0): """ @@ -1752,7 +1754,7 @@ def set_aha_device(self, ain: str = '', set_switch: bool = False): switch_state = "ON" if set_switch is True else "OFF" # self.client.InternetGatewayDevice.X_AVM_DE_Homeauto.SetSwitch(NewAIN=ain, NewSwitchState=switch_state) - return self._set_fritz_device('set_aha_device', f"NewAIN={ain}, NewSwitchState={switch_state}") + return self._set_fritz_device('set_aha_device', f"NewAIN={ain}, NewSwitchState='{switch_state}'") # ---------------------------------- # deflection @@ -1768,7 +1770,7 @@ def set_deflection(self, deflection_id: int = 0, new_enable: bool = False): """ # self.client.InternetGatewayDevice.X_AVM_DE_OnTel.SetDeflectionEnable(NewDeflectionId=deflection_id, NewEnable=int(new_enable)) - return self._set_fritz_device('set_deflection', f"NewDeflectionId={deflection_id}, NewEnable={int(new_enable)}") + return self._set_fritz_device('set_deflection', f"NewDeflectionId='{deflection_id}', NewEnable='{int(new_enable)}'") def get_deflection(self, deflection_id: int = 0): """Get Deflection state of deflection_id""" @@ -1866,7 +1868,7 @@ def get_hosts_dict(self) -> Union[dict, None]: url = f"{self._build_url()}{hosts_url}" hosts_xml = self._request_response_to_xml(self._request(url, self._timeout, self._verify)) - if not hosts_xml: + if hosts_xml is not None: return hosts_dict = {} From 7c9c3cff21d888a65d843e944d1127af1eccbeed Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sun, 5 Feb 2023 12:01:53 +0100 Subject: [PATCH 050/178] Finetuning WebIF --- webif/templates/index.html | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/webif/templates/index.html b/webif/templates/index.html index 9bad16eee..061c5ee34 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -77,33 +77,33 @@ } try { - table0 = $('#itemtable').DataTable( { + itemtable = $('#itemtable').DataTable( { pageLength: webif_pagelength, pageResize: resize } ); - console.log("Init table0 for page length " + webif_pagelength + ", pageResize: " + resize); - table1 = $('#smarthomeitemtable').DataTable( { + console.log("Init itemtable for page length " + webif_pagelength + ", pageResize: " + resize); + smarthomeitemtable = $('#smarthomeitemtable').DataTable( { pageLength: webif_pagelength, pageResize: resize } ); - console.log("Init table1 for page length " + webif_pagelength + ", pageResize: " + resize); - table2 = $('#logtable').DataTable( { + console.log("Init smarthomeitemtable for page length " + webif_pagelength + ", pageResize: " + resize); + logtable = $('#logtable').DataTable( { pageLength: webif_pagelength, order: [[ 0, 'desc' ]], columnDefs: [{ "targets": [2], "className": "truncate logentry"}].concat($.fn.dataTable.defaults.columnDefs), pageResize: resize } ); - console.log("Init table2 for page length " + webif_pagelength + ", pageResize: " + resize); - table3 = $('#call_monitortable').DataTable( { + console.log("Init logtable for page length " + webif_pagelength + ", pageResize: " + resize); + call_monitortable = $('#call_monitortable').DataTable( { pageLength: webif_pagelength, pageResize: resize } ); - console.log("Init table3 for page length " + webif_pagelength + ", pageResize: " + resize); - table4 = $('#smarthome_devicetable').DataTable( { + console.log("Init call_monitortable for page length " + webif_pagelength + ", pageResize: " + resize); + smarthome_devicetable = $('#smarthome_devicetable').DataTable( { pageLength: -1, pageResize: resize } ); - console.log("Init table4 for page length -1, pageResize: " + resize); + console.log("Init smarthome_devicetable for page length -1, pageResize: " + resize); } catch (e) { console.log("Datatable JS not loaded, showing standard table without reorder option " +e); From 3ca00082ec0ed7d4f617ef0213103634e985d11d Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sun, 5 Feb 2023 12:58:13 +0100 Subject: [PATCH 051/178] Errorhandling get_hosts_count --- __init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 67e87608a..1bdd99da3 100644 --- a/__init__.py +++ b/__init__.py @@ -1893,7 +1893,9 @@ def get_hosts_dict(self) -> Union[dict, None]: def get_hosts_count(self) -> int: """Returns count of hosts""" - return len(self.get_hosts_dict()) + host_dict = self.get_hosts_dict() + + return len(host_dict) if host_dict else None def get_mesh_topology(self) -> Union[dict, None]: """Get mesh topology information as dict""" From 9ef60777a7278a48fcad79c4d811de95ecf7ddd9 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Fri, 10 Feb 2023 14:30:49 +0100 Subject: [PATCH 052/178] Change everything vom avm2_ to avm_ --- __init__.py | 73 ++++------ plugin.yaml | 270 ++++++++++++++++++------------------- webif/templates/index.html | 5 +- 3 files changed, 164 insertions(+), 184 deletions(-) diff --git a/__init__.py b/__init__.py index 1bdd99da3..5cd386646 100644 --- a/__init__.py +++ b/__init__.py @@ -46,23 +46,6 @@ from .webif import WebInterface -""" -Definition of TR-064 details -""" - -#FRITZ_TR64_DESC_FILE = "tr64desc.xml" -#FRITZ_IGD_DESC_FILE = "igddesc.xml" -#FRITZ_IGD2_DESC_FILE = "igd2desc.xml" -#FRITZ_L2TPV3_FILE = "l2tpv3.xml" -#FRITZ_FBOX_DESC_FILE = "fboxdesc.xml" - -#IGD_DEVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:device-1-0'} -#IGD_SERVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:service-1-0'} -#TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} -#TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} -#TR064_CONTROL_NAMESPACE = {'': 'urn:dslforum-org:control-1-0'} - - """ Definition of attribute value groups of avm_data_type """ @@ -248,10 +231,9 @@ *_avm_rw_attributes] -class AVM2(SmartPlugin): +class AVM(SmartPlugin): """ - Main class of the Plugin. Does all plugin specific stuff and provides - the update functions for the items + Main class of the Plugin. Does all plugin specific stuff """ PLUGIN_VERSION = '2.0.0' @@ -369,12 +351,12 @@ def parse_item(self, item): can be sent to the knx with a knx write function within the knx plugin. """ - if self.has_iattr(item.conf, 'avm2_data_type'): + if self.has_iattr(item.conf, 'avm_data_type'): self.logger.debug(f"parse item: {item}") # get avm_data_type and avm_data_cycle - avm_data_type = self.get_iattr_value(item.conf, 'avm2_data_type') - avm_data_cycle = self.get_iattr_value(item.conf, 'avm2_data_cycle') if self.has_iattr(item.conf, 'avm2_data_cycle') else self._cycle + avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') + avm_data_cycle = self.get_iattr_value(item.conf, 'avm_data_cycle') if self.has_iattr(item.conf, 'avm_data_cycle') else self._cycle avm_data_cycle = 30 if 1 < avm_data_cycle < 29 else avm_data_cycle # handle items specific to call monitor @@ -422,13 +404,13 @@ def update_item(self, item, caller=None, source=None, dest=None): if self.alive and caller != self.get_fullname(): # get avm_data_type - avm_data_type = self.get_iattr_value(item.conf, 'avm2_data_type') + avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') self.logger.info(f"Updated item: {item.property.path} with avm_data_type={avm_data_type} item has been changed outside this plugin from caller={caller}") readafterwrite = None - if self.has_iattr(item.conf, 'avm2_read_after_write'): - readafterwrite = int(self.get_iattr_value(item.conf, 'avm2_read_after_write')) + if self.has_iattr(item.conf, 'avm_read_after_write'): + readafterwrite = int(self.get_iattr_value(item.conf, 'avm_read_after_write')) if self.debug_log: self.logger.debug(f'Attempting read after write for item: {item.id()}, avm_data_type: {avm_data_type}, delay: {readafterwrite}s') @@ -663,33 +645,33 @@ def register_item(self, item, avm_data_type: str, avm_data_cycle: int): if avm_data_type in _wlan_config_attributes: index = self._get_wlan_index(item) if index is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_wlan_index' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_wlan_index' found; append to list.") else: - self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm2_wlan_index' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_wlan_index' is not defined; Item will be ignored.") # handle network_device related items elif avm_data_type in (_host_attribute + _host_child_attributes): index = self._get_mac(item) if index is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_mac' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_mac' found; append to list.") else: - self._plugin_instance.logger.warning("Item {item.id()} with avm attribute found, but 'avm2_mac' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning("Item {item.id()} with avm attribute found, but 'avm_mac' is not defined; Item will be ignored.") # handle tam related items elif avm_data_type in _tam_attributes: index = self._get_tam_index(item) if index is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_tam_index' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") else: - self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm2_tam_index' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") # handle deflection related items elif avm_data_type in _deflection_attributes: index = self._get_deflection_index(item) if index is not None: - self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm2_tam_index' found; append to list.") + self._plugin_instance.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") else: - self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm2_tam_index' is not defined; Item will be ignored.") + self._plugin_instance.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") # register item self._items[item] = (avm_data_type, index, avm_data_cycle, time.time()) @@ -769,7 +751,7 @@ def _get_wlan_index(self, item) -> int: wlan_index = None for i in range(2): - attribute = 'avm2_wlan_index' + attribute = 'avm_wlan_index' wlan_index = self._plugin_instance.get_iattr_value(item.conf, attribute) if wlan_index: @@ -781,7 +763,7 @@ def _get_wlan_index(self, item) -> int: wlan_index = int(wlan_index) - 1 if not 0 <= wlan_index <= 2: wlan_index = None - self._plugin_instance.logger.warning(f"Attribute 'avm2_wlan_index' for item {item.id()} not in valid range 1-3.") + self._plugin_instance.logger.warning(f"Attribute 'avm_wlan_index' for item {item.id()} not in valid range 1-3.") return wlan_index @@ -792,7 +774,7 @@ def _get_tam_index(self, item) -> int: tam_index = None for i in range(2): - attribute = 'avm2_tam_index' + attribute = 'avm_tam_index' tam_index = self._plugin_instance.get_iattr_value(item.conf, attribute) if tam_index: @@ -815,7 +797,7 @@ def _get_deflection_index(self, item) -> int: deflection_index = None for i in range(2): - attribute = 'avm2_deflection_index' + attribute = 'avm_deflection_index' deflection_index = self._plugin_instance.get_iattr_value(item.conf, attribute) if deflection_index: @@ -838,7 +820,7 @@ def _get_mac(self, item) -> str: mac = None for i in range(2): - attribute = 'avm2_mac' + attribute = 'avm_mac' mac = self._plugin_instance.get_iattr_value(item.conf, attribute) if mac: @@ -1266,7 +1248,6 @@ def _set_fritz_device(self, avm_data_type: str, args: str = None, wlan_index=Non cmd = f"self.client.{device}.{service}.{action}({args})" response = eval(cmd) - if self._plugin_instance.debug_log: self._plugin_instance.logger.debug(f"response={response} for cmd={cmd}.") @@ -2177,8 +2158,8 @@ def __init__(self, xml, auth, base_url, name, service_type, service_id, control_ self.headers = {'content-type': 'text/xml; charset="utf-8"'} self.envelope = etree.Element('{http://schemas.xmlsoap.org/soap/envelope/}Envelope', - attrib={'{http://schemas.xmlsoap.org/soap/envelope/}encodingStyle': - 'http://schemas.xmlsoap.org/soap/encoding/'}) + attrib={'{http://schemas.xmlsoap.org/soap/envelope/}encodingStyle': + 'http://schemas.xmlsoap.org/soap/encoding/'}) self.body = etree.SubElement(self.envelope, '{http://schemas.xmlsoap.org/soap/envelope/}Body') self.in_arguments = {} @@ -2324,7 +2305,7 @@ def _get_item_ain(self, item) -> Union[str, None]: else: lookup_item = item for i in range(2): - attribute = 'avm2_ain' + attribute = 'avm_ain' ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute) if ain_device is not None: break @@ -4111,11 +4092,11 @@ def register_item(self, item, avm_data_type: str): self._items[item] = (avm_data_type, None) elif avm_data_type in _call_monitor_attributes_trigger: - avm_incoming_allowed = self._plugin_instance.get_iattr_value(item.conf, 'avm2_incoming_allowed') - avm_target_number = self._plugin_instance.get_iattr_value(item.conf, 'avm2_target_number') + avm_incoming_allowed = self._plugin_instance.get_iattr_value(item.conf, 'avm_incoming_allowed') + avm_target_number = self._plugin_instance.get_iattr_value(item.conf, 'avm_target_number') if not avm_incoming_allowed or not avm_target_number: - self._plugin_instance.logger.error(f"For Trigger-item={item.id()} both 'avm2_incoming_allowed' and 'avm2_target_number' must be specified as attributes. Item will be ignored.") + self._plugin_instance.logger.error(f"For Trigger-item={item.id()} both 'avm_incoming_allowed' and 'avm_target_number' must be specified as attributes. Item will be ignored.") else: self._trigger_items[item] = (avm_data_type, avm_incoming_allowed, avm_target_number) diff --git a/plugin.yaml b/plugin.yaml index 0a648b435..796877494 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -19,7 +19,7 @@ plugin: # py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) multi_instance: true # plugin supports multi instance restartable: unknown - classname: AVM2 # class containing the plugin + classname: AVM # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml @@ -114,7 +114,7 @@ parameters: item_attributes: # Definition of item attributes defined by this plugin - avm2_data_type: + avm_data_type: type: str mandatory: True description: @@ -146,7 +146,7 @@ item_attributes: - 'call_event_outgoing' # r/o str Status des letzten ausgehenden Anrufs - 'call_direction' # r/o str Richtung des letzten Anrufes - 'call_event' # r/o str Status des letzten Anrufes - # TAM Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_tam_index' + # TAM Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm_tam_index' - 'tam' # r/w bool TAM an/aus - 'tam_name' # r/o str Name des TAM - 'tam_old_message_number' # r/o num Anzahl der alten Nachrichten @@ -169,7 +169,7 @@ item_attributes: - 'wan_current_bytes_sent' # r/o num WAN Verbindung-Anzahl insgesamt versendeter Bytes - 'wan_current_bytes_received' # r/o num WAN Verbindung-Anzahl insgesamt empfangener Bytes - 'wan_link' # r/o bool WAN Link - # WLAN Config Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_wlan_index' + # WLAN Config Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm_wlan_index' - 'wlanconfig' # r/w bool WLAN An/Aus - 'wlanconfig_ssid' # r/o str WLAN SSID - 'wlan_guest_time_remaining' # r/o num Verbleibende Zeit, bis zum automatischen Abschalten des Gäste-WLAN @@ -179,7 +179,7 @@ item_attributes: - 'wps_mode' # r/o str WPS Modus des entsprechenden WlAN # WLAN Attribute - 'wlan_total_associates' # r/o num Anzahl der verbundenen Geräte im WLAN - # Host Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_mac' + # Host Attribute Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm_mac' - 'network_device' # r/o bool Verbindungsstatus // Defines Network device via MAC-Adresse - 'device_ip' # r/o str Geräte-IP (Muss Child von 'network_device' sein) - 'device_connection_type' # r/o str Verbindungstyp (Muss Child von 'network_device' sein) @@ -197,7 +197,7 @@ item_attributes: - 'set_temperature_reduced' # r/o num siehe "temperature_reduced" - 'set_temperature_comfort' # r/o num siehe "temperature_comfort" - 'firmware_version' # r/o str siehe "fw_version" - # Deflections Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_deflection_index' + # Deflections Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm_deflection_index' - 'number_of_deflections' # r/o num Anzahl der eingestellten Rufumleitungen - 'deflection_details' # r/o dict Details zur Rufumleitung (als dict); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item - 'deflections_details' # r/o dict Details zu allen Rufumleitung (als dict) @@ -208,7 +208,7 @@ item_attributes: - 'deflection_mode' # r/o str Modus der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - 'deflection_outgoing' # r/o str Outgoing der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - 'deflection_phonebook_id' # r/o str Phonebook_ID der Zielrufnummer (Only valid if Type==fromPB); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item - # AHA Interface attributes Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm2_tam_ain' + # AHA Interface attributes Hinweis: alle Attribute benötigen zusätzlich das Attribut 'avm_tam_ain' - 'device_id' # r/o str Geräte -ID - 'manufacturer' # r/o str Hersteller - 'product_name' # r/o str Produktname @@ -265,21 +265,21 @@ item_attributes: - 'alert_state' # r/o bool letzter übermittelter Alarmzustand - avm2_incoming_allowed: + avm_incoming_allowed: type: str mandatory: False description: de: '(optional) Definition der erlaubten eingehenden Rufnummer in Items vom avm_data_type `monitor_trigger`.' en: '(optional) Definition of the allowed incoming number. Only in items of avm_data_type `monitor_trigger`.' - avm2_target_number: + avm_target_number: type: str mandatory: False description: de: '(optional) Definition der erlaubten angerufenen Rufnummer in Items vom avm_data_type `monitor_trigger`.' en: '(optional) Definition of the allowed called number. Only in items of avm_data_type `monitor_trigger`.' - avm2_wlan_index: + avm_wlan_index: type: int description: de: '(optional) Definition des Wlans ueber index: (1: 2.4Ghz, 2: 5Ghz, 3: Gaeste).' @@ -287,21 +287,21 @@ item_attributes: valid_min: 1 valid_max: 3 - avm2_mac: + avm_mac: type: mac mandatory: False description: de: '(optional) Definition der MAC Adresse für Items vom avm_data_type `network_device`. Nur für diese Items mandatory!' en: '(optional) Definition of the MAC address for items of avm_data_type `network_device`. Only mandatory for these items!' - avm2_ain: + avm_ain: type: str mandatory: False description: de: "(optional) Definition der AktorIdentifikationsNummer (AIN) Items vom avm_data_types für `AHA-Interface`. Nur für diese Items mandatory!" en: "(optional) Definition of the ActorIdentificationNumber (AIN) for items of avm_data_types `AHA-Interface`. Only mandatory for these items!" - avm2_tam_index: + avm_tam_index: type: int mandatory: False description: @@ -310,7 +310,7 @@ item_attributes: valid_min: 1 valid_max: 5 - avm2_deflection_index: + avm_deflection_index: type: int mandatory: False description: @@ -319,13 +319,13 @@ item_attributes: valid_min: 1 valid_max: 32 - avm2_read_after_write: + avm_read_after_write: type: int description: de: '(optional) Konfiguriert eine Verzögerung in Sekunden nachdem ein Lesekommando nach einem Schreibkommando gesendet wird.' en: '(optional) Configures delay in seconds to issue a read command after write command' - avm2_data_cycle: + avm_data_cycle: type: int mandatory: False description: @@ -337,189 +337,189 @@ item_structs: uptime: type: num visu_acl: ro - avm2_data_type@instance: uptime - avm2_data_cycle@instance: 600 + avm_data_type@instance: uptime + avm_data_cycle@instance: 600 serial_number: type: str visu_acl: ro - avm2_data_type@instance: serial_number - avm2_data_cycle@instance: 0 + avm_data_type@instance: serial_number + avm_data_cycle@instance: 0 firmware: type: str visu_acl: ro - avm2_data_type@instance: software_version - avm2_data_cycle@instance: 3600 + avm_data_type@instance: software_version + avm_data_cycle@instance: 3600 hardware_version: type: str visu_acl: ro - avm2_data_type@instance: hardware_version - avm2_data_cycle@instance: 0 + avm_data_type@instance: hardware_version + avm_data_cycle@instance: 0 myfritz: type: bool - avm2_data_type@instance: myfritz_status + avm_data_type@instance: myfritz_status monitor: trigger: type: bool - avm2_data_type@instance: monitor_trigger - avm2_incoming_allowed@instance: xxxxxxxx - avm2_target_number@instance: xxxxxxxx + avm_data_type@instance: monitor_trigger + avm_incoming_allowed@instance: xxxxxxxx + avm_target_number@instance: xxxxxxxx enforce_updates: yes incoming: is_call_incoming: type: bool - avm2_data_type@instance: is_call_incoming + avm_data_type@instance: is_call_incoming duration: type: num - avm2_data_type@instance: call_duration_incoming + avm_data_type@instance: call_duration_incoming last_caller: type: str - avm2_data_type@instance: last_caller_incoming + avm_data_type@instance: last_caller_incoming last_calling_number: type: str - avm2_data_type@instance: last_number_incoming + avm_data_type@instance: last_number_incoming last_called_number: type: str - avm2_data_type@instance: last_called_number_incoming + avm_data_type@instance: last_called_number_incoming last_call_date: type: str - avm2_data_type@instance: last_call_date_incoming + avm_data_type@instance: last_call_date_incoming event: type: str - avm2_data_type@instance: call_event_incoming + avm_data_type@instance: call_event_incoming outgoing: is_call_outgoing: type: bool - avm2_data_type@instance: is_call_outgoing + avm_data_type@instance: is_call_outgoing duration: type: num - avm2_data_type@instance: call_duration_outgoing + avm_data_type@instance: call_duration_outgoing last_caller: type: str - avm2_data_type@instance: last_caller_outgoing + avm_data_type@instance: last_caller_outgoing last_calling_number: type: str - avm2_data_type@instance: last_number_outgoing + avm_data_type@instance: last_number_outgoing last_called_number: type: str - avm2_data_type@instance: last_called_number_outgoing + avm_data_type@instance: last_called_number_outgoing last_call_date: type: str - avm2_data_type@instance: last_call_date_outgoing + avm_data_type@instance: last_call_date_outgoing event: type: str - avm2_data_type@instance: call_event_outgoing + avm_data_type@instance: call_event_outgoing newest: direction: type: str - avm2_data_type@instance: call_direction + avm_data_type@instance: call_direction cache: yes event: type: str - avm2_data_type@instance: call_event + avm_data_type@instance: call_event cache: yes tam: type: bool visu_acl: rw - avm2_data_type@instance: tam - avm2_tam_index@instance: 1 - avm2_read_after_write@instance: 5 + avm_data_type@instance: tam + avm_tam_index@instance: 1 + avm_read_after_write@instance: 5 name: type: str visu_acl: ro - avm2_data_type@instance: tam_name + avm_data_type@instance: tam_name message_number_old: type: num visu_acl: ro - avm2_data_type@instance: tam_old_message_number + avm_data_type@instance: tam_old_message_number message_number_new: type: num visu_acl: ro - avm2_data_type@instance: tam_new_message_number + avm_data_type@instance: tam_new_message_number message_number_total: type: num visu_acl: ro - avm2_data_type@instance: tam_total_message_number + avm_data_type@instance: tam_total_message_number deflection: type: bool visu_acl: rw - avm2_data_type@instance: deflection_enable - avm2_deflection_index@instance: 1 - avm2_read_after_write@instance: 5 + avm_data_type@instance: deflection_enable + avm_deflection_index@instance: 1 + avm_read_after_write@instance: 5 deflection_type: type: str visu_acl: ro - avm2_data_type@instance: deflection_type + avm_data_type@instance: deflection_type deflection_number: type: str visu_acl: ro - avm2_data_type@instance: deflection_number + avm_data_type@instance: deflection_number deflection_to_number: type: str visu_acl: ro - avm2_data_type@instance: deflection_to_number + avm_data_type@instance: deflection_to_number deflection_mode: type: str visu_acl: ro - avm2_data_type@instance: deflection_mode + avm_data_type@instance: deflection_mode deflection_outgoing: type: str visu_acl: ro - avm2_data_type@instance: deflection_outgoing + avm_data_type@instance: deflection_outgoing deflection_phonebook_id: type: num visu_acl: ro - avm2_data_type@instance: deflection_phonebook_id + avm_data_type@instance: deflection_phonebook_id wan: connection_status: type: str visu_acl: ro - avm2_data_type@instance: wan_connection_status + avm_data_type@instance: wan_connection_status connection_error: type: str visu_acl: ro - avm2_data_type@instance: wan_connection_error + avm_data_type@instance: wan_connection_error is_connected: type: bool visu_acl: ro - avm2_data_type@instance: wan_is_connected + avm_data_type@instance: wan_is_connected uptime: type: num visu_acl: ro - avm2_data_type@instance: wan_uptime + avm_data_type@instance: wan_uptime upstream: type: num visu_acl: ro - avm2_data_type@instance: wan_upstream + avm_data_type@instance: wan_upstream downstream: type: num visu_acl: ro - avm2_data_type@instance: wan_downstream + avm_data_type@instance: wan_downstream total_packets_sent: type: num visu_acl: ro - avm2_data_type@instance: wan_total_packets_sent + avm_data_type@instance: wan_total_packets_sent total_packets_received: type: num visu_acl: ro - avm2_data_type@instance: wan_total_packets_received + avm_data_type@instance: wan_total_packets_received total_bytes_sent: type: num visu_acl: ro - avm2_data_type@instance: wan_total_bytes_sent + avm_data_type@instance: wan_total_bytes_sent total_bytes_received: type: num visu_acl: ro - avm2_data_type@instance: wan_total_bytes_received + avm_data_type@instance: wan_total_bytes_received link: type: bool visu_acl: ro - avm2_data_type@instance: wan_link + avm_data_type@instance: wan_link reconnect: type: bool visu_acl: rw @@ -529,180 +529,180 @@ item_structs: wlan_1: type: bool visu_acl: rw - avm2_data_type@instance: wlanconfig # 2,4ghz - avm2_wlan_index@instance: 1 - avm2_read_after_write@instance: 5 + avm_data_type@instance: wlanconfig # 2,4ghz + avm_wlan_index@instance: 1 + avm_read_after_write@instance: 5 wlan_1_ssid: type: str visu_acl: ro - avm2_data_type@instance: wlanconfig_ssid - avm2_wlan_index@instance: 1 + avm_data_type@instance: wlanconfig_ssid + avm_wlan_index@instance: 1 wlan_1_associates: type: num visu_acl: ro - avm2_data_type@instance: wlan_associates - avm2_wlan_index@instance: 1 + avm_data_type@instance: wlan_associates + avm_wlan_index@instance: 1 wlan_2: type: bool visu_acl: rw - avm2_data_type@instance: wlanconfig # 5 GHz - avm2_wlan_index@instance: 2 - avm2_read_after_write@instance: 5 + avm_data_type@instance: wlanconfig # 5 GHz + avm_wlan_index@instance: 2 + avm_read_after_write@instance: 5 wlan_2_ssid: type: str visu_acl: ro - avm2_data_type@instance: wlanconfig_ssid - avm2_wlan_index@instance: 2 + avm_data_type@instance: wlanconfig_ssid + avm_wlan_index@instance: 2 wlan_2_associates: type: num visu_acl: ro - avm2_data_type@instance: wlan_associates - avm2_wlan_index@instance: 2 + avm_data_type@instance: wlan_associates + avm_wlan_index@instance: 2 wlan_gast: type: bool visu_acl: rw - avm2_data_type@instance: wlanconfig # Guest - avm2_wlan_index@instance: 3 - avm2_read_after_write@instance: 5 + avm_data_type@instance: wlanconfig # Guest + avm_wlan_index@instance: 3 + avm_read_after_write@instance: 5 wlan_gast_ssid: type: str visu_acl: ro - avm2_data_type@instance: wlanconfig_ssid - avm2_wlan_index@instance: 3 + avm_data_type@instance: wlanconfig_ssid + avm_wlan_index@instance: 3 wlan_gast_associates: type: num visu_acl: ro - avm2_data_type@instance: wlan_associates - avm2_wlan_index@instance: 3 + avm_data_type@instance: wlan_associates + avm_wlan_index@instance: 3 wlan_gast_tr: type: num visu_acl: rw - avm2_data_type@instance: wlan_guest_time_remaining - avm2_wlan_index@instance: 3 + avm_data_type@instance: wlan_guest_time_remaining + avm_wlan_index@instance: 3 device: - avm2_data_type@instance: network_device + avm_data_type@instance: network_device type: bool visu_acl: ro ip: type: str - avm2_data_type@instance: device_ip + avm_data_type@instance: device_ip visu_acl: ro connection_type: type: str - avm2_data_type@instance: device_connection_type + avm_data_type@instance: device_connection_type visu_acl: ro hostname: type: str - avm2_data_type@instance: device_hostname + avm_data_type@instance: device_hostname visu_acl: ro aha_general: name: type: str - avm2_data_type@instance: device_name - avm2_data_cycle@instance: 0 + avm_data_type@instance: device_name + avm_data_cycle@instance: 0 identifier: type: str - avm2_data_type@instance: device_id - avm2_data_cycle@instance: 0 + avm_data_type@instance: device_id + avm_data_cycle@instance: 0 productname: type: str - avm2_data_type@instance: product_name - avm2_data_cycle@instance: 0 + avm_data_type@instance: product_name + avm_data_cycle@instance: 0 manufacturer: type: str - avm2_data_type@instance: manufacturer - avm2_data_cycle@instance: 0 + avm_data_type@instance: manufacturer + avm_data_cycle@instance: 0 firmware_version: type: str - avm2_data_type@instance: fw_version - avm2_data_cycle@instance: 0 + avm_data_type@instance: fw_version + avm_data_cycle@instance: 0 present: type: bool - avm2_data_type@instance: connected - avm2_data_cycle@instance: 0 + avm_data_type@instance: connected + avm_data_cycle@instance: 0 functions: type: list - avm2_data_type@instance: device_functions - avm2_data_cycle@instance: 0 + avm_data_type@instance: device_functions + avm_data_cycle@instance: 0 aha_thermostat: target_temperature: - avm2_data_type@instance: target_temperature - avm2_read_after_write@instance: 5 + avm_data_type@instance: target_temperature + avm_read_after_write@instance: 5 type: num comfort_temperature: - avm2_data_type@instance: temperature_comfort + avm_data_type@instance: temperature_comfort type: num eco_temperature: - avm2_data_type@instance: temperature_reduced + avm_data_type@instance: temperature_reduced type: num battery_low: - avm2_data_type@instance: battery_low + avm_data_type@instance: battery_low type: bool battery_level: - avm2_data_type@instance: battery_level + avm_data_type@instance: battery_level type: num window_open: - avm2_data_type@instance: window_open + avm_data_type@instance: window_open type: bool windowopenactiveendtime: - avm2_data_type@instance: windowopenactiveendtime + avm_data_type@instance: windowopenactiveendtime type: num summer_active: - avm2_data_type@instance: summer_active + avm_data_type@instance: summer_active type: bool holiday_active: - avm2_data_type@instance: holiday_active + avm_data_type@instance: holiday_active type: bool errorcode: - avm2_data_type@instance: errorcode + avm_data_type@instance: errorcode type: num hkr_boost: - avm2_data_type@instance: hkr_boost + avm_data_type@instance: hkr_boost type: bool boostactiveendtime: - avm2_data_type@instance: boostactiveendtime + avm_data_type@instance: boostactiveendtime type: num lock: - avm2_data_type@instance: lock + avm_data_type@instance: lock type: bool device_lock: - avm2_data_type@instance: device_lock + avm_data_type@instance: device_lock type: bool aha_temperature_sensor: current_temperature: - avm2_data_type@instance: current_temperature + avm_data_type@instance: current_temperature type: num temperature_offset: - avm2_data_type@instance: temperature_offset + avm_data_type@instance: temperature_offset type: num aha_alert: state: - avm2_data_type@instance: alert + avm_data_type@instance: alert type: bool aha_switch: switch_state: - avm2_data_type@instance: switch_state + avm_data_type@instance: switch_state type: bool switch_toggle: - avm2_data_type@instance: switch_toggle + avm_data_type@instance: switch_toggle type: bool enforce_updates: yes aha_powermeter: power: - avm2_data_type@instance: power + avm_data_type@instance: power type: num energy: - avm2_data_type@instance: energy + avm_data_type@instance: energy type: num voltage: - avm2_data_type@instance: voltage + avm_data_type@instance: voltage type: num #item_attribute_prefixes: diff --git a/webif/templates/index.html b/webif/templates/index.html index 061c5ee34..7b8529f13 100644 --- a/webif/templates/index.html +++ b/webif/templates/index.html @@ -108,7 +108,6 @@ catch (e) { console.log("Datatable JS not loaded, showing standard table without reorder option " +e); } - }); + +{% endblock pluginscripts %} + + + +{% set tabcount = 6 %} + +{% set tab1title = _(""'AVM Items'" (" ~ avm_item_count ~ ") ") %} + +{% if p.aha_http_interface and smarthome_item_count > 0 %} + {% set tab2title = _(""'AVM Smarthome Items'" (" ~ smarthome_item_count ~ ") ") %} +{% else %} + {% set tab2title = "hidden" %} +{% endif %} + +{% if p.aha_http_interface %} + {% set tab3title = _(""'AVM Smarthome Devices'" (" ~ len(p._fritz_device._smarthome_devices) ~ ") ") %} +{% else %} + {% set tab3title = "hidden" %} +{% endif %} + +{% if p._call_monitor and call_monitor_item_count > 0 %} + {% set tab4title = _(""'Call Monitor Items'" (" ~ call_monitor_item_count ~ ") ") %} +{% else %} + {% set tab4title = "hidden" %} +{% endif %} + +{% set tab5title = _(""'Log-Einträge'"") %} + +{% set tab6title = _(""'Plugin-API'"") %} + + + +{% set language = p.get_sh().get_defaultlanguage() %} +{% if language not in ['en','de'] %} + {% set language = 'en' %} +{% endif %} + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + +
+ {% if p.get_fritz_device().is_available() %} + {{ _('Gerät verfügbar') }} + {% else %} + {{ _('Gerät nicht verfügbar') }} + {% endif %} + {{ _('Verbunden') }} + + {% if p.get_fritz_device().is_available() %} + {{ _('Ja') }}{% if p._fritz_device.is_ssl() %}, SSL{% endif %} + {% else %} + {{ _('Nein') }} + {% endif %} + {{ _('Benutzer') }}{{ p.get_parameter_value_for_display('username') }}
+ {% if p._call_monitor %} + {% if p.get_monitoring_service()._listen_active %} + {{ _('Call Monitor verbunden') }} + {% else %} + {{ _('Call Monitor nicht verbunden') }} + {% endif %} + {% endif %} + {{ _('Call Monitor') }} + {% if p._call_monitor %}{{ _('Ja') }}{% if not p.get_monitoring_service()._listen_active %}, {{ _('nicht verbunden') }}{% endif %}{% else %}{{ _('Nein') }}{% endif %}{{ _('Passwort') }}{{ p.get_parameter_value_for_display('password') }}
{{ _('Host') }}{{ p._fritz_device.get_host() }}{{ _('Port') }}{{ p._fritz_device.get_port() }} {% if p._fritz_device.is_ssl() %}(HTTPS){% endif %}
+{% endblock %} + + + +{% block buttons %} + + +{% endblock buttons %} + +{% block bodytab1 %} +
+ + + + + + + + + + + + + {% for item in avm_items %} + {% set item_id = item.id() %} + {% if p.get_instance_name() %} + {% set instance_key = "avm_data_type@"+p.get_instance_name() %} + {% else %} + {% set instance_key = "avm_data_type" %} + {% endif %} + + + + + + + + + {% endfor %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ item() }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
+
+{% endblock %} + +{% block bodytab2 %} +
+ + + + + + + + + + + + + {% for item in smarthome_items %} + {% set item_id = item.id() %} + {% if p.get_instance_name() %} + {% set instance_key = "avm_data_type@"+p.get_instance_name() %} + {% else %} + {% set instance_key = "avm_data_type" %} + {% endif %} + + + + + + + + + {% endfor %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ item() }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
+
+{% endblock %} + +{% block bodytab3 %} +{% if p._fritz_device._smarthome_devices %} +
+ + + + + + + + + + {% for ain in p._fritz_device._smarthome_devices %} + + + + + + {% endfor %} + +
{{ 'No' }}{{ 'Device AIN' }}{{ 'Device Details (dict)' }}
{{ loop.index }}{{ ain }}{{ p._fritz_device._smarthome_devices[ain] }}
+
+{% endif %} +{% endblock %} + +{% block bodytab4 %} +
+ + + + + + + + + + + + + {% if p._call_monitor %} + {% for item in call_monitor_items %} + {% set item_id = item.id() %} + {% if p.get_instance_name() %} + {% set instance_key = "avm_data_type@"+p.get_instance_name() %} + {% else %} + {% set instance_key = "avm_data_type" %} + {% endif %} + + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ item() }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
+
+{% endblock %} + +{% block bodytab5 %} +
+ + + + + + + + + + + {% set logentries = p.get_device_log_from_lua_separated() %} + {% if logentries %} + {% for logentry in logentries%} + + + + + + + {% endfor %} + {% endif %} + +
{{ 'Datum/Uhrzeit' }}{{ 'Meldung' }}{{ 'Typ' }}{{ 'Kategorie' }}
{{ logentry[0] }}{{ logentry[1] }} + + {{ logentry[2] }} + + {{ _('cat_'+logentry[3]|string) }}
+
+{% endblock %} + +{% block bodytab6 %} +
+ {% for function, dict in p.metadata.plugin_functions.items() %} +
+
+ {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) +
+
+ {{ dict['description'][language] }}
+ {% if dict['parameters'] is not none %} +
+
+ {{ _('Parameter') }}: +
+
+
    + {% for name, paramdict in dict['parameters'].items() %} +
  • + {{ name }}: {{ paramdict['type'] }}
    + {{ paramdict['description'][language] }} +
  • + {% endfor %} +
+
+
+ {% endif %} +
+
+ {% endfor %} +
+{% endblock %} \ No newline at end of file From 45f0bd0251cb5fdafd47a77b1e6ea0d73ee8ed74 Mon Sep 17 00:00:00 2001 From: msinn Date: Mon, 20 Mar 2023 10:18:49 +0100 Subject: [PATCH 110/178] avm: Removed older previous versions --- avm/_pv_1_5_12/README.md | 819 ---- avm/_pv_1_5_12/__init__.py | 2636 ----------- avm/_pv_1_5_12/assets/webif1.jpg | Bin 299580 -> 0 bytes avm/_pv_1_5_12/assets/webif2.jpg | Bin 250756 -> 0 bytes avm/_pv_1_5_12/locale.yaml | 43 - avm/_pv_1_5_12/plugin.yaml | 388 -- avm/_pv_1_5_12/requirements.txt | 1 - avm/_pv_1_5_12/sv_widgets/widget_avm.html | 105 - avm/_pv_1_5_12/user_doc.rst | 62 - avm/_pv_1_5_12/webif/__init__.py | 88 - .../webif/static/img/lamp_green.png | Bin 3757 -> 0 bytes avm/_pv_1_5_12/webif/static/img/lamp_red.png | Bin 3740 -> 0 bytes .../webif/static/img/plugin_logo.png | Bin 10661 -> 0 bytes avm/_pv_1_5_12/webif/templates/index.html | 241 - avm/_pv_1_6_5/README.md | 819 ---- avm/_pv_1_6_5/__init__.py | 4136 ----------------- avm/_pv_1_6_5/assets/webif1.jpg | Bin 299580 -> 0 bytes avm/_pv_1_6_5/assets/webif2.jpg | Bin 250756 -> 0 bytes avm/_pv_1_6_5/locale.yaml | 43 - avm/_pv_1_6_5/plugin.yaml | 909 ---- avm/_pv_1_6_5/requirements.txt | 1 - avm/_pv_1_6_5/sv_widgets/widget_avm.html | 105 - avm/_pv_1_6_5/user_doc.rst | 477 -- avm/_pv_1_6_5/webif/__init__.py | 137 - avm/_pv_1_6_5/webif/static/img/lamp_green.png | Bin 3757 -> 0 bytes avm/_pv_1_6_5/webif/static/img/lamp_red.png | Bin 3740 -> 0 bytes .../webif/static/img/plugin_logo.png | Bin 10661 -> 0 bytes avm/_pv_1_6_5/webif/templates/index.html | 359 -- 28 files changed, 11369 deletions(-) delete mode 100755 avm/_pv_1_5_12/README.md delete mode 100755 avm/_pv_1_5_12/__init__.py delete mode 100755 avm/_pv_1_5_12/assets/webif1.jpg delete mode 100755 avm/_pv_1_5_12/assets/webif2.jpg delete mode 100755 avm/_pv_1_5_12/locale.yaml delete mode 100755 avm/_pv_1_5_12/plugin.yaml delete mode 100755 avm/_pv_1_5_12/requirements.txt delete mode 100755 avm/_pv_1_5_12/sv_widgets/widget_avm.html delete mode 100755 avm/_pv_1_5_12/user_doc.rst delete mode 100755 avm/_pv_1_5_12/webif/__init__.py delete mode 100755 avm/_pv_1_5_12/webif/static/img/lamp_green.png delete mode 100755 avm/_pv_1_5_12/webif/static/img/lamp_red.png delete mode 100755 avm/_pv_1_5_12/webif/static/img/plugin_logo.png delete mode 100755 avm/_pv_1_5_12/webif/templates/index.html delete mode 100755 avm/_pv_1_6_5/README.md delete mode 100755 avm/_pv_1_6_5/__init__.py delete mode 100755 avm/_pv_1_6_5/assets/webif1.jpg delete mode 100755 avm/_pv_1_6_5/assets/webif2.jpg delete mode 100755 avm/_pv_1_6_5/locale.yaml delete mode 100755 avm/_pv_1_6_5/plugin.yaml delete mode 100755 avm/_pv_1_6_5/requirements.txt delete mode 100755 avm/_pv_1_6_5/sv_widgets/widget_avm.html delete mode 100755 avm/_pv_1_6_5/user_doc.rst delete mode 100755 avm/_pv_1_6_5/webif/__init__.py delete mode 100755 avm/_pv_1_6_5/webif/static/img/lamp_green.png delete mode 100755 avm/_pv_1_6_5/webif/static/img/lamp_red.png delete mode 100755 avm/_pv_1_6_5/webif/static/img/plugin_logo.png delete mode 100755 avm/_pv_1_6_5/webif/templates/index.html diff --git a/avm/_pv_1_5_12/README.md b/avm/_pv_1_5_12/README.md deleted file mode 100755 index 84cdc39dc..000000000 --- a/avm/_pv_1_5_12/README.md +++ /dev/null @@ -1,819 +0,0 @@ -# AVM - -## Description -The AVM Plugin can be used to connect to a device from AVM (e.g. the Fritzbox). You can control functionality and read data from the device. Moreover you can connect to the live call monitor and monitor incoming or outgoing calls and trigger items based on these events. - -## Requirements -This plugin requires lib requests. You can install this lib with: - -``` -sudo pip3 install requests --upgrade -``` - -It is completely based on the TR-064 interface from AVM (http://avm.de/service/schnittstellen/) - -Forum thread to the plugin: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin - -Version 1.1.2 tested with a FRITZ!Box 7490 (FRITZ!OS 06.51), a FRITZ! WLAN Repeater 1750E (FRITZ!OS 06.32) and a -WLAN Repeater 300E (FRITZ!OS 06.30). It was also tested with a FRITZ!Box 7390 with FW 84.06.36 and a Fritz!Box 7390 -with v6.30 und v6.51. - -The avm_data_types listed in the example items under "devices" only work correctly with firmware <= v6.30, if the -FRITZ!Box does not handle more than 16 devices in parallel (under "Heimnetz/Netzwerk"). Otherwise some of the devices -won't work. - -The MonitoringService currently does not support multiple parallel incoming or outgoing calls. For being able to -connect to the FritzDevice's CallMonitor, you have to activate it, by typing `#96*5*` on a directly connected phone. - -The version is tested with new multi-instance functionality of SmartHomeNG. - -## Configuration - -### plugin.yaml - -```yaml -fb1: - class_name: AVM - class_path: plugins.avm - username: ... # optional - password: '...' - host: fritz.box - port: 49443 - cycle: 300 - ssl: True # use https or not - verify: False # verify ssl certificate - call_monitor: 'True' - call_monitor_incoming_filter: "... ## optional, don't set if you don't want to watch only one specific number with your call monitor" - instance: fritzbox_7490 - -fb2: - class_name: AVM - class_path: plugins.avm - username: ... # optional - password: '...' - host: '...' - port: 49443 - cycle: 300 - ssl: True # use https or not - verify: False # verify ssl certificate - call_monitor: 'True' - instance: wlan_repeater_1750 -``` - -Note: Depending on the FritzDevice a shorter cycle time can result in problems with CPU rating and, in consequence with the accessibility of the webservices on the device. -If cycle time is reduced, please carefully watch your device and your sh.log. In the development process, 120 Seconds also worked worked fine on the used devices. - -#### Attributes - * `username`: Optional login information - * `password`: Required login information - * `host`: Hostname or ip address of the FritzDevice. - * `port`: Port of the FritzDevice, typically 49433 for https or 49000 for http - * `cycle`: timeperiod between two update cycles. Default is 300 seconds. - * `ssl`: True or False => True will add "https", False "http" to the URLs in the plugin - * `verify`: True or False => Turns certificate verification on or off. Typically False - * `call_monitor`: True or False => Activates or deactivates the MonitoringService, which connects to the FritzDevice's call monitor - * `instance`: Unique identifier for each FritzDevice / each instance of the plugin - -### items.yaml - -#### avm_data_type -This attribute defines supported functions that can be set for an item. Full set see example below. -For most items, the avm_data_type can be bound to an instance via @... . Only in some points the items -are parsed as child items. In the example below there is a comment in the respective spots. - -### Example: - -```yaml -avm: - - uptime_7490: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: uptime - - uptime_1750: - type: num - visu_acl: ro - avm_data_type@wlan_repeater_1750: uptime - - serial_number_7490: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: serial_number - - serial_number_1750: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: serial_number - - firmware_7490: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: software_version - - firmware_1750: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: software_version - - hardware_version_7490: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: hardware_version - - hardware_version_1750: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: hardware_version - - myfritz: - type: bool - avm_data_type@fritzbox_7490: myfritz_status - - monitor: - - trigger1: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - trigger2: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - trigger3: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - trigger4: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - incoming: - - is_call_incoming: - type: bool - avm_data_type@fritzbox_7490: is_call_incoming - - duration: - type: num - avm_data_type@fritzbox_7490: call_duration_incoming - - last_caller: - type: str - avm_data_type@fritzbox_7490: last_caller_incoming - - last_calling_number: - type: str - avm_data_type@fritzbox_7490: last_number_incoming - - last_called_number: - type: str - avm_data_type@fritzbox_7490: last_called_number_incoming - - last_call_date: - type: str - avm_data_type@fritzbox_7490: last_call_date_incoming - - event: - type: str - avm_data_type@fritzbox_7490: call_event_incoming - - outgoing: - - is_call_outgoing: - type: bool - avm_data_type@fritzbox_7490: is_call_outgoing - - duration: - type: num - avm_data_type@fritzbox_7490: call_duration_outgoing - - last_caller: - type: str - avm_data_type@fritzbox_7490: last_caller_outgoing - - last_calling_number: - type: str - avm_data_type@fritzbox_7490: last_number_outgoing - - last_called_number: - type: str - avm_data_type@fritzbox_7490: last_called_number_outgoing - - last_call_date: - type: str - avm_data_type@fritzbox_7490: last_call_date_outgoing - - event: - type: str - avm_data_type@fritzbox_7490: call_event_outgoing - - newest: - - direction: - type: str - avm_data_type@fritzbox_7490: call_direction - cache: 'yes' - - event: - type: str - avm_data_type@fritzbox_7490: call_event - cache: 'yes' - - tam: - index@fritzbox_7490: 1 - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: tam - - name: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: tam_name - - message_number_old: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: tam_old_message_number - eval: (sh.avm.tam.message_number_total()-sh.avm.tam.message_number_new()) - eval_trigger: - - avm.tam.message_number_total - - avm.tam.message_number_new - - message_number_new: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: tam_new_message_number - - message_number_total: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: tam_total_message_number - - wan: - - connection_status: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wan_connection_status - - connection_error: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wan_connection_error - - is_connected: - type: bool - visu_acl: ro - avm_data_type@fritzbox_7490: wan_is_connected - - uptime: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_uptime - - ip: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wan_ip - - upstream: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_upstream - - downstream: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_downstream - - total_packets_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_packets_sent - - total_packets_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_packets_received - - current_packets_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_packets_sent - - current_packets_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_packets_received - - total_bytes_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_bytes_sent - - total_bytes_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_bytes_received - - current_bytes_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_bytes_sent - - current_bytes_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_bytes_received - - link: - type: bool - visu_acl: ro - avm_data_type@fritzbox_7490: wan_link - - wlan: - - gf_wlan_1: - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: wlanconfig # 2,4ghz - avm_wlan_index@fritzbox_7490: 1 - - gf_wlan_1_ssid: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wlanconfig_ssid # 2,4ghz - avm_wlan_index@fritzbox_7490: 1 - - gf_wlan_2: - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: wlanconfig # 5 GHz - avm_wlan_index@fritzbox_7490: 2 - - gf_wlan_3: - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: wlanconfig # Guest - avm_wlan_index@fritzbox_7490: 3 - - gf_wlan_3_ssid: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wlanconfig_ssid # 2,4ghz - avm_wlan_index@fritzbox_7490: 3 - - gf_wlan_3_tr: - type: num - visu_acl: rw - avm_data_type@fritzbox_7490: wlan_guest_time_remaining # Guest - avm_wlan_index@fritzbox_7490: 3 - - uf_wlan_1: - type: bool - visu_acl: rw - avm_data_type@wlan_repeater_1750: wlanconfig # 2,4ghz - avm_wlan_index@wlan_repeater_1750: 1 - - uf_wlan_1_ssid: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: wlanconfig_ssid # 2,4ghz - avm_wlan_index@wlan_repeater_1750: 1 - - uf_wlan_2: - type: bool - visu_acl: rw - avm_data_type@wlan_repeater_1750: wlanconfig # 5 GHz - avm_wlan_index@wlan_repeater_1750: 2 - - uf_wlan_3: - type: bool - visu_acl: rw - avm_data_type@wlan_repeater_1750: wlanconfig # Guest - avm_wlan_index@wlan_repeater_1750: 3 - - devices: - - wlan_repeater_1750: - - GalaxyS5: - avm_mac@wlan_repeater_1750: xx:xx:xx:xx:xx:xx - avm_data_type@wlan_repeater_1750: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@wlan_repeater_1750: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@wlan_repeater_1750: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@wlan_repeater_1750: device_hostname - visu_acl: ro - - iPhone: - avm_mac@wlan_repeater_1750: xx:xx:xx:xx:xx:xx - avm_data_type@wlan_repeater_1750: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@wlan_repeater_1750: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@wlan_repeater_1750: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@wlan_repeater_1750: device_hostname - visu_acl: ro - - fritzbox_7490: - - iPad: - avm_mac@fritzbox_7490: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - hauptrechner: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - GalaxyS5: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - GalaxyTabS2: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - iPhone: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - dect: - - socket_living: - type: bool - avm_data_type@fritzbox_7490: aha_device - ain@fritzbox_7490: 14324 0432601 # has to be identical to id in fritzbox (also with spaces!) - visu_acl: rw - - # these items need to be child items from aha_device - energy: - avm_data_type@fritzbox_7490: energy - type: num - visu_acl: ro - - # these items need to be child items from aha_device - power: - avm_data_type@fritzbox_7490: power - type: num - sqlite: 'yes' - enforce_updates: 'true' - visu_acl: ro - eval: value / 100 - - # these items need to be child items from aha_device - temperature: - avm_data_type@fritzbox_7490: temperature - type: num - visu_acl: ro - - socket_office: - type: bool - avm_data_type@fritzbox_7490: aha_device - ain@fritzbox_7490: 03456 0221393 # has to be identical to id in fritzbox (also with spaces!) - visu_acl: rw - - # these items need to be child items from aha_device - energy: - avm_data_type@fritzbox_7490: energy - type: num - visu_acl: ro - - # these items need to be child items from aha_device - power: - avm_data_type@fritzbox_7490: power - type: num - sqlite: 'yes' - enforce_updates: 'true' - visu_acl: ro - eval: value / 100 - - # these items need to be child items from aha_device - temperature: - avm_data_type@fritzbox_7490: temperature - type: num - visu_acl: ro - - hkr_bathroom: - # Current hkr state: 0 = closed, 1: open, 2: temperature controlled, 3: error - type: num - value: 3 - avm_data_type@fritzbox_7490: hkr_device - ain@fritzbox_7490: 09995 0191234 # has to be identical to id in fritzbox (also with spaces!) - visu_acl: ro - - # these items need to be child items from hkr_device. They are read only items. - is_temperature: - value: -1 - avm_data_type@fritzbox_7490: temperature - type: num - visu_acl: ro - - set_temperature_reduced: - value: -1 - avm_data_type@fritzbox_7490: set_temperature_reduced - type: num - visu_acl: ro - - set_temperature_comfort: - value: -1 - avm_data_type@fritzbox_7490: set_temperature_comfort - type: num - visu_acl: ro - - # these items are also mandatory and used to read and write the setpoint temperature - set_temperature: - value: -1 - avm_data_type@fritzbox_7490: set_temperature - type: num - visu_acl: rw - - set_hkrwindowopen: - value: False - avm_data_type@fritzbox_7490: set_hkrwindowopen - type: bool - visu_acl: rw - enforce_updates: true - -``` - -## Functions - -### get_phone_name -Get the phone name at a specific index. The returend value can be used as phone_name for set_call_origin. Parameter is an INT, starting from 1. In case an index does not exist, an error is logged. -The used function X_AVM-DE_GetPhonePort() does not deliver analog connections like FON 1 and FON 2 (BUG in AVM Software). - -```python -phone_name = sh.fb1.get_phone_name(1) -``` - -CURL for this function: - -```bash -curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/x_voip" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:X_VoIP:1#X_AVM-DE_GetPhonePort" -d "1" -s -k -``` - -###get_call_origin - -Gets the phone name, currently set as call_origin. - -```python -phone_name = sh.fritzbox_7490.get_call_origin() -``` - -CURL for this function: -```bash -curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/x_voip" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:X_VoIP:1#X_AVM-DE_DialGetConfig" -d "" -s -k -``` - -### set_call_origin(phone_name) - -Sets the origin of a call. E.g. a DECT phone. Typically set before using "start_call". -You can also set the origin on your FritzDevice via "Telefonie -> Anrufe -> Wählhilfe verwenden -> Verbindung mit dem Telefon". -The used function X_AVM-DE_SetDialConfig() does not allow the configuration of analog connections (BUG in AVM Software). - -```python -sh.fb1.set_call_origin("") -``` - -### start_call(phone_number) -This function starts a call. Parameter can be an external or internal number. - -```python -sh.fb1.start_call('0891234567') -sh.fb1.start_call('**9') -``` - -### cancel_call() -This function cancels a running call. - -### wol(mac_address) -This function executes a wake on lan command to the specified MAC address - -### reconnect() -This function reconnects the WAN (=internet) connection. - -### reboot() -This function reboots the FritzDevice. - -### get_hosts(only_active) -Gets the data of get_host_details for all hosts as array. If only_active is True, only active hosts are returned. - -Example of a logic which is merging hosts of three devices into one list and rendering them to an HTML list, which is written to the item -'avm.devices.device_list' - -```python -hosts = sh.fritzbox_7490.get_hosts(True) -hosts_300 = sh.wlan_repeater_300.get_hosts(True) -hosts_1750 = sh.wlan_repeater_1750.get_hosts(True) - -for host_300 in hosts_300: - new = True - for host in hosts: - if host_300['mac_address'] == host['mac_address']: - new = False - if new: - hosts.append(host_300) -for host_1750 in hosts_1750: - new = True - for host in hosts: - if host_1750['mac_address'] == host['mac_address']: - new = False - if new: - hosts.append(host_1750) - -string = '
    ' -for host in hosts: - device_string = '
  • '+host['name']+': '+host['ip_address']+', '+host['mac_address']+'
  • ' - string += device_string - -string += '
' -sh.avm.devices.device_list(string) -``` - -### get_host_details(index) -Gets the data of a host as dict: -dict keys: name, interface_type, ip_address, mac_address, is_active, lease_time_remaining - -### is_host_active(mac_address) -This function checks, if a device running on a given mac address is active on the FritzDevice. Can be used for presence detection. - -CURL for this function: -```bash -curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/hosts" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:Hosts:1#GetSpecificHostEntry" -d "XX:XX:XX:XX:XX:XX" -s -k -``` - -### get_contact_name_by_phone_number(phone_number) -This is a function to search for telephone numbers in the contacts stored on the devices phone book - -### get_phone_numbers_by_name(name) - -This is a function to search for contact names and retrieve the related telephone numbers - -Set an item with a html of all found numbers e.g. by: - -```python -result_numbers = sh.fritzbox_7490.get_phone_numbers_by_name('Mustermann') -result_string = '' -keys = {'work': 'Geschäftlich', 'home': 'Privat', 'mobile': 'Mobil', 'fax_work': 'Fax', 'intern': 'Intern'} -for contact in result_numbers: - result_string += '

'+contact+'

' - i = 0 - result_string += '' - while i < len(result_numbers[contact]): - number = result_numbers[contact][i]['number'] - type_number = keys[result_numbers[contact][i]['type']] - result_string += '' - i += 1 - result_string += '
' + type_number + ':' + number + '

' -sh.general_items.number_search_results(result_string) -``` - -### get_calllist() -Returns an array with calllist entries diff --git a/avm/_pv_1_5_12/__init__.py b/avm/_pv_1_5_12/__init__.py deleted file mode 100755 index a3b1d54e1..000000000 --- a/avm/_pv_1_5_12/__init__.py +++ /dev/null @@ -1,2636 +0,0 @@ -#!/usr/bin/env python3 -# -######################################################################### -# Copyright 2016 René Frieß rene.friess(a)gmail.com -######################################################################### -# -# This file is part of SmartHomeNG. -# -# SmartHomeNG is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SmartHomeNG is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SmartHomeNG. If not, see . -# -######################################################################### - -import datetime -import logging -import socket -import time -import threading -import requests -from time import mktime -from xml.dom import minidom -from requests.packages import urllib3 -from requests.auth import HTTPDigestAuth -from lib.model.smartplugin import * -from lib.module import Modules -from json.decoder import JSONDecodeError -from datetime import datetime -from .webif import WebInterface -import cherrypy - -# for session id generation: -import hashlib - - -class MonitoringService: - """ - Class which connects to the FritzBox service of the Callmonitor: http://www.wehavemorefun.de/fritzbox/Callmonitor - - | Can currently handle three items: - | - avm_data_type = is_call_incoming, type = bool - | - avm_data_type = last_caller, type = str - | - avm_data_type = last_call_date, type = str - """ - - def __init__(self, host, port, callback, call_monitor_incoming_filter, plugin_instance): - self._plugin_instance = plugin_instance - self._plugin_instance.logger.debug("starting monitoring service") - self._host = host - self._port = port - self._callback = callback - self._trigger_items = [] # items which can be used to trigger sth, e.g. a logic - self._items = [] # more general items for the call monitor - self._items_incoming = [] # items for incoming calls - self._items_outgoing = [] # items for outgoing calls - self._duration_item = dict() # 2 items, on for counting the incoming, one for counting the outgoing call duration - self._duration_item['call_duration_outgoing'] = None - self._duration_item['call_duration_incoming'] = None - self._call_active = dict() - self._listen_active = False - self._call_active['incoming'] = False - self._call_active['outgoing'] = False - self._call_incoming_cid = dict() - self._call_outgoing_cid = dict() - self._call_monitor_incoming_filter = call_monitor_incoming_filter - self.conn = None - - def connect(self): - """ - Connects to the call monitor of the AVM device - """ - if self._listen_active: - self._plugin_instance.logger.debug("MonitoringService: Connect called while listen active") - return - - self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.conn.settimeout(None) - try: - self.conn.connect((self._host, self._port)) - _name = 'plugins.' + self._plugin_instance.get_fullname() + '.Monitoring_Service' - self._listen_thread = threading.Thread(target=self._listen, name=_name) - self._listen_thread.start() - self._plugin_instance.logger.debug("MonitoringService: connection established") - except Exception as e: - self.conn = None - self._plugin_instance.logger.error( - "MonitoringService: Cannot connect to " + self._host + " on port: " + str( - self._port) + ", CallMonitor activated by #96*5*? - Error: " + str(e)) - return - - def disconnect(self): - """ - Disconnects from the call monitor of the AVM device - """ - self._plugin_instance.logger.debug("MonitoringService: disconnecting") - self._listen_active = False - self._stop_counter('incoming') - self._stop_counter('outgoing') - # Close the socket to stop the listen thread - try: - self.conn.shutdown(2) - self.conn.close() - except: - pass - try: - self._listen_thread.join(1) - except: - pass - - def reconnect(self): - """ - Reconnects to the call monitor of the AVM device - """ - self.disconnect() - self.connect() - - def register_item(self, item): - """ - Registers an item to the MonitoringService - - :param item: item to register - """ - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['is_call_incoming', - 'last_caller_incoming', - 'last_number_incoming', - 'last_called_number_incoming', - 'last_call_date_incoming', - 'call_event_incoming']: - self._items_incoming.append(item) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['is_call_outgoing', - 'last_caller_outgoing', - 'last_number_outgoing', - 'last_called_number_outgoing', - 'last_call_date_outgoing', - 'call_event_outgoing']: - self._items_outgoing.append(item) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'monitor_trigger': - self._trigger_items.append(item) - else: - self._items.append(item) - - def get_items(self): - return self._items - - def get_trigger_items(self): - return self._trigger_items - - def get_items_incoming(self): - return self._items_incoming - - def get_items_outgoing(self): - return self._items_outgoing - - def get_item_count_total(self): - """ - Returns number of added items (all items of MonitoringService service - - :return: number of items hold by the MonitoringService - """ - return len(self._items) + len(self._trigger_items) + len(self._items_incoming) + len(self._items_outgoing) - - def set_duration_item(self, item): - """ - Sets specific items which count the duration of an incoming or outgoing call - """ - self._duration_item[self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type').split("@")[0]] = item - - def _listen(self, recv_buffer=4096): - """ - Function which listens to the established connection. - """ - self._listen_active = True - buffer = "" - while self._listen_active: - try: - data = self.conn.recv(recv_buffer) - except Exception as e: - self._plugin_instance.logger.error("CallMonitor connection receive error: " + str(e)) - break - if not data: - if self._listen_active: - self._plugin_instance.logger.error("CallMonitor connection not open anymore.") - break - data = data.decode("utf-8") - self._plugin_instance.logger.debug("Data Received from CallMonitor: %s" % data) - buffer += data - while buffer.find("\n") != -1: - line, buffer = buffer.split("\n", 1) - self._parse_line(line) - self._listen_active = False - try: - self.conn.shutdown(2) - self.conn.close() - except: - pass - return - - def _start_counter(self, timestamp, direction): - if direction == 'incoming': - self._call_connect_timestamp = time.mktime( - datetime.strptime(timestamp, "%d.%m.%y %H:%M:%S").timetuple()) - self._duration_counter_thread_incoming = threading.Thread(target=self._count_duration_incoming, - name="MonitoringService_Duration_Incoming_%s" % self._plugin_instance.get_instance_name()).start() - self._plugin_instance.logger.debug('Counter incoming - STARTED') - elif direction == 'outgoing': - self._call_connect_timestamp = time.mktime( - datetime.strptime(timestamp, "%d.%m.%y %H:%M:%S").timetuple()) - self._duration_counter_thread_outgoing = threading.Thread(target=self._count_duration_outgoing, - name="MonitoringService_Duration_Outgoing_%s" % self._plugin_instance.get_instance_name()).start() - self._plugin_instance.logger.debug('Counter outgoing - STARTED') - - def _stop_counter(self, direction): - # only stop of thread is active - - if self._call_active[direction]: - self._call_active[direction] = False - self._plugin_instance.logger.debug('STOPPING ' + direction) - try: - if direction == 'incoming': - self._duration_counter_thread_incoming.join(1) - elif direction == 'outgoing': - self._duration_counter_thread_outgoing.join(1) - except: - pass - - def _count_duration_incoming(self): - self._call_active['incoming'] = True - while self._call_active['incoming']: - if not self._duration_item['call_duration_incoming'] is None: - duration = time.time() - self._call_connect_timestamp - self._duration_item['call_duration_incoming'](int(duration)) - time.sleep(1) - - def _count_duration_outgoing(self): - self._call_active['outgoing'] = True - while self._call_active['outgoing']: - if not self._duration_item['call_duration_outgoing'] is None: - duration = time.time() - self._call_connect_timestamp - self._duration_item['call_duration_outgoing'](int(duration)) - time.sleep(1) - - def _parse_line(self, line): - """ - Parses a data set in the form of a line. - - Data Format: - Ausgehende Anrufe: datum;CALL;ConnectionID;Nebenstelle;GenutzteNummer;AngerufeneNummer;SIP+Nummer - Eingehende Anrufe: datum;RING;ConnectionID;Anrufer-Nr;Angerufene-Nummer;SIP+Nummer - Zustandegekommene Verbindung: datum;CONNECT;ConnectionID;Nebenstelle;Nummer; - Ende der Verbindung: datum;DISCONNECT;ConnectionID;dauerInSekunden; - - :param line: data line which is parsed - """ - self._plugin_instance.logger.debug(line) - line = line.split(";") - - try: - if line[1] == "RING": - call_from = line[3] - call_to = line[4] - self._trigger(call_from, call_to, line[0], line[2], line[1], '') - elif line[1] == "CALL": - call_from = line[4] - call_to = line[5] - self._trigger(call_from, call_to, line[0], line[2], line[1], line[3]) - elif line[1] == "CONNECT": - self._trigger('', '', line[0], line[2], line[1], line[3]) - elif line[1] == "DISCONNECT": - self._trigger('', '', '', line[2], line[1], '') - except Exception as e: - self._plugin_instance.logger.error( - "MonitoringService: " + type(e).__name__ + " while handling Callmonitor response: " + str(e)) - return - - def _trigger(self, call_from, call_to, time, callid, event, branch): - """ - Triggers the event: sets item values and looks up numbers in the phone book. - """ - self._plugin_instance.logger.debug( - "Event: %s, Call From: %s, Call To: %s, Time: %s, CallID: %s" % (event, call_from, call_to, time, callid)) - # in each case set current call event and direction - for item in self._items: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_event': - item(event.lower(), self._plugin_instance.get_shortname()) - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_direction': - if event == 'RING': - item("incoming", self._plugin_instance.get_shortname()) - else: - item("outgoing", self._plugin_instance.get_shortname()) - - # call is incoming - if event == 'RING': - # process "trigger items" - for trigger_item in self._trigger_items: - if self._plugin_instance.get_iattr_value(trigger_item.conf, 'avm_data_type') == 'monitor_trigger': - trigger_item(0, self._plugin_instance.get_shortname()) - self._plugin_instance.logger.debug( - self._plugin_instance.get_iattr_value(trigger_item.conf, 'avm_data_type') + " " + - trigger_item.conf['avm_incoming_allowed'] + " " + trigger_item.conf[ - 'avm_target_number']) - if 'avm_incoming_allowed' not in trigger_item.conf or 'avm_target_number' not in trigger_item.conf: - self._plugin_instance.logger.error( - "both 'avm_incoming_allowed' and 'avm_target_number' must be specified as attributes in a trigger item.") - elif trigger_item.conf['avm_incoming_allowed'] == call_from and trigger_item.conf[ - 'avm_target_number'] == call_to: - trigger_item(1, self._plugin_instance.get_shortname()) - - if self._call_monitor_incoming_filter in call_to: - # set call id for incoming call - self._call_incoming_cid = callid - - # reset duration for incoming calls - if not self._duration_item['call_duration_incoming'] is None: - self._duration_item['call_duration_incoming'](0, self._plugin_instance.get_shortname()) - - # process items specific to incoming calls - for item in self._items_incoming: # update items for incoming calls - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['is_call_incoming']: - self._plugin_instance.logger.debug("Setting is_call_incoming: %s" % True) - item(True, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_caller_incoming']: - if call_from != '' and call_from is not None: - name = self._callback(call_from) - if name != '' and not name is None: - item(name, self._plugin_instance.get_shortname()) - else: - item(call_from, self._plugin_instance.get_shortname()) - else: - item("Unbekannt", self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in [ - 'last_call_date_incoming']: - self._plugin_instance.logger.debug("Setting last_call_date_incoming: %s" % time) - item(time, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_incoming']: - self._plugin_instance.logger.debug("Setting call_event_incoming: %s" % event.lower()) - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_number_incoming']: - self._plugin_instance.logger.debug("Setting last_number_incoming: %s" % call_from) - item(call_from, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in [ - 'last_called_number_incoming']: - self._plugin_instance.logger.debug("Setting last_called_number_incoming: %s" % call_to) - item(call_to, self._plugin_instance.get_shortname()) - - # call is outgoing - elif event == 'CALL': - # set call id for outgoing call - self._call_outgoing_cid = callid - - # reset duration for outgoing calls - self._duration_item['call_duration_outgoing'](0) - - # process items specific to outgoing calls - for item in self._items_outgoing: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['is_call_outgoing']: - item(True, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_caller_outgoing']: - name = self._callback(call_to) - if name != '' and not name is None: - item(name, self._plugin_instance.get_shortname()) - else: - item(call_to, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_call_date_outgoing']: - item(time, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_outgoing']: - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_number_outgoing']: - item(call_from, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in [ - 'last_called_number_outgoing']: - item(call_to, self._plugin_instance.get_shortname()) - - # connection established - elif event == 'CONNECT': - # handle OUTGOING calls - if callid == self._call_outgoing_cid: - if not self._duration_item[ - 'call_duration_outgoing'] is None: # start counter thread only if duration item set and call is outgoing - self._stop_counter('outgoing') # stop potential running counter for parallel (older) outgoing call - self._start_counter(time, 'outgoing') - for item in self._items_outgoing: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_outgoing']: - item(event.lower(), self._plugin_instance.get_shortname()) - - # handle INCOMING calls - elif callid == self._call_incoming_cid: - if not self._duration_item[ - 'call_duration_incoming'] is None: # start counter thread only if duration item set and call is incoming - self._stop_counter('incoming') # stop potential running counter for parallel (older) incoming call - self._plugin_instance.logger.debug("Starting Counter for Call Time") - self._start_counter(time, 'incoming') - for item in self._items_incoming: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_incoming']: - self._plugin_instance.logger.debug("Setting call_event_incoming: %s" % event.lower()) - item(event.lower(), self._plugin_instance.get_shortname()) - - # connection ended - elif event == 'DISCONNECT': - # handle OUTGOING calls - if callid == self._call_outgoing_cid: - for item in self._items_outgoing: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_event_outgoing': - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'is_call_outgoing': - item(False, self._plugin_instance.get_shortname()) - if not self._duration_item['call_duration_outgoing'] is None: # stop counter threads - self._stop_counter('outgoing') - self._call_outgoing_cid = None - - # handle INCOMING calls - elif callid == self._call_incoming_cid: - for item in self._items_incoming: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_event_incoming': - self._plugin_instance.logger.debug("Setting call_event_incoming: %s" % event.lower()) - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'is_call_incoming': - self._plugin_instance.logger.debug("Setting is_call_incoming: %s" % False) - item(False, self._plugin_instance.get_shortname()) - if not self._duration_item['call_duration_incoming'] is None: # stop counter threads - self._plugin_instance.logger.debug("Stopping Counter for Call Time") - self._stop_counter('incoming') - self._call_incoming_cid = None - - -class FritzDevice: - """ - This class encapsulates information related to a specific FritzDevice, such has host, port, ssl, username, password, or related items - """ - - def __init__(self, host, port, ssl, username, password, identifier='default'): - self.logger = logging.getLogger(__name__) - self._host = host - self._port = port - self._ssl = ssl - self._username = username - self._password = password - self._identifier = identifier - self._available = True - self._items = [] - - def get_identifier(self): - """ - Returns the internal identifier of the FritzDevice - - :return: identifier of the device, as set in plugin.conf - """ - return self._identifier - - def get_host(self): - """ - Returns the hostname / IP of the FritzDevice - - :return: hostname of the device, as set in plugin.conf - """ - return self._host - - def get_port(self): - """ - Returns the port of the FritzDevice - - :return: port of the device, as set in plugin.conf - """ - return self._port - - def get_items(self): - """ - Returns added items - - :return: array of items hold by the device - """ - return self._items - - def get_item_count(self): - """ - Returns number of added items - - :return: number of items hold by the device - """ - return len(self._items) - - def is_ssl(self): - """ - Returns information if SSL is enabled - - :return: is ssl enabled, as set in plugin.conf - """ - return self._ssl - - def is_available(self): - """ - Returns information if the device is currently available - - :return: boolean, if device is available - """ - return self._available - - def set_available(self, is_available): - """ - Sets the boolean, if the device is available - - :param is_available: boolean of the availability status - """ - self._available = is_available - - def get_user(self): - """ - Returns the user for the FritzDevice - - :return: user, as set in plugin.conf - """ - return self._username - - def get_password(self): - """ - Returns the password for the FritzDevice - - :return: password, as set in plugin.conf - """ - return self._password - - -class AVM(SmartPlugin): - """ - Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the different TR-064 services on the FritzDevice - """ - - PLUGIN_VERSION = "1.5.12" - - _header = {'SOAPACTION': '', 'CONTENT-TYPE': 'text/xml; charset="utf-8"'} - _envelope = """ - - %s - - """ - _body = """ - - %(arguments)s - - - """ - _argument = """ - %(value)s""" - - _urn_map = dict([('WLANConfiguration', 'urn:dslforum-org:service:WLANConfiguration:%s'), - # index needs to be adjusted from 1 to 3 - ('WANCommonInterfaceConfig', 'urn:dslforum-org:service:WANCommonInterfaceConfig:1'), - ('WANCommonInterfaceConfig_alt', 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1'), - ('WANIPConnection', 'urn:schemas-upnp-org:service:WANIPConnection:1'), - ('TAM', 'urn:dslforum-org:service:X_AVM-DE_TAM:1'), - ('OnTel', 'urn:dslforum-org:service:X_AVM-DE_OnTel:1'), - ('Homeauto', 'urn:dslforum-org:service:X_AVM-DE_Homeauto:1'), - ('Hosts', 'urn:dslforum-org:service:Hosts:1'), - ('X_VoIP', 'urn:dslforum-org:service:X_VoIP:1'), - ('DeviceConfig', 'urn:dslforum-org:service:DeviceConfig:1'), - ('DeviceInfo', 'urn:dslforum-org:service:DeviceInfo:1'), - ('WANDSLInterfaceConfig', 'urn:dslforum-org:service:WANDSLInterfaceConfig:1'), - ('MyFritz', 'urn:dslforum-org:service:X_AVM-DE_MyFritz:1')]) - - def __init__(self, sh, *args, **kwargs): - """ - Initalizes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf. - """ - self.logger.info('Init AVM Plugin') - - self._session = requests.Session() - self._lua_session = requests.Session() - self._timeout = 10 - - self._verify = self.get_parameter_value('verify') - ssl = self.get_parameter_value('ssl') - - if ssl and not self._verify: - urllib3.disable_warnings() - - self._fritz_device = FritzDevice(self.get_parameter_value('host'), self.get_parameter_value('port'), ssl, - self.get_parameter_value('username'), self.get_parameter_value('password'), - self.get_instance_name()) - - self._call_monitor = self.to_bool(self.get_parameter_value('call_monitor')) - if self._call_monitor: - self._monitoring_service = MonitoringService(self._fritz_device.get_host(), 1012, - self.get_contact_name_by_phone_number, - self.get_parameter_value('call_monitor_incoming_filter'), self) - self._monitoring_service.connect() - else: - self._monitoring_service = None - - self._call_monitor_incoming_filter = self.get_parameter_value('call_monitor_incoming_filter') - - self._cycle = int(self.get_parameter_value('cycle')) - self._sh = sh - # Response Cache: Dictionary for storing the result of requests which is used for several different items, refreshed each update cycle. Please use distinct keys! - self._response_cache = dict() - self._calllist_cache = [] - self.logger.debug("Plugin initialized with host: %s, port: %s, ssl: %s, verify: %s, user: %s, call_monitor: %s" - % (self._fritz_device.get_host(), self._fritz_device.get_port(), self._fritz_device.is_ssl(), - self._verify, self._fritz_device.get_user(), self._call_monitor)) - if not self.init_webinterface(WebInterface): - self._init_complete = False - - def run(self): - """ - Run method for the plugin - """ - self.scheduler_add('update', self._update_loop, prio=5, cycle=self._cycle, offset=2) - self.alive = True - - def stop(self): - """ - Stop method for the plugin - """ - if self._call_monitor: - self._monitoring_service.disconnect() - self.scheduler_remove('update') - self.alive = False - - def _assemble_soap_data(self, action, service, argument=''): - """ - Builds the soap data set (from body and envelope templates for a given request. - - :param action: string of the action - :param service: string of the service - :param argument: dictionary (name : value) of arguments - :return: string of the soap data - """ - argument_string = '' - if argument: - arguments = [ - self._argument % {'name': name, 'value': value} - for name, value in argument.items() - ] - argument_string = argument_string.join(arguments) - body = self._body.strip() % {'action': action, 'service': service, 'arguments': argument_string} - soap_data = self._envelope.strip() % body - return soap_data - - def _build_url(self, suffix, lua=False): - """ - Builds a request url - - :param suffix: url suffix, e.g. "/upnp/control/x_tam" - :return: string of the url, dependent on settings of the FritzDevice - """ - if self._fritz_device.is_ssl(): - url_prefix = "https" - else: - url_prefix = "http" - if not lua: - url = "%s://%s:%s%s" % (url_prefix, self._fritz_device.get_host(), self._fritz_device.get_port(), suffix) - else: - url = "%s://%s%s" % (url_prefix, self._fritz_device.get_host(), suffix) - - return url - - def _update_loop(self): - """ - Starts the update loop for all known items. - """ - self.logger.debug('Starting update loop for instance %s' % self._fritz_device.get_identifier()) - for item in self._fritz_device.get_items(): - if not self.alive: - return - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_connection_status', 'wan_connection_error', - 'wan_is_connected', 'wan_uptime', 'wan_ip']: - self._update_wan_ip_connection(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['tam', 'tam_name', 'tam_new_message_number', - 'tam_total_message_number']: - self._update_tam(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device': - self._update_home_automation(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'hkr_device': - self._update_home_automation(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['wlanconfig', 'wlanconfig_ssid', - 'wlan_guest_time_remaining']: - self._update_wlan_config(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_total_packets_sent', - 'wan_total_packets_received', - 'wan_current_packets_sent', - 'wan_current_packets_received', - 'wan_total_bytes_sent', - 'wan_total_bytes_received', - 'wan_current_bytes_sent', - 'wan_current_bytes_received', - 'wan_link']: - self._update_wan_common_interface_configuration(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['network_device']: - self._update_host(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['uptime', 'software_version', 'hardware_version', - 'serial_number']: - self._update_fritz_device_info(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_upstream', 'wan_downstream']: - self._update_wan_dsl_interface_config(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'myfritz_status': - self._update_myfritz(item) - # empty response cache - self._response_cache = dict() - - if self._call_monitor: - if not self.alive: - return - if self._fritz_device.is_available(): - self._monitoring_service.connect() - - def get_fritz_device(self): - return self._fritz_device - - def get_monitoring_service(self): - return self._monitoring_service - - def set_device_availability(self, availability): - self._fritz_device.set_available(availability) - self.logger.debug('Availability for FritzDevice set to %s' % availability) - if not availability and self._call_monitor: - self._monitoring_service.disconnect() - elif availability and self._call_monitor and self.alive: - self._monitoring_service.connect() - - def get_calllist_from_cache(self): - """ - returns the cached calllist when all items are initialized. The filter set by plugin.conf is applied. - - :return: Array of calllist entries - """ - # request and cache calllist - if self._calllist_cache is None: - self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) - elif len(self._calllist_cache) == 0: - self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) - return self._calllist_cache - - def parse_item(self, item): - """ - Default plugin parse_item method. Is called when the plugin is initialized. Selects each item corresponding to - the AVM identifier and adds it to an internal array - - :param item: The item to process. - """ - # items specific to call monitor - if self.get_iattr_value(item.conf, 'avm_data_type') in ['is_call_incoming', 'last_caller_incoming', - 'last_call_date_incoming', - 'call_event_incoming', 'last_number_incoming', - 'last_called_number_incoming', - 'is_call_outgoing', 'last_caller_outgoing', - 'last_call_date_outgoing', - 'call_event_outgoing', 'last_number_outgoing', - 'last_called_number_outgoing', - 'call_event', 'call_direction', 'monitor_trigger']: - # initially - if item empty - get data from calllist - if self.get_iattr_value(item.conf, 'avm_data_type') == 'last_caller_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - if 'Name' in element: - item(element['Name'], self.get_shortname()) - else: - item(element['Caller'], self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'last_number_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - if 'Caller' in element: - item(element['Caller'], self.get_shortname()) - else: - item("", self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'last_called_number_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - item(element['CalledNumber'], self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'last_call_date_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - date = str(element['Date']) - date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'call_event_incoming' and item() == '': - item('disconnect', self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'is_call_incoming' and item() == '': - item(0, self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'last_caller_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - if 'Name' in element: - item(element['Name'], self.get_shortname()) - else: - item(element['Called'], self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'last_number_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - if 'Caller' in element: - item(''.join(filter(lambda x: x.isdigit(), element['Caller'])), self.get_shortname()) - else: - item("", self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'last_called_number_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - item(element['Called'], self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'last_call_date_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - date = str(element['Date']) - date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'call_event_outgoing' and item() == '': - item('disconnect', self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'is_call_outgoing' and item() == '': - item(0, self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'call_event' and item() == '': - item('disconnect', self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'call_direction' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - item('incoming', self.get_shortname()) - break - if element['Type'] in ['3', '4']: - item('outgoing', self.get_shortname()) - break - if self._call_monitor: - if self._monitoring_service is not None: - self._monitoring_service.register_item(item) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['call_duration_incoming', 'call_duration_outgoing']: - # items specific to call monitor duration calculation - # initially get data from calllist - if self.get_iattr_value(item.conf, 'avm_data_type') == 'call_duration_incoming' and item() == 0: - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - duration = element['Duration'] - duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self.get_shortname()) - break - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'call_duration_outgoing' and item() == 0: - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - duration = element['Duration'] - duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self.get_shortname()) - break - if not self._monitoring_service is None: - self._monitoring_service.set_duration_item(item) - elif self.has_iattr(item.conf, 'avm_data_type'): - # normal items - self._fritz_device._items.append(item) - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wlanconfig', 'tam', 'aha_device', 'set_temperature', - 'set_hkrwindowopen']: - # special items which can be changed outside the plugin context and need to be submitted to the FritzDevice - return self.update_item - - def _get_hash_response(self, challenge, pwd): - my_md5_hash_string = (challenge + '-' + pwd).encode('utf-16LE') - m = hashlib.md5() - m.update(my_md5_hash_string) - return challenge + "-" + m.hexdigest() - - def _request_session_id(self): - user = self._fritz_device.get_user() - pwd = self._fritz_device.get_password() - - response = self._lua_session.get(self._build_url('/login_sid.lua', lua=True), verify=self._verify) - my_xml = response.text - self.logger.debug("Session request response text: {0}".format(my_xml)) - xml = minidom.parseString(my_xml) - challenge_xml = xml.getElementsByTagName('Challenge') - sid_xml = xml.getElementsByTagName('SID') - if len(challenge_xml) > 0: - my_sid = sid_xml[0].firstChild.data - if len(challenge_xml) > 0: - my_challenge = challenge_xml[0].firstChild.data - - self.logger.info("Debug apriori SID: {0}, Challenge: {1}".format(my_sid, my_challenge)) - hash_response = self._get_hash_response(my_challenge, pwd) - - # Doublecheck: Shall we send this request via self._session.get instead? - response = self._lua_session.get(self._build_url('/login_sid.lua?username=%s&response=%s' % (user, - hash_response), lua=True), - verify=self._verify) - myXML = response.text - xml = minidom.parseString(myXML) - challenge_xml = xml.getElementsByTagName('Challenge') - sid_xml = xml.getElementsByTagName('SID') - if len(challenge_xml) > 0: - mySID = sid_xml[0].firstChild.data - if len(challenge_xml) > 0: - myChallenge = challenge_xml[0].firstChild.data - - self.logger.info("Debug posterior SID: {0}, Challenge: {1}".format(mySID, myChallenge)) - return mySID - - def _assemble_aha_interface(self, ain='', aha_action='', aha_param='', sid='', endtimestamp=''): - """ - Builds the AVM home automation (AHA) http interface command string - https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf - https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID.pdf - - :param action: string of the action - :param param: optional parameter - :param sid: session ID - :return: string of aha data - """ - # Example request: - # https://fritz.box/webservices/homeautoswitch.lua?ain=099950196524&switchcmd=sethkrtsoll¶m=254&sid=9c977765016899f8 - # - # Command string with session id parameter: - if endtimestamp == '': - aha_string = "/webservices/homeautoswitch.lua?ain={0}&switchcmd={1}¶m={2}&sid={3}".format( - ain.replace(" ", ""), aha_action, aha_param, sid) - else: - aha_string = "/webservices/homeautoswitch.lua?ain={0}&switchcmd={1}&endtimestamp={2}&sid={3}".format( - ain.replace(" ", ""), aha_action, endtimestamp, sid) - - return aha_string - - def update_item(self, item, caller=None, source=None, dest=None): - """ - | Write items values - in case they were changed from somewhere else than the AVM plugin (=the FritzDevice) to - | the FritzDevice. - - | Uses: - | - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_tam.pdf - | - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf - | - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_homeauto.pdf - - :param item: item to be updated towards the FritzDevice (Supported item avm_data_types: wlanconfig, tam, aha_device) - """ - if caller.lower() != 'avm': - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wlanconfig', 'tam']: - action = 'SetEnable' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device': - action = 'SetSwitch' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'set_temperature': - action = 'sethkrtsoll' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'set_hkrwindowopen': - action = 'sethkrwindowopen' - else: - self.logger.error("%s is not defined to be updated." % self.get_iattr_value(item.conf, 'avm_data_type')) - return - - headers = self._header.copy() - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wlanconfig': - if int(self.get_iattr_value(item.conf, 'avm_wlan_index')) > 0: - headers['SOAPACTION'] = "%s#%s" % ( - self._urn_map['WLANConfiguration'] % str(self.get_iattr_value(item.conf, 'avm_wlan_index')), - action) - soap_data = self._assemble_soap_data(action, self._urn_map['WLANConfiguration'] % str( - self.get_iattr_value(item.conf, 'avm_wlan_index')), {'NewEnable': int(item())}) - else: - self.logger.error( - 'No avm_wlan_index attribute provided: %s' % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'tam': - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['TAM'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['TAM'], - {'NewIndex': 0, 'NewEnable': int(item())}) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device': - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['Homeauto'], action) - # SwitchState: OFF, ON, TOGGLE, UNDEFINED - if int(item()) == 1: - switch_state = "ON" - else: - switch_state = "OFF" - ain = self.get_iattr_value(item.conf, 'ain') - soap_data = self._assemble_soap_data(action, self._urn_map['Homeauto'], - {'NewAIN': ain.strip(), - 'NewSwitchState': switch_state}) - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wlanconfig': - param = "%s%s%s" % ( - "/upnp/control/", self.get_iattr_value(item.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_wlan_index')) - url = self._build_url(param) - - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'tam': - url = self._build_url("/upnp/control/x_tam") - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device': - url = self._build_url("/upnp/control/x_homeauto") - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'set_hkrwindowopen': - self.logger.debug("hkrwindowopen caller is: {0}".format(caller)) - cmd_enable = bool(item()) - self.logger.debug("Debug enable is: {0}".format(cmd_enable)) - parentItem = item.return_parent() - ainDevice = '0' - parent_ain = self.get_iattr_value(parentItem.conf, 'ain') - if isinstance(parent_ain, str): - ainDevice = parent_ain - else: - self.logger.error('hkrt ain is not a string value') - self.logger.info("Debug ain is {0}".format(ainDevice)) - - # request new session ID: - try: - mySID = self._request_session_id() - - # Assemble endtimestamp: - if cmd_enable == False: - endtime = 0 - else: - now = self.shtime.now() - unix_secs = mktime(now.timetuple()) - # set endtime to now + 12h: - endtime = int(unix_secs + 12 * 3600) - self.logger.debug("HKR endtimestamp is: {0}".format(endtime)) - - aha_string = self._assemble_aha_interface(ain=ainDevice, aha_action=action, endtimestamp=endtime, - sid=mySID) - self.logger.debug("Debug ahastring: {0}".format(aha_string)) - except Exception as e: - self.logger.error("Exception HKR window open: %s" % str(e)) - - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'set_temperature': - self.logger.info("Debug caller is: {0}".format(caller)) - # Check commanded temperature range: - cmd_temperature = float(item()) - self.logger.debug("Debug cmd_temp is: {0}".format(cmd_temperature)) - parentItem = item.return_parent() - ainDevice = '0' - parent_ain = self.get_iattr_value(parentItem.conf, 'ain') - if isinstance(parent_ain, str): - ainDevice = parent_ain - else: - self.logger.error('hkrt ain is not a string value') - - self.logger.info("Debug ain is {0}".format(ainDevice)) - - # Set hkrt to state off (253) if command is out of range - temp_scaled = 253 - if 28 >= cmd_temperature >= 8: - # convert commanded temperature in degree into AVM scaled command value: - temp_scaled = 2 * cmd_temperature - elif cmd_temperature > 28: - temp_scaled = 254 - elif cmd_temperature < 8: - temp_scaled = 253 - else: - self.logger.error( - "Commanded hkrt temperature {0} is out of range. Aborting.".format(cmd_temperature)) - - # request new session ID: - try: - my_sid = self._request_session_id() - - aha_string = self._assemble_aha_interface(ain=ainDevice, aha_action=action, aha_param=temp_scaled, - sid=my_sid) - - except Exception as e: - self.logger.error("Exception settemperature: %s" % str(e)) - - # Function used for AHA http interface only: - current_avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') - use_aha_interface = current_avm_data_type in ['set_temperature', 'set_hkrwindowopen'] - if use_aha_interface: - # build_url method cannot be used because it uses another IP port. - # url = self._build_url(aha_string) - - if self._fritz_device.is_ssl(): - url_prefix = "https" - else: - url_prefix = "http" - - url = "%s://%s%s" % (url_prefix, self._fritz_device.get_host(), aha_string) - self.logger.debug("Debug param: {0}".format(aha_string)) - self.logger.debug("Debug url: {0}".format(url)) - - try: - - if use_aha_interface: - r = self._session.get(url, timeout=self._timeout, verify=self._verify) - self.logger.debug("Return value aha interface: {0}".format(r)) - - status_code = r.status_code - if status_code == 200: - self.logger.debug("Sending AHA command successful") - else: - self.logger.error("AHA command error code: {0}".format(status_code)) - - else: - self._lua_session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - - - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error( - "Exception when sending POST request for updating item towards the FritzDevice: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - if self.get_iattr_value(item.conf, - 'avm_data_type') == 'wlanconfig': # check if item was guest wifi item and remaining time is set as item.. - for citem in self._fritz_device.get_items(): # search for guest time remaining item. - if self.get_iattr_value(citem.conf, - 'avm_data_type') == 'wlan_guest_time_remaining' and self.get_iattr_value( - citem.conf, - 'avm_wlan_index') == self.get_iattr_value(item.conf, 'avm_wlan_index'): - self._response_cache.pop("wlanconfig_%s_%s" % ( - self.get_iattr_value(citem.conf, 'avm_wlan_index'), "X_AVM-DE_GetWLANExtInfo"), - None) # reset response cache - self._update_wlan_config(citem) # immediately update remaining guest time - - def get_contact_name_by_phone_number(self, phone_number='', phonebook_id=0): - """ - Searches the phonebook for a contact by a given (complete) phone number - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - | Implementation of this method used information from https://www.symcon.de/forum/threads/25745-FritzBox-mit-SOAP-auslesen-und-steuern - - :param phone_number: full phone number of contact - :param: ID of the phone book (default: 0) - :return: string of the contact's real name - """ - url = self._build_url("/upnp/control/x_contact") - headers = self._header.copy() - action = "GetPhonebook" - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['OnTel'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], {'NewPhonebookID': phonebook_id}) - - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - xml = minidom.parseString(response.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - pb_url_xml = xml.getElementsByTagName('NewPhonebookURL') - if len(pb_url_xml) > 0: - pb_url = pb_url_xml[0].firstChild.data - try: - pb_result = self._session.get(pb_url, timeout=self._timeout, verify=self._verify) - pb_xml = minidom.parseString(pb_result.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending GET request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - contacts = pb_xml.getElementsByTagName('contact') - if len(contacts) > 0: - for contact in contacts: - phone_numbers = contact.getElementsByTagName('number') - if phone_numbers.length > 0: - i = phone_numbers.length - while i >= 0: - i -= 1 - if phone_number in phone_numbers[i].firstChild.data: - return contact.getElementsByTagName('realName')[0].firstChild.data.strip() - # no contact with phone number found, return number only - return phone_number - else: - self.logger.error("Phonebook not available on the FritzDevice") - - return phone_number - - def get_phone_numbers_by_name(self, name='', phonebook_id=0): - """ - Searches the phonebook for a contact by a given name - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - | CURL for testing which phonebooks exists: - | curl --anyauth -u user:'password' 'https://192.168.178.1:49443/upnp/control/x_contact' -H 'Content-Type: text/xml; charset="utf-8"' -H 'SoapAction: urn:dslforum-org:service:X_AVM-DE_OnTel:1#GetPhonebook' -d ' 0 ' -s -k - | Implementation of this method used information from https://www.symcon.de/forum/threads/25745-FritzBox-mit-SOAP-auslesen-und-steuern - - :param name: partial or full name of contact as defined in the phonebook. - :param: ID of the phone book (default: 0) - :return: dict of found contact names (keys) with each containing an array of dicts (keys: type, number) - """ - url = self._build_url("/upnp/control/x_contact") - headers = self._header.copy() - action = "GetPhonebook" - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['OnTel'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], {'NewPhonebookID': phonebook_id}) - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - xml = minidom.parseString(response.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - pb_url_xml = xml.getElementsByTagName('NewPhonebookURL') - if len(pb_url_xml) > 0: - pb_url = pb_url_xml[0].firstChild.data - try: - pb_result = self._session.get(pb_url, timeout=self._timeout, verify=self._verify) - pb_xml = minidom.parseString(pb_result.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending GET request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - contacts = pb_xml.getElementsByTagName('contact') - result_numbers = {} - if name == '': - return result_numbers - if len(contacts) > 0: - for contact in contacts: - real_names = contact.getElementsByTagName('realName') - if real_names.length > 0: - i = 0 - while i < real_names.length: - if name.lower() in real_names[i].firstChild.data.lower(): - phone_numbers = contact.getElementsByTagName('number') - if phone_numbers.length > 0: - result_numbers[real_names[i].firstChild.data] = [] - j = 0 - while j < phone_numbers.length: - if phone_numbers[j].firstChild.data: - result_number_dict = {} - result_number_dict['number'] = phone_numbers[j].firstChild.data - result_number_dict['type'] = phone_numbers[j].attributes["type"].value - result_numbers[real_names[i].firstChild.data].append(result_number_dict) - j += 1 - i += 1 - else: - self.logger.error("Phonebook not available on the FritzDevice") - - return result_numbers - - def get_calllist(self, filter_incoming='', phonebook_id=0): - """ - Returns an array of all calllist entries - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - Curl for testing if the calllist url is returned: - curl --anyauth -u user:'password' 'https://192.168.178.1:49443/upnp/control/x_contact' -H 'Content-Type: text/xml; charset="utf-8"' -H 'SoapAction: urn:dslforum-org:service:X_AVM-DE_OnTel:1#GetCallList' -d ' 0 ' -s -k - :param: Filter to filter incoming calls to a specific destination phone number - :param: ID of the phone book (default: 0) - :return: Array of calllist entries with the attributes 'Id','Type','Caller','Called','CalledNumber','Name','Numbertype','Device','Port','Date','Duration' (some optional) - """ - - url = self._build_url("/upnp/control/x_contact") - headers = self._header.copy() - action = "GetCallList" - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['OnTel'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], {'NewPhonebookID': phonebook_id}) - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - xml = minidom.parseString(response.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - calllist_url_xml = xml.getElementsByTagName('NewCallListURL') - if (len(calllist_url_xml) > 0): - calllist_url = calllist_url_xml[0].firstChild.data - - try: - calllist_result = self._session.get(calllist_url, timeout=self._timeout, verify=self._verify) - calllist_xml = minidom.parseString(calllist_result.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending GET request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - calllist_entries = calllist_xml.getElementsByTagName('Call') - result_entries = [] - if len(calllist_entries) > 0: - for calllist_entry in calllist_entries: - result_entry = {} - - progress = True - - if len(filter_incoming) > 0: - type_element = calllist_entry.getElementsByTagName("Type") - if len(type_element) > 0: - if type_element[0].hasChildNodes(): - type = int(type_element[0].firstChild.data) - - if type == 1 or type == 2: - called_number_element = calllist_entry.getElementsByTagName("CalledNumber") - if len(called_number_element) > 0: - if called_number_element[0].hasChildNodes(): - called_number = called_number_element[0].firstChild.data - # self.logger.debug(called_number+" "+filter_incoming) - if not filter_incoming in called_number: - progress = False - if progress: - attributes = ['Id', 'Type', 'Caller', 'Called', 'CalledNumber', 'Name', 'Numbertype', 'Device', - 'Port', 'Date', 'Duration'] - for attribute in attributes: - attribute_value = calllist_entry.getElementsByTagName(attribute) - if len(attribute_value) > 0: - if attribute_value[0].hasChildNodes(): - if attribute != 'Date': - result_entry[attribute] = attribute_value[0].firstChild.data - else: - result_entry[attribute] = datetime.strptime( - attribute_value[0].firstChild.data, '%d.%m.%y %H:%M') - - result_entries.append(result_entry) - return result_entries - else: - self.logger.debug("No calllist entries on the FritzDevice") - else: - self.logger.error("Calllist not available on the FritzDevice") - - return - - def reboot(self): - """ - Reboots the FritzDevice - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceconfigSCPD.pdf - """ - url = self._build_url("/upnp/control/deviceconfig") - action = 'Reboot' - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['DeviceConfig'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['DeviceConfig']) - try: - self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), self._fritz_device.get_password()), - verify=self._verify) - if self._call_monitor: - self._monitoring_service.disconnect() - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method reboot: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - def wol(self, mac_address): - """ - Sends a WOL (WakeOnLAN) command to a MAC address - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - - :param mac_address: MAC address of the device to wake up - """ - url = self._build_url("/upnp/control/hosts") - headers = self._header.copy() - action = 'X_AVM-DE_WakeOnLANByMACAddress' - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['Hosts'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], {'NewMACAddress': mac_address}) - self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - return - - def get_hosts(self, only_active): - """ - Gets the information (host details) of all hosts as an array of dicts - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - - :param only_active: bool, if only active hosts shall be returned - :return: Array host dicts (see get_host_details) - """ - url = self._build_url("/upnp/control/hosts") - headers = self._header.copy() - action = 'GetHostNumberOfEntries' - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['Hosts'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts']) - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method get_hosts: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - xml = minidom.parseString(response.content) - - number_of_hosts = int(self._get_value_from_xml_node(xml, 'NewHostNumberOfEntries')) - hosts = [] - for i in range(1, number_of_hosts): - host = self.get_host_details(i) - if not only_active or (only_active and self.to_bool(host['is_active'])): - hosts.append(host) - - return hosts - - def get_host_details(self, index): - """ - Gets the information of a hosts at a specific index - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - - :param index: index of host in hosts list - :return: Dict host data: name, interface_type, ip_address, address_source, mac_address, is_active, lease_time_remaining - """ - url = self._build_url("/upnp/control/hosts") - headers = self._header.copy() - action = 'GetGenericHostEntry' - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['Hosts'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], {'NewIndex': index}) - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method get_host_details: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - xml = minidom.parseString(response.content) - host = { - 'name': self._get_value_from_xml_node(xml, 'NewHostName'), - 'interface_type': self._get_value_from_xml_node(xml, 'NewInterfaceType'), - 'ip_address': self._get_value_from_xml_node(xml, 'NewIPAddress'), - 'address_source': self._get_value_from_xml_node(xml, 'NewAddressSource'), - 'mac_address': self._get_value_from_xml_node(xml, 'NewMACAddress'), - 'is_active': self._get_value_from_xml_node(xml, 'NewActive'), - 'lease_time_remaining': self._get_value_from_xml_node(xml, 'NewLeaseTimeRemaining') - } - - return host - - def reconnect(self): - """ - Reconnects the FritzDevice to the WAN - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf - """ - url = self._build_url("/igdupnp/control/WANIPConn1") - action = 'ForceTermination' - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['WANIPConnection'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['WANIPConnection']) - try: - self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method reconnect: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - def get_call_origin(self): - """ - Gets the phone name, currently set as call_origin. - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - :return: String phone name - """ - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialGetConfig' - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['X_VoIP'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP']) - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method get_call_origin: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - xml = minidom.parseString(response.content) - - phone_name = self._get_value_from_xml_node(xml, 'NewX_AVM-DE_PhoneName') - if phone_name is not None: - return phone_name - - self.logger.error("No call origin available.") - return - - def get_phone_name(self, index=1): - """ - Get the phone name at a specific index. The returend value can be used as phone_name for set_call_origin. - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - - :param index: Parameter is an INT, starting from 1. In case an index does not exist, an error is logged. - :return: String phone name - """ - if not self.is_int(index): - self.logger.error("Index parameter \"%s\" is no INT." % index) - return - - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_GetPhonePort' - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['X_VoIP'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP'], - {'NewIndex': index}) - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method get_phone_name: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - xml = minidom.parseString(response.content) - - phone_name = self._get_value_from_xml_node(xml, 'NewX_AVM-DE_PhoneName') - if phone_name is not None: - return phone_name - - self.logger.error("No phone name available at provided index %s." % index) - return - - def set_call_origin(self, phone_name): - """ - Sets the call origin, e.g. before running 'start_call' - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - - :param phone_name: full phone identifier, could be e.g. '\*\*610' for an internal device - """ - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialSetConfig' - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['X_VoIP'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP'], - {'NewX_AVM-DE_PhoneName': phone_name.strip()}) - try: - self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method set_call_origin: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - def start_call(self, phone_number): - """ - Triggers a call for a given phone number - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - - :param phone_number: full phone number to call - """ - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialNumber' - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['X_VoIP'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP'], - {'NewX_AVM-DE_PhoneNumber': phone_number.strip()}) - try: - self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method start_call: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - def cancel_call(self): - """ - Cancels an active call - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - """ - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialHangup' - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['X_VoIP'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP']) - try: - self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, mathod cancel_call: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - def get_device_log_from_lua(self): - """ - Gets the Device Log from the LUA HTTP Interface via LUA Scripts (more complete than the get_device_log TR-064 version. - :return: Array of Device Log Entries (text, type, category, timestamp, date, time) - """ - my_sid = self._request_session_id() - query_string = "/query.lua?mq_log=logger:status/log&sid={0}".format(my_sid) - r = self._lua_session.get(self._build_url(query_string, lua=True), timeout=self._timeout, verify=self._verify) - status_code = r.status_code - if status_code == 200: - self.logger.debug("get_device_log_from_lua: Sending query.lua command successful") - else: - self.logger.error("get_device_log_from_lua: query.lua command error code: {0}".format(status_code)) - return - - try: - data = r.json()['mq_log'] - newlog = [] - - for text, typ, cat in data: - l_date = text[:8] - l_time = text[9:17] - l_text = text[18:] - l_cat = int(cat) - l_type = int(typ) - l_ts = int(datetime.timestamp(datetime.strptime(text[:17], '%d.%m.%y %H:%M:%S'))) - newlog.append([l_text, l_type, l_cat, l_ts, l_date, l_time]) - - return newlog - except JSONDecodeError: - self.logger.error('get_device_log_from_web: SID seems invalid.Please try again.') - return - - def get_device_log_from_tr064(self): - """ - Gets the Device Log via TR-064 - :return: Array of Device Log Entries (Strings) - """ - url = self._build_url("/upnp/control/deviceinfo") - headers = self._header.copy() - action = 'GetDeviceLog' - - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['DeviceInfo'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['DeviceInfo']) - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("get_device_log_from_tr064: Exception when sending POST request, method get_device_log: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["dev_info_" + action] = response.content - - try: - xml = minidom.parseString(self._response_cache["dev_info_" + action]) - except Exception as e: - self.logger.error("get_device_log_from_tr064: Exception when parsing response: %s" % str(e)) - return - - element_xml = xml.getElementsByTagName('NewDeviceLog') - if element_xml[0].firstChild is not None: - return element_xml[0].firstChild.nodeValue.split("\n") - else: - return "" - - def is_host_active(self, mac_address): - """ - Checks if a MAC address is active on the FritzDevice, e.g. the status can be used for simple presence detection - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - | Also reference: https://blog.pregos.info/2015/11/07/anwesenheitserkennung-fuer-smarthome-mit-der-fritzbox-via-tr-064/ - - :param: MAC address of the host - :return: True or False, depending if the host is active on the FritzDevice - """ - url = self._build_url("/upnp/control/hosts") - headers = self._header.copy() - action = 'GetSpecificHostEntry' - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['Hosts'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], {'NewMACAddress': mac_address}) - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - - xml = minidom.parseString(response.content) - tag_content = xml.getElementsByTagName('NewActive') - if (len(tag_content) > 0): - if (tag_content[0].firstChild.data == "1"): - is_active = True - else: - is_active = False - else: - is_active = False - self.logger.debug("MAC Address %s not available on the FritzDevice - ID: %s" % ( - mac_address, self._fritz_device.get_identifier())) - return bool(is_active) - - def _update_myfritz(self, item): - """ - Retrieves information related to myfritz status of the FritzDevice - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_myfritzSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: myfritz_status) - """ - url = self._build_url("/upnp/control/x_myfritz") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'myfritz_status': - action = 'GetInfo' - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['MyFritz'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['MyFritz']) - else: - self.logger.error( - "Attribute %s not supported by plugin method (updatemyfritz)" % self.get_iattr_value(item.conf, - 'avm_data_type')) - return - - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - xml = minidom.parseString(response.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - tag_content = xml.getElementsByTagName('NewEnabled') - if len(tag_content) > 0: - item(tag_content[0].firstChild.data, self.get_shortname()) - - def _update_host(self, item): - """ - Retrieves information related to a network_device represented by its MAC address, e.g. the status of the network_device can be used for simple presence detection - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - | Also reference: https://blog.pregos.info/2015/11/07/anwesenheitserkennung-fuer-smarthome-mit-der-fritzbox-via-tr-064/ - - :param item: item to be updated (Supported item avm_data_types: network_device, child item avm_data_types: device_ip, device_connection_type, device_hostname) - """ - url = self._build_url("/upnp/control/hosts") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'network_device': - if not self.has_iattr(item.conf, 'avm_mac'): - self.logger.error("No avm_mac attribute provided in network_device item %s" % item.property.path) - return - action = 'GetSpecificHostEntry' - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['Hosts'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], - {'NewMACAddress': self.get_iattr_value(item.conf, - 'avm_mac')}) - else: - self.logger.error( - "Attribute %s not supported by plugin (update hosts)" % self.get_iattr_value(item.conf, - 'avm_data_type')) - return - - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - # self.logger.debug(response.content) - xml = minidom.parseString(response.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request. method _update_host: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - tag_content = xml.getElementsByTagName('NewActive') - if len(tag_content) > 0: - item(tag_content[0].firstChild.data, self.get_shortname()) - for child in item.return_children(): - if self.has_iattr(child.conf, 'avm_data_type'): - if self.get_iattr_value(child.conf, 'avm_data_type') == 'device_ip': - device_ip = xml.getElementsByTagName('NewIPAddress') - if len(device_ip) > 0: - if not device_ip[0].firstChild is None: - child(device_ip[0].firstChild.data, self.get_shortname()) - else: - child('', self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(child.conf, - 'avm_data_type')) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_connection_type': - device_connection_type = xml.getElementsByTagName('NewInterfaceType') - if len(device_connection_type) > 0: - if not device_connection_type[0].firstChild is None: - child(device_connection_type[0].firstChild.data, self.get_shortname()) - else: - child('', self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(child.conf, - 'avm_data_type')) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_hostname': - data = self._get_value_from_xml_node(xml, 'NewHostName') - if data is not None: - child(data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(child.conf, - 'avm_data_type')) - else: - item(0) - self.logger.debug( - "MAC Address %s for item %s not available on the FritzDevice - ID: %s" % ( - self.get_iattr_value(item.conf, 'avm_mac'), item.property.path, - self._fritz_device.get_identifier())) - - def _update_home_automation(self, item): - """ - Updates AVM home automation device related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_homeauto.pdf - CURL for testing which data is coming back: - curl --anyauth -u user:'password' "https://192.168.178.1:49443/upnp/control/x_homeauto" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:X_AVM-DE_Homeauto:1#GetSpecificDeviceInfos" -d "xxxxx xxxxxxx" -s -k - - :param item: item to be updated (Supported item avm_data_types: aha_device, hkr_device) - """ - url = self._build_url("/upnp/control/x_homeauto") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device' or self.get_iattr_value(item.conf, - 'avm_data_type') == 'hkr_device': - if not self.has_iattr(item.conf, 'ain'): - self.logger.error("Cannot update AVM item {0} as AIN is not specified.".format(item)) - return - ain = self.get_iattr_value(item.conf, 'ain') - action = 'GetSpecificDeviceInfos' - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['Homeauto'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['Homeauto'], - {'NewAIN': ain.strip()}) - else: - self.logger.error( - "Attribute %s not supported by plugin method (home automation)" % self.get_iattr_value(item.conf, - 'avm_data_type')) - return - - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - xml = minidom.parseString(response.content) - - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request or parsing response: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device': - element_xml = xml.getElementsByTagName('NewSwitchState') - if len(element_xml) > 0: - if element_xml[0].firstChild.data not in ['UNDEFINED', 'TOGGLE']: - item(element_xml[0].firstChild.data, self.get_shortname()) - elif element_xml[0].firstChild.data in 'TOGGLE': - value = item() - item(not value, self.get_shortname()) - else: - self.logger.error( - 'NewSwitchState für AHA Device has a non-supported value of %s' % element_xml[ - 0].firstChild.data) - for child in item.return_children(): - if self.has_iattr(child.conf, 'avm_data_type'): - if self.get_iattr_value(child.conf, 'avm_data_type') == 'temperature': - temp = xml.getElementsByTagName('NewTemperatureCelsius') - if len(temp) > 0: - child(int(temp[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, - 'avm_data_type')) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'power': - power = xml.getElementsByTagName('NewMultimeterPower') - if len(power) > 0: - child(int(power[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, - 'avm_data_type')) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'energy': - energy = xml.getElementsByTagName('NewMultimeterEnergy') - if len(energy) > 0: - child(int(energy[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, - 'avm_data_type')) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - - # handling hkr devices (AVM dect 301) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'hkr_device': - self.logger.debug('handling hkr device') - element_xml = xml.getElementsByTagName('NewHkrSetVentilStatus') - if len(element_xml) > 0: - # Decoding hrk valve state: open, closed or temp (temperature controlled) - tempstring = element_xml[0].firstChild.data - tempstate = 3 - if tempstring == 'OPEN': - tempstate = 1 - elif tempstring == 'CLOSED': - tempstate = 0 - elif tempstring == 'TEMP': - tempstate = 2 - else: - tempstate = 3 - item(int(tempstate)) - for child in item.return_children(): - if self.has_iattr(child.conf, 'avm_data_type'): - if self.get_iattr_value(child.conf, 'avm_data_type') == 'temperature': - is_temperature = xml.getElementsByTagName('NewTemperatureCelsius') - if len(is_temperature) > 0: - child(int(is_temperature[0].firstChild.data) / 10) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'set_temperature': - set_temperature = xml.getElementsByTagName('NewHkrSetTemperature') - if len(set_temperature) > 0: - child(int(set_temperature[0].firstChild.data) / 10, self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'set_temperature_reduced': - set_temperature_reduced = xml.getElementsByTagName('NewHkrReduceTemperature') - if len(set_temperature_reduced) > 0: - child(int(set_temperature_reduced[0].firstChild.data) / 10, self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'set_temperature_comfort': - set_temperature_comfort = xml.getElementsByTagName('NewHkrComfortTemperature') - if len(set_temperature_comfort) > 0: - child(int(set_temperature_comfort[0].firstChild.data) / 10, self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'firmware_version': - firmware_version = xml.getElementsByTagName('NewFirmwareVersion') - if len(firmware_version) > 0: - child(str(firmware_version[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'manufacturer': - manufacturer = xml.getElementsByTagName('NewManufacturer') - if len(manufacturer) > 0: - child(str(manufacturer[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'product_name': - product_name = xml.getElementsByTagName('NewProductName') - if len(product_name) > 0: - child(str(product_name[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_name': - device_name = xml.getElementsByTagName('NewDeviceName') - if len(device_name) > 0: - child(str(device_name[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'connection_status': - connection_status = xml.getElementsByTagName('NewPresent') - if len(connection_status) > 0: - child(str(connection_status[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_id': - device_id = xml.getElementsByTagName('NewDeviceId') - if len(device_id) > 0: - child(str(device_id[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_function': - device_function = xml.getElementsByTagName('NewFunctionBitMask') - if len(device_function) > 0: - child(str(device_function[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - 'Argument {} of Attribute {} not available on the FritzDevice with AIN {}.' - .format(self.get_iattr_value(child.conf, 'avm_data_type'), - self.get_iattr_value(item.conf, 'avm_data_type'), - item.conf['ain'].strip())) - - else: - pass - - else: - self.logger.warning("Response NewHkrSetVentilStatus is empty") - else: - self.logger.warning('Unsupported avm_data_type {0} in _update_home_automation()'.format(self.get_iattr_value(item.conf, 'avm_data_type'))) - - - - def _update_fritz_device_info(self, item): - """ - Updates FritzDevice specific information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceinfoSCPD.pdf - CURL for testing: - curl --anyauth -u user:'password' 'https://192.168.178.1:49443/upnp/control/deviceinfo' -H 'Content-Type: text/xml; charset="utf-8"' -H 'SoapAction: urn:dslforum-org:service:DeviceInfo:1#GetInfo' -d ' ' -s -k - - :param item: Item to be updated (Supported item avm_data_types: uptime, software_version, hardware_version,serial_number, description) - """ - url = self._build_url("/upnp/control/deviceinfo") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['uptime', 'software_version', 'hardware_version', - 'serial_number']: - action = 'GetInfo' - else: - self.logger.error("Attribute %s not supported by plugin" % self.get_iattr_value(item.conf, 'avm_data_type')) - return - - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['DeviceInfo'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['DeviceInfo']) - - if "dev_info_" + action not in self._response_cache: - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error( - "Exception when sending POST request, method _update_fritz_device_info: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["dev_info_" + action] = response.content - else: - self.logger.debug( - "Accessing dev_info response cache for action %s and item %s!" % (action, item.property.path)) - - try: - xml = minidom.parseString(self._response_cache["dev_info_" + action]) - except Exception as e: - self.logger.error("Exception when parsing response: %s" % str(e)) - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'uptime': - element_xml = xml.getElementsByTagName('NewUpTime') - if len(element_xml) > 0: - item(int(element_xml[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'software_version': - element_xml = xml.getElementsByTagName('NewSoftwareVersion') - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'hardware_version': - element_xml = xml.getElementsByTagName('NewHardwareVersion') - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'serial_number': - element_xml = xml.getElementsByTagName('NewSerialNumber') - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - - def _update_tam(self, item): - """ - Updates telephone answering machine (TAM) related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_tam.pdf - - :param item: item to be updated (Supported item avm_data_types: tam, child item avm_data_types: tam_name) - """ - url = self._build_url("/upnp/control/x_tam") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['tam', 'tam_name']: - action = 'GetInfo' - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['tam_new_message_number', 'tam_total_message_number']: - action = 'GetMessageList' - else: - self.logger.error("Attribute %s not supported by plugin" % self.get_iattr_value(item.conf, 'avm_data_type')) - return - - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['TAM'], action) - - index = 0 - if self.has_iattr(item.conf, 'index'): - index = int(self.get_iattr_value(item.conf, 'index')) - 1 - - soap_data = self._assemble_soap_data(action, self._urn_map['TAM'], {'NewIndex': index}) - - if "tam_" + action not in self._response_cache: - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method _update_tam: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["tam_" + action] = response.content - else: - self.logger.debug("Accessing TAM response cache for action %s and item %s!" % (action, item.property.path)) - - try: - xml = minidom.parseString(self._response_cache["tam_" + action]) - except Exception as e: - self.logger.error("Exception when parsing response: %s" % str(e)) - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'tam': - element_xml = xml.getElementsByTagName('NewEnable') - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'tam_name': - element_xml = xml.getElementsByTagName('NewName') - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['tam_new_message_number', 'tam_total_message_number']: - message_url_xml = xml.getElementsByTagName('NewURL') - if len(message_url_xml) > 0: - message_url = message_url_xml[0].firstChild.data - - if "tam_messages" not in self._response_cache: - try: - message_result = self._session.get(message_url, timeout=self._timeout, verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending GET request: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["tam_messages"] = message_result.content - else: - self.logger.debug("Accessing tam_messages response cache for action %s and item %s!" % ( - action, item.property.path)) - - try: - message_xml = minidom.parseString(self._response_cache["tam_messages"]) - except Exception as e: - self.logger.error("Exception when parsing response: %s" % str(e)) - return - - messages = message_xml.getElementsByTagName('Message') - message_count = 0 - if len(messages) > 0: - if self.get_iattr_value(item.conf, 'avm_data_type') == 'tam_total_message_number': - message_count = len(messages) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'tam_new_message_number': - for message in messages: - is_new = message.getElementsByTagName('New') - if int(is_new[0].firstChild.data) == 1: - message_count = message_count + 1 - item(message_count, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - - def _update_wlan_config(self, item): - """ - Updates wlan related information, all items of this method need an numeric avm_wlan_index (typically 1-3) - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: wlanconfig, wlan_guest_time_remaining - """ - if self.has_iattr(item.conf, 'avm_wlan_index'): - if int(self.get_iattr_value(item.conf, 'avm_wlan_index')) > 0: - url = self._build_url("/upnp/control/wlanconfig%s" % self.get_iattr_value(item.conf, 'avm_wlan_index')) - else: - self.logger.error('No avm_wlan_index attribute provided') - else: - self.logger.error('No avm_wlan_index attribute provided for {}'.format(item)) - - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wlanconfig', 'wlanconfig_ssid']: - action = 'GetInfo' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wlan_guest_time_remaining': - action = 'X_AVM-DE_GetWLANExtInfo' - else: - self.logger.error("Attribute %s not supported by plugin" % self.get_iattr_value(item.conf, 'avm_data_type')) - return - - headers['SOAPACTION'] = "%s#%s" % ( - self._urn_map['WLANConfiguration'] % str(self.get_iattr_value(item.conf, 'avm_wlan_index')), action) - soap_data = self._assemble_soap_data(action, - self._urn_map['WLANConfiguration'] % str( - self.get_iattr_value(item.conf, 'avm_wlan_index'))) - - if not "wlanconfig_%s_%s" % (self.get_iattr_value(item.conf, 'avm_wlan_index'), action) in self._response_cache: - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error("Exception when sending POST request, method _update_wlan_config: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache[ - "wlanconfig_%s_%s" % (self.get_iattr_value(item.conf, 'avm_wlan_index'), action)] = response.content - else: - self.logger.debug( - "Accessing wlanconfig response cache for action %s and item %s!" % (action, item.property.path)) - - try: - xml = minidom.parseString( - self._response_cache["wlanconfig_%s_%s" % (self.get_iattr_value(item.conf, 'avm_wlan_index'), action)]) - except Exception as e: - self.logger.error("Exception when parsing response: %s" % str(e)) - return - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wlanconfig': - newEnable = self._get_value_from_xml_node(xml, 'NewEnable') - if newEnable is not None: - item(newEnable, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wlanconfig_ssid': - newSSID = self._get_value_from_xml_node(xml, 'NewSSID') - if newSSID is not None: - item(newSSID, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wlan_guest_time_remaining': - element_xml = xml.getElementsByTagName('NewX_AVM-DE_TimeRemain') - if len(element_xml) > 0: - item(int(element_xml[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - - def _update_wan_dsl_interface_config(self, item): - """ - Updates wide area network (WAN) speed related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wandslifconfigSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: wan_upstream, wan_downstream) - """ - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_upstream', 'wan_downstream']: - action = 'GetInfo' - else: - self.logger.error("Attribute %s not supported by plugin" % self.get_iattr_value(item.conf, 'avm_data_type')) - return - - url = self._build_url("/upnp/control/wandslifconfig1") - - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['WANDSLInterfaceConfig'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['WANDSLInterfaceConfig']) - - # if action has not been called in a cycle so far, request it and cache response - if "wan_dsl_interface_config_" + action not in self._response_cache: - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error( - "Exception when sending POST request, method _update_wan_dsl_interface_config: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["wan_dsl_interface_config_" + action] = response.content - else: - self.logger.debug("Accessing wan_dsl_interface_config response cache for action %s and item %s!" % ( - action, item.property.path)) - - try: - xml = minidom.parseString(self._response_cache["wan_dsl_interface_config_" + action]) - except Exception as e: - self.logger.error("Exception when parsing response: %s" % str(e)) - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_upstream': - element_xml = xml.getElementsByTagName('NewUpstreamCurrRate') - if len(element_xml) > 0: - item(int(element_xml[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_downstream': - element_xml = xml.getElementsByTagName('NewDownstreamCurrRate') - if len(element_xml) > 0: - item(int(element_xml[0].firstChild.data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - - def _update_wan_common_interface_configuration(self, item): - """ - Updates wide area network (WAN) related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wancommonifconfigSCPD.pdf - https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/IGD1.pdf - - :param item: item to be updated (Supported item avm_data_types: wan_total_packets_sent, wan_total_packets_received, wan_current_packets_sent, wan_current_packets_received, wan_total_bytes_sent, wan_total_bytes_received, wan_current_bytes_sent, wan_current_bytes_received, wan_link) - """ - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_sent': - action = 'GetTotalPacketsSent' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_received': - action = 'GetTotalPacketsReceived' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_sent': - action = 'GetTotalBytesSent' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_received': - action = 'GetTotalBytesReceived' - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_current_packets_sent', - 'wan_current_packets_received', - 'wan_current_bytes_sent', - 'wan_current_bytes_received']: - action = 'GetAddonInfos' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_link': - action = 'GetCommonLinkProperties' - else: - self.logger.error("Attribute %s not supported by plugin" % self.get_iattr_value(item.conf, 'avm_data_type')) - return - - headers = self._header.copy() - if action != 'GetAddonInfos': - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['WANCommonInterfaceConfig'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['WANCommonInterfaceConfig']) - url = self._build_url("/upnp/control/wancommonifconfig1") - else: - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['WANCommonInterfaceConfig_alt'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['WANCommonInterfaceConfig_alt']) - url = self._build_url("/igdupnp/control/WANCommonIFC1") - # if action has not been called in a cycle so far, request it and cache response - if "wan_common_interface_configuration_" + action not in self._response_cache: - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error( - "Exception when sending POST request, method _update_wan_common_interface_configuration: %s" % str( - e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["wan_common_interface_configuration_" + action] = response.content - else: - self.logger.debug( - "Accessing wan_common_interface_configuration response cache for action %s and item %s!" % ( - action, item.property.path)) - - try: - xml = minidom.parseString(self._response_cache["wan_common_interface_configuration_" + action]) - except Exception as e: - self.logger.error("Exception when parsing response: %s" % str(e)) - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_sent': - data = self._get_value_from_xml_node(xml, 'NewTotalPacketsSent') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_received': - data = self._get_value_from_xml_node(xml, 'NewTotalPacketsReceived') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_packets_sent': - data = self._get_value_from_xml_node(xml, 'NewPacketSendRate') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_packets_received': - data = self._get_value_from_xml_node(xml, 'NewPacketReceiveRate') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_sent': - data = self._get_value_from_xml_node(xml, 'NewTotalBytesSent') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_received': - data = self._get_value_from_xml_node(xml, 'NewTotalBytesReceived') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_bytes_sent': - data = self._get_value_from_xml_node(xml, 'NewByteSendRate') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_bytes_received': - data = self._get_value_from_xml_node(xml, 'NewByteReceiveRate') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_link': - data = self._get_value_from_xml_node(xml, 'NewPhysicalLinkStatus') - if data is not None: - if data == 'Up': - item(True, self.get_shortname()) - else: - item(False, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - - def _update_wan_ip_connection(self, item): - """ - Updates wide area network (WAN) IP related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: wan_connection_status, wan_is_connected, wan_uptime, wan_ip) - """ - url = self._build_url("/igdupnp/control/WANIPConn1") - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_connection_status', 'wan_is_connected', - 'wan_uptime', 'wan_connection_error']: - action = 'GetStatusInfo' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_ip': - action = 'GetExternalIPAddress' - else: - self.logger.error("Attribute %s not supported by plugin" % self.get_iattr_value(item.conf, 'avm_data_type')) - return - - headers = self._header.copy() - headers['SOAPACTION'] = "%s#%s" % (self._urn_map['WANIPConnection'], action) - soap_data = self._assemble_soap_data(action, self._urn_map['WANIPConnection']) - - # if action has not been called in a cycle so far, request it and cache response - if "wan_ip_connection_" + action not in self._response_cache: - try: - response = self._session.post(url, data=soap_data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error( - "Exception when sending POST request, method _update_wan_ip_connection: %s" % str(e)) - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["wan_ip_connection_" + action] = response.content - else: - self.logger.debug( - "Accessing wan_ip_connection response cache for action %s and item %s!" % (action, item.property.path)) - - try: - xml = minidom.parseString(self._response_cache["wan_ip_connection_" + action]) - except Exception as e: - self.logger.error("Exception when parsing response: %s" % str(e)) - return - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_connection_status', 'wan_is_connected']: - data = self._get_value_from_xml_node(xml, 'NewConnectionStatus') - if data is not None: - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_connection_status': - item(data, self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_is_connected': - if data == 'Connected': - item(True, self.get_shortname()) - else: - item(False, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_uptime': - data = self._get_value_from_xml_node(xml, 'NewUptime') - if data is not None: - item(int(data), self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_connection_error': - data = self._get_value_from_xml_node(xml, 'NewLastConnectionError') - if data is not None: - item(data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_ip': - data = self._get_value_from_xml_node(xml, 'NewExternalIPAddress') - if data is not None: - item(data, self.get_shortname()) - else: - self.logger.error( - "Attribute %s not available on the FritzDevice" % self.get_iattr_value(item.conf, 'avm_data_type')) - - def _get_value_from_xml_node(self, node, tag_name): - data = None - xml = node.getElementsByTagName(tag_name) - if len(xml) > 0: - if not xml[0].firstChild is None: - data = xml[0].firstChild.data - return data diff --git a/avm/_pv_1_5_12/assets/webif1.jpg b/avm/_pv_1_5_12/assets/webif1.jpg deleted file mode 100755 index 664d58af1443381b23831d765e9f06f8e55743bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 299580 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

Oe^XVXo0KJ?}Up5MPx^i^BMonc%XNRPy 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 -

-{% endmacro %} - -/** -* Fritzbox Wifi-Control -* -* @param unique id for this widget -* @param a gad/item for the name of the FritzDevice -* @param a gad/item for the wlan 24 ghz -* @param a gad/item for the wlan 5 ghz (optional) -* @param a gad/item for wlan guest (optional) -* @param a gad/item fpr remaining guest time (optional) -* -*/ -{% macro wifi(id, gad_name, gad_wlan24, gad_wlan5, gad_wlanguest, gad_guesttime) %} - {% import "basic.html" as basic %} - {% set uid = uid(page, id) %} - -
- {% if gad_name %}WLAN ({{ basic.print(id~'name', gad_name) }}):
{% endif %} - - {% if gad_wlan24 %}{% endif %} - {% if gad_wlan5 %}{% endif %} - {% if gad_wlanguest %} - {% endif %} -
2.4 GHz: {{ basic.flip(id~'wlan24', gad_wlan24) }}
5.0 GHz: {{ basic.flip(id~'wlan5', gad_wlan5) }}
Gast {% if gad_guesttime %}({{ basic.print(id~'guesttime', gad_guesttime) }}min): {% endif %}{{ basic.flip(id~'wlanguest', gad_wlanguest) }}
-
-{% endmacro %} - -/** - * Fritzbox Systeminfo Widget - * - * @param unique id for this widget - * @param the gad/item for the uptime of the fritzdevice (not the wan uptime!) - * @param the gad/item for the firmware of the fritzdevice - * @param the gad/item for the hardware version of the fritzdevice - * @param the gad/item for the serial number of the fritzdevice - */ -{% macro system_info(id, gad_uptime, gad_firmware, gad_hardware_version, gad_serial_number) %} - {% import "basic.html" as basic %} - {% set uid = uid(page, id) %} -
- {% if gad_uptime %}Uptime: {{ basic.print(id~'uptime', gad_uptime, 'float', 'VAR / 3600') }} hours
{% endif %} - {% if gad_firmware %}Firmware: {{ basic.print(id~'firmware', gad_firmware) }}
{% endif %} - {% if gad_hardware_version %}Gerätetyp: {{ basic.print(id~'hardware_version', gad_hardware_version) }}
{% endif %} - {% if gad_serial_number %}Seriennummer: {{ basic.print(id~'serial_number', gad_serial_number) }}{% endif %} -
-{% endmacro %} - -/** - * Fritzbox WAN-info Widget - * - * @param unique id for this widget - * @param the gad/item for the internet ip - * @param the gad/item for the wan (internet) uptime - * @param the gad/item for the connection status - * @param the gad/item for the last connection error - * @param the gad/item for the total sent packets - * @param the gad/item for the total received packets - * @param the gad/item for the total sent bytes - * @param the gad/item for the total received bytes - * @param the gad/item for the upstream rate - * @param the gad/item for the downstream rate - * - */ -{% macro wan_info(id, gad_ip, gad_uptime, gad_connection_status, gad_connection_error, gad_total_packets_sent, gad_total_packets_received, gad_total_bytes_sent, gad_total_bytes_received, gad_upstream, gad_downstream) %} - {% import "basic.html" as basic %} - {% set uid = uid(page, id) %} -
- {% if gad_ip %}Internet-IP: {{ basic.print(id~'ip', gad_ip) }}
{% endif %} - {% if gad_uptime %}WAN-Uptime: {{ basic.print(id~'wan_uptime', gad_uptime, 'float', 'VAR / 3600') }} hours
{% endif %} - {% if gad_connection_status %}Verbindungsstatus: {{ basic.print(id~'connection_status', gad_connection_status) }}
{% endif %} - {% if gad_connection_error %}Letzter Fehler: {{ basic.print(id~'wan_connection_error', gad_connection_error) }}
{% endif %} - {% if gad_total_packets_sent %}Gesendete Pakete: {{ basic.print(id~'total_packets_sent', gad_total_packets_sent) }}
{% endif %} - {% if gad_total_packets_received %}Empfangene Pakete: {{ basic.print(id~'total_packets_rcv', gad_total_packets_received) }}
{% endif %} - {% if gad_total_bytes_sent %}Gesendetes Bytes: {{ basic.print(id~'total_bytes_sent', gad_total_bytes_sent, 'float', 'VAR / (1024*1024)') }} mb
{% endif %} - {% if gad_total_bytes_received %}Empfangene Bytes: {{ basic.print(id~'total_bytes_received', gad_total_bytes_received, 'float', 'VAR / (1024*1024)') }} mb
{% endif %} - {% if gad_upstream %}Upstream: {{ basic.print(id~'upstream', gad_upstream, 'float', 'VAR / (1000)') }} mbit
{% endif %} - {% if gad_downstream %}Downstream: {{ basic.print(id~'downstream', gad_downstream, 'float', 'VAR / (1000)') }} mbit{% endif %} -
-{% endmacro %} \ No newline at end of file diff --git a/avm/_pv_1_5_12/user_doc.rst b/avm/_pv_1_5_12/user_doc.rst deleted file mode 100755 index a15893d75..000000000 --- a/avm/_pv_1_5_12/user_doc.rst +++ /dev/null @@ -1,62 +0,0 @@ -.. index:: Plugins; AVM (Unterstützung für Fritz!Box usw.) -.. index:: AVM - -avm -### - -Konfiguration -============= - -Die Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/avm` beschrieben. - - -Konfiguration der Fritz!Box -=========================== - -Für die Nutzung der Informationen über Telefonereignisse muss der CallMonitor aktiviert werden. Dazu muss auf -einem direkt an die Fritz!Box angeschlossenen Telefon (Analog, ISDN S0 oder DECT) \*96#5# eingegeben werden. - -Bei neueren Firmware Versionen (ab Fritz!OS v7) Muss die Anmeldung an der Box von "nur mit Kennwort" auf "Benutzername -und Kennwort umgestellt werden" und es sollte ein eigener User für das AVM Plugin auf der Fritz!Box eingerichtet werden. - - -Web Interface -============= - -Das avm Plugin verfügt über ein Webinterface, mit dessen Hilfe die Items die das Plugin nutzen -übersichtlich dargestellt werden. - -.. important:: - - Das Webinterface des Plugins kann mit SmartHomeNG v1.4.2 und davor **nicht** genutzt werden. - Es wird dann nicht geladen. Diese Einschränkung gilt nur für das Webinterface. Ansonsten gilt - für das Plugin die in den Metadaten angegebene minimale SmartHomeNG Version. - - -Aufruf des Webinterfaces ------------------------- - -Das Plugin kann aus dem backend aufgerufen werden. Dazu auf der Seite Plugins in der entsprechenden -Zeile das Icon in der Spalte **Web Interface** anklicken. - -Außerdem kann das Webinterface direkt über ``http://smarthome.local:8383/avm`` bzw. -``http://smarthome.local:8383/avm_`` aufgerufen werden. - - -Beispiele ---------- - -Folgende Informationen können im Webinterface angezeigt werden: - -Oben rechts werden allgemeine Parameter zum Plugin angezeigt. - -Im ersten Tab werden die Items angezeigt, die das avm Plugin nutzen: - -.. image:: assets/webif1.jpg - :class: screenshot - -Im zweiten Tab werden Call Monitor Items zum avm Plugin angezeigt: - -.. image:: assets/webif2.jpg - :class: screenshot - diff --git a/avm/_pv_1_5_12/webif/__init__.py b/avm/_pv_1_5_12/webif/__init__.py deleted file mode 100755 index c06effb25..000000000 --- a/avm/_pv_1_5_12/webif/__init__.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 -# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab -######################################################################### -# Copyright 2020- -######################################################################### -# This file is part of SmartHomeNG. -# https://www.smarthomeNG.de -# https://knx-user-forum.de/forum/supportforen/smarthome-py -# -# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and -# upwards. -# -# SmartHomeNG is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SmartHomeNG is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SmartHomeNG. If not, see . -# -######################################################################### - -import datetime -import time -import os - -from lib.item import Items -from lib.model.smartplugin import SmartPluginWebIf - - -# ------------------------------------------ -# Webinterface of the plugin -# ------------------------------------------ - -import cherrypy -import csv -from jinja2 import Environment, FileSystemLoader - -class WebInterface(SmartPluginWebIf): - - def __init__(self, webif_dir, plugin): - """ - Initialization of instance of class WebInterface - - :param webif_dir: directory where the webinterface of the plugin resides - :param plugin: instance of the plugin - :type webif_dir: str - :type plugin: object - """ - self.webif_dir = webif_dir - self.plugin = plugin - self.logger = plugin.logger - self.tplenv = self.init_template_environment() - - @cherrypy.expose - def index(self, reload=None, action=None): - """ - Build index.html for cherrypy - - Render the template and return the html file to be delivered to the browser - - :return: contents of the template after beeing rendered - """ - tabcount = 3 - call_monitor_items = 0 - if self.plugin._call_monitor: - call_monitor_items = self.plugin._monitoring_service.get_item_count_total() - tabcount = 4 - - tmpl = self.tplenv.get_template('index.html') - return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), - plugin_info=self.plugin.get_info(), tabcount=tabcount, - avm_items=self.plugin.get_fritz_device().get_item_count(), - call_monitor_items=call_monitor_items, - p=self.plugin) - - @cherrypy.expose - def reboot(self): - self.plugin.reboot() - - @cherrypy.expose - def reconnect(self): - self.plugin.reconnect() \ No newline at end of file diff --git a/avm/_pv_1_5_12/webif/static/img/lamp_green.png b/avm/_pv_1_5_12/webif/static/img/lamp_green.png deleted file mode 100755 index fb130568b7d788da9a89b16c97f60d30798bf0ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3757 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 - - - - {% if p.get_fritz_device().is_available() %} - {{ _('Gerät verfügbar') }} - {% else %} - {{ _('Gerät nicht verfügbar') }} - {% endif %} - {{ _('Verbunden') }} - - - {% if p.get_fritz_device().is_available() %} - {{ _('Ja') }}{% if p._fritz_device.is_ssl() %}, SSL{% endif %} - {% else %} - {{ _('Nein') }} - {% endif %} - - {{ _('Benutzer') }} - {{ p.get_parameter_value_for_display('username') }} - - - - {% if p._call_monitor %} - {% if p.get_monitoring_service()._listen_active %} - {{ _('Call Monitor verbunden') }} - {% else %} - {{ _('Call Monitor nicht verbunden') }} - {% endif %} - {% endif %} - {{ _('Call Monitor') }} - - {% if p._call_monitor %}{{ _('Ja') }}{% if not p.get_monitoring_service()._listen_active %}, {{ _('nicht verbunden') }}{% endif %}{% else %}{{ _('Nein') }}{% endif %} - {{ _('Passwort') }} - {{ p.get_parameter_value_for_display('password') }} - - - {{ _('Host') }} -
{{ p._fritz_device.get_host() }} - {{ _('Port') }} - {{ p._fritz_device.get_port() }} {% if p._fritz_device.is_ssl() %}(HTTPS){% endif %} - - - -{% endblock %} - - -{% block buttons %} - - -{% endblock buttons %} - -{% block bodytab1 %} -
-
- - - - - - - - - - - - - {% for item in p._fritz_device.get_items() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - {% endfor %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item.property.path }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ item() }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
-
-{% endblock %} -{% block bodytab2 %} -
-{% for function, dict in p.metadata.plugin_functions.items() %} -
-
- {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) -
-
- {{ dict['description'][language] }}
- {% if dict['parameters'] is not none %} -
-
- {{ _('Parameter') }}: -
-
-
    - {% for name, paramdict in dict['parameters'].items() %} -
  • - {{ name }}: {{ paramdict['type'] }}
    - {{ paramdict['description'][language] }} -
  • - {% endfor %} -
-
-
- {% endif %} -
-
-{% endfor %} -
-{% endblock %} -{% block bodytab3 %} -{% set logentries = p.get_device_log_from_lua() %} - {% if logentries %} - - - - - - - - - - - {% for logentry in p.get_device_log_from_lua() %} - - - - - - - - {% endfor %} - -
{{ _('Datum')}}{{ _('Uhrzeit')}}{{ _('Meldung')}}{{ _('Nachrichtennummer')}}{{ _('Kategorie')}}
{{ logentry[4] }}{{ logentry[5] }}{{ logentry[0] }} - - {{ logentry[1] }} - - {{ _('cat_'+logentry[2]|string) }}
- {% else %} -
- {{ _('Keine Logeinträge oder Zugriffsfehler (ggf. SmartHomeNG Log prüfen).') }} -
- {% endif %} -{% endblock %} -{% block bodytab4 %} -{% if p._call_monitor %} - - - - - - - - - - - - - {% for item in p._monitoring_service.get_items() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - {% endfor %} - {% for item in p._monitoring_service.get_trigger_items() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - {% endfor %} - {% for item in p._monitoring_service.get_items_incoming() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - {% endfor %} - {% for item in p._monitoring_service.get_items_outgoing() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - {% endfor %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item.property.path }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ item() }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{{ item.id() }}{{ item.type() }}{{ item.conf[instance_key] }}{{ item() }}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
{{ item.id() }}{{ item.type() }}{{ item.conf[instance_key] }}{{ item() }}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
{{ item.id() }}{{ item.type() }}{{ item.conf[instance_key] }}{{ item() }}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
-{% endif %} -{% endblock %} \ No newline at end of file diff --git a/avm/_pv_1_6_5/README.md b/avm/_pv_1_6_5/README.md deleted file mode 100755 index b2430c08e..000000000 --- a/avm/_pv_1_6_5/README.md +++ /dev/null @@ -1,819 +0,0 @@ -# AVM - -## Description -The AVM Plugin can be used to connect to a device from AVM (e.g. the Fritzbox). You can control functionality and read data from the device. Moreover you can connect to the live call monitor and monitor incoming or outgoing calls and trigger items based on these events. - -## Requirements -This plugin requires lib requests. You can install this lib with: - -``` -sudo pip3 install requests --upgrade -``` - -It is completely based on the TR-064 interface from AVM (http://avm.de/service/schnittstellen/) - -Forum thread to the plugin: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin - -Version 1.1.2 tested with a FRITZ!Box 7490 (FRITZ!OS 06.51), a FRITZ! WLAN Repeater 1750E (FRITZ!OS 06.32) and a -WLAN Repeater 300E (FRITZ!OS 06.30). It was also tested with a FRITZ!Box 7390 with FW 84.06.36 and a Fritz!Box 7390 -with v6.30 und v6.51. - -The avm_data_types listed in the example items under "devices" only work correctly with firmware <= v6.30, if the -FRITZ!Box does not handle more than 16 devices in parallel (under "Heimnetz/Netzwerk"). Otherwise some of the devices -won't work. - -The MonitoringService currently does not support multiple parallel incoming or outgoing calls. For being able to -connect to the FritzDevice's CallMonitor, you have to activate it, by typing `#96*5*` on a directly connected phone. - -The version is tested with new multi-instance functionality of SmartHomeNG. - -## Configuration - -### plugin.yaml - -```yaml -fb1: - class_name: AVM - class_path: plugins.avm - username: ... # optional - password: '...' - host: fritz.box - port: 49443 - cycle: 300 - ssl: True # use https or not - verify: False # verify ssl certificate - call_monitor: 'True' - call_monitor_incoming_filter: "... ## optional, don't set if you don't want to watch only one specific number with your call monitor" - instance: fritzbox_7490 - -fb2: - class_name: AVM - class_path: plugins.avm - username: ... # optional - password: '...' - host: '...' - port: 49443 - cycle: 300 - ssl: True # use https or not - verify: False # verify ssl certificate - call_monitor: 'True' - instance: wlan_repeater_1750 -``` - -Note: Depending on the FritzDevice a shorter cycle time can result in problems with CPU rating and, in consequence with the accessibility of the webservices on the device. -If cycle time is reduced, please carefully watch your device and your sh.log. In the development process, 120 Seconds also worked worked fine on the used devices. - -#### Attributes - * `username`: Optional login information - * `password`: Required login information - * `host`: Hostname or ip address of the FritzDevice. - * `port`: Port of the FritzDevice, typically 49433 for https or 49000 for http - * `cycle`: timeperiod between two update cycles. Default is 300 seconds. - * `ssl`: True or False => True will add "https", False "http" to the URLs in the plugin - * `verify`: True or False => Turns certificate verification on or off. Typically False - * `call_monitor`: True or False => Activates or deactivates the MonitoringService, which connects to the FritzDevice's call monitor - * `instance`: Unique identifier for each FritzDevice / each instance of the plugin - -### items.yaml - -#### avm_data_type -This attribute defines supported functions that can be set for an item. Full set see example below. -For most items, the avm_data_type can be bound to an instance via @... . Only in some points the items -are parsed as child items. In the example below there is a comment in the respective spots. - -### Example: - -```yaml -avm: - - uptime_7490: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: uptime - - uptime_1750: - type: num - visu_acl: ro - avm_data_type@wlan_repeater_1750: uptime - - serial_number_7490: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: serial_number - - serial_number_1750: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: serial_number - - firmware_7490: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: software_version - - firmware_1750: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: software_version - - hardware_version_7490: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: hardware_version - - hardware_version_1750: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: hardware_version - - myfritz: - type: bool - avm_data_type@fritzbox_7490: myfritz_status - - monitor: - - trigger1: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - trigger2: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - trigger3: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - trigger4: - type: bool - avm_data_type@fritzbox_7490: monitor_trigger - avm_incoming_allowed: xxxxxxxx - avm_target_number: xxxxxxxx - enforce_updates: 'yes' - - incoming: - - is_call_incoming: - type: bool - avm_data_type@fritzbox_7490: is_call_incoming - - duration: - type: num - avm_data_type@fritzbox_7490: call_duration_incoming - - last_caller: - type: str - avm_data_type@fritzbox_7490: last_caller_incoming - - last_calling_number: - type: str - avm_data_type@fritzbox_7490: last_number_incoming - - last_called_number: - type: str - avm_data_type@fritzbox_7490: last_called_number_incoming - - last_call_date: - type: str - avm_data_type@fritzbox_7490: last_call_date_incoming - - event: - type: str - avm_data_type@fritzbox_7490: call_event_incoming - - outgoing: - - is_call_outgoing: - type: bool - avm_data_type@fritzbox_7490: is_call_outgoing - - duration: - type: num - avm_data_type@fritzbox_7490: call_duration_outgoing - - last_caller: - type: str - avm_data_type@fritzbox_7490: last_caller_outgoing - - last_calling_number: - type: str - avm_data_type@fritzbox_7490: last_number_outgoing - - last_called_number: - type: str - avm_data_type@fritzbox_7490: last_called_number_outgoing - - last_call_date: - type: str - avm_data_type@fritzbox_7490: last_call_date_outgoing - - event: - type: str - avm_data_type@fritzbox_7490: call_event_outgoing - - newest: - - direction: - type: str - avm_data_type@fritzbox_7490: call_direction - cache: 'yes' - - event: - type: str - avm_data_type@fritzbox_7490: call_event - cache: 'yes' - - tam: - avm_tam_index@fritzbox_7490: 1 - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: tam - - name: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: tam_name - - message_number_old: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: tam_old_message_number - eval: (sh.avm.tam.message_number_total()-sh.avm.tam.message_number_new()) - eval_trigger: - - avm.tam.message_number_total - - avm.tam.message_number_new - - message_number_new: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: tam_new_message_number - - message_number_total: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: tam_total_message_number - - wan: - - connection_status: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wan_connection_status - - connection_error: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wan_connection_error - - is_connected: - type: bool - visu_acl: ro - avm_data_type@fritzbox_7490: wan_is_connected - - uptime: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_uptime - - ip: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wan_ip - - upstream: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_upstream - - downstream: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_downstream - - total_packets_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_packets_sent - - total_packets_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_packets_received - - current_packets_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_packets_sent - - current_packets_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_packets_received - - total_bytes_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_bytes_sent - - total_bytes_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_total_bytes_received - - current_bytes_sent: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_bytes_sent - - current_bytes_received: - type: num - visu_acl: ro - avm_data_type@fritzbox_7490: wan_current_bytes_received - - link: - type: bool - visu_acl: ro - avm_data_type@fritzbox_7490: wan_link - - wlan: - - gf_wlan_1: - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: wlanconfig # 2,4ghz - avm_wlan_index@fritzbox_7490: 1 - - gf_wlan_1_ssid: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wlanconfig_ssid # 2,4ghz - avm_wlan_index@fritzbox_7490: 1 - - gf_wlan_2: - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: wlanconfig # 5 GHz - avm_wlan_index@fritzbox_7490: 2 - - gf_wlan_3: - type: bool - visu_acl: rw - avm_data_type@fritzbox_7490: wlanconfig # Guest - avm_wlan_index@fritzbox_7490: 3 - - gf_wlan_3_ssid: - type: str - visu_acl: ro - avm_data_type@fritzbox_7490: wlanconfig_ssid # 2,4ghz - avm_wlan_index@fritzbox_7490: 3 - - gf_wlan_3_tr: - type: num - visu_acl: rw - avm_data_type@fritzbox_7490: wlan_guest_time_remaining # Guest - avm_wlan_index@fritzbox_7490: 3 - - uf_wlan_1: - type: bool - visu_acl: rw - avm_data_type@wlan_repeater_1750: wlanconfig # 2,4ghz - avm_wlan_index@wlan_repeater_1750: 1 - - uf_wlan_1_ssid: - type: str - visu_acl: ro - avm_data_type@wlan_repeater_1750: wlanconfig_ssid # 2,4ghz - avm_wlan_index@wlan_repeater_1750: 1 - - uf_wlan_2: - type: bool - visu_acl: rw - avm_data_type@wlan_repeater_1750: wlanconfig # 5 GHz - avm_wlan_index@wlan_repeater_1750: 2 - - uf_wlan_3: - type: bool - visu_acl: rw - avm_data_type@wlan_repeater_1750: wlanconfig # Guest - avm_wlan_index@wlan_repeater_1750: 3 - - devices: - - wlan_repeater_1750: - - GalaxyS5: - avm_mac@wlan_repeater_1750: xx:xx:xx:xx:xx:xx - avm_data_type@wlan_repeater_1750: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@wlan_repeater_1750: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@wlan_repeater_1750: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@wlan_repeater_1750: device_hostname - visu_acl: ro - - iPhone: - avm_mac@wlan_repeater_1750: xx:xx:xx:xx:xx:xx - avm_data_type@wlan_repeater_1750: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@wlan_repeater_1750: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@wlan_repeater_1750: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@wlan_repeater_1750: device_hostname - visu_acl: ro - - fritzbox_7490: - - iPad: - avm_mac@fritzbox_7490: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - hauptrechner: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - GalaxyS5: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - GalaxyTabS2: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - iPhone: - mac: xx:xx:xx:xx:xx:xx - avm_data_type@fritzbox_7490: network_device - type: bool - cache: 'yes' - visu_acl: ro - - # these items need to be child items from network_device - ip: - type: str - avm_data_type@fritzbox_7490: device_ip - visu_acl: ro - - # these items need to be child items from network_device - connection_type: - type: str - avm_data_type@fritzbox_7490: device_connection_type - visu_acl: ro - - # these items need to be child items from network_device - hostname: - type: str - avm_data_type@fritzbox_7490: device_hostname - visu_acl: ro - - dect: - - socket_living: - type: bool - avm_data_type@fritzbox_7490: aha_device - ain@fritzbox_7490: 14324 0432601 # has to be identical to id in fritzbox (also with spaces!) - visu_acl: rw - - # these items need to be child items from aha_device - energy: - avm_data_type@fritzbox_7490: energy - type: num - visu_acl: ro - - # these items need to be child items from aha_device - power: - avm_data_type@fritzbox_7490: power - type: num - sqlite: 'yes' - enforce_updates: 'true' - visu_acl: ro - eval: value / 100 - - # these items need to be child items from aha_device - temperature: - avm_data_type@fritzbox_7490: temperature - type: num - visu_acl: ro - - socket_office: - type: bool - avm_data_type@fritzbox_7490: aha_device - ain@fritzbox_7490: 03456 0221393 # has to be identical to id in fritzbox (also with spaces!) - visu_acl: rw - - # these items need to be child items from aha_device - energy: - avm_data_type@fritzbox_7490: energy - type: num - visu_acl: ro - - # these items need to be child items from aha_device - power: - avm_data_type@fritzbox_7490: power - type: num - sqlite: 'yes' - enforce_updates: 'true' - visu_acl: ro - eval: value / 100 - - # these items need to be child items from aha_device - temperature: - avm_data_type@fritzbox_7490: temperature - type: num - visu_acl: ro - - hkr_bathroom: - # Current hkr state: 0 = closed, 1: open, 2: temperature controlled, 3: error - type: num - value: 3 - avm_data_type@fritzbox_7490: hkr_device - ain@fritzbox_7490: 09995 0191234 # has to be identical to id in fritzbox (also with spaces!) - visu_acl: ro - - # these items need to be child items from hkr_device. They are read only items. - is_temperature: - value: -1 - avm_data_type@fritzbox_7490: temperature - type: num - visu_acl: ro - - set_temperature_reduced: - value: -1 - avm_data_type@fritzbox_7490: set_temperature_reduced - type: num - visu_acl: ro - - set_temperature_comfort: - value: -1 - avm_data_type@fritzbox_7490: set_temperature_comfort - type: num - visu_acl: ro - - # these items are also mandatory and used to read and write the setpoint temperature - set_temperature: - value: -1 - avm_data_type@fritzbox_7490: set_temperature - type: num - visu_acl: rw - - set_hkrwindowopen: - value: False - avm_data_type@fritzbox_7490: set_hkrwindowopen - type: bool - visu_acl: rw - enforce_updates: true - -``` - -## Functions - -### get_phone_name -Get the phone name at a specific index. The returend value can be used as phone_name for set_call_origin. Parameter is an INT, starting from 1. In case an index does not exist, an error is logged. -The used function X_AVM-DE_GetPhonePort() does not deliver analog connections like FON 1 and FON 2 (BUG in AVM Software). - -```python -phone_name = sh.fb1.get_phone_name(1) -``` - -CURL for this function: - -```bash -curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/x_voip" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:X_VoIP:1#X_AVM-DE_GetPhonePort" -d "1" -s -k -``` - -###get_call_origin - -Gets the phone name, currently set as call_origin. - -```python -phone_name = sh.fritzbox_7490.get_call_origin() -``` - -CURL for this function: -```bash -curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/x_voip" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:X_VoIP:1#X_AVM-DE_DialGetConfig" -d "" -s -k -``` - -### set_call_origin(phone_name) - -Sets the origin of a call. E.g. a DECT phone. Typically set before using "start_call". -You can also set the origin on your FritzDevice via "Telefonie -> Anrufe -> Wählhilfe verwenden -> Verbindung mit dem Telefon". -The used function X_AVM-DE_SetDialConfig() does not allow the configuration of analog connections (BUG in AVM Software). - -```python -sh.fb1.set_call_origin("") -``` - -### start_call(phone_number) -This function starts a call. Parameter can be an external or internal number. - -```python -sh.fb1.start_call('0891234567') -sh.fb1.start_call('**9') -``` - -### cancel_call() -This function cancels a running call. - -### wol(mac_address) -This function executes a wake on lan command to the specified MAC address - -### reconnect() -This function reconnects the WAN (=internet) connection. - -### reboot() -This function reboots the FritzDevice. - -### get_hosts(only_active) -Gets the data of get_host_details for all hosts as array. If only_active is True, only active hosts are returned. - -Example of a logic which is merging hosts of three devices into one list and rendering them to an HTML list, which is written to the item -'avm.devices.device_list' - -```python -hosts = sh.fritzbox_7490.get_hosts(True) -hosts_300 = sh.wlan_repeater_300.get_hosts(True) -hosts_1750 = sh.wlan_repeater_1750.get_hosts(True) - -for host_300 in hosts_300: - new = True - for host in hosts: - if host_300['mac_address'] == host['mac_address']: - new = False - if new: - hosts.append(host_300) -for host_1750 in hosts_1750: - new = True - for host in hosts: - if host_1750['mac_address'] == host['mac_address']: - new = False - if new: - hosts.append(host_1750) - -string = '
    ' -for host in hosts: - device_string = '
  • '+host['name']+': '+host['ip_address']+', '+host['mac_address']+'
  • ' - string += device_string - -string += '
' -sh.avm.devices.device_list(string) -``` - -### get_host_details(index) -Gets the data of a host as dict: -dict keys: name, interface_type, ip_address, mac_address, is_active, lease_time_remaining - -### is_host_active(mac_address) -This function checks, if a device running on a given mac address is active on the FritzDevice. Can be used for presence detection. - -CURL for this function: -```bash -curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/hosts" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:Hosts:1#GetSpecificHostEntry" -d "XX:XX:XX:XX:XX:XX" -s -k -``` - -### get_contact_name_by_phone_number(phone_number) -This is a function to search for telephone numbers in the contacts stored on the devices phone book - -### get_phone_numbers_by_name(name) - -This is a function to search for contact names and retrieve the related telephone numbers - -Set an item with a html of all found numbers e.g. by: - -```python -result_numbers = sh.fritzbox_7490.get_phone_numbers_by_name('Mustermann') -result_string = '' -keys = {'work': 'Geschäftlich', 'home': 'Privat', 'mobile': 'Mobil', 'fax_work': 'Fax', 'intern': 'Intern'} -for contact in result_numbers: - result_string += '

'+contact+'

' - i = 0 - result_string += '' - while i < len(result_numbers[contact]): - number = result_numbers[contact][i]['number'] - type_number = keys[result_numbers[contact][i]['type']] - result_string += '' - i += 1 - result_string += '
' + type_number + ':' + number + '

' -sh.general_items.number_search_results(result_string) -``` - -### get_calllist() -Returns an array with calllist entries diff --git a/avm/_pv_1_6_5/__init__.py b/avm/_pv_1_6_5/__init__.py deleted file mode 100755 index d4ee56a0d..000000000 --- a/avm/_pv_1_6_5/__init__.py +++ /dev/null @@ -1,4136 +0,0 @@ -#!/usr/bin/env python3 -# -######################################################################### -# Copyright 2016 René Frieß rene.friess(a)gmail.com -# 2021 Michael Wenzel wenzel_michael(a)web.de -######################################################################### -# -# This file is part of SmartHomeNG. -# -# SmartHomeNG is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SmartHomeNG is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SmartHomeNG. If not, see . -# -######################################################################### - -import hashlib # for session id generation -import logging -import socket -import threading -import time -import requests - -from datetime import datetime -from json.decoder import JSONDecodeError -from time import mktime -from xml.dom import minidom -from requests.auth import HTTPDigestAuth -from requests.packages import urllib3 - -from lib.model.smartplugin import SmartPlugin -from lib.item import Items -from .webif import WebInterface - -""" -Definition of attribute value groups of avm_data_type -""" -_aha_ro_attributes = ['device_id', - 'manufacturer', - 'product_name', - 'fw_version', - 'connected', - 'device_name', - 'tx_busy', - 'device_functions', - 'current_temperature', - 'temperature_reduced', - 'temperature_comfort', - 'temperature_offset', - 'windowopenactiveendtime', - 'boost_active', - 'boostactiveendtime', - 'summer_active', - 'holiday_active', - 'battery_low', - 'lock', - 'device_lock', - 'errorcode', - 'switch_mode', - 'power', 'energy', - 'voltage', - 'humidity', - 'alert_state'] - -_aha_wo_attributes = ['set_target_temperature', - 'set_window_open', - 'set_hkr_boost', - 'battery_level', - 'set_simpleonoff', - 'set_level', - 'set_levelpercentage', - 'set_hue', - 'set_saturation', - 'set_colortemperature', - 'switch_toggle'] - -_aha_rw_attributes = ['target_temperature', - 'window_open', - 'hkr_boost', - 'simpleonoff', - 'level', - 'levelpercentage', - 'hue', - 'saturation', - 'colortemperature', - 'switch_state'] - -_aha_attributes = _aha_ro_attributes + _aha_wo_attributes + _aha_rw_attributes - -_avm_rw_attributes = ['wlanconfig', - 'tam', - 'aha_device', - 'deflection_enable'] - -_call_monitor_attributes_gen = ['call_event', - 'call_direction', - 'monitor_trigger'] - -_call_monitor_attributes_in = ['is_call_incoming', - 'last_caller_incoming', - 'last_call_date_incoming', - 'call_event_incoming', - 'last_number_incoming', - 'last_called_number_incoming'] - -_call_monitor_attributes_out = ['is_call_outgoing', - 'last_caller_outgoing', - 'last_call_date_outgoing', - 'call_event_outgoing', - 'last_number_outgoing', - 'last_called_number_outgoing'] - -_call_monitor_attributes = _call_monitor_attributes_gen + _call_monitor_attributes_in + _call_monitor_attributes_out - -_call_duration_attributes = ['call_duration_incoming', - 'call_duration_outgoing'] - -_wan_ip_connection_attributes = ['wan_connection_status', - 'wan_connection_error', - 'wan_is_connected', - 'wan_uptime', - 'wan_ip'] - -_tam_attributes = ['tam', - 'tam_name', - 'tam_new_message_number', - 'tam_total_message_number'] - -_wlan_config_attributes = ['wlanconfig', - 'wlanconfig_ssid', - 'wlan_guest_time_remaining'] - -_wan_common_interface_attributes = ['wan_total_packets_sent', - 'wan_total_packets_received', - 'wan_current_packets_sent', - 'wan_current_packets_received', - 'wan_total_bytes_sent', - 'wan_total_bytes_received', - 'wan_current_bytes_sent', - 'wan_current_bytes_received', - 'wan_link'] - -_fritz_device_attributes = ['uptime', - 'software_version', - 'hardware_version', - 'serial_number'] - -_host_attribute = ['network_device'] - -_host_child_attributes = ['device_ip', - 'device_connection_type', - 'device_hostname' - 'connection_status'] - -_host_attributes = _host_attribute + _host_child_attributes - -_deflection_attributes = ['deflections_details', - 'deflection_enable', - 'deflection_type', - 'deflection_number', - 'deflection_to_number', - 'deflection_mode', - 'deflection_outgoing', - 'deflection_phonebook_id'] - -_wan_dsl_interface_attributes = ['wan_upstream', - 'wan_downstream'] - -_aha_attributes_old = ['aha_device', - 'hkr_device'] - -_myfritz_attributes = ['myfritz_status'] - -_deprecated_attributes = ['temperature', - 'set_temperature_reduced', - 'set_temperature_comfort', - 'firmware_version', - 'aha_device', - 'hkr_device'] - - -class MonitoringService: - """ - Class which connects to the FritzBox service of the Callmonitor: http://www.wehavemorefun.de/fritzbox/Callmonitor - - | Can currently handle three items: - | - avm_data_type = is_call_incoming, type = bool - | - avm_data_type = last_caller, type = str - | - avm_data_type = last_call_date, type = str - """ - - def __init__(self, host, port, callback, call_monitor_incoming_filter, plugin_instance): - self._plugin_instance = plugin_instance - self._plugin_instance.logger.debug("starting monitoring service") - self._host = host - self._port = port - self._callback = callback - self._trigger_items = [] # items which can be used to trigger sth, e.g. a logic - self._items = [] # more general items for the call monitor - self._items_incoming = [] # items for incoming calls - self._items_outgoing = [] # items for outgoing calls - self._duration_item = dict() # 2 items, one for counting the incoming, one for counting the outgoing call duration - self._duration_item['call_duration_outgoing'] = None - self._duration_item['call_duration_incoming'] = None - self._call_active = dict() - self._listen_active = False - self._call_active['incoming'] = False - self._call_active['outgoing'] = False - self._call_incoming_cid = dict() - self._call_outgoing_cid = dict() - self._call_monitor_incoming_filter = call_monitor_incoming_filter - self.conn = None - self._listen_thread = None - - if self._plugin_instance.logger.isEnabledFor(logging.DEBUG): - self.debug_log = True - else: - self.debug_log = False - - def connect(self): - """ - Connects to the call monitor of the AVM device - """ - if self._listen_active: - self._plugin_instance.logger.debug("MonitoringService: Connect called while listen active") - return - - self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - self.conn.connect((self._host, self._port)) - _name = f'plugins.{self._plugin_instance.get_fullname()}.Monitoring_Service' - self._listen_thread = threading.Thread(target=self._listen, name=_name).start() - self._plugin_instance.logger.debug("MonitoringService: connection established") - except Exception as e: - self.conn = None - self._plugin_instance.logger.error( - f"MonitoringService: Cannot connect to {self._host} on port: {self._port}, CallMonitor activated by #96*5*? - Error: {e}") - return - - def disconnect(self): - """ - Disconnects from the call monitor of the AVM device - """ - self._plugin_instance.logger.debug("MonitoringService: disconnecting") - self._listen_active = False - self._stop_counter('incoming') - self._stop_counter('outgoing') - try: - self._listen_thread.join(1) - except Exception: - pass - - try: - self.conn.shutdown(2) - except Exception: - pass - - def reconnect(self): - """ - Reconnects to the call monitor of the AVM device - """ - self.disconnect() - self.connect() - - def register_item(self, item): - """ - Registers an item to the MonitoringService - - :param item: item to register - """ - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in _call_monitor_attributes_in: - self._items_incoming.append(item) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in _call_monitor_attributes_out: - self._items_outgoing.append(item) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'monitor_trigger': - self._trigger_items.append(item) - else: - self._items.append(item) - - def get_items(self): - return self._items - - def get_trigger_items(self): - return self._trigger_items - - def get_items_incoming(self): - return self._items_incoming - - def get_items_outgoing(self): - return self._items_outgoing - - def get_item_count_total(self): - """ - Returns number of added items (all items of MonitoringService service - - :return: number of items hold by the MonitoringService - """ - return len(self._items) + len(self._trigger_items) + len(self._items_incoming) + len(self._items_outgoing) - - def set_duration_item(self, item): - """ - Sets specific items which count the duration of an incoming or outgoing call - """ - self._duration_item[self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type').split("@")[0]] = item - - def _listen(self, recv_buffer=4096): - """ - Function which listens to the established connection. - """ - self._listen_active = True - buffer = "" - while self._listen_active: - data = self.conn.recv(recv_buffer) - if data == "": - self._plugin_instance.logger.error("CallMonitor connection not open anymore.") - else: - if self.debug_log: - self._plugin_instance.logger.debug(f"Data Received from CallMonitor: {data.decode('utf-8')}") - buffer += data.decode("utf-8") - while buffer.find("\n") != -1: - line, buffer = buffer.split("\n", 1) - if line: - self._parse_line(line) - - # time.sleep(1) - return - - def _start_counter(self, timestamp, direction): - """ - Start counter to measure duration of a call - """ - if direction == 'incoming': - self._call_connect_timestamp = time.mktime( - datetime.strptime(timestamp, "%d.%m.%y %H:%M:%S").timetuple()) - self._duration_counter_thread_incoming = threading.Thread(target=self._count_duration_incoming, - name=f"MonitoringService_Duration_Incoming_{self._plugin_instance.get_instance_name()}").start() - self._plugin_instance.logger.debug('Counter incoming - STARTED') - elif direction == 'outgoing': - self._call_connect_timestamp = time.mktime( - datetime.strptime(timestamp, "%d.%m.%y %H:%M:%S").timetuple()) - self._duration_counter_thread_outgoing = threading.Thread(target=self._count_duration_outgoing, - name=f"MonitoringService_Duration_Outgoing_{self._plugin_instance.get_instance_name()}").start() - self._plugin_instance.logger.debug('Counter outgoing - STARTED') - - def _stop_counter(self, direction): - """ - Stop counter to measure duration of a call, but only stop of thread is active - """ - - if self._call_active[direction]: - self._call_active[direction] = False - if self.debug_log: - self._plugin_instance.logger.debug(f'STOPPING {direction}') - try: - if direction == 'incoming': - self._duration_counter_thread_incoming.join(1) - elif direction == 'outgoing': - self._duration_counter_thread_outgoing.join(1) - except Exception: - pass - - def _count_duration_incoming(self): - """ - Count duration of incoming call and set item value - """ - self._call_active['incoming'] = True - while self._call_active['incoming']: - if not self._duration_item['call_duration_incoming'] is None: - duration = time.time() - self._call_connect_timestamp - self._duration_item['call_duration_incoming'](int(duration), self._plugin_instance.get_shortname()) - time.sleep(1) - - def _count_duration_outgoing(self): - """ - Count duration of outgoing call and set item value - """ - self._call_active['outgoing'] = True - while self._call_active['outgoing']: - if not self._duration_item['call_duration_outgoing'] is None: - duration = time.time() - self._call_connect_timestamp - self._duration_item['call_duration_outgoing'](int(duration), self._plugin_instance.get_shortname()) - time.sleep(1) - - def _parse_line(self, line): - """ - Parses a data set in the form of a line. - - Data Format: - Ausgehende Anrufe: datum;CALL;ConnectionID;Nebenstelle;GenutzteNummer;AngerufeneNummer;SIP+Nummer - Eingehende Anrufe: datum;RING;ConnectionID;Anrufer-Nr;Angerufene-Nummer;SIP+Nummer - Zustandegekommene Verbindung: datum;CONNECT;ConnectionID;Nebenstelle;Nummer; - Ende der Verbindung: datum;DISCONNECT;ConnectionID;dauerInSekunden; - - :param line: data line which is parsed - """ - if self.debug_log: - self._plugin_instance.logger.debug(line) - line = line.split(";") - - try: - if line[1] == "RING": - call_from = line[3] - call_to = line[4] - self._trigger(call_from, call_to, line[0], line[2], line[1], '') - elif line[1] == "CALL": - call_from = line[4] - call_to = line[5] - self._trigger(call_from, call_to, line[0], line[2], line[1], line[3]) - elif line[1] == "CONNECT": - self._trigger('', '', line[0], line[2], line[1], line[3]) - elif line[1] == "DISCONNECT": - self._trigger('', '', '', line[2], line[1], '') - except Exception as e: - self._plugin_instance.logger.error( - f"MonitoringService: {type(e).__name__} while handling Callmonitor response: {e}") - return - - def _trigger(self, call_from, call_to, time, callid, event, branch): - """ - Triggers the event: sets item values and looks up numbers in the phone book. - """ - - if self.debug_log: - self._plugin_instance.logger.debug( - f"Event: {event}, Call From: {call_from}, Call To: {call_to}, Time: {time}, CallID:{callid}") - # in each case set current call event and direction - for item in self._items: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_event': - item(event.lower(), self._plugin_instance.get_shortname()) - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_direction': - if event == 'RING': - item("incoming", self._plugin_instance.get_shortname()) - else: - item("outgoing", self._plugin_instance.get_shortname()) - - # call is incoming - if event == 'RING': - # process "trigger items" - for trigger_item in self._trigger_items: - if self._plugin_instance.get_iattr_value(trigger_item.conf, 'avm_data_type') == 'monitor_trigger': - trigger_item(0, self._plugin_instance.get_shortname()) - if self.debug_log: - self._plugin_instance.logger.debug( - f"{self._plugin_instance.get_iattr_value(trigger_item.conf, 'avm_data_type')} {trigger_item.conf['avm_incoming_allowed']} {trigger_item.conf['avm_target_number']}") - if 'avm_incoming_allowed' not in trigger_item.conf or 'avm_target_number' not in trigger_item.conf: - self._plugin_instance.logger.error( - "both 'avm_incoming_allowed' and 'avm_target_number' must be specified as attributes in a trigger item.") - elif trigger_item.conf['avm_incoming_allowed'] == call_from and trigger_item.conf['avm_target_number'] == call_to: - trigger_item(1, self._plugin_instance.get_shortname()) - - if self._call_monitor_incoming_filter in call_to: - # set call id for incoming call - self._call_incoming_cid = callid - - # reset duration for incoming calls - if not self._duration_item['call_duration_incoming'] is None: - self._duration_item['call_duration_incoming'](0, self._plugin_instance.get_shortname()) - - # process items specific to incoming calls - for item in self._items_incoming: # update items for incoming calls - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['is_call_incoming']: - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting is_call_incoming: {True}") - item(True, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_caller_incoming']: - if call_from != '' and call_from is not None: - name = self._callback(call_from) - if name != '' and name is not None: - item(name, self._plugin_instance.get_shortname()) - else: - item(call_from, self._plugin_instance.get_shortname()) - else: - item("Unbekannt", self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_call_date_incoming']: - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting last_call_date_incoming: {time}") - item(time, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_incoming']: - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_number_incoming']: - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting last_number_incoming: {call_from}") - item(call_from, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_called_number_incoming']: - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting last_called_number_incoming: {call_to}") - item(call_to, self._plugin_instance.get_shortname()) - - # call is outgoing - elif event == 'CALL': - # set call id for outgoing call - self._call_outgoing_cid = callid - - # reset duration for outgoing calls - self._duration_item['call_duration_outgoing'](0) - - # process items specific to outgoing calls - for item in self._items_outgoing: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['is_call_outgoing']: - item(True, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_caller_outgoing']: - name = self._callback(call_to) - if name != '' and name is not None: - item(name, self._plugin_instance.get_shortname()) - else: - item(call_to, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_call_date_outgoing']: - item(time, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_outgoing']: - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_number_outgoing']: - item(call_from, self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['last_called_number_outgoing']: - item(call_to, self._plugin_instance.get_shortname()) - - # connection established - elif event == 'CONNECT': - # handle OUTGOING calls - if callid == self._call_outgoing_cid: - if not self._duration_item[ - 'call_duration_outgoing'] is None: # start counter thread only if duration item set and call is outgoing - self._stop_counter('outgoing') # stop potential running counter for parallel (older) outgoing call - self._start_counter(time, 'outgoing') - for item in self._items_outgoing: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_outgoing']: - item(event.lower(), self._plugin_instance.get_shortname()) - - # handle INCOMING calls - elif callid == self._call_incoming_cid: - if not self._duration_item[ - 'call_duration_incoming'] is None: # start counter thread only if duration item set and call is incoming - self._stop_counter('incoming') # stop potential running counter for parallel (older) incoming call - if self.debug_log: - self._plugin_instance.logger.debug("Starting Counter for Call Time") - self._start_counter(time, 'incoming') - for item in self._items_incoming: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') in ['call_event_incoming']: - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") - item(event.lower(), self._plugin_instance.get_shortname()) - - # connection ended - elif event == 'DISCONNECT': - # handle OUTGOING calls - if callid == self._call_outgoing_cid: - for item in self._items_outgoing: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_event_outgoing': - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'is_call_outgoing': - item(False, self._plugin_instance.get_shortname()) - if not self._duration_item['call_duration_outgoing'] is None: # stop counter threads - self._stop_counter('outgoing') - self._call_outgoing_cid = None - - # handle INCOMING calls - elif callid == self._call_incoming_cid: - for item in self._items_incoming: - if self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'call_event_incoming': - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting call_event_incoming: {event.lower()}") - item(event.lower(), self._plugin_instance.get_shortname()) - elif self._plugin_instance.get_iattr_value(item.conf, 'avm_data_type') == 'is_call_incoming': - if self.debug_log: - self._plugin_instance.logger.debug(f"Setting is_call_incoming: {False}") - item(False, self._plugin_instance.get_shortname()) - if not self._duration_item['call_duration_incoming'] is None: # stop counter threads - if self.debug_log: - self._plugin_instance.logger.debug("Stopping Counter for Call Time") - self._stop_counter('incoming') - self._call_incoming_cid = None - - -class FritzDevice: - """ - This class encapsulates information related to a specific FritzDevice, such has host, port, ssl, username, password, or related items - """ - - def __init__(self, host, port, ssl, username, password, identifier='default'): - self.logger = logging.getLogger(__name__) - self._host = host - self._port = port - self._ssl = ssl - self._username = username - self._password = password - self._identifier = identifier - self._available = True - self._items = [] - self._smarthome_items = [] - self._smarthome_devices = {} - - def get_identifier(self): - """ - Returns the internal identifier of the FritzDevice - - :return: identifier of the device, as set in plugin.conf - """ - return self._identifier - - def get_host(self): - """ - Returns the hostname / IP of the FritzDevice - - :return: hostname of the device, as set in plugin.conf - """ - return self._host - - def get_port(self): - """ - Returns the port of the FritzDevice - - :return: port of the device, as set in plugin.conf - """ - return self._port - - def get_items(self): - """ - Returns added items - - :return: array of items hold by the device - """ - return self._items - - def get_item_count(self): - """ - Returns number of added items - - :return: number of items hold by the device - """ - return len(self._items) - - def is_ssl(self): - """ - Returns information if SSL is enabled - - :return: is ssl enabled, as set in plugin.conf - """ - return self._ssl - - def is_available(self): - """ - Returns information if the device is currently available - - :return: boolean, if device is available - """ - return self._available - - def set_available(self, is_available): - """ - Sets the boolean, if the device is available - - :param is_available: boolean of the availability status - """ - self._available = is_available - - def get_user(self): - """ - Returns the user for the FritzDevice - - :return: user, as set in plugin.conf - """ - return self._username - - def get_password(self): - """ - Returns the password for the FritzDevice - - :return: password, as set in plugin.conf - """ - return self._password - - def get_smarthome_items(self): - """ - Returns added items - - :return: array of smarthome items hold by the plugin - """ - return self._smarthome_items - - def get_smarthome_devices(self): - """ - Returns added items - - :return: dict of smarthome devices hold by the plugin - """ - return self._smarthome_devices - - -class AVM(SmartPlugin): - """ - Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the different TR-064 services on the FritzDevice - """ - - PLUGIN_VERSION = "1.6.6" - - _header = {'SOAPACTION': '', 'CONTENT-TYPE': 'text/xml; charset="utf-8"'} - - _envelope = """ - - %s - - """ - _body = """ - - %(arguments)s - - - """ - _argument = """ - %(value)s""" - - _urn_map = dict([('WLANConfiguration', 'urn:dslforum-org:service:WLANConfiguration:%s'), - # index needs to be adjusted from 1 to 3 - ('WANCommonInterfaceConfig', 'urn:dslforum-org:service:WANCommonInterfaceConfig:1'), - ('WANCommonInterfaceConfig_alt', 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1'), - ('WANIPConnection', 'urn:schemas-upnp-org:service:WANIPConnection:1'), - ('TAM', 'urn:dslforum-org:service:X_AVM-DE_TAM:1'), - ('OnTel', 'urn:dslforum-org:service:X_AVM-DE_OnTel:1'), - ('Homeauto', 'urn:dslforum-org:service:X_AVM-DE_Homeauto:1'), - ('Hosts', 'urn:dslforum-org:service:Hosts:1'), - ('X_VoIP', 'urn:dslforum-org:service:X_VoIP:1'), - ('DeviceConfig', 'urn:dslforum-org:service:DeviceConfig:1'), - ('DeviceInfo', 'urn:dslforum-org:service:DeviceInfo:1'), - ('WANDSLInterfaceConfig', 'urn:dslforum-org:service:WANDSLInterfaceConfig:1'), - ('MyFritz', 'urn:dslforum-org:service:X_AVM-DE_MyFritz:1')]) - - _login_sid_route = "/login_sid.lua?version=2" - - def __init__(self, sh): - """ - Initializes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf. - """ - - # Call init code of parent class (SmartPlugin) - super().__init__() - - self.logger.info('Init AVM Plugin') - - self._session = requests.Session() - self._lua_session = requests.Session() - self._timeout = 10 - self._verify = self.get_parameter_value('verify') - self._response_cache = dict() # Response Cache: Dictionary for storing the result of requests which is used for several different items, refreshed each update cycle. Please use distinct keys! - self._calllist_cache = [] # - self._host_info = dict() # Dict to hold basic info of that host, gathered at startup - - ssl = self.get_parameter_value('ssl') - if ssl and not self._verify: - urllib3.disable_warnings() - - self._fritz_device = FritzDevice(self.get_parameter_value('host'), self.get_parameter_value('port'), ssl, - self.get_parameter_value('username'), self.get_parameter_value('password'), - self.get_instance_name()) - - self._call_monitor = self.to_bool(self.get_parameter_value('call_monitor')) - if self._call_monitor: - self._monitoring_service = MonitoringService(self._fritz_device.get_host(), 1012, - self.get_contact_name_by_phone_number, - self.get_parameter_value('call_monitor_incoming_filter'), self) - self._monitoring_service.connect() - else: - self._monitoring_service = None - - self._call_monitor_incoming_filter = self.get_parameter_value('call_monitor_incoming_filter') - - self.aha_http_interface = self.get_parameter_value('avm_home_automation') - self._cycle = int(self.get_parameter_value('cycle')) - self.webif_pagelength = self.get_parameter_value('webif_pagelength') - - # Enable / Disable debug log generation depending on log level - if self.logger.isEnabledFor(logging.DEBUG): - self.debug_log = True - else: - self.debug_log = False - - if self.debug_log: - self.logger.debug( - f"Plugin initialized with host: {self._fritz_device.get_host()}, port: {self._fritz_device.get_port()}, ssl: {self._fritz_device.is_ssl()}, verify: {self._verify}, user: {self._fritz_device.get_user()}, call_monitor: {self._call_monitor}") - - self.alive = False - self.sid = None - - self.init_webinterface(WebInterface) - # if not self.init_webinterface(): - # self._init_complete = False - - def run(self): - """ - Run method for the plugin - """ - - # add scheduler für update_loop - self.scheduler_add('update', self._update_loop, prio=5, cycle=self._cycle, offset=2) - - # get infos about host to be able to start methods depending on that (e.g. if Device is Fritzbox, check für AHA-Interface) - self._get_host_device_info() - - # add scheduler for checking validity of session id - if self.aha_http_interface: - self.scheduler_add('check_sid', self._check_sid, prio=5, cycle=900, offset=30) - - # set plugin alive to True - self.alive = True - - def stop(self): - """ - Stop method for the plugin - """ - - if self._call_monitor: - self._monitoring_service.disconnect() - - self.scheduler_remove('update') - - if self.aha_http_interface: - self.scheduler_remove('check_sid') - - if self.sid: - self._http_logout_request() - - self.alive = False - - def _assemble_soap_data(self, action, service, argument=''): - """ - Builds the soap data set (from body and envelope templates for a given request. - - :param action: string of the action - :type action: str - :param service: string of the service - :type service: str - :param argument: dictionary (name: value) of arguments - :type argument: dict - :return: string of the soap data - :type return: str - """ - - argument_string = '' - if argument: - arguments = [ - self._argument % {'name': name, 'value': value} - for name, value in argument.items() - ] - argument_string = argument_string.join(arguments) - body = self._body.strip() % {'action': action, 'service': service, 'arguments': argument_string} - soap_data = self._envelope.strip() % body - return soap_data - - def _build_url(self, suffix, lua=False): - """ - Builds a request url - - :param suffix: url suffix, e.g. "/upnp/control/x_tam" - :return: string of the url, dependent on settings of the FritzDevice - """ - - if self._fritz_device.is_ssl(): - url_prefix = "https" - else: - url_prefix = "http" - if not lua: - url = f"{url_prefix}://{self._fritz_device.get_host()}:{self._fritz_device.get_port()}{suffix}" - else: - url = f"{url_prefix}://{self._fritz_device.get_host()}{suffix}" - - return url - - def _update_loop(self): - """ - Starts the update loop for all known items. - """ - - if self.debug_log: - self.logger.debug(f'Starting update loop for instance {self._fritz_device.get_identifier()}') - # Update item values using TR-064 interface - for item in self._fritz_device.get_items(): - if self.debug_log: - self.logger.debug(f'Request Update for {item}') - - if not self.alive: - return - - avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') - if avm_data_type in _wan_ip_connection_attributes: - self._update_wan_ip_connection(item) - elif avm_data_type in _tam_attributes: - self._update_tam(item) - elif avm_data_type in _aha_attributes_old: - self._update_home_automation(item) - elif avm_data_type in _wlan_config_attributes: - self._update_wlan_config(item) - elif avm_data_type in _wan_common_interface_attributes: - self._update_wan_common_interface_configuration(item) - elif avm_data_type in _host_attribute: - self._update_host(item) - elif avm_data_type in _fritz_device_attributes: - self._update_fritz_device_info(item) - elif avm_data_type in _wan_dsl_interface_attributes: - self._update_wan_dsl_interface_config(item) - elif avm_data_type in _myfritz_attributes: - self._update_myfritz(item) - elif avm_data_type in ['number_of_deflections']: - self._update_number_of_deflections(item) - elif avm_data_type in ['deflection_details']: - self._update_deflection(item) - elif avm_data_type in _deflection_attributes: - self._update_deflections(item) - elif avm_data_type in ['deflection']: - self._update_deflection_status(item) - elif avm_data_type in ['product_class']: - item(self._host_info['product_class'], self.get_shortname()) - elif avm_data_type in ['manufacturer']: - item(self._host_info['manufacturer'], self.get_shortname()) - elif avm_data_type in ['model']: - item(self._host_info['model'], self.get_shortname()) - elif avm_data_type in ['description']: - item(self._host_info['description'], self.get_shortname()) - - # clean TR-064 response cache - self._response_cache = dict() - - # update internal dict holding information of smarthome-devices queried via aha-http-interface if host is fritzbox and aha-http_interface is enabled for plugin instance - if self.aha_http_interface and 'box' in self._host_info['model'].lower(): - self._update_aha_devices() - - if self._call_monitor: - if not self.alive: - return - if self._fritz_device.is_available(): - self._monitoring_service.connect() - - def get_fritz_device(self): - """ - Return _fritz_device - """ - - return self._fritz_device - - def get_monitoring_service(self): - """ - Return _monitoring_service - """ - - return self._monitoring_service - - def set_device_availability(self, availability): - """ - Set device availability - """ - - self._fritz_device.set_available(availability) - if self.debug_log: - self.logger.debug(f'Availability for FritzDevice set to {availability}') - if not availability and self._call_monitor: - self._monitoring_service.disconnect() - elif availability and self._call_monitor and self.alive: - self._monitoring_service.connect() - - def get_calllist_from_cache(self): - """ - returns the cached calllist when all items are initialized. The filter set by plugin.conf is applied. - - :return: Array of calllist entries - """ - - # request and cache calllist - if self._calllist_cache is None: - self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) - elif len(self._calllist_cache) == 0: - self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) - return self._calllist_cache - - def parse_item(self, item): - """ - Default plugin parse_item method. Is called when the plugin is initialized. Selects each item corresponding to - the AVM identifier and adds it to an internal array - - :param item: The item to process. - """ - - # get avm_data_type - avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') - - # Deprecated warning for old avm_data_types: - if avm_data_type in _deprecated_attributes: - self.logger.warning( - f"Item {item.id()} uses deprecated avm_data_type attribute. Please consider to switch to avm_data_type for new Fritz AHA interface") - - # handle items specific to call monitor - if avm_data_type in _call_monitor_attributes: - # initially - if item empty - get data from calllist - if avm_data_type == 'last_caller_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - if 'Name' in element: - item(element['Name'], self.get_shortname()) - else: - item(element['Caller'], self.get_shortname()) - break - elif avm_data_type == 'last_number_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - if 'Caller' in element: - item(element['Caller'], self.get_shortname()) - else: - item("", self.get_shortname()) - break - elif avm_data_type == 'last_called_number_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - item(element['CalledNumber'], self.get_shortname()) - break - elif avm_data_type == 'last_call_date_incoming' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - date = str(element['Date']) - date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self.get_shortname()) - break - elif avm_data_type == 'call_event_incoming' and item() == '': - item('disconnect', self.get_shortname()) - elif avm_data_type == 'is_call_incoming' and item() == '': - item(0, self.get_shortname()) - elif avm_data_type == 'last_caller_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - if 'Name' in element: - item(element['Name'], self.get_shortname()) - else: - item(element['Called'], self.get_shortname()) - break - elif avm_data_type == 'last_number_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - if 'Caller' in element: - item(''.join(filter(lambda x: x.isdigit(), element['Caller'])), self.get_shortname()) - else: - item("", self.get_shortname()) - break - elif avm_data_type == 'last_called_number_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - item(element['Called'], self.get_shortname()) - break - elif avm_data_type == 'last_call_date_outgoing' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - date = str(element['Date']) - date = date[8:10] + "." + date[5:7] + "." + date[2:4] + " " + date[11:19] - item(date, self.get_shortname()) - break - elif avm_data_type == 'call_event_outgoing' and item() == '': - item('disconnect', self.get_shortname()) - elif avm_data_type == 'is_call_outgoing' and item() == '': - item(0, self.get_shortname()) - elif avm_data_type == 'call_event' and item() == '': - item('disconnect', self.get_shortname()) - elif avm_data_type == 'call_direction' and item() == '': - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - item('incoming', self.get_shortname()) - break - if element['Type'] in ['3', '4']: - item('outgoing', self.get_shortname()) - break - if self._call_monitor: - if self._monitoring_service is not None: - self._monitoring_service.register_item(item) - - # handle items specific to call-duration - elif avm_data_type in _call_duration_attributes: - # items specific to call monitor duration calculation - # initially get data from calllist - if avm_data_type == 'call_duration_incoming' and item() == 0: - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['1', '2']: - duration = element['Duration'] - duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self.get_shortname()) - break - elif avm_data_type == 'call_duration_outgoing' and item() == 0: - if not self.get_calllist_from_cache() is None: - for element in self.get_calllist_from_cache(): - if element['Type'] in ['3', '4']: - duration = element['Duration'] - duration = int(duration[0:1]) * 3600 + int(duration[2:4]) * 60 - item(duration, self.get_shortname()) - break - if self._monitoring_service is not None: - self._monitoring_service.set_duration_item(item) - - # handle smarthome items using aha-interface (old / new) - elif avm_data_type in (_aha_attributes + _aha_attributes_old): - if self._get_item_ain(item) is not None: - if self.debug_log: - self.logger.debug( - f"Item {item.id()} with avm smarthome attribute and defined AIN found; append to list.") - self._fritz_device.get_smarthome_items().append(item) - else: - self.logger.warning( - f"Item {item.id()} with avm smarthome attribute found, but AIN is not defined; Item will be ignored.") - - # handle network_device related items - elif avm_data_type in _host_attribute: - if self.has_iattr(item.conf, 'avm_mac'): - if self.debug_log: - self.logger.debug( - f"Item {item.id()} with avm attribute 'network_device' and defined 'avm_mac' found; append to list.") - self._fritz_device.get_items().append(item) - else: - self.logger.warning( - f"Item {item.id()} with avm attribute found, but 'avm_mac' is not defined; Item will be ignored.") - - # handle network_device related items (mac address in parent items defined) - elif avm_data_type in _host_child_attributes: - avm_mac = self._get_mac(item) - if avm_mac: - if self.debug_log: - self.logger.debug( - f"Item {item.id()} with avm device attribute and defined 'avm_mac' found; append to list.") - self._fritz_device.get_items().append(item) - else: - self.logger.warning( - f"Item {item.id()} with avm attribute found, but 'avm_mac' is not defined in parent item; Item will be ignored.") - - # handle wlan related items - elif avm_data_type in _wlan_config_attributes: - avm_wlan_index = self._get_wlan_index(item) - if avm_wlan_index is not None: - if self.debug_log: - self.logger.debug( - f"Item {item.id()} with avm device attribute and defined 'avm_wlan_index' found; append to list.") - self._fritz_device.get_items().append(item) - else: - self.logger.warning( - f"Item {item.id()} with avm attribute found, but 'avm_wlan_index' is not defined; Item will be ignored.") - - # handle tam related items - elif avm_data_type in _tam_attributes: - avm_tam_index = self._get_tam_index(item) - if avm_tam_index is not None: - if self.debug_log: - self.logger.debug( - f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") - self._fritz_device.get_items().append(item) - else: - self.logger.warning( - f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") - - # handle remaining items not needing further attribute - elif self.has_iattr(item.conf, 'avm_data_type'): - if self.debug_log: - self.logger.debug(f"Item {item.id()} with avm attribute found; append to list") - self._fritz_device.get_items().append(item) - - # items which can be changed outside the plugin context and need to be submitted to the FritzDevice - if avm_data_type in (_avm_rw_attributes + _aha_wo_attributes + _aha_rw_attributes): - return self.update_item - - def _get_sid(self): - """ - Get a sid by solving the PBKDF2 (or MD5) challenge-response process. - """ - - self.logger.debug(f"HTTP Login requested, getting Session-ID") - - username = self._fritz_device.get_user() - password = self._fritz_device.get_password() - url = self._build_url(self._login_sid_route, lua=True) - - try: - response = self._request(url) - if self.debug_log: - self.logger.debug(f"Debug apriori Session request response text: {response}") - sid, challenge, blocktime = self._get_login_infos_from_http_request(response) - if self.debug_log: - self.logger.debug(f"Debug apriori SID: {sid}, Challenge: {challenge}, BlockTime: {blocktime}") - except Exception as ex: - self.logger.warning(f"failed to get challenge, error={ex}") - return - - if challenge.startswith('2$'): - if self.debug_log: - self.logger.debug("PBKDF2 supported") - challenge_response = self._calculate_pbkdf2_response(challenge, password) - else: - if self.debug_log: - self.logger.debug("Falling back to MD5") - challenge_response = self._calculate_md5_response(challenge, password) - if blocktime > 0: - if self.debug_log: - self.logger.debug(f"Waiting for {blocktime} seconds...") - time.sleep(blocktime) - - try: - if self.debug_log: - self.logger.debug('Sending response...') - params = {"username": username, "response": challenge_response} - response = self._request(url, params=params) - if self.debug_log: - self.logger.debug(f"Debug posterior Session request response text: {response}") - sid, challenge, blocktime = self._get_login_infos_from_http_request(response) - if self.debug_log: - self.logger.debug(f"Debug posterior SID: {sid}, Challenge: {challenge}, BlockTime: {blocktime}") - except Exception as ex: - self.logger.warning(f"failed to login, error={ex}") - return - - if sid == "0000000000000000": - self.logger.warning(f"wrong username or password") - return - - self.sid = sid - - def _get_login_infos_from_http_request(self, response): - """ - Get login info from http request response - """ - - xml = minidom.parseString(response) - sid = self._get_value_from_xml_node(xml, 'SID') - challenge = self._get_value_from_xml_node(xml, 'Challenge') - blocktime = int(self._get_value_from_xml_node(xml, 'BlockTime')) - - if self.debug_log: - self.logger.debug(f"_get_login_infos_from_http_request: sid={sid}, challenge={challenge}, blocktime={blocktime}") - - return sid, challenge, blocktime - - def _http_logout_request(self): - """ - Send a logout request. - """ - - if self.debug_log: - self.logger.warning(f"_http_logout_request called") - - url = self._build_url(self._login_sid_route, lua=True) - params = {"logout": "1", "sid": self.sid} - response = self._request(url, params=params) - sid, challenge, blocktime = self._get_login_infos_from_http_request(response) - - if self.debug_log: - self.logger.warning(f"_http_logout_request: SID: {sid}, Challenge: {challenge}, BlockTime: {blocktime}") - - if sid == "0000000000000000": - self.logger.warning(f"HTTP Logout successful.") - self.sid = None - - def _check_sid(self): - """ - Check if known Session ID is still valid - """ - - if self.debug_log: - self.logger.debug(f"_check_sid called") - - url = self._build_url(self._login_sid_route, lua=True) - params = {"sid": self.sid} - response = self._request(url, params) - sid, challenge, blocktime = self._get_login_infos_from_http_request(response) - if self.debug_log: - self.logger.debug(f"_check_sid: SID: {sid}, Challenge: {challenge}, BlockTime: {blocktime}") - - if sid == "0000000000000000": - self.logger.warning(f"Session ID is invalid. Trying to generate new one.") - self._get_sid() - else: - self.logger.info(f"Session ID is still valid.") - - @staticmethod - def _calculate_pbkdf2_response(challenge: str, password: str) -> str: - """ - Calculate the response for a given challenge via PBKDF2 - """ - - challenge_parts = challenge.split("$") - # Extract all necessary values encoded into the challenge - iter1 = int(challenge_parts[1]) - salt1 = bytes.fromhex(challenge_parts[2]) - iter2 = int(challenge_parts[3]) - salt2 = bytes.fromhex(challenge_parts[4]) - # Hash twice, once with static salt... - hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1) - # Once with dynamic salt. - hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2) - return f"{challenge_parts[4]}${hash2.hex()}" - - @staticmethod - def _calculate_md5_response(challenge: str, password: str) -> str: - """ - Calculate the response for a challenge using legacy MD5 - """ - - response = challenge + "-" + password - # the legacy response needs utf_16_le encoding - response = response.encode("utf_16_le") - md5_sum = hashlib.md5() - md5_sum.update(response) - response = challenge + "-" + md5_sum.hexdigest() - return response - - def _get_lua_post_request(self, url, data, headers): - """ - Do Lua POST request - """ - - try: - self._lua_session.post(url, data=data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error( - f"Exception when sending LUA POST request for updating item towards the FritzDevice: {e}") - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - def _get_post_request(self, url, data, headers): - """ - Do POST request - """ - - try: - response = self._session.post(url, data=data, timeout=self._timeout, headers=headers, - auth=HTTPDigestAuth(self._fritz_device.get_user(), - self._fritz_device.get_password()), - verify=self._verify) - except Exception as e: - self.logger.error(f'Exception while sending POST request: {e}') - if self._fritz_device.is_available(): - self.set_device_availability(False) - return - else: - if response.status_code == 200: - self.logger.debug("Sending POST request successful") - if not self._fritz_device.is_available(): - self.set_device_availability(True) - return response - else: - try: - response.raise_for_status() - except requests.exceptions.HTTPError as e: - status_code = e.response.status_code - start = data.find('NewMACAddress') + 14 - mac = data[start:start + 17] - self.logger.warning( - f'Error code {status_code} with Exception {e} while sending POST request. Check correctness of MAC-addresses {mac} in item.yaml') - if self._fritz_device.is_available(): - self.set_device_availability(False) - return - - def _get_post_request_as_xml(self, url, data, headers): - """ - Get POST request response as xml - """ - - response = self._get_post_request(url, data, headers) - if response is None: - return - - try: - xml = minidom.parseString(response.content) - except Exception as e: - self.logger.error(f'Exception while parsing response: {e}') - return - else: - return xml - - def update_item(self, item, caller=None, source=None, dest=None): - """ - | Write items values - in case they were changed from somewhere else than the AVM plugin (=the FritzDevice) to - | the FritzDevice. - - | Uses: - | - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_tam.pdf - | - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf - | - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_homeauto.pdf - - :param item: item to be updated towards the FritzDevice (Supported item avm_data_types: wlanconfig, tam, aha_device) - :param caller: caller - :param source: source - :param dest: destination - """ - - if self.alive and caller != self.get_shortname(): - - # get avm_data_type and readafterwrite - avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') - readafterwrite = None - if self.has_iattr(item.conf, 'avm_read_after_write'): - readafterwrite = int(self.get_iattr_value(item.conf, 'avm_read_after_write')) - if self.debug_log: - self.logger.debug( - f'Attempting read after write for item: {item.id()}, avm_data_type: {avm_data_type}, delay: {readafterwrite}s') - - # handle wlanconfig attributes - if avm_data_type == 'wlanconfig': - # get wlan_index - wlan_index = self._get_wlan_index(item) - - # set wlan config - self.set_wlan_config(wlan_index, bool(item())) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_wlan_config(item) - - # handle tam attributes - elif avm_data_type == 'tam': - # get tam_index - tam_index = self._get_tam_index(item) - - # set tam - if not tam_index or tam_index <= 0: - self.logger.error('Parameter for item not defined in item.conf') - else: - self.set_tam(tam_index, bool(item())) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_tam(item) - - # handle aha_device - elif avm_data_type == 'aha_device': - # get ain - ain = self._get_item_ain(item) - - # set aha-device - if not ain: - self.logger.error('Parameter or for item not defined in item.conf') - else: - self.set_aha_device(ain, bool(item())) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_home_automation(item) - - # handle deflection_enable - elif avm_data_type == 'deflection_enable': - # get deflection index - deflection_index = self._get_deflection_index(item) - - # set deflection - if not deflection_index or deflection_index <= 0: - self.logger.error('Parameter for item not defined in item.conf') - else: - self.set_deflection(deflection_index, bool(item())) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_deflection_status(item) - - # handle hkr window_open - elif avm_data_type in ['set_window_open', 'window_open']: - new_value = bool(item()) - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; new value to be set is {new_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # assemble endtimestamp: - if new_value is False: - endtime = 0 - else: - now = self.shtime.now() - unix_secs = mktime(now.timetuple()) - # set endtime to now + 12h: - endtime = int(unix_secs + 12 * 3600) - if self.debug_log: - self.logger.debug(f"HKR endtimestamp is: {endtime}") - - # write new value - self.set_hkr_windowopen(ain_device, endtime) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle hkr target temperature - elif avm_data_type in ['set_target_temperature', 'target_temperature']: - new_value = float(item()) - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; new value to be set is {new_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # write new target temp - self.set_target_temperature(ain_device, new_value) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle hkr boost mode - elif avm_data_type in ['set_hkr_boost', 'hkr_boost']: - new_value = bool(item()) - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; new value to be set is {new_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # Assemble endtimestamp: - if new_value is False: - endtime = 0 - else: - now = self.shtime.now() - unix_secs = mktime(now.timetuple()) - # set endtime to now + 12h: - endtime = int(unix_secs + 12 * 3600) - if self.debug_log: - self.logger.debug(f"HKR boost endtimestamp is: {endtime}") - - # write new value - self.set_hkr_boost(ain_device, endtime) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle light on/off - elif avm_data_type in ['set_simpleonoff', 'simpleonoff']: - new_value = bool(item()) - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; new value to be set is {new_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # write value - self.set_switch_onoff(ain_device, new_value) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle level - elif avm_data_type in ['set_level', 'level']: - new_value = int(item()) - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; new value to be set is {new_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # write value - self.set_level(ain_device, new_value) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle level percentage - elif avm_data_type in ['set_levelpercentage', 'levelpercentage']: - cmd_level = int(item()) - if self.debug_log: - self.logger.debug(f"set_level caller is: {caller}; switch to be set to level: {cmd_level}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # write value - self.set_level_percentage(ain_device, cmd_level) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle socket switch/relais on/off - elif avm_data_type == 'switch_state': - new_value = bool(item()) - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; new value to be set is {new_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # write value - if new_value is True: - self.set_switch_on(ain_device) - else: - self.set_switch_off(ain_device) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle socket switch/relais toggle - elif avm_data_type == 'switch_toggle': - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; switch will be toggled") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # write value - self.set_switch_toggle(ain_device) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle set hue - elif avm_data_type in ['set_hue', 'hue']: - hue_value = int(item()) - if self.debug_log: - self.logger.debug(f"{avm_data_type} caller is: {caller}; new value to be set is {hue_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # Full RGB hue will be supported by Fritzbox approximately from Q2 2022 on: - # Currently, only use default RGB colors that are supported by default (getcolordefaults) - # These default colors have given saturation values, therefore, the following saturation command is - # temporarily commented out. - - self.set_color_discrete(ain_device, hue_value, duration=0) - - ## search saturation: - #saturation = -1 - #parentItem = item.return_parent() - #for child in parentItem.return_children(): - # if self.has_iattr(child.conf, 'avm_data_type'): - # if self.get_iattr_value(child.conf, 'avm_data_type') in ['set_saturation', 'saturation']: - # saturation = int(child()) - # #self.logger.debug(f"Debug hue {cmd_hue}, saturation {saturation}") - # # write value - # # RGB hue will be supported by Fritzbox approximately from Q2 2022 on: - # #self.set_color(ain_device, hue_value, saturation, duration=0) - #if saturation == -1: - # self.logger.warning( - # f"Cannot execute hue command because saturation value cannot be found in item tree") - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - # handle set saturation - elif avm_data_type in ['set_saturation', 'saturation']: - saturation_value = int(item()) - if self.debug_log: - self.logger.debug( - f"{avm_data_type} caller is: {caller}; Saturation to be set to: {saturation_value}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # Currently, only use default RGB colors that are supported by default (getcolordefaults). - # However, full RGB color control will be supported approximately from Q2 2022 on. - # These default colors have given saturation values, therefore, the following saturation command is - # temporarily commented out. - - # # search hue: - # hue = -1 - # parentItem = item.return_parent() - # for child in parentItem.return_children(): - # if self.has_iattr(child.conf, 'avm_data_type'): - # if self.get_iattr_value(child.conf, 'avm_data_type') in ['set_hue', 'hue']: - # hue = int(child()) - # if self.debug_log: - # self.logger.debug(f"Debug saturation {saturation_value}, hue {hue}") - # # write value - # #self.set_color(ain_device, hue, saturation_value, duration=0) - # if hue == -1: - # self.logger.warning( - # f"Cannot execute saturation command because hue value cannot be found in item tree") - - # # read new value after writing - # if readafterwrite: - # time.sleep(readafterwrite) - # self._update_aha_devices() - - # handle set color temperature - elif avm_data_type in ['set_colortemperature', 'colortemperature']: - cmd_colortemperature = int(item()) - if self.debug_log: - self.logger.debug( - f"set_colortemperature caller is: {caller}; colortemperature to be set to: {cmd_colortemperature}") - - # get AIN - ain_device = self._get_item_ain(item) - if self.debug_log: - self.logger.debug(f"Device AIN is {ain_device}") - - # write value - self.set_colortemperature(ain_device, cmd_colortemperature) - - # read new value after writing - if readafterwrite: - time.sleep(readafterwrite) - self._update_aha_devices() - - else: - self.logger.error(f"{avm_data_type} is not defined to be updated.") - - def set_wlan_config(self, wlan_index, new_enable=False): - """ - Set WLAN Config - """ - - param = f"/upnp/control/wlanconfig{wlan_index}" - url = self._build_url(param) - headers = self._header.copy() - action = 'SetEnable' - headers['SOAPACTION'] = f"{self._urn_map['WLANConfiguration']}"[:43] + f"{wlan_index}#{action}" - soap_data = self._assemble_soap_data(action, f"{self._urn_map['WLANConfiguration']}"[:43] + f"{wlan_index}", - {'NewEnable': int(new_enable)}) - - self._get_lua_post_request(url, soap_data, headers) - - # check if remaining time is set as item - for citem in self._fritz_device.get_items(): # search for guest time remaining item. - if self.get_iattr_value(citem.conf, 'avm_data_type') == 'wlan_guest_time_remaining' and int( - self.get_iattr_value(citem.conf, 'avm_wlan_index')) == wlan_index: - self._response_cache.pop( - f"wlanconfig_{self.get_iattr_value(citem.conf, 'avm_wlan_index')}_X_AVM-DE_GetWLANExtInfo", - None) # reset response cache - self._update_wlan_config(citem) # immediately update remaining guest time - - def set_tam(self, tam_index=0, new_enable=False): - """ - Set TAM - """ - - url = self._build_url("/upnp/control/x_tam") - headers = self._header.copy() - action = 'SetEnable' - headers['SOAPACTION'] = f"{self._urn_map['TAM']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['TAM'], - {'NewIndex': tam_index, 'NewEnable': int(new_enable)}) - - self._get_lua_post_request(url, soap_data, headers) - - def set_aha_device(self, ain='', set_switch=False): - """ - Set AHA-Device via TR-064 protocol - """ - - url = self._build_url("/upnp/control/x_homeauto") - headers = self._header.copy() - action = 'SetSwitch' - headers['SOAPACTION'] = f"{self._urn_map['Homeauto']}#{action}" - # SwitchState: OFF, ON, TOGGLE, UNDEFINED - switch_state = "ON" if set_switch is True else "OFF" - soap_data = self._assemble_soap_data(action, self._urn_map['Homeauto'], - {'NewAIN': ain.strip(), 'NewSwitchState': switch_state}) - - self._get_lua_post_request(url, soap_data, headers) - - def get_contact_name_by_phone_number(self, phone_number='', phonebook_id=0): - """ - Searches the phonebook for a contact by a given (complete) phone number - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - | Implementation of this method used information from https://www.symcon.de/forum/threads/25745-FritzBox-mit-SOAP-auslesen-und-steuern - - :param phone_number: full phone number of contact - :param phonebook_id: ID of the phone book (default: 0) - :return: string of the contact's real name - """ - - url = self._build_url("/upnp/control/x_contact") - action = "GetPhonebook" - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['OnTel']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], {'NewPhonebookID': phonebook_id}) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - pb_url_xml = xml.getElementsByTagName('NewPhonebookURL') - - if len(pb_url_xml) > 0: - pb_url = pb_url_xml[0].firstChild.data - try: - pb_result = self._session.get(pb_url, timeout=self._timeout, verify=self._verify) - pb_xml = minidom.parseString(pb_result.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error(f"Exception when sending GET request or parsing response: {e}") - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - contacts = pb_xml.getElementsByTagName('contact') - if len(contacts) > 0: - for contact in contacts: - phone_numbers = contact.getElementsByTagName('number') - if phone_numbers.length > 0: - i = phone_numbers.length - while i >= 0: - i -= 1 - if phone_number in phone_numbers[i].firstChild.data: - return contact.getElementsByTagName('realName')[0].firstChild.data.strip() - # no contact with phone number found, return number only - return phone_number - else: - self.logger.error("Phonebook not available on the FritzDevice") - - return phone_number - - def get_phone_numbers_by_name(self, name='', phonebook_id=0): - """ - Searches the phonebook for a contact by a given name - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - | CURL for testing which phonebooks exists: - | curl --anyauth -u user:'password' 'https://192.168.178.1:49443/upnp/control/x_contact' -H 'Content-Type: text/xml; charset="utf-8"' -H 'SoapAction: urn:dslforum-org:service:X_AVM-DE_OnTel:1#GetPhonebook' -d ' 0 ' -s -k - | Implementation of this method used information from https://www.symcon.de/forum/threads/25745-FritzBox-mit-SOAP-auslesen-und-steuern - - :param name: partial or full name of contact as defined in the phonebook. - :param phonebook_id: ID of the phone book (default: 0) - :return: dict of found contact names (keys) with each containing an array of dicts (keys: type, number) - """ - - url = self._build_url("/upnp/control/x_contact") - action = "GetPhonebook" - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['OnTel']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], {'NewPhonebookID': phonebook_id}) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - pb_url_xml = xml.getElementsByTagName('NewPhonebookURL') - - if len(pb_url_xml) > 0: - pb_url = pb_url_xml[0].firstChild.data - try: - pb_result = self._session.get(pb_url, timeout=self._timeout, verify=self._verify) - pb_xml = minidom.parseString(pb_result.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error(f"Exception when sending GET request or parsing response: {e}") - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - contacts = pb_xml.getElementsByTagName('contact') - result_numbers = {} - if name == '': - return result_numbers - if len(contacts) > 0: - for contact in contacts: - real_names = contact.getElementsByTagName('realName') - if real_names.length > 0: - i = 0 - while i < real_names.length: - if name.lower() in real_names[i].firstChild.data.lower(): - phone_numbers = contact.getElementsByTagName('number') - if phone_numbers.length > 0: - result_numbers[real_names[i].firstChild.data] = [] - j = 0 - while j < phone_numbers.length: - if phone_numbers[j].firstChild.data: - result_number_dict = dict() - result_number_dict['number'] = phone_numbers[j].firstChild.data - result_number_dict['type'] = phone_numbers[j].attributes["type"].value - result_numbers[real_names[i].firstChild.data].append(result_number_dict) - j += 1 - i += 1 - return result_numbers - else: - self.logger.error("Phonebook not available on the FritzDevice") - - def get_calllist(self, filter_incoming='', phonebook_id=0): - """ - Returns an array of all calllist entries - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - Curl for testing if the calllist url is returned: - curl --anyauth -u user:'password' 'https://192.168.178.1:49443/upnp/control/x_contact' -H 'Content-Type: text/xml; charset="utf-8"' -H 'SoapAction: urn:dslforum-org:service:X_AVM-DE_OnTel:1#GetCallList' -d ' 0 ' -s -k - :param: Filter to filter incoming calls to a specific destination phone number - :param: ID of the phone book (default: 0) - :return: Array of calllist entries with the attributes 'Id','Type','Caller','Called','CalledNumber','Name','Numbertype','Device','Port','Date','Duration' (some optional) - """ - - url = self._build_url("/upnp/control/x_contact") - action = "GetCallList" - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['OnTel']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], {'NewPhonebookID': phonebook_id}) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - if xml is not None: - calllist_url_xml = xml.getElementsByTagName('NewCallListURL') - - if len(calllist_url_xml) > 0: - calllist_url = calllist_url_xml[0].firstChild.data - - try: - calllist_result = self._session.get(calllist_url, timeout=self._timeout, verify=self._verify) - calllist_xml = minidom.parseString(calllist_result.content) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error(f"Exception when sending GET request or parsing response: {e}") - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - calllist_entries = calllist_xml.getElementsByTagName('Call') - result_entries = [] - if len(calllist_entries) > 0: - for calllist_entry in calllist_entries: - result_entry = {} - - progress = True - - if len(filter_incoming) > 0: - type_element = calllist_entry.getElementsByTagName("Type") - if len(type_element) > 0: - if type_element[0].hasChildNodes(): - type = int(type_element[0].firstChild.data) - - if type == 1 or type == 2: - called_number_element = calllist_entry.getElementsByTagName("CalledNumber") - if len(called_number_element) > 0: - if called_number_element[0].hasChildNodes(): - called_number = called_number_element[0].firstChild.data - # if self.debug_log: - # self.logger.debug(called_number+" "+filter_incoming) - if filter_incoming not in called_number: - progress = False - if progress: - attributes = ['Id', 'Type', 'Caller', 'Called', 'CalledNumber', 'Name', 'Numbertype', - 'Device', - 'Port', 'Date', 'Duration'] - for attribute in attributes: - attribute_value = calllist_entry.getElementsByTagName(attribute) - if len(attribute_value) > 0: - if attribute_value[0].hasChildNodes(): - if attribute != 'Date': - result_entry[attribute] = attribute_value[0].firstChild.data - else: - result_entry[attribute] = datetime.strptime( - attribute_value[0].firstChild.data, '%d.%m.%y %H:%M') - - result_entries.append(result_entry) - return result_entries - else: - if self.debug_log: - self.logger.debug("No calllist entries on the FritzDevice") - else: - self.logger.error("Calllist not available on the FritzDevice") - - return - - def reboot(self): - """ - Reboots the FritzDevice - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceconfigSCPD.pdf - """ - - url = self._build_url("/upnp/control/deviceconfig") - action = 'Reboot' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['DeviceConfig']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['DeviceConfig']) - - self._get_post_request_as_xml(url, soap_data, headers) - - def wol(self, mac_address): - """ - Sends a WOL (WakeOnLAN) command to a MAC address - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - - :param mac_address: MAC address of the device to wake up - """ - - url = self._build_url("/upnp/control/hosts") - action = 'X_AVM-DE_WakeOnLANByMACAddress' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['Hosts']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], {'NewMACAddress': mac_address}) - - self._get_post_request_as_xml(url, soap_data, headers) - - return - - def get_hosts(self, only_active): - """ - Gets the information (host details) of all hosts as an array of dicts - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - - :param only_active: bool, if only active hosts shall be returned - :return: Array host dicts (see get_host_details) - """ - - url = self._build_url("/upnp/control/hosts") - action = 'GetHostNumberOfEntries' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['Hosts']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts']) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - number_of_hosts = int(self._get_value_from_xml_node(xml, 'NewHostNumberOfEntries')) - hosts = [] - for i in range(1, number_of_hosts): - host = self.get_host_details(i) - if not only_active or (only_active and self.to_bool(host['is_active'])): - hosts.append(host) - return hosts - - def get_host_details(self, index): - """ - Gets the information of a hosts at a specific index - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - - :param index: index of host in hosts list - :return: Dict host data: name, interface_type, ip_address, address_source, mac_address, is_active, lease_time_remaining - """ - - url = self._build_url("/upnp/control/hosts") - action = 'GetGenericHostEntry' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['Hosts']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], {'NewIndex': index}) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - host = { - 'name': self._get_value_from_xml_node(xml, 'NewHostName'), - 'interface_type': self._get_value_from_xml_node(xml, 'NewInterfaceType'), - 'ip_address': self._get_value_from_xml_node(xml, 'NewIPAddress'), - 'address_source': self._get_value_from_xml_node(xml, 'NewAddressSource'), - 'mac_address': self._get_value_from_xml_node(xml, 'NewMACAddress'), - 'is_active': self._get_value_from_xml_node(xml, 'NewActive'), - 'lease_time_remaining': self._get_value_from_xml_node(xml, 'NewLeaseTimeRemaining') - } - return host - - def _get_host_device_info(self): - """ - Gets the detailed information of the host as device - - Uses: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceinfoSCPD.pdf - """ - - url = self._build_url("/upnp/control/deviceinfo") - action = 'GetInfo' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['DeviceInfo']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['DeviceInfo']) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - if not xml: - return - - host_info = { - "product_class": self._get_value_from_xml_node(xml, "NewProductClass"), - "manufacturer": self._get_value_from_xml_node(xml, "NewManufacturerName"), - "model": self._get_value_from_xml_node(xml, "NewModelName"), - "description": self._get_value_from_xml_node(xml, "NewDescription"), - "sw_version": self._get_value_from_xml_node(xml, "NewSoftwareVersion"), - "hw_version": self._get_value_from_xml_node(xml, "NewHardwareVersion"), - } - - self._host_info.update(host_info) - - - def _get_colordefaults(self, ain): - """ - Get the DOM elements for the RGM default colors using minidom. - - """ - colors = None - plain = self._aha_request("getcolordefaults", ain=ain) - if plain: - try: - dom = minidom.parseString(plain) - colors = dom.getElementsByTagName('colordefaults') - except Exception as e: - self.logger.error(f'_get_colordefaults: error {e} during parsing. Plain={plain}') - return colors - - - def reconnect(self): - """ - Reconnects the FritzDevice to the WAN - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf - """ - - url = self._build_url("/igdupnp/control/WANIPConn1") - action = 'ForceTermination' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['WANIPConnection']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['WANIPConnection']) - - self._get_post_request_as_xml(url, soap_data, headers) - - def get_call_origin(self): - """ - Gets the phone name, currently set as call_origin. - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - :return: String phone name - """ - - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialGetConfig' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['X_VoIP']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP']) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - phone_name = self._get_value_from_xml_node(xml, 'NewX_AVM-DE_PhoneName') - if phone_name is not None: - return phone_name - - self.logger.error("No call origin available.") - return - - def get_phone_name(self, index=1): - """ - Get the phone name at a specific index. The returned value can be used as phone_name for set_call_origin. - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - - :param index: Parameter is an INT, starting from 1. In case an index does not exist, an error is logged. - :return: String phone name - """ - - if not self.is_int(index): - self.logger.error(f"Index parameter {index} is no INT.") - return - - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_GetPhonePort' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['X_VoIP']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP'], {'NewIndex': index}) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - phone_name = self._get_value_from_xml_node(xml, 'NewX_AVM-DE_PhoneName') - if phone_name is not None: - return phone_name - - self.logger.error(f"No phone name available at provided index {index}") - return - - def set_call_origin(self, phone_name): - """ - Sets the call origin, e.g. before running 'start_call' - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - - :param phone_name: full phone identifier, could be e.g. '\*\*610' for an internal device - """ - - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialSetConfig' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['X_VoIP']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP'], - {'NewX_AVM-DE_PhoneName': phone_name.strip()}) - self._get_post_request_as_xml(url, soap_data, headers) - - def start_call(self, phone_number): - """ - Triggers a call for a given phone number - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - - :param phone_number: full phone number to call - """ - - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialNumber' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['X_VoIP']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP'], - {'NewX_AVM-DE_PhoneNumber': phone_number.strip()}) - self._get_post_request_as_xml(url, soap_data, headers) - - def cancel_call(self): - """ - Cancels an active call - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - """ - - url = self._build_url("/upnp/control/x_voip") - action = 'X_AVM-DE_DialHangup' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['X_VoIP']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['X_VoIP']) - self._get_post_request_as_xml(url, soap_data, headers) - - def get_device_log_from_lua(self): - """ - Gets the Device Log from the LUA HTTP Interface via LUA Scripts (more complete than the get_device_log TR-064 version. - :return: Array of Device Log Entries (text, type, category, timestamp, date, time) - """ - - if not self.sid: - self._get_sid() - my_sid = self.sid - - query_string = f"/query.lua?mq_log=logger:status/log&sid={my_sid}" - try: - r = self._lua_session.get(self._build_url(query_string, lua=True), timeout=self._timeout, verify=self._verify) - except requests.exceptions.Timeout: - self.logger.debug(f"get_device_log_from_lua: get request timed out.") - return - except Exception as e: - self.logger.debug(f"get_device_log_from_lua: Error {e} occurred.") - return - - status_code = r.status_code - if status_code == 200: - if self.debug_log: - self.logger.debug("get_device_log_from_lua: Sending query.lua command successful") - else: - self.logger.error(f"get_device_log_from_lua: query.lua command error code: {status_code}") - return - - try: - data = r.json()['mq_log'] - newlog = [] - - for text, typ, cat in data: - l_date = text[:8] - l_time = text[9:17] - l_text = text[18:] - l_cat = int(cat) - l_type = int(typ) - l_ts = int(datetime.timestamp(datetime.strptime(text[:17], '%d.%m.%y %H:%M:%S'))) - newlog.append([l_text, l_type, l_cat, l_ts, l_date, l_time]) - - return newlog - except JSONDecodeError: - self.logger.error('get_device_log_from_web: SID seems invalid.Please try again.') - return - - def get_device_log_from_tr064(self): - """ - Gets the Device Log via TR-064 - :return: Array of Device Log Entries (Strings) - """ - - url = self._build_url("/upnp/control/deviceinfo") - action = 'GetDeviceLog' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['DeviceInfo']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['DeviceInfo']) - - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"dev_info_{action}"] = response.content - - try: - xml = minidom.parseString(self._response_cache[f"dev_info_{action}"]) - except Exception as e: - self.logger.error(f"get_device_log_from_tr064: Exception when parsing response: {e}") - return - - element_xml = xml.getElementsByTagName('NewDeviceLog') - - if xml is not None: - if element_xml[0].firstChild is not None: - return element_xml[0].firstChild.nodeValue.split("\n") - else: - return "" - - def is_host_active(self, mac_address): - """ - Checks if a MAC address is active on the FritzDevice, e.g. the status can be used for simple presence detection - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - | Also reference: https://blog.pregos.info/2015/11/07/anwesenheitserkennung-fuer-smarthome-mit-der-fritzbox-via-tr-064/ - - :param: MAC address of the host - :return: True or False, depending if the host is active on the FritzDevice - """ - - url = self._build_url("/upnp/control/hosts") - action = 'GetSpecificHostEntry' - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['Hosts']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], {'NewMACAddress': mac_address}) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - if xml is not None: - tag_content = xml.getElementsByTagName('NewActive') - if len(tag_content) > 0: - if tag_content[0].firstChild.data == "1": - is_active = True - else: - is_active = False - else: - is_active = False - if self.debug_log: - self.logger.debug( - f"MAC Address {mac_address} not available on the FritzDevice - ID: {self._fritz_device.get_identifier()}") - return bool(is_active) - - def _update_myfritz(self, item): - """ - Retrieves information related to myfritz status of the FritzDevice - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_myfritzSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: myfritz_status) - """ - - url = self._build_url("/upnp/control/x_myfritz") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'myfritz_status': - action = 'GetInfo' - headers['SOAPACTION'] = f"{self._urn_map['MyFritz']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['MyFritz']) - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin method (_update_myfritz)") - return - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - if xml is not None: - tag_content = xml.getElementsByTagName('NewEnabled') - if len(tag_content) > 0: - item(tag_content[0].firstChild.data, self.get_shortname()) - - def _update_host(self, item): - """ - Retrieves information related to a network_device represented by its MAC address, e.g. the status of the network_device can be used for simple presence detection - - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - | Also reference: https://blog.pregos.info/2015/11/07/anwesenheitserkennung-fuer-smarthome-mit-der-fritzbox-via-tr-064/ - - :param item: item to be updated (Supported item avm_data_types: network_device, child item avm_data_types: device_ip, device_connection_type, device_hostname) - """ - - url = self._build_url("/upnp/control/hosts") - headers = self._header.copy() - - if self.debug_log: - self.logger.debug(f'_update_host called: item.conf={item.conf}') - if self.get_iattr_value(item.conf, 'avm_data_type') == 'network_device': - if not self.has_iattr(item.conf, 'avm_mac'): - self.logger.error(f"No avm_mac attribute provided in network_device item {item.property.path}") - return - action = 'GetSpecificHostEntry' - headers['SOAPACTION'] = f"{self._urn_map['Hosts']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['Hosts'], - {'NewMACAddress': self.get_iattr_value(item.conf, 'avm_mac')}) - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin (_update_host)") - return - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - if xml is not None: - tag_content = xml.getElementsByTagName('NewActive') - if len(tag_content) > 0: - item(tag_content[0].firstChild.data, self.get_shortname()) - for child in item.return_children(): - data = None - if self.has_iattr(child.conf, 'avm_data_type'): - if self.get_iattr_value(child.conf, 'avm_data_type') == 'device_ip': - device_ip = xml.getElementsByTagName('NewIPAddress') - if len(device_ip) > 0: - if device_ip[0].firstChild is not None: - data = device_ip[0].firstChild.data - else: - data = '' - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_connection_type': - device_connection_type = xml.getElementsByTagName('NewInterfaceType') - if len(device_connection_type) > 0: - if device_connection_type[0].firstChild is not None: - data = device_connection_type[0].firstChild.data - else: - data = '' - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_hostname': - data = self._get_value_from_xml_node(xml, 'NewHostName') - - if data is not None: - child(data, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - - else: - item(0) - if self.debug_log: - self.logger.debug( - f"MAC Address {self.get_iattr_value(item.conf, 'avm_mac')} for item {item.property.path} not available on the FritzDevice - ID: {self._fritz_device.get_identifier()}") - - def _update_home_automation(self, item): - """ - Updates AVM home automation device related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_homeauto.pdf - CURL for testing which data is coming back: - curl --anyauth -u user:'password' "https://192.168.178.1:49443/upnp/control/x_homeauto" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:X_AVM-DE_Homeauto:1#GetSpecificDeviceInfos" -d "xxxxx xxxxxxx" -s -k - - :param item: item to be updated (Supported item avm_data_types: aha_device, hkr_device) - """ - - url = self._build_url("/upnp/control/x_homeauto") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device' or self.get_iattr_value(item.conf, - 'avm_data_type') == 'hkr_device': - if not self.has_iattr(item.conf, 'ain'): - self.logger.error(f"Cannot update AVM item {item} as AIN is not specified.") - return - ain = self.get_iattr_value(item.conf, 'ain') - action = 'GetSpecificDeviceInfos' - headers['SOAPACTION'] = f"{self._urn_map['Homeauto']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['Homeauto'], {'NewAIN': ain.strip()}) - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin method (_update_home_automation)") - return - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - if not xml: - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'aha_device': - element_xml = xml.getElementsByTagName('NewSwitchState') - if len(element_xml) > 0: - if element_xml[0].firstChild.data not in ['UNDEFINED', 'TOGGLE']: - item(element_xml[0].firstChild.data, self.get_shortname()) - elif element_xml[0].firstChild.data in 'TOGGLE': - value = item() - item(not value, self.get_shortname()) - else: - self.logger.error( - f'NewSwitchState für AHA Device has a non-supported value of {element_xml[0].firstChild.data}') - for child in item.return_children(): - value = None - if self.has_iattr(child.conf, 'avm_data_type'): - if self.get_iattr_value(child.conf, 'avm_data_type') == 'temperature': - temp = xml.getElementsByTagName('NewTemperatureCelsius') - if len(temp) > 0: - value = int(temp[0].firstChild.data) / 10 - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'power': - power = xml.getElementsByTagName('NewMultimeterPower') - if len(power) > 0: - value = int(power[0].firstChild.data) / 100 - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'energy': - energy = xml.getElementsByTagName('NewMultimeterEnergy') - if len(energy) > 0: - value = int(energy[0].firstChild.data) - - if value: - child(value, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not available on the FritzDevice") - - # handling hkr devices (AVM dect 301) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'hkr_device': - self.logger.debug('handling hkr device') - element_xml = xml.getElementsByTagName('NewHkrSetVentilStatus') - if len(element_xml) > 0: - # Decoding hrk valve state: open, closed or temp (temperature controlled) - tempstring = element_xml[0].firstChild.data - if tempstring == 'OPEN': - tempstate = 1 - elif tempstring == 'CLOSED': - tempstate = 0 - elif tempstring == 'TEMP': - tempstate = 2 - else: - tempstate = 3 - item(int(tempstate)) - for child in item.return_children(): - value = None - if self.has_iattr(child.conf, 'avm_data_type'): - if self.get_iattr_value(child.conf, 'avm_data_type') == 'temperature': - is_temperature = xml.getElementsByTagName('NewTemperatureCelsius') - if len(is_temperature) > 0: - value = int(is_temperature[0].firstChild.data) / 10 - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'set_temperature': - set_temperature = xml.getElementsByTagName('NewHkrSetTemperature') - if len(set_temperature) > 0: - value = int(set_temperature[0].firstChild.data) / 10 - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'set_temperature_reduced': - set_temperature_reduced = xml.getElementsByTagName('NewHkrReduceTemperature') - if len(set_temperature_reduced) > 0: - value = int(set_temperature_reduced[0].firstChild.data) / 10 - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'set_temperature_comfort': - set_temperature_comfort = xml.getElementsByTagName('NewHkrComfortTemperature') - if len(set_temperature_comfort) > 0: - value = int(set_temperature_comfort[0].firstChild.data) / 10 - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'firmware_version': - firmware_version = xml.getElementsByTagName('NewFirmwareVersion') - if len(firmware_version) > 0: - value = str(firmware_version[0].firstChild.data) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'manufacturer': - manufacturer = xml.getElementsByTagName('NewManufacturer') - if len(manufacturer) > 0: - value = str(manufacturer[0].firstChild.data) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'product_name': - product_name = xml.getElementsByTagName('NewProductName') - if len(product_name) > 0: - value = str(product_name[0].firstChild.data) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_name': - device_name = xml.getElementsByTagName('NewDeviceName') - if len(device_name) > 0: - value = str(device_name[0].firstChild.data) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'connection_status': - connection_status = xml.getElementsByTagName('NewPresent') - if len(connection_status) > 0: - value = str(connection_status[0].firstChild.data) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_id': - device_id = xml.getElementsByTagName('NewDeviceId') - if len(device_id) > 0: - value = str(device_id[0].firstChild.data) - elif self.get_iattr_value(child.conf, 'avm_data_type') == 'device_function': - device_function = xml.getElementsByTagName('NewFunctionBitMask') - if len(device_function) > 0: - value = str(device_function[0].firstChild.data) - - if value: - child(value, self.get_shortname()) - else: - self.logger.info( - f"Argument {self.get_iattr_value(child.conf, 'avm_data_type')} of Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not available on the FritzDevice with AIN {item.conf['ain'].strip()}.") - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not available on the FritzDevice with AIN {item.conf['ain'].strip()}.") - - def _get_url_prefix(self): - """ - Choose the correct protocol prefix for the host. - """ - - if self._fritz_device.is_ssl(): - url_prefix = "https" - else: - url_prefix = "http" - - return url_prefix - - def _request(self, url, params=None): - """ - Send a request with parameters. - """ - - try: - rsp = self._session.get(url, params=params, timeout=self._timeout, verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error(f"Exception when sending POST request for updating item towards the FritzDevice: {e}") - self.set_device_availability(False) - else: - status_code = rsp.status_code - if status_code == 200: - if self.debug_log: - self.logger.debug("Sending HTTP request successful") - elif status_code == 403: - if self.debug_log: - self.logger.debug("HTTP access denied. Try to get new Session ID.") - self._get_sid() - else: - self.logger.error(f"HTTP request error code: {status_code}") - rsp.raise_for_status() - if self.debug_log: - self.logger.debug(f"Url: {url}") - self.logger.debug(f"Params: {params}") - - if not self._fritz_device.is_available(): - self.set_device_availability(True) - - return rsp.text.strip() - - def _aha_request(self, cmd, ain=None, param=None, rf=str): - """ - Create an request for AHA-device get response - :param cmd: command to be sent - :type cmd: str - :param ain: ain of device - :type ain: str - :param param: params for request - :type param: dict - :param rf: type of returned value - :type rf: any - :return: response - :type return: any - """ - - url = f"{self._get_url_prefix()}://{self._fritz_device.get_host()}/webservices/homeautoswitch.lua" - if self.debug_log: - self.logger.debug(f"_aha_request: built request url: {url}") - - if not self.sid: - self._get_sid() - - # Es wird angenommen, dass die SID noch gültig ist. Wenn nicht, wird bei Fehler 403 eine neue generiert. - # Alternativ könnte man auch vor jedem Senden prüfen, dass die SID noch gültig ist. - mySID = self.sid - - try: - params = {"switchcmd": cmd, "sid": mySID} - if param: - params.update(param) - if ain: - params["ain"] = ain - - plain = self._request(url, params) - if self.debug_log: - self.logger.debug(f"Plain AHA request response is: {plain}") - self.logger.debug(f"Params were: {params}") - - if plain == "inval": - self.logger.error(f"Response of AHA request {cmd} was invalid") - return None - else: - if rf == bool: - return bool(int(plain)) - return rf(plain) - except Exception: - pass - - def _get_aha_device_elements(self): - """ - Get the DOM elements for the device list. - """ - - devices = None - plain = self._aha_request("getdevicelistinfos") - if plain is not None: - try: - dom = minidom.parseString(plain) - devices = dom.getElementsByTagName('device') - except Exception as e: - self.logger.error(f'_get_aha_device_elements: error {e} during parsing') - return devices - - def _update_aha_devices(self): - """ - Update smarthome devices dict '_smarthome_devices' with DOM elements. - """ - - if self.debug_log: - self.logger.debug("Updating AHA Devices ...") - - devices = self._get_aha_device_elements() - if devices is not None: - for element in devices: - ain = element.getAttribute('identifier') - if ain not in self._fritz_device.get_smarthome_devices().keys(): - if self.debug_log: - self.logger.debug(f"Adding new Device with AIN {ain}") - self._fritz_device.get_smarthome_devices()[ain] = {} - - # general information of AVM smarthome device - self._fritz_device.get_smarthome_devices()[ain]['device_id'] = element.getAttribute('id') - self._fritz_device.get_smarthome_devices()[ain]['fw_version'] = element.getAttribute('fwversion') - self._fritz_device.get_smarthome_devices()[ain]['product_name'] = element.getAttribute('productname') - self._fritz_device.get_smarthome_devices()[ain]['manufacturer'] = element.getAttribute('manufacturer') - - # get functions of AVM smarthome device - functions = [] - functionbitmask = int(element.getAttribute('functionbitmask')) - functions.append('light') if bool(functionbitmask & (1 << 2) > 0) is True else None - functions.append('alarm') if bool(functionbitmask & (1 << 4) > 0) is True else None - functions.append('button') if bool(functionbitmask & (1 << 5) > 0) is True else None - functions.append('thermostat') if bool(functionbitmask & (1 << 6) > 0) is True else None - functions.append('powermeter') if bool(functionbitmask & (1 << 7) > 0) is True else None - functions.append('temperature_sensor') if bool(functionbitmask & (1 << 8) > 0) is True else None - functions.append('switch') if bool(functionbitmask & (1 << 9) > 0) is True else None - functions.append('repeater') if bool(functionbitmask & (1 << 10) > 0) is True else None - functions.append('mic') if bool(functionbitmask & (1 << 11) > 0) is True else None - functions.append('han_fun') if bool(functionbitmask & (1 << 13) > 0) is True else None - functions.append('on_off_device') if bool(functionbitmask & (1 << 15) > 0) is True else None - functions.append('dimmable_device') if bool(functionbitmask & (1 << 16) > 0) is True else None - functions.append('color_device') if bool(functionbitmask & (1 << 17) > 0) is True else None - functions.append('blind') if bool(functionbitmask & (1 << 18) > 0) is True else None - if self.debug_log: - self.logger.debug(f'Identified function of device with AIN {ain} are {functions}') - - self._fritz_device.get_smarthome_devices()[ain]['device_functions'] = functions - - # optional general information of AVM smarthome device - try: - self._fritz_device.get_smarthome_devices()[ain]['batterylow'] = bool( - int(element.getElementsByTagName('batterylow')[0].firstChild.data)) - except Exception: - if self.debug_log: - self.logger.debug( - f'DECT Smarthome Device with AIN {ain} does not support Attribute {"batterylow"}.') - try: - self._fritz_device.get_smarthome_devices()[ain]['battery_level'] = int( - element.getElementsByTagName('battery')[0].firstChild.data) - except Exception: - if self.debug_log: - self.logger.debug(f'DECT Smarthome Device with AIN {ain} does not support Attribute "battery".') - try: - self._fritz_device.get_smarthome_devices()[ain]['connected'] = bool( - int(element.getElementsByTagName('present')[0].firstChild.data)) - except Exception: - if self.debug_log: - self.logger.debug(f'DECT Smarthome Device with AIN {ain} does not support Attribute "present".') - try: - self._fritz_device.get_smarthome_devices()[ain]['tx_busy'] = bool( - int(element.getElementsByTagName('txbusy')[0].firstChild.data)) - except Exception: - if self.debug_log: - self.logger.debug(f'DECT Smarthome Device with AIN {ain} does not support Attribute "txbusy".') - try: - self._fritz_device.get_smarthome_devices()[ain]['device_name'] = str( - element.getElementsByTagName('name')[0].firstChild.data) - except Exception: - if self.debug_log: - self.logger.debug(f'DECT Smarthome Device with AIN {ain} does not support Attribute "name".') - - # information of AVM smarthome device having thermostat - if 'thermostat' in functions: - hkr = element.getElementsByTagName('hkr') - if len(hkr) > 0: - for child in hkr: - try: - self._fritz_device.get_smarthome_devices()[ain]['current_temperature'] = (int( - child.getElementsByTagName('tist')[0].firstChild.data) - 16) / 2 + 8 - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['target_temperature'] = (int( - child.getElementsByTagName('tsoll')[0].firstChild.data) - 16) / 2 + 8 - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['temperature_comfort'] = (int( - child.getElementsByTagName('komfort')[0].firstChild.data) - 16) / 2 + 8 - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['temperature_reduced'] = (int( - child.getElementsByTagName('absenk')[0].firstChild.data) - 16) / 2 + 8 - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['battery_level'] = int( - child.getElementsByTagName('battery')[0].firstChild.data) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['battery_low'] = bool( - int(child.getElementsByTagName('batterylow')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['window_open'] = bool( - int(child.getElementsByTagName('windowopenactiv')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['summer_active'] = bool( - int(child.getElementsByTagName('summeractive')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['holiday_active'] = bool( - int(child.getElementsByTagName('holidayactive')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['hkr_boost'] = bool( - int(child.getElementsByTagName('boostactive')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['lock'] = bool( - int(child.getElementsByTagName('lock')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['device_lock'] = bool( - int(child.getElementsByTagName('devicelock')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['errorcode'] = int( - child.getElementsByTagName('errorcode')[0].firstChild.data) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['windowopenactiveendtime'] = int( - child.getElementsByTagName('windowopenactiveendtime')[0].firstChild.data) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['boostactiveendtime'] = int( - child.getElementsByTagName('boostactiveendtime')[0].firstChild.data) - except AttributeError: - pass - - # information of AVM smarthome device having temperature sensor - if 'temperature_sensor' in functions: - temperature_element = element.getElementsByTagName('temperature') - if len(temperature_element) > 0: - for child in temperature_element: - try: - self._fritz_device.get_smarthome_devices()[ain]['current_temperature'] = int( - child.getElementsByTagName('celsius')[0].firstChild.data) / 10 - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['temperature_offset'] = int( - child.getElementsByTagName('offset')[0].firstChild.data) / 10 - except AttributeError: - pass - - humidity_element = element.getElementsByTagName('humidity') - if len(humidity_element) > 0: - for child in humidity_element: - try: - self._fritz_device.get_smarthome_devices()[ain]['humidity'] = int( - child.getElementsByTagName('rel_humidity')[0].firstChild.data) - except AttributeError: - pass - - # information of AVM smarthome device having switch - if 'switch' in functions: - switch = element.getElementsByTagName('switch') - if len(switch) > 0: - for child in switch: - try: - self._fritz_device.get_smarthome_devices()[ain]['switch_state'] = bool( - int(child.getElementsByTagName('state')[0].firstChild.data)) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['switch_mode'] = str( - child.getElementsByTagName('mode')[0].firstChild.data) - except AttributeError: - pass - - # information of AVM smarthome device having powermeter - if 'powermeter' in functions: - powermeter = element.getElementsByTagName('powermeter') - if len(powermeter) > 0: - for child in powermeter: - try: - self._fritz_device.get_smarthome_devices()[ain]['power'] = int( - child.getElementsByTagName('power')[0].firstChild.data) / 1000 - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['energy'] = int( - child.getElementsByTagName('energy')[0].firstChild.data) / 1000 - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['voltage'] = int( - child.getElementsByTagName('voltage')[0].firstChild.data) / 1000 - except AttributeError: - pass - - # information of AVM smarthome device having button - if 'button' in functions: - button_element = element.getElementsByTagName('button') - if len(button_element) > 0: - for child in button_element: - try: - self._fritz_device.get_smarthome_devices()[ain]['lastpressedtimestamp'] = int( - child.getElementsByTagName('lastpressedtimestamp')[0].firstChild.data) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['identifier'] = str( - child.getElementsByTagName('identifier')[0].firstChild.data) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['id'] = str( - child.getElementsByTagName('id')[0].firstChild.data) - except AttributeError: - pass - - # information of AVM smarthome device having alarm - if 'alarm' in functions: - alarm_element = element.getElementsByTagName('alert') - if len(alarm_element) > 0: - for child in alarm_element: - try: - self._fritz_device.get_smarthome_devices()[ain]['alarm'] = int( - child.getElementsByTagName('state')[0].firstChild.data) - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['lastalertchgtimestamp'] = int( - child.getElementsByTagName('lastalertchgtimestamp')[0].firstChild.data) - except AttributeError: - pass - - # information of AVM smarthome device having switch state - if 'on_off_device' in functions: - simpleonoff_element = element.getElementsByTagName('simpleonoff') - if len(simpleonoff_element) > 0: - for child in simpleonoff_element: - try: - self._fritz_device.get_smarthome_devices()[ain]['simpleonoff'] = int( - child.getElementsByTagName('state')[0].firstChild.data) - except AttributeError: - # Set device state to 0 (off) if device is not connected (= no state available in xml) - self._fritz_device.get_smarthome_devices()[ain]['simpleonoff'] = 0 - pass - - # information of AVM smarthome device having dimmer level information - if 'dimmable_device' in functions: - levelcontrol_element = element.getElementsByTagName('levelcontrol') - if len(levelcontrol_element) > 0: - for child in levelcontrol_element: - try: - self._fritz_device.get_smarthome_devices()[ain]['level'] = int( - child.getElementsByTagName('level')[0].firstChild.data) - except AttributeError: - # Set dimmer level to 0 (off) if device is not connected (= no state available in xml) - self._fritz_device.get_smarthome_devices()[ain]['level'] = 0 - pass - else: - # Set Level to zero for consistency, if light is off: - try: - onoff = self._fritz_device.get_smarthome_devices()[ain]['simpleonoff'] - if onoff == 0: - self._fritz_device.get_smarthome_devices()[ain]['level'] = 0 - if self.debug_log: - self.logger.debug(f"Debug: Level set to zero due to onoff state") - except AttributeError: - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['levelpercentage'] = int( - child.getElementsByTagName('levelpercentage')[0].firstChild.data) - except AttributeError: - # Set dimmer level to 0 (off) if device is not connected (= no state available in xml) - self._fritz_device.get_smarthome_devices()[ain]['levelpercentage'] = 0 - pass - else: - # Set Level to zero for consistency, if light is off: - try: - onoff = self._fritz_device.get_smarthome_devices()[ain]['simpleonoff'] - if onoff == 0: - self._fritz_device.get_smarthome_devices()[ain]['levelpercentage'] = 0 - if self.debug_log: - self.logger.debug(f"Debug: Level set to zero due to onoff state") - except AttributeError: - pass - - # information of AVM smarthome device having color information - if 'color_device' in functions: - colorcontrol_element = element.getElementsByTagName('colorcontrol') - if len(colorcontrol_element) > 0: - for child in colorcontrol_element: - # Hue readout mode: currently not used - # try: - # self._fritz_device.get_smarthome_devices()[ain]['current_mode'] = int(child.getAttribute('current_mode')) - # self._fritz_device.get_smarthome_devices()[ain]['supported_modes'] = int(child.getAttribute('supported_modes')) - # except AttributeError: - # self._fritz_device.get_smarthome_devices()[ain]['current_mode'] = 0 - # self._fritz_device.get_smarthome_devices()[ain]['supported_modes'] = 0 - # pass - # else: - # self.logger.warning(f"Debug: HanFun current_mode: {self._fritz_device.get_smarthome_devices()[ain]['current_mode']}") - # self.logger.warning(f"Debug: HanFun supported_modes: {self._fritz_device.get_smarthome_devices()[ain]['supported_modes']}") - try: - self._fritz_device.get_smarthome_devices()[ain]['hue'] = int( - child.getElementsByTagName('hue')[0].firstChild.data) - except AttributeError: - self._fritz_device.get_smarthome_devices()[ain]['hue'] = 0 - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['saturation'] = int( - child.getElementsByTagName('saturation')[0].firstChild.data) - except AttributeError: - self._fritz_device.get_smarthome_devices()[ain]['saturation'] = 0 - pass - try: - self._fritz_device.get_smarthome_devices()[ain]['colortemperature'] = int( - child.getElementsByTagName('temperature')[0].firstChild.data) - except AttributeError: - self._fritz_device.get_smarthome_devices()[ain]['colortemperature'] = 0 - pass - else: - self.logger.warning(f"Debug: _get_aha_device_elements returned no devices") - - # update items - self._update_smarthome_items() - - def _update_smarthome_items(self): - """ - Update smarthome item values using information from dict '_smarthome_devices' - """ - - for item in self._fritz_device.get_smarthome_items(): - # get AIN - ain_device = self._get_item_ain(item) - - # get device sub-dict from dict - device = self._fritz_device.get_smarthome_devices().get(ain_device, None) - - if device is not None: - # get avm_data_type of item - current_avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') - # Attributes that are write only commands with no corresponding read commands are excluded from status updates via update black list: - update_black_list = ['switch_toggle'] - - if current_avm_data_type not in update_black_list: - # Remove "set_" prefix to set corresponding r/o or r/w item to returned value: - if current_avm_data_type.startswith('set_'): - current_avm_data_type = current_avm_data_type[len('set_'):] - # set item - if current_avm_data_type in device: - item(device[current_avm_data_type], self.get_shortname()) - else: - self.logger.warning( - f'Attribute <{current_avm_data_type}> at device <{ain_device}> to be set to Item <{item}> is not available.') - else: - self.logger.warning(f'No values for item {item.id()} with AIN {ain_device} available.') - - def _get_aha_devices_as_dict(self): - """ - Get the dict of all known devices. - """ - - if self._fritz_device.get_smarthome_devices() == {}: - self._update_aha_devices() - return self._fritz_device.get_smarthome_devices() - - def _get_aha_device_by_ain(self, ain): - """ - Return a device specified by the AIN. - """ - - return self._get_aha_devices_as_dict()[ain] - - def get_devices(self): - """ - Get the list of all known devices. - """ - - return list(self._get_aha_devices_as_dict().values()) - - def get_device_present(self, ain): - """ - Get the device presence. - """ - - return self._aha_request("getswitchpresent", ain=ain, rf=bool) - - def get_device_name(self, ain): - """ - Get the device name. - """ - - return self._aha_request("getswitchname", ain=ain) - - def get_switch_state(self, ain): - """ - Get the switch state. - """ - - return self._aha_request("getswitchstate", ain=ain, rf=bool) - - def set_switch_on(self, ain): - """ - Set the switch to on state. - """ - - return self._aha_request("setswitchon", ain=ain, rf=bool) - - def set_switch_off(self, ain): - """ - Set the switch to off state. - """ - - return self._aha_request("setswitchoff", ain=ain, rf=bool) - - def set_switch_toggle(self, ain): - """ - Toggle the switch state. - """ - - return self._aha_request("setswitchtoggle", ain=ain, rf=bool) - - def get_switch_power(self, ain): - """ - Get the switch power consumption. - """ - - return self._aha_request("getswitchpower", ain=ain, rf=int) - - def get_switch_energy(self, ain): - """ - Get the switch energy. - """ - - return self._aha_request("getswitchenergy", ain=ain, rf=int) - - def set_switch_onoff(self, ain, on): - """ - Set Led on/off. - """ - return self._aha_request("setsimpleonoff", ain=ain, param={'onoff': int(on)}, rf=bool) - - def set_level(self, ain, level): - """ - Set level 0-255. - """ - - if not 0 <= int(level) <= 255: - self.logger.warning( - f"Value for level={level} not in expected range of 0-255. Value will be limited to min/max.") - - # limit level it is out of range - level = 0 if level < 0 else 255 if level > 255 else level - - return self._aha_request("setlevel", ain=ain, param={'level': int(level)}, rf=int) - - def set_level_percentage(self, ain, level): - """ - Set level 0-100. - """ - - if not 0 <= int(level) <= 100: - self.logger.warning( - f"Value for level={level} not in expected range of 0%-100%. Value will be limited to min/max.") - - # limit level it is out of range - level = 0 if level < 0 else 100 if level > 100 else level - - return self._aha_request("setlevelpercentage", ain=ain, param={'level': int(level)}, rf=int) - - def set_colortemperature(self, ain, colortemperature, duration=8): - """ - Set Led to specific color temperature in Kelvin (2700K-6500K). - """ - - if not 2700 <= int(colortemperature) <= 6500: - self.logger.warning( - f"Value for colortemperature={colortemperature} not in expected range of 2700K-6500K. Value will be limited to min/max.") - - # limit colortemperature it is out of range - colortemperature = 2700 if colortemperature < 2700 else 6500 if colortemperature > 6500 else colortemperature - - return self._aha_request("setcolortemperature", ain=ain, - param={'temperature': int(colortemperature), 'duration': int(duration)}, rf=int) - - def set_color(self, ain, hue, saturation, duration=3): - """ - Set Led color. - """ - - if not 0 <= int(hue) <= 359 and not 0 <= int(saturation) <= 255: - self.logger.warning( - f"Value for hue={hue} and/or saturation={saturation} not in expected range. Values will be limited to min/max.") - - # n = minn if n < minn else maxn if n > maxn else n - - # limit hue and saturation values if they are out of range - hue = 0 if hue < 0 else 359 if hue > 359 else hue - saturation = 0 if saturation < 0 else 255 if saturation > 255 else saturation - - return self._aha_request("setcolor", ain=ain, - param={'hue': int(hue), 'saturation': int(saturation), 'duration': int(duration)}, - rf=bool) - - def set_color_discrete(self, ain, hue, duration=0): - """ - Set Led color to closest discrete hue value. Currently, only those are supported for FritzDect500 RGB LED bulbs - """ - - if hue <= 20: - #self.logger.debug(f'setcolor to red (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 358, 'saturation': 180, 'duration': int(duration)}, rf=bool) - elif hue <= 45: - #self.logger.debug(f'setcolor to orange (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 35, 'saturation': 214, 'duration': int(duration)}, rf=bool) - elif hue <= 55: - #self.logger.debug(f'setcolor to yellow (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 52, 'saturation': 153, 'duration': int(duration)}, rf=bool) - elif hue <= 100: - #self.logger.debug(f'setcolor to grasgreen (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 92, 'saturation': 123, 'duration': int(duration)}, rf=bool) - elif hue <= 135: - #self.logger.debug(f'setcolor to green (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 120, 'saturation': 160, 'duration': int(duration)}, rf=bool) - elif hue <= 175: - #self.logger.debug(f'setcolor to turquoise (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 160, 'saturation': 145, 'duration': int(duration)}, rf=bool) - elif hue <= 210: - #self.logger.debug(f'setcolor to cyan (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 195, 'saturation': 179, 'duration': int(duration)}, rf=bool) - elif hue <= 240: - #self.logger.debug(f'setcolor to blue (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 225, 'saturation': 204, 'duration': int(duration)}, rf=bool) - elif hue <= 280: - #self.logger.debug(f'setcolor to violett (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 266, 'saturation': 169, 'duration': int(duration)}, rf=bool) - elif hue <= 310: - #self.logger.debug(f'setcolor to magenta (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 296, 'saturation': 140, 'duration': int(duration)}, rf=bool) - elif hue <= 350: - #self.logger.debug(f'setcolor to pink (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 335, 'saturation': 180, 'duration': int(duration)}, rf=bool) - elif hue <= 360: - #self.logger.debug(f'setcolor to red (hue={hue})') - return self._aha_request("setcolor", ain=ain, param={'hue': 358, 'saturation': 180, 'duration': int(duration)}, rf=bool) - else: - self.logger.error(f'setcolor hue out of range (hue={hue})') - - - def get_temperature(self, ain): - """ - Get the device temperature sensor value. - """ - - return self._aha_request("gettemperature", ain=ain, rf=float) / 10.0 - - def _get_temperature(self, ain, name): - """ - Get temperature with value correction - """ - - plain = self._aha_request(name, ain=ain, rf=float) - return (plain - 16) / 2 + 8 - - def get_target_temperature(self, ain): - """ - Get the thermostate target temperature. - """ - - return self._get_temperature(ain, "gethkrtsoll") - - def set_target_temperature(self, ain, temperature): - """ - Set the thermostate target temperature. - """ - - temp = int(16 + ((float(temperature) - 8) * 2)) - - if temp < min(range(16, 56)): - temp = 253 - elif temp > max(range(16, 56)): - temp = 254 - - self._aha_request("sethkrtsoll", ain=ain, param={'param': temp}) - - def get_comfort_temperature(self, ain): - """ - Get the thermostate comfort temperature. - """ - - return self._get_temperature(ain, "gethkrkomfort") - - def get_eco_temperature(self, ain): - """ - Get the thermostate eco temperature. - """ - - return self._get_temperature(ain, "gethkrabsenk") - - def get_device_statistics(self, ain): - """ - Get device statistics. - """ - - plain = self._aha_request("getbasicdevicestats", ain=ain) - return plain - - def set_hkr_boost(self, ain, endtimestamp): - """ - Set HKR to boost mode. - """ - self._aha_request("sethkrboost", ain=ain, param={'endtimestamp': endtimestamp}) - - def set_hkr_windowopen(self, ain, endtimestamp): - """ - Set HKR windowopen. - """ - self._aha_request("sethkrwindowopen", ain=ain, param={'endtimestamp': endtimestamp}) - - def set_name(self, ain, name): - """ - Sets name of device - """ - - self._aha_request("setname", ain=ain, param={'name': name}) - - def set_blind(self, ain, cmd): - """ - Sets blind; Possible cmd „open“, "close“, „stop“ - """ - - self._aha_request("setblind", ain=ain, param={'target:': cmd}) - - def _get_item_ain(self, item): - """ - Get AIN of device from item.conf - """ - - ain_device = None - - lookup_item = item - for i in range(2): - attribute = 'ain' - attribute_w_instance = f"{attribute}@{self.get_instance_name()}" - - ain_device = self.get_iattr_value(lookup_item.conf, attribute) - if ain_device is not None: - break - ain_device = self.get_iattr_value(lookup_item.conf, attribute_w_instance) - if ain_device is not None: - break - else: - lookup_item = lookup_item.return_parent() - - if ain_device: - # deprecated warning for attribute 'ain' - self.logger.warning( - f"Item {item.id()} uses deprecated 'ain' attribute. Please consider to switch to 'avm_ain'.") - else: - lookup_item = item - for i in range(2): - attribute = 'avm_ain' - attribute_w_instance = f"{attribute}@{self.get_instance_name()}" - - ain_device = self.get_iattr_value(lookup_item.conf, attribute_w_instance) - if ain_device is not None: - break - ain_device = self.get_iattr_value(lookup_item.conf, attribute) - if ain_device is not None: - break - else: - lookup_item = lookup_item.return_parent() - - if ain_device is None: - self.logger.error('Device AIN is not defined or instance not given') - return str(ain_device) - - def _get_wlan_index(self, item): - """ - return wlan index for given item - """ - - wlan_index = None - for i in range(2): - attribute = 'avm_wlan_index' - attribute_w_instance = f"{attribute}@{self.get_instance_name()}" - - wlan_index = self.get_iattr_value(item.conf, attribute) - if wlan_index: - break - wlan_index = self.get_iattr_value(item.conf, attribute_w_instance) - if wlan_index: - break - else: - item = item.return_parent() - - if wlan_index is not None: - wlan_index = int(wlan_index) - 1 - if not 0 <= wlan_index <= 2: - wlan_index = None - self.logger.warning(f"Attribute 'avm_wlan_index' for item {item.id()} not in valid range 1-3.") - - return wlan_index - - def _get_tam_index(self, item): - """ - return tam index for given item - """ - - tam_index = None - for i in range(2): - attribute = 'avm_tam_index' - attribute_w_instance = f"{attribute}@{self.get_instance_name()}" - - tam_index = self.get_iattr_value(item.conf, attribute) - if tam_index: - break - tam_index = self.get_iattr_value(item.conf, attribute_w_instance) - if tam_index: - break - else: - item = item.return_parent() - - if tam_index is not None: - tam_index = int(tam_index) - 1 - if not 0 <= tam_index <= 4: - tam_index = None - self.logger.warning(f"Attribute 'avm_tam_index' for item {item.id()} not in valid range 1-5.") - - return tam_index - - def _get_deflection_index(self, item): - """ - return deflection index for given item - """ - - deflection_index = None - for i in range(2): - attribute = 'avm_deflection_index' - attribute_w_instance = f"{attribute}@{self.get_instance_name()}" - - deflection_index = self.get_iattr_value(item.conf, attribute) - if deflection_index: - break - deflection_index = self.get_iattr_value(item.conf, attribute_w_instance) - if deflection_index: - break - else: - item = item.return_parent() - - if deflection_index is not None: - deflection_index = int(deflection_index) - 1 - if not 0 <= deflection_index <= 31: - deflection_index = None - self.logger.warning(f"Attribute 'avm_deflection_index' for item {item.id()} not in valid range 1-5.") - - return deflection_index - - def _get_mac(self, item): - """ - return mac for given item - """ - - mac = None - for i in range(2): - attribute = 'avm_mac' - attribute_w_instance = f"{attribute}@{self.get_instance_name()}" - - mac = self.get_iattr_value(item.conf, attribute) - if mac: - break - mac = self.get_iattr_value(item.conf, attribute_w_instance) - if mac: - break - else: - item = item.return_parent() - - return mac - - def _update_fritz_device_info(self, item): - """ - Updates FritzDevice specific information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceinfoSCPD.pdf - CURL for testing: - curl --anyauth -u user:'password' 'https://192.168.178.1:49443/upnp/control/deviceinfo' -H 'Content-Type: text/xml; charset="utf-8"' -H 'SoapAction: urn:dslforum-org:service:DeviceInfo:1#GetInfo' -d ' ' -s -k - - :param item: Item to be updated (Supported item avm_data_types: uptime, software_version, hardware_version,serial_number, description) - """ - - url = self._build_url("/upnp/control/deviceinfo") - headers = self._header.copy() - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['uptime', 'software_version', 'hardware_version', - 'serial_number']: - action = 'GetInfo' - else: - self.logger.error(f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin") - return - - headers['SOAPACTION'] = f"{self._urn_map['DeviceInfo']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['DeviceInfo']) - - if f"dev_info_{action}" not in self._response_cache: - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"dev_info_{action}"] = response.content - else: - return - - else: - if self.debug_log: - self.logger.debug( - f"Accessing dev_info response cache for action {action} and item {item.property.path}!") - - try: - xml = minidom.parseString(self._response_cache[f"dev_info_{action}"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'uptime': - element_xml = xml.getElementsByTagName('NewUpTime') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'software_version': - element_xml = xml.getElementsByTagName('NewSoftwareVersion') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'hardware_version': - element_xml = xml.getElementsByTagName('NewHardwareVersion') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'serial_number': - element_xml = xml.getElementsByTagName('NewSerialNumber') - else: - element_xml = None - - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - - def _update_tam(self, item): - """ - Updates telephone answering machine (TAM) related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_tam.pdf - - :param item: item to be updated (Supported item avm_data_types: tam, child item avm_data_types: tam_name) - """ - - url = self._build_url("/upnp/control/x_tam") - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['tam', 'tam_name']: - action = 'GetInfo' - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['tam_new_message_number', 'tam_total_message_number']: - action = 'GetMessageList' - else: - self.logger.error(f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin") - return - - tam_index = self._get_tam_index(item) - if not tam_index: - return - - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['TAM']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['TAM'], {'NewIndex': tam_index}) - - if f"tam_{action}" not in self._response_cache: - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"tam_{action}"] = response.content - else: - if self.debug_log: - self.logger.debug(f"Accessing TAM response cache for action {action} and item {item.property.path}!") - - try: - xml = minidom.parseString(self._response_cache[f"tam_{action}"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'tam': - element_xml = xml.getElementsByTagName('NewEnable') - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not available on the FritzDevice") - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'tam_name': - element_xml = xml.getElementsByTagName('NewName') - if len(element_xml) > 0: - item(element_xml[0].firstChild.data, self.get_shortname()) - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not available on the FritzDevice") - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['tam_new_message_number', 'tam_total_message_number']: - message_url_xml = xml.getElementsByTagName('NewURL') - if len(message_url_xml) > 0: - message_url = message_url_xml[0].firstChild.data - - if "tam_messages" not in self._response_cache: - try: - message_result = self._session.get(message_url, timeout=self._timeout, verify=self._verify) - except Exception as e: - if self._fritz_device.is_available(): - self.logger.error(f"Exception when sending GET request: {e}") - self.set_device_availability(False) - return - if not self._fritz_device.is_available(): - self.set_device_availability(True) - self._response_cache["tam_messages"] = message_result.content - else: - if self.debug_log: - self.logger.debug( - f"Accessing tam_messages response cache for action {action} and item {item.property.path}!") - - try: - message_xml = minidom.parseString(self._response_cache["tam_messages"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - messages = message_xml.getElementsByTagName('Message') - message_count = 0 - if len(messages) > 0: - if self.get_iattr_value(item.conf, 'avm_data_type') == 'tam_total_message_number': - message_count = len(messages) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'tam_new_message_number': - for message in messages: - is_new = message.getElementsByTagName('New') - if int(is_new[0].firstChild.data) == 1: - message_count = message_count + 1 - item(message_count, self.get_shortname()) - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not available on the FritzDevice") - - def _update_wlan_config(self, item): - """ - Updates wlan related information, all items of this method need an numeric avm_wlan_index (typically 1-3) - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: wlanconfig, wlan_guest_time_remaining - """ - - url = None - wlan_index = self.get_iattr_value(item.conf, 'avm_wlan_index') - - if wlan_index: - url = self._build_url(f"/upnp/control/wlanconfig{wlan_index}") - else: - self.logger.error(f'No or incorrect avm_wlan_index attribute provided for {item}') - if not url: - return - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wlanconfig', 'wlanconfig_ssid']: - action = 'GetInfo' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wlan_guest_time_remaining': - action = 'X_AVM-DE_GetWLANExtInfo' - else: - self.logger.error(f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin") - return - - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['WLANConfiguration']}"[:43] + f"{wlan_index}#{action}" - soap_data = self._assemble_soap_data(action, f"{self._urn_map['WLANConfiguration']}"[:43] + f"{wlan_index}") - - if f"wlanconfig_{wlan_index}_{action}" not in self._response_cache and url: - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"wlanconfig_{wlan_index}_{action}"] = response.content - else: - if self.debug_log: - self.logger.debug( - f"Accessing wlanconfig response cache for action {action} and item {item.property.path}!") - - try: - xml = minidom.parseString(self._response_cache[f"wlanconfig_{wlan_index}_{action}"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - data = None - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wlanconfig': - data = self._get_value_from_xml_node(xml, 'NewEnable') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wlanconfig_ssid': - data = self._get_value_from_xml_node(xml, 'NewSSID') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wlan_guest_time_remaining': - data = self._get_value_from_xml_node(xml, 'NewX_AVM-DE_TimeRemain') - try: - data = int(data) - except Exception: - pass - - # element_xml = xml.getElementsByTagName('NewX_AVM-DE_TimeRemain') - # if len(element_xml) > 0: - # data = int(element_xml[0].firstChild.data) - - if data is not None: - item(data, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - - def _update_wan_dsl_interface_config(self, item): - """ - Updates wide area network (WAN) speed related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wandslifconfigSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: wan_upstream, wan_downstream) - """ - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_upstream', 'wan_downstream']: - action = 'GetInfo' - else: - self.logger.error(f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin") - return - - url = self._build_url("/upnp/control/wandslifconfig1") - - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['WANDSLInterfaceConfig']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['WANDSLInterfaceConfig']) - - # if action has not been called in a cycle so far, request it and cache response - if f"wan_dsl_interface_config_{action}" not in self._response_cache: - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"wan_dsl_interface_config_{action}"] = response.content - else: - if self.debug_log: - self.logger.debug( - f"Accessing wan_dsl_interface_config response cache for action {action} and item {item.property.path}!") - - try: - xml = minidom.parseString(self._response_cache[f"wan_dsl_interface_config_{action}"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_upstream': - element_xml = xml.getElementsByTagName('NewUpstreamCurrRate') - - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_downstream': - element_xml = xml.getElementsByTagName('NewDownstreamCurrRate') - else: - element_xml = None - - if element_xml is not None and len(element_xml) > 0: - item(int(element_xml[0].firstChild.data), self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - - def _update_wan_common_interface_configuration(self, item): - """ - Updates wide area network (WAN) related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wancommonifconfigSCPD.pdf - https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/IGD1.pdf - - :param item: item to be updated (Supported item avm_data_types: wan_total_packets_sent, wan_total_packets_received, wan_current_packets_sent, wan_current_packets_received, wan_total_bytes_sent, wan_total_bytes_received, wan_current_bytes_sent, wan_current_bytes_received, wan_link) - """ - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_sent': - action = 'GetTotalPacketsSent' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_received': - action = 'GetTotalPacketsReceived' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_sent': - action = 'GetTotalBytesSent' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_received': - action = 'GetTotalBytesReceived' - elif self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_current_packets_sent', - 'wan_current_packets_received', - 'wan_current_bytes_sent', - 'wan_current_bytes_received']: - action = 'GetAddonInfos' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_link': - action = 'GetCommonLinkProperties' - else: - self.logger.error(f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin") - return - - headers = self._header.copy() - if action != 'GetAddonInfos': - headers['SOAPACTION'] = f"{self._urn_map['WANCommonInterfaceConfig']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['WANCommonInterfaceConfig']) - url = self._build_url("/upnp/control/wancommonifconfig1") - else: - headers['SOAPACTION'] = f"{self._urn_map['WANCommonInterfaceConfig_alt']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['WANCommonInterfaceConfig_alt']) - url = self._build_url("/igdupnp/control/WANCommonIFC1") - # if action has not been called in a cycle so far, request it and cache response - if f"wan_common_interface_configuration_{action}" not in self._response_cache: - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"wan_common_interface_configuration_{action}"] = response.content - else: - if self.debug_log: - self.logger.debug( - f"Accessing wan_common_interface_configuration response cache for action {action} and item {item.property.path}!") - - try: - xml = minidom.parseString(self._response_cache[f"wan_common_interface_configuration_{action}"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_sent': - data = self._get_value_from_xml_node(xml, 'NewTotalPacketsSent') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_packets_received': - data = self._get_value_from_xml_node(xml, 'NewTotalPacketsReceived') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_packets_sent': - data = self._get_value_from_xml_node(xml, 'NewPacketSendRate') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_packets_received': - data = self._get_value_from_xml_node(xml, 'NewPacketReceiveRate') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_sent': - data = self._get_value_from_xml_node(xml, 'NewTotalBytesSent') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_total_bytes_received': - data = self._get_value_from_xml_node(xml, 'NewTotalBytesReceived') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_bytes_sent': - data = self._get_value_from_xml_node(xml, 'NewByteSendRate') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_current_bytes_received': - data = self._get_value_from_xml_node(xml, 'NewByteReceiveRate') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_link': - data = self._get_value_from_xml_node(xml, 'NewPhysicalLinkStatus') - data = True if data == 'Up' else False - else: - data = None - - if isinstance(data, str) and data.isdigit(): - data = int(data) - - if data is not None: - item(data, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - - def _update_wan_ip_connection(self, item): - """ - Updates wide area network (WAN) IP related information - - Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf - - :param item: item to be updated (Supported item avm_data_types: wan_connection_status, wan_is_connected, wan_uptime, wan_ip) - """ - - url = self._build_url("/igdupnp/control/WANIPConn1") - - if self.get_iattr_value(item.conf, 'avm_data_type') in ['wan_connection_status', 'wan_is_connected', - 'wan_uptime', 'wan_connection_error']: - action = 'GetStatusInfo' - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_ip': - action = 'GetExternalIPAddress' - else: - self.logger.error(f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not supported by plugin") - return - - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['WANIPConnection']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['WANIPConnection']) - - # if action has not been called in a cycle so far, request it and cache response - if f"wan_ip_connection_{action}" not in self._response_cache: - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"wan_ip_connection_{action}"] = response.content - else: - if self.debug_log: - self.logger.debug( - f"Accessing wan_ip_connection response cache for action {action} and item {item.property.path}!") - - try: - xml = minidom.parseString(self._response_cache[f"wan_ip_connection_{action}"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - if self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_connection_status': - data = self._get_value_from_xml_node(xml, 'NewConnectionStatus') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_is_connected': - data = True if self._get_value_from_xml_node(xml, 'NewConnectionStatus') == 'Connected' else False - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_uptime': - data = self._get_value_from_xml_node(xml, 'NewUptime') - if isinstance(data, str) and data.isdigit(): - data = int(data) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_connection_error': - data = self._get_value_from_xml_node(xml, 'NewLastConnectionError') - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'wan_ip': - data = self._get_value_from_xml_node(xml, 'NewExternalIPAddress') - else: - data = None - - if data is not None: - item(data, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - - @staticmethod - def _get_value_from_xml_node(node, tag_name): - """ - Returns value of tag_name from given xml-node - """ - - data = None - xml = node.getElementsByTagName(tag_name) - if len(xml) > 0: - if not xml[0].firstChild is None: - data = xml[0].firstChild.data - return data - - def get_number_of_deflections(self): - """ - Get the number of deflection entrys - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - :return: number of deflections - """ - - url = self._build_url("/upnp/control/x_contact") - action = "GetNumberOfDeflections" - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['OnTel']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel']) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - data = self._get_value_from_xml_node(xml, 'NewNumberOfDeflections') - return data - - def _update_number_of_deflections(self, item): - """ - Updates the number of acitve deflections - """ - - result = self.get_number_of_deflections() - if result is not None: - item(result, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - - def get_deflection(self, deflection_id=0): - """ - Get the parameter for a deflection entry. - DeflectionID is in the range of 0 .. NumberOfDeflections-1. - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - :param: deflection_id (default: 0) - :return: dict with all deflection details of deflection_id - """ - - url = self._build_url("/upnp/control/x_contact") - action = "GetDeflection" - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['OnTel']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], {'NewDeflectionId': deflection_id}) - - xml = self._get_post_request_as_xml(url, soap_data, headers) - - deflection = dict() - deflection[deflection_id] = dict() - - attributes = ['NewEnable', 'NewType', 'NewNumber', 'NewDeflectionToNumber', 'NewMode', 'NewOutgoing', - 'NewPhonebookID'] - for attribute in attributes: - data = self._get_value_from_xml_node(xml, attribute) - if data: - attribute = attribute[3:] - deflection[deflection_id][attribute] = data - return deflection - - def _update_deflection(self, item): - """ - Updates Item value for deflection - """ - - deflection_index = self._get_deflection_index(item) - if deflection_index: - result = self.get_deflection(deflection_index) - if result is not None: - item(result, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - else: - self.logger.error('Deflection Index not given or incorrect in Item Config') - - def _update_deflection_status(self, item): - """ - Updates Item value for deflection status - """ - - deflection_index = self._get_deflection_index(item) - if deflection_index: - deflection = self.get_deflection(deflection_index) - if deflection is not None: - status = bool(int(deflection[deflection_index]['Enable'])) - item(status, self.get_shortname()) - else: - self.logger.info( - f"Request of attribute {self.get_iattr_value(item.conf, 'avm_data_type')} returned None. Seems that data are not available/supported.") - else: - self.logger.error('Deflection Index not given or incorrect in Item Config') - - def get_deflections(self): - """ - Returns a list of deflecttions - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - :return: dict with all deflection details - """ - - url = self._build_url("/upnp/control/x_contact") - action = "GetDeflections" - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['OnTel']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel']) - - if f"deflections{action}" not in self._response_cache: - response = self._get_post_request(url, soap_data, headers) - if response is not None: - self._response_cache[f"deflections{action}"] = response.content - else: - if self.debug_log: - self.logger.debug(f'Accessing dev_info response cache for action {action}') - - try: - xml = minidom.parseString(self._response_cache[f"deflections{action}"]) - except Exception as e: - self.logger.error(f"Exception when parsing response: {e}") - return - - deflection_list_xml = minidom.parseString(xml.getElementsByTagName('NewDeflectionList')[0].firstChild.data) - item_list = deflection_list_xml.getElementsByTagName('Item') - - deflections = {} - if len(item_list) > 0: - for item in item_list: - deflection_id = int(item.getElementsByTagName('DeflectionId')[0].firstChild.data) - deflections[deflection_id] = {'Enable': '', 'Type': '', 'Number': '', 'DeflectionToNumber': '', - 'Mode': '', 'Outgoing': '', 'PhonebookID': ''} - - for attribute in deflections[deflection_id]: - attribute_value = item.getElementsByTagName(attribute) - if len(attribute_value) > 0: - if attribute_value[0].hasChildNodes(): - deflections[deflection_id][attribute] = attribute_value[0].firstChild.data - return deflections - - def _update_deflections(self, item): - """ - Updates Item value for deflections - """ - - result = self.get_deflections() - if result is not None and dict: - if self.get_iattr_value(item.conf, 'avm_data_type') == 'deflections_details': - item(result, self.get_shortname()) - else: - # Get deflection index from item or parent item - deflection_index = self._get_deflection_index(item) - - # Set Item values - if deflection_index is not None: - if self.get_iattr_value(item.conf, 'avm_data_type') == 'deflection_enable': - item(result[deflection_index]['Enable'], self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'deflection_type': - item(result[deflection_index]['Type'], self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'deflection_number': - item(result[deflection_index]['Number'], self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'deflection_to_number': - item(result[deflection_index]['DeflectionToNumber'], self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'deflection_mode': - item(result[deflection_index]['Mode'], self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'deflection_outgoing': - item(result[deflection_index]['Outgoing'], self.get_shortname()) - elif self.get_iattr_value(item.conf, 'avm_data_type') == 'deflection_phonebook_id': - item(result[deflection_index]['PhonebookID'], self.get_shortname()) - else: - self.logger.error( - f"Attribute {self.get_iattr_value(item.conf, 'avm_data_type')} not available on the FritzDevice") - else: - self.logger.error(f"Deflection Index for {item} not defined") - - def set_deflection(self, deflection_id=0, new_enable=False): - """ - Enable or disable a deflection. - DeflectionID is in the range of 0 .. NumberOfDeflections-1 - | Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_contactSCPD.pdf - :param deflection_id: deflection id (default: 0) - :param new_enable: new enable (default: False) - """ - - url = self._build_url("/upnp/control/x_contact") - action = "SetDeflectionEnable" - headers = self._header.copy() - headers['SOAPACTION'] = f"{self._urn_map['OnTel']}#{action}" - soap_data = self._assemble_soap_data(action, self._urn_map['OnTel'], - {'NewDeflectionId': deflection_id, 'NewEnable': int(new_enable)}) - - self._get_post_request_as_xml(url, soap_data, headers) - - # read deflection after setting - self._update_loop() diff --git a/avm/_pv_1_6_5/assets/webif1.jpg b/avm/_pv_1_6_5/assets/webif1.jpg deleted file mode 100755 index 664d58af1443381b23831d765e9f06f8e55743bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 299580 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

Oe^XVXo0KJ?}Up5MPx^i^BMonc%XNRPy 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 -

-{% endmacro %} - -/** -* Fritzbox Wifi-Control -* -* @param unique id for this widget -* @param a gad/item for the name of the FritzDevice -* @param a gad/item for the wlan 24 ghz -* @param a gad/item for the wlan 5 ghz (optional) -* @param a gad/item for wlan guest (optional) -* @param a gad/item fpr remaining guest time (optional) -* -*/ -{% macro wifi(id, gad_name, gad_wlan24, gad_wlan5, gad_wlanguest, gad_guesttime) %} - {% import "basic.html" as basic %} - {% set uid = uid(page, id) %} - -
- {% if gad_name %}WLAN ({{ basic.print(id~'name', gad_name) }}):
{% endif %} - - {% if gad_wlan24 %}{% endif %} - {% if gad_wlan5 %}{% endif %} - {% if gad_wlanguest %} - {% endif %} -
2.4 GHz: {{ basic.flip(id~'wlan24', gad_wlan24) }}
5.0 GHz: {{ basic.flip(id~'wlan5', gad_wlan5) }}
Gast {% if gad_guesttime %}({{ basic.print(id~'guesttime', gad_guesttime) }}min): {% endif %}{{ basic.flip(id~'wlanguest', gad_wlanguest) }}
-
-{% endmacro %} - -/** - * Fritzbox Systeminfo Widget - * - * @param unique id for this widget - * @param the gad/item for the uptime of the fritzdevice (not the wan uptime!) - * @param the gad/item for the firmware of the fritzdevice - * @param the gad/item for the hardware version of the fritzdevice - * @param the gad/item for the serial number of the fritzdevice - */ -{% macro system_info(id, gad_uptime, gad_firmware, gad_hardware_version, gad_serial_number) %} - {% import "basic.html" as basic %} - {% set uid = uid(page, id) %} -
- {% if gad_uptime %}Uptime: {{ basic.print(id~'uptime', gad_uptime, 'float', 'VAR / 3600') }} hours
{% endif %} - {% if gad_firmware %}Firmware: {{ basic.print(id~'firmware', gad_firmware) }}
{% endif %} - {% if gad_hardware_version %}Gerätetyp: {{ basic.print(id~'hardware_version', gad_hardware_version) }}
{% endif %} - {% if gad_serial_number %}Seriennummer: {{ basic.print(id~'serial_number', gad_serial_number) }}{% endif %} -
-{% endmacro %} - -/** - * Fritzbox WAN-info Widget - * - * @param unique id for this widget - * @param the gad/item for the internet ip - * @param the gad/item for the wan (internet) uptime - * @param the gad/item for the connection status - * @param the gad/item for the last connection error - * @param the gad/item for the total sent packets - * @param the gad/item for the total received packets - * @param the gad/item for the total sent bytes - * @param the gad/item for the total received bytes - * @param the gad/item for the upstream rate - * @param the gad/item for the downstream rate - * - */ -{% macro wan_info(id, gad_ip, gad_uptime, gad_connection_status, gad_connection_error, gad_total_packets_sent, gad_total_packets_received, gad_total_bytes_sent, gad_total_bytes_received, gad_upstream, gad_downstream) %} - {% import "basic.html" as basic %} - {% set uid = uid(page, id) %} -
- {% if gad_ip %}Internet-IP: {{ basic.print(id~'ip', gad_ip) }}
{% endif %} - {% if gad_uptime %}WAN-Uptime: {{ basic.print(id~'wan_uptime', gad_uptime, 'float', 'VAR / 3600') }} hours
{% endif %} - {% if gad_connection_status %}Verbindungsstatus: {{ basic.print(id~'connection_status', gad_connection_status) }}
{% endif %} - {% if gad_connection_error %}Letzter Fehler: {{ basic.print(id~'wan_connection_error', gad_connection_error) }}
{% endif %} - {% if gad_total_packets_sent %}Gesendete Pakete: {{ basic.print(id~'total_packets_sent', gad_total_packets_sent) }}
{% endif %} - {% if gad_total_packets_received %}Empfangene Pakete: {{ basic.print(id~'total_packets_rcv', gad_total_packets_received) }}
{% endif %} - {% if gad_total_bytes_sent %}Gesendetes Bytes: {{ basic.print(id~'total_bytes_sent', gad_total_bytes_sent, 'float', 'VAR / (1024*1024)') }} mb
{% endif %} - {% if gad_total_bytes_received %}Empfangene Bytes: {{ basic.print(id~'total_bytes_received', gad_total_bytes_received, 'float', 'VAR / (1024*1024)') }} mb
{% endif %} - {% if gad_upstream %}Upstream: {{ basic.print(id~'upstream', gad_upstream, 'float', 'VAR / (1000)') }} mbit
{% endif %} - {% if gad_downstream %}Downstream: {{ basic.print(id~'downstream', gad_downstream, 'float', 'VAR / (1000)') }} mbit{% endif %} -
-{% endmacro %} \ No newline at end of file diff --git a/avm/_pv_1_6_5/user_doc.rst b/avm/_pv_1_6_5/user_doc.rst deleted file mode 100755 index ab064ddb6..000000000 --- a/avm/_pv_1_6_5/user_doc.rst +++ /dev/null @@ -1,477 +0,0 @@ - -AVM -=== - -Changelog ---------- -1.6.6 -~~~~~ - -- Methode get_color_defaults implementiert, welche die aktuell von AVM unterstützen diskreten RGB Farben ausliest. -- Methode set_color und color auf diskrete RGB farben umgestellt. Beliebige RGB Farbwerte werden erst ab Q2 2022 von AVM unterstützt. - -1.6.5 -~~~~~ - -- Session_ID wird nur noch bei Bedarf erzeugt (bleibt nach Erstellung 20min gültig und verlängert sich bei erfolgreichem Login) -- Update auf PBKDF2 zur Absicherung der Anmeldung; MD5 als Backup -- Methoden "_get_sid", "_get_login_infos_from_http_request", "_http_logout_request", "_check_sid", "_calculate_pbkdf2_response", "_calculate_md5_response" hinzu -- Bugfixing, Verbesserung der Logausgaben -- Codevereinfachung und Korrektur -- Plugin Parameter für WebIF Page Length hinzu -- Plugin Parameter für Aktivierung der Nutzung des AHA-HTTP Interfaces hinzu - -1.6.4 -~~~~~ - -- Attribut 'ain' deprecated gesetzt; Verwendung von 'avm_ain', so dass alle avm Plugin Attribute auch mit 'avm' beginnen -- Überprüfung aller unterstützen Attributwerte avm_data_type -- Anpassung der update_black_list -- Verbesserung des Handling von avm_data_type für r/o, w/o und r/w Items -- Vereinfachung des Code bei _update_x methods -- Ergänzung weiterer Attributwerte bei avm_data_type - -1.6.3 -~~~~~ - -- Debug des Attributs set_hkr_boost und hkr_boost -- Update der Datatables im WebIF -- Update des automatischen Update im WebIF -- Information zu Attributwerten von 'avm_data_type' ergänzt -- Beispiel für Anwendung der structs mit Instanz erstellt -- Änderung von 'temperatur' auf 'temperature' im struct -- Verbesserung der LogAusgabe, wenn AbfrageAttribut vorhanden aber Wert nicht vorhanden ist - -1.6.2 -~~~~~ -- Bugfixing der neuen Funktionen in 1.6.0 - -1.6.1 -~~~~~ -- Bugfixing der neuen Funktionen in 1.6.0 - -1.6.0 -~~~~~ - -- Anbindung der Smarthome Devices über AHA-Interface hinzugefügt (getestet mit Fritz 440, Comet Dect) -- Funktionen für Rufumleitungen hinzugefügt (getestet mit Fritzbox 7530) -- Plugin Parameter "index" in "avm_tam_index" umbenannt -- Code Cleanup (add new functions to minimize code repetitions) -- Verbesserung der Fehlerbehandlung (insbesondere bei Zugriffsfehlern auf des FritzDevice) - -Allgemeine Informationen ------------------------- - -Im Plugin wird das TR-064 Protokoll und das AHA Protokoll verwendet. - -Links zur Definition des TR-064 Protokolls: - https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/X_contactSCPD.pdf - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf - http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_voipSCPD.pdf - - -Links zur Definition des AHA Protokolls: - https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf - - -Unterstützung erhält man im Forum unter: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin - - -Konfiguration der Fritz!Box ---------------------------- - -Für die Nutzung der Informationen über Telefonereignisse muss der CallMonitor aktiviert werden. Dazu muss auf -einem direkt an die Fritz!Box angeschlossenen Telefon (Analog, ISDN S0 oder DECT) \*96#5# eingegeben werden. - -Bei neueren Firmware Versionen (ab Fritz!OS v7) Muss die Anmeldung an der Box von "nur mit Kennwort" auf "Benutzername -und Kennwort umgestellt werden" und es sollte ein eigener User für das AVM Plugin auf der Fritz!Box eingerichtet werden. - - -Konfiguration des Plugins ---------------------------- - -Die Konfiguration des Plugins erfolgt über das Admin-Interface. -Dafür stehen die folgenden Einstellungen zur Verfügung: - -- `username`: Required login information -- `password`: Required login information -- `host`: Hostname or ip address of the FritzDevice. -- `port`: Port of the FritzDevice, typically 49433 for https or 49000 for http -- `cycle`: timeperiod between two update cycles. Default is 300 seconds. -- `ssl`: True or False => True will add "https", False "http" to the URLs in the plugin -- `verify`: True or False => Turns certificate verification on or off. Typically False -- `call_monitor`: True or False => Activates or deactivates the MonitoringService, which connects to the FritzDevice's call monitor -- `instance`: Unique identifier for each FritzDevice / each instance of the plugin - -Alternativ kann das Plugin auch manuell konfiguriert werden. - - -.. code-block:: yaml - - fb1: - class_name: AVM - class_path: plugins.avm - username: ... # optional - password: '...' - host: fritz.box - port: 49443 - cycle: 300 - ssl: True # use https or not - verify: False # verify ssl certificate - call_monitor: 'True' - call_monitor_incoming_filter: "... ## optional, don't set if you don't want to watch only one specific number with your call monitor" - instance: fritzbox_7490 - - fb2: - class_name: AVM - class_path: plugins.avm - username: ... # optional - password: '...' - host: '...' - port: 49443 - cycle: 300 - ssl: True # use https or not - verify: False # verify ssl certificate - call_monitor: 'True' - instance: wlan_repeater_1750 - -.. note:: - - Kürzere Updatezyklen können abhängig vm Fritzdevice aufgrund von CPU Auslastung und damit zu Problemen (u.a. - zu Nichterreichbarkeit des Webservice) führen. Wird ein kürzerer Updatezyklus benötigt, sollte das shNG Log - beobachtet werden. Dort werden entsprechende Fehlermeldungen hinterlegt. - - -Konfiguration des Items ------------------------ - -Zur Konfiguration der Items stehen folgende Parameter zur Verfügung: - -avm_data_type -~~~~~~~~~~~~~ -This attribute defines supported functions that can be set for an item. Full set see plugin.yaml. -For most items, the avm_data_type can be bound to an instance via @... . Only in some points the items -are parsed as child items. - -avm_incoming_allowed -~~~~~~~~~~~~~~~~~~~~ -Definition der erlaubten eingehenden Rufnummer in Items vom avm_data_type `monitor_trigger`.' - -avm_target_number -~~~~~~~~~~~~~~~~~ -Definition der erlaubten angerufenen Rufnummer in Items vom avm_data_type `monitor_trigger`.' - -avm_wlan_index -~~~~~~~~~~~~~~ -Definition des Wlans ueber index: (1: 2.4Ghz, 2: 5Ghz, 3: Gaeste).' - -avm_mac -~~~~~~~ -Definition der MAC Adresse für Items vom avm_data_type `network_device`. Nur für diese Items mandatory!' - -ain -~~~ -Definition der Aktor Identifikationsnummer (AIN)Items für smarthome Items. Nur für diese Items mandatory!' - -avm_tam_index -~~~~~~~~~~~~~ -Index für den Anrufbeantworter, normalerweise für den ersten eine "1". Es werden bis zu 5 Anrufbeantworter vom Gerät -unterstützt.' - -avm_deflection_index -~~~~~~~~~~~~~~~~~~~~ -Index für die Rufumleitung, normalerweise für die erste eine "1".' - - -item_structs ------------- -Zur Vereinfachung der Einrichtung von Items sind für folgende Item-structs vordefiniert: - -- ``info`` - General Information about Fritzbox -- ``monitor`` - Coll Monitor -- ``tam`` - (für einen) Anrufbeantworter -- ``deflection`` - (für eine) Rufumleitung -- ``wan`` - WAN Items -- ``wlan`` - Wireless Lan Items -- ``device`` - Item eines verbundenen Gerätes -- ``smarthome_general`` - Allgemeine Informationen eines DECT smarthome Devices -- ``smarthome_hkr`` - spezifische Informationen eines DECT Thermostat Devices -- ``smarthome_temperatur_sensor`` - spezifische Informationen eines DECT smarthome Devices mit Temperatursensor -- ``smarthome_alert`` - spezifische Informationen eines DECT smarthome Devices mit Alarmfunktion -- ``smarthome_switch`` - spezifische Informationen eines DECT smarthome Devices mit Schalter -- ``smarthome_powermeter`` - spezifische Informationen eines DECT smarthome Devices mit Strommessung - - -Item Beispiel mit Verwendung der structs ohne Instanz -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: yaml - - avm: - fritzbox: - info: - struct: - - avm.info - reboot: - type: bool - visu_acl: rw - enforce_updates: yes - monitor: - struct: - - avm.monitor - tam: - struct: - - avm.tam - rufumleitung: - rufumleitung_1: - struct: - - avm.deflection - rufumleitung_2: - avm_deflection_index: 2 - struct: - - avm.deflection - wan: - struct: - - avm.wan - wlan: - struct: - - avm.wlan - connected_devices: - mobile_1: - avm_mac: xx:xx:xx:xx:xx:xx - struct: - - avm.device - mobile_2: - avm_mac: xx:xx:xx:xx:xx:xx - struct: - - avm.device - smarthome: - hkr_og_bad: - type: foo - ain: 'xxxxx xxxxxxx' - struct: - - avm.smarthome_general - - avm.smarthome_hkr - - avm.smarthome_temperatur_sensor - - -Item Beispiel mit Verwendung der structs mit Instanz -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: yaml - - smarthome: - socket_3D_Drucker: - type: foo - ain@fritzbox_1: 'xxxxx xxxxxxx' - instance: fritzbox_1 - struct: - - avm.smarthome_general - - avm.smarthome_switch - - avm.smarthome_powermeter - - avm.smarthome_temperature_sensor - temperature: - database: 'yes' - power: - database: 'yes' - -Hier wird zusätzlich das Item "smarthome.socket_3D_Drucker.temperature", welches durch das struct erstellt wird, um das -Attribut "database" ergänzt, um den Wert in die Datenbank zuschreiben. - - -Plugin Funktionen ------------------ - -cancel_call -~~~~~~~~~~~ - -Beendet einen aktiven Anruf. - -get_call_origin -~~~~~~~~~~~~~~~ - -Gib den Namen des Telefons zurück, das aktuell als 'call origin' gesetzt ist. - -.. code-block:: python - - phone_name = sh.fritzbox_7490.get_call_origin() - - -CURL for this function: - -.. code-block:: bash - - curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/x_voip" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:X_VoIP:1#X_AVM-DE_DialGetConfig" -d "" -s -k - -get_calllist -~~~~~~~~~~~~ -Ermittelt ein Array mit dicts aller Einträge der Anrufliste (Attribute 'Id', 'Type', 'Caller', 'Called', 'CalledNumber', 'Name', 'Numbertype', 'Device', 'Port', 'Date',' Duration' (einige optional)). - -get_contact_name_by_phone_number(phone_number) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Durchsucht das Telefonbuch mit einer (vollständigen) Telefonnummer nach Kontakten. Falls kein Name gefunden wird, wird die Telefonnummer zurückgeliefert. - -get_device_log_from_lua -~~~~~~~~~~~~~~~~~~~~~~~ -Ermittelt die Logeinträge auf dem Gerät über die LUA Schnittstelle /query.lua?mq_log=logger:status/log. - -get_device_log_from_tr064 -~~~~~~~~~~~~~~~~~~~~~~~~~ -Ermittelt die Logeinträge auf dem Gerät über die TR-064 Schnittstelle. - -get_host_details -~~~~~~~~~~~~~~~~ -Ermittelt die Informationen zu einem Host an einem angegebenen Index. -dict keys: name, interface_type, ip_address, mac_address, is_active, lease_time_remaining - -get_hosts -~~~~~~~~~ -Ermittelt ein Array mit den Details aller verbundenen Hosts. Verwendet wird die Funktion "get_host_details" - -Beispiel einer Logik, die die Host von 3 verbundenen Geräten in eine Liste zusammenführt und in ein Item schreibt. -'avm.devices.device_list' - -.. code-block:: python - - hosts = sh.fritzbox_7490.get_hosts(True) - hosts_300 = sh.wlan_repeater_300.get_hosts(True) - hosts_1750 = sh.wlan_repeater_1750.get_hosts(True) - - for host_300 in hosts_300: - new = True - for host in hosts: - if host_300['mac_address'] == host['mac_address']: - new = False - if new: - hosts.append(host_300) - for host_1750 in hosts_1750: - new = True - for host in hosts: - if host_1750['mac_address'] == host['mac_address']: - new = False - if new: - hosts.append(host_1750) - - string = '
    ' - for host in hosts: - device_string = '
  • '+host['name']+': '+host['ip_address']+', '+host['mac_address']+'
  • ' - string += device_string - - string += '
' - sh.avm.devices.device_list(string) - -get_phone_name -~~~~~~~~~~~~~~ -Gibt den Namen eines Telefons an einem Index zurück. Der zurückgegebene Wert kann in 'set_call_origin' verwendet werden. - -.. code-block:: python - - phone_name = sh.fb1.get_phone_name(1) - -get_phone_numbers_by_name(name) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Durchsucht das Telefonbuch mit einem Namen nach nach Kontakten und liefert die zugehörigen Telefonnummern. - -.. code-block:: python - - result_numbers = sh.fritzbox_7490.get_phone_numbers_by_name('Mustermann') - result_string = '' - keys = {'work': 'Geschäftlich', 'home': 'Privat', 'mobile': 'Mobil', 'fax_work': 'Fax', 'intern': 'Intern'} - for contact in result_numbers: - result_string += '

'+contact+'

' - i = 0 - result_string += '' - while i < len(result_numbers[contact]): - number = result_numbers[contact][i]['number'] - type_number = keys[result_numbers[contact][i]['type']] - result_string += '' - i += 1 - result_string += '
' + type_number + ':' + number + '

' - sh.general_items.number_search_results(result_string) - -is_host_active -~~~~~~~~~~~~~~ -Prüft, ob eine MAC Adresse auf dem Gerät aktiv ist. Das kann bspw. für die Umsetzung einer Präsenzerkennung genutzt -werden. - -CURL for this function: - -.. code-block:: bash - - curl --anyauth -u user:password "https://fritz.box:49443/upnp/control/hosts" -H "Content-Type: text/xml; charset="utf-8"" -H "SoapAction:urn:dslforum-org:service:Hosts:1#GetSpecificHostEntry" -d "XX:XX:XX:XX:XX:XX" -s -k - -reboot -~~~~~~ -Startet das Gerät neu. - -reconnect -~~~~~~~~~ -Verbindet das Gerät neu mit dem WAN (Wide Area Network). - -set_call_origin -~~~~~~~~~~~~~~~ -Setzt den 'call origin', bspw. vor dem Aufruf von 'start_call'. Typischerweise genutzt vor der Verwendung von "start_call". -Der Origin kann auch mit direkt am Fritzdevice eingerichtet werden: "Telefonie -> Anrufe -> Wählhilfe verwenden -> -Verbindung mit dem Telefon". - -.. code-block:: python - - sh.fb1.set_call_origin("") - -start_call -~~~~~~~~~~ -Startet einen Anruf an eine übergebene Telefonnummer (intern oder extern). - -.. code-block:: python - - sh.fb1.start_call('0891234567') - sh.fb1.start_call('**9') - -wol(mac_address) -~~~~~~~~~~~~~~~~ -Sendet einen WOL (WakeOnLAN) Befehl an eine MAC Adresse. - -get_number_of_deflections -~~~~~~~~~~~~~~~~~~~~~~~~~ -Liefert die Anzahl der Rufumleitungen zurück. - -get_deflection -~~~~~~~~~~~~~~ -Liefert die Details der Rufumleitung der angegebenen ID zurück (Default-ID = 0) - -get_deflections -~~~~~~~~~~~~~~~ -Liefert die Details aller Rufumleitungen zurück. - -set_deflection_enable -~~~~~~~~~~~~~~~~~~~~~ -Schaltet die Rufumleitung mit angegebener ID an oder aus. - - -Web Interface -------------- - -Das avm Plugin verfügt über ein Webinterface, mit dessen Hilfe die Items die das Plugin nutzen -übersichtlich dargestellt werden. - -.. important:: - - Das Webinterface des Plugins kann mit SmartHomeNG v1.4.2 und davor **nicht** genutzt werden. - Es wird dann nicht geladen. Diese Einschränkung gilt nur für das Webinterface. Ansonsten gilt - für das Plugin die in den Metadaten angegebene minimale SmartHomeNG Version. - - -Aufruf des Webinterfaces -~~~~~~~~~~~~~~~~~~~~~~~~ - -Das Plugin kann aus dem Admin-IF aufgerufen werden. Dazu auf der Seite Plugins in der entsprechenden -Zeile das Icon in der Spalte **Web Interface** anklicken. - -Im WebIF stehen folgende Reiter zur Verfügung: - - AVM Items - Tabellarische Auflistung aller Items, die mit dem TR-064 Protokoll ausgelesen werden - - AVM Smarthome Items - Tabellarische Auflistung aller Items, die mit dem AHA Protokoll ausgelesen werden (Items der Smarthome Geräte) - - Plugin-API - Beschreibung der Plugin-API - - Log-Einträge - Listung der Logeinträge der Fritzbox - - Call Monitor Items - Tabellarische Auflistung des Anrufmonitors (nur wenn dieser konfiguriert ist) - - AVM Smarthome Devices - Auflistung der mit der Fritzbox verbundenen Geräte diff --git a/avm/_pv_1_6_5/webif/__init__.py b/avm/_pv_1_6_5/webif/__init__.py deleted file mode 100755 index a70d399d4..000000000 --- a/avm/_pv_1_6_5/webif/__init__.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab -######################################################################### -# Copyright 2021- Michael Wenzel wenzel_michael@web.de -######################################################################### -# This file is part of SmartHomeNG. -# https://www.smarthomeNG.de -# https://knx-user-forum.de/forum/supportforen/smarthome-py -# -# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and -# upwards. -# -# SmartHomeNG is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SmartHomeNG is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SmartHomeNG. If not, see . -# -######################################################################### - -import json -from lib.item import Items -from lib.model.smartplugin import SmartPluginWebIf - - -# ------------------------------------------ -# Webinterface of the plugin -# ------------------------------------------ - -import cherrypy -from jinja2 import Environment, FileSystemLoader - - -class WebInterface(SmartPluginWebIf): - - def __init__(self, webif_dir, plugin): - """ - Initialization of instance of class WebInterface - - :param webif_dir: directory where the webinterface of the plugin resides - :param plugin: instance of the plugin - :type webif_dir: str - :type plugin: object - """ - self.logger = plugin.logger - self.webif_dir = webif_dir - self.plugin = plugin - self.items = Items.get_instance() - - self.call_monitor_items = [] - self.tplenv = self.init_template_environment() - - @cherrypy.expose - def index(self, reload=None, action=None): - """ - Build index.html for cherrypy - Render the template and return the html file to be delivered to the browser - :return: contents of the template after beeing rendered - """ - - if self.plugin._call_monitor: - self.call_monitor_items = [] - self.call_monitor_items.extend(self.plugin._monitoring_service.get_items()) - self.call_monitor_items.extend(self.plugin._monitoring_service.get_trigger_items()) - self.call_monitor_items.extend(self.plugin._monitoring_service.get_items_incoming()) - self.call_monitor_items.extend(self.plugin._monitoring_service.get_items_outgoing()) - - tmpl = self.tplenv.get_template('index.html') - return tmpl.render(plugin_shortname=self.plugin.get_shortname(), - plugin_version=self.plugin.get_version(), - plugin_info=self.plugin.get_info(), - avm_items=sorted(self.plugin.get_fritz_device().get_items(), key=lambda k: str.lower(k['_path'])), - avm_item_count=len(self.plugin.get_fritz_device().get_items()), - call_monitor_items=sorted(self.call_monitor_items, key=lambda k: str.lower(k['_path'])), - call_monitor_item_count=len(self.call_monitor_items), - smarthome_items=sorted(self.plugin.get_fritz_device().get_smarthome_items(), key=lambda k: str.lower(k['_path'])), - smarthome_item_count=len(self.plugin.get_fritz_device().get_smarthome_items()), - p=self.plugin, - webif_pagelength=self.plugin.webif_pagelength, - ) - - @cherrypy.expose - def get_data_html(self, dataSet=None): - """ - Return data to update the webpage - - For the standard update mechanism of the web interface, the dataSet to return the data for is None - - :param dataSet: Dataset for which the data should be returned (standard: None) - :return: dict with the data needed to update the web page. - """ - if dataSet is None: - # get the new data - data = dict() - if self.plugin._call_monitor: - if self.call_monitor_items: - data['call_monitor'] = {} - for item in self.call_monitor_items: - data['call_monitor'][item.id()] = {} - data['call_monitor'][item.id()]['value'] = item() - - if self.plugin.get_fritz_device().get_items(): - data['avm_items'] = {} - for item in self.plugin.get_fritz_device().get_items(): - data['avm_items'][item.id()] = {} - data['avm_items'][item.id()]['value'] = item() - data['avm_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') - data['avm_items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') - - if self.plugin.get_fritz_device().get_smarthome_items(): - data['avm_smarthome_items'] = {} - for item in self.plugin.get_fritz_device().get_smarthome_items(): - data['avm_smarthome_items'][item.id()] = {} - data['avm_smarthome_items'][item.id()]['value'] = item() - data['avm_smarthome_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') - data['avm_smarthome_items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') - - try: - return json.dumps(data, default=str) - except Exception as e: - self.logger.error(f"get_data_html exception: {e}") - return {} - - @cherrypy.expose - def reboot(self): - self.plugin.reboot() - - @cherrypy.expose - def reconnect(self): - self.plugin.reconnect() diff --git a/avm/_pv_1_6_5/webif/static/img/lamp_green.png b/avm/_pv_1_6_5/webif/static/img/lamp_green.png deleted file mode 100755 index fb130568b7d788da9a89b16c97f60d30798bf0ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3757 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 - table th.no { - width: 50; - } - table th.value { - width: 250px; - } - #itemtable { - display: none; - table-layout: fixed; - } - #smarthomeitemtable { - display: none; - } - -{% endblock pluginstyles %} - -{% block pluginscripts %} - - -{% endblock pluginscripts %} - -{% set tabcount = 6 %} - -{% set tab1title = _(""'AVM Items'" (" ~ avm_item_count ~ ") ") %} -{% set tab2title = _(""'AVM Smarthome Items'" (" ~ smarthome_item_count ~ ") ") %} -{% set tab3title = _(""'Plugin-API'"") %} -{% set tab4title = _(""'Log-Einträge'"") %} -{% set tab5title = _(""'Call Monitor Items'" (" ~ call_monitor_item_count ~ ") ") %} -{% set tab6title = _(""'AVM Smarthome Devices'"") %} - -{% set language = p.get_sh().get_defaultlanguage() %} -{% if language not in ['en','de'] %} - {% set language = 'en' %} -{% endif %} - -{% block headtable %} - - - - - - - - - - - - - - - - - - - - - -
- {% if p.get_fritz_device().is_available() %} - {{ _('Gerät verfügbar') }} - {% else %} - {{ _('Gerät nicht verfügbar') }} - {% endif %} - {{ _('Verbunden') }} - - {% if p.get_fritz_device().is_available() %} - {{ _('Ja') }}{% if p._fritz_device.is_ssl() %}, SSL{% endif %} - {% else %} - {{ _('Nein') }} - {% endif %} - {{ _('Benutzer') }}{{ p.get_parameter_value_for_display('username') }}
- {% if p._call_monitor %} - {% if p.get_monitoring_service()._listen_active %} - {{ _('Call Monitor verbunden') }} - {% else %} - {{ _('Call Monitor nicht verbunden') }} - {% endif %} - {% endif %} - {{ _('Call Monitor') }} - {% if p._call_monitor %}{{ _('Ja') }}{% if not p.get_monitoring_service()._listen_active %}, {{ _('nicht verbunden') }}{% endif %}{% else %}{{ _('Nein') }}{% endif %}{{ _('Passwort') }}{{ p.get_parameter_value_for_display('password') }}
{{ _('Host') }}{{ p._fritz_device.get_host() }}{{ _('Port') }}{{ p._fritz_device.get_port() }} {% if p._fritz_device.is_ssl() %}(HTTPS){% endif %}
-{% endblock %} - - -{% block buttons %} - - -{% endblock buttons %} - - -{% block bodytab1 %} -
-
- -
-
- - - - - - - - - - - - - - {% for item in avm_items %} - {% set item_id = item.id() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - - {% endfor %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ _('.') }}{{ item() }}{{ _('.') }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ _('.') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
-
-{% endblock %} - - -{% block bodytab2 %} -
-
- -
-
- - - - - - - - - - - - - {% for item in smarthome_items %} - {% set item_id = item.id() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - {% endfor %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ _('.') }}{{ item() }}{{ _('.') }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ _('.') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
-
-{% endblock %} - - -{% block bodytab3 %} -
-
- {% for function, dict in p.metadata.plugin_functions.items() %} -
-
- {{ dict['type'] }} {{ function }}({% if dict['parameters'] is not none %}{% for name, paramdict in dict['parameters'].items() %}{% if loop.index > 1 %}, {% endif %}{{ name }}: {{ paramdict['type'] }}{% endfor %}{% endif %}) -
-
- {{ dict['description'][language] }}
- {% if dict['parameters'] is not none %} -
-
- {{ _('Parameter') }}: -
-
-
    - {% for name, paramdict in dict['parameters'].items() %} -
  • - {{ name }}: {{ paramdict['type'] }}
    - {{ paramdict['description'][language] }} -
  • - {% endfor %} -
-
-
- {% endif %} -
-
- {% endfor %} -
-
-{% endblock %} - - -{% block bodytab4 %} -{% set logentries = p.get_device_log_from_lua() %} -
-
- - - - - - - - - - - - {% if logentries %} - {% for logentry in logentries %} - - - - - - - - {% endfor %} - {% endif %} - -
{{ _('Datum')}}{{ _('Uhrzeit')}}{{ _('Meldung')}}{{ _('Nachrichtennummer')}}{{ _('Kategorie')}}
{{ logentry[4] }}{{ logentry[5] }}{{ logentry[0] }} - - {{ logentry[1] }} - - {{ _('cat_'+logentry[2]|string) }}
-
-
-{% endblock %} - - -{% block bodytab5 %} -{% if p._call_monitor %} -
-
- -
-
- - - - - - - - - - - - - {% for item in call_monitor_items %} - {% set item_id = item.id() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - {% endfor %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key] }}{{ item() }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
-
-{% endif %} -{% endblock %} - -{% block bodytab6 %} -{% if p._fritz_device._smarthome_devices %} -
-
- -
-
- - - - - - - - - - {% for ain in p._fritz_device._smarthome_devices %} - - - - - - {% endfor %} - -
{{ _('No') }}{{ _('Device ain') }}{{ _('Device Details (dict)') }}
{{ loop.index }}{{ ain }}{{ p._fritz_device._smarthome_devices[ain] }}
-
-
- -{% endif %} -{% endblock %} \ No newline at end of file From 34fd793b63fc6b0e62933995e03cbbd8287f5559 Mon Sep 17 00:00:00 2001 From: msinn Date: Mon, 20 Mar 2023 10:19:27 +0100 Subject: [PATCH 111/178] uzsu: Changed requirement for numpy to 'numpy>=1.23.4' --- uzsu/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uzsu/requirements.txt b/uzsu/requirements.txt index bca03044c..9df08d1fb 100755 --- a/uzsu/requirements.txt +++ b/uzsu/requirements.txt @@ -1,4 +1,4 @@ -numpy +numpy>=1.23.4 scipy>=1.1.0,<=1.3.0;python_version<'3.7' scipy>=1.2.0,<=1.7.3;python_version=='3.7' #scipy>=1.5.0,<=1.8.1;python_version>'3.7' From 4cd71329623527a77bfe7dc89110f97f9e4caed8 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 20 Mar 2023 15:23:40 +0100 Subject: [PATCH 112/178] avm: change plugin class name declaration --- avm/plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avm/plugin.yaml b/avm/plugin.yaml index f60588f70..7f8ce47e4 100644 --- a/avm/plugin.yaml +++ b/avm/plugin.yaml @@ -19,7 +19,7 @@ plugin: # py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) multi_instance: true # plugin supports multi instance restartable: unknown - classname: AVM # class containing the plugin + classname: avm # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml From 7c7fa97880565a41f0ca415ec527cca25273763e Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 20 Mar 2023 15:42:57 +0100 Subject: [PATCH 113/178] Update __init__.py --- avm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avm/__init__.py b/avm/__init__.py index 7eb452291..3322b7dc6 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -45,7 +45,7 @@ from .webif import WebInterface -class AVM(SmartPlugin): +class avm(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ From c48a446cf03e8c2d69e2efaef8dc8cb3116d4200 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 20 Mar 2023 15:59:26 +0100 Subject: [PATCH 114/178] Update __init__.py --- avm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avm/__init__.py b/avm/__init__.py index 3322b7dc6..7eb452291 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -45,7 +45,7 @@ from .webif import WebInterface -class avm(SmartPlugin): +class AVM(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ From b7a784200b98bba0ea01479a5340005c7cfeb177 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 20 Mar 2023 15:59:44 +0100 Subject: [PATCH 115/178] Update plugin.yaml --- avm/plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avm/plugin.yaml b/avm/plugin.yaml index 7f8ce47e4..f60588f70 100644 --- a/avm/plugin.yaml +++ b/avm/plugin.yaml @@ -19,7 +19,7 @@ plugin: # py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) multi_instance: true # plugin supports multi instance restartable: unknown - classname: avm # class containing the plugin + classname: AVM # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml From 40abb0c7c869c49bc2ab9b91f25a74438e6de02d Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 20 Mar 2023 17:46:31 +0100 Subject: [PATCH 116/178] AVM-Plugin: - bump to 2.0.2 - define details of avm_data_type as dict for better maintainability in separate file and import it - import item_attributes.py and create attribute lists - improve item attribute lists - make logging info more clear --- avm/__init__.py | 320 +++++++++++++++-------------------------- avm/item_attributes.py | 157 ++++++++++++++++++++ 2 files changed, 274 insertions(+), 203 deletions(-) create mode 100644 avm/item_attributes.py diff --git a/avm/__init__.py b/avm/__init__.py index 7eb452291..c3c9f5b56 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -43,19 +43,25 @@ from lib.model.smartplugin import SmartPlugin from .webif import WebInterface +from . import item_attributes class AVM(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ - PLUGIN_VERSION = '2.0.1' + PLUGIN_VERSION = '2.0.2' # ToDo: Clean dead code in Fritzdevice - # Todo: check setting of level to 0 if simpleonoff is off -> Status: Implemented, need to be tested - # Todo: implement HSB into AHA - # Todo: implement HS into AHA - # Todo: Update to new smartplugin methods to prepare for plugin being restartable + + # ToDo: check setting of level to 0 if simpleonoff is off -> Status: Implemented, need to be tested + # ToDo: FritzHome.handle_updated_item: implement 'saturation' + # ToDo: FritzHome.handle_updated_item: implement 'unmapped_hue' + # ToDo: FritzHome.handle_updated_item: implement 'unmapped_saturation' + # ToDo: FritzHome.handle_updated_item: implement 'hsv' + # ToDo: FritzHome.handle_updated_item: implement 'hs' + + # ToDo: Update to new smartplugin methods to prepare for plugin being restartable def __init__(self, sh): """Initializes the plugin.""" @@ -174,31 +180,31 @@ def parse_item(self, item): avm_data_cycle = 30 if 1 < avm_data_cycle < 29 else avm_data_cycle # handle items specific to call monitor - if avm_data_type in (CALL_MONITOR_ATTRIBUTES + CALL_DURATION_ATTRIBUTES): + if avm_data_type in CALL_MONITOR_ATTRIBUTES: if self.monitoring_service: self.monitoring_service.register_item(item, avm_data_type) else: - self.logger.warning("Items with avm attribute found, which needs Call-Monitoring-Service. This is not available/enabled for that plugin; Item will be ignored.") + self.logger.warning(f"Items with avm attribute {avm_data_type!r} found, which needs Call-Monitoring-Service. This is not available/enabled for that plugin; Item will be ignored.") # handle smarthome items using aha-interface (new) elif avm_data_type in AHA_ATTRIBUTES: if self.fritz_home: self.fritz_home.register_item(item, avm_data_type, avm_data_cycle) else: - self.logger.warning("Items with avm attribute found, which needs aha-http-interface. This is not available/enabled for that plugin; Item will be ignored.") + self.logger.warning(f"Items with avm attribute {avm_data_type!r} found, which needs aha-http-interface. This is not available/enabled for that plugin; Item will be ignored.") # handle items updated by tr-064 interface elif avm_data_type in TR064_ATTRIBUTES: if self.fritz_device: self.fritz_device.register_item(item, avm_data_type, avm_data_cycle) else: - self.logger.warning("Items with avm attribute found, which needs tr064 interface. This is not available/enabled; Item will be ignored.") + self.logger.warning(f"Items with avm attribute {avm_data_type!r} found, which needs tr064 interface. This is not available/enabled; Item will be ignored.") # handle anything else else: - self.logger.warning(f"Item={item.id()} has unknown avm_data_type={avm_data_type}. Item will be ignored.") + self.logger.warning(f"Item={item.id()} has unknown avm_data_type {avm_data_type!r}. Item will be ignored.") # items which can be changed outside the plugin context - if avm_data_type in (AVM_RW_ATTRIBUTES + AHA_WO_ATTRIBUTES + AHA_RW_ATTRIBUTES + HOMEAUTO_RW_ATTRIBUTES): + if avm_data_type in ALL_ATTRIBUTES_WRITEABLE: return self.update_item def update_item(self, item, caller=None, source=None, dest=None): @@ -538,7 +544,7 @@ def register_item(self, item, avm_data_type: str, avm_data_cycle: int): self.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_wlan_index' is not defined; Item will be ignored.") # handle network_device related items - elif avm_data_type in (HOST_ATTRIBUTE + HOST_CHILD_ATTRIBUTES): + elif avm_data_type in HOST_ATTRIBUTES: index = self._get_mac(item) if index is not None: self.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_mac' found; append to list.") @@ -3980,7 +3986,7 @@ def register_item(self, item, avm_data_type: str): else: self._trigger_items[item] = (avm_data_type, avm_incoming_allowed, avm_target_number) - elif avm_data_type in CALL_DURATION_ATTRIBUTES: + elif avm_data_type in CALL_MONITOR_ATTRIBUTES_DURATION: if avm_data_type == 'call_duration_incoming': self._duration_item_in[item] = (avm_data_type, None) else: @@ -4417,193 +4423,101 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st """ Definition of attribute value groups of avm_data_type """ -AHA_RO_ATTRIBUTES = ['device_id', - 'manufacturer', - 'product_name', - 'fw_version', - 'connected', - 'device_name', - 'tx_busy', - 'device_functions', - 'current_temperature', - 'temperature_reduced', - 'temperature_comfort', - 'temperature_offset', - 'windowopenactiveendtime', - 'boost_active', - 'boostactiveendtime', - 'summer_active', - 'holiday_active', - 'battery_low', - 'lock', - 'device_lock', - 'errorcode', - 'switch_mode', - 'power', 'energy', - 'voltage', - 'humidity', - 'alert_state', - 'color_mode', - 'supported_color_mode', - 'fullcolorsupport', - 'mapped', - 'blind_mode', - 'endpositionsset' - ] - -AHA_WO_ATTRIBUTES = ['set_target_temperature', - 'set_window_open', - 'set_hkr_boost', - 'battery_level', - 'set_simpleonoff', - 'set_level', - 'set_levelpercentage', - 'set_hue', - 'set_saturation', - 'set_colortemperature', - 'switch_toggle'] - -AHA_RW_ATTRIBUTES = ['target_temperature', - 'window_open', - 'hkr_boost', - 'simpleonoff', - 'level', - 'levelpercentage', - 'hue', - 'saturation', - 'colortemperature', - 'switch_state', - 'unmapped_hue', - 'unmapped_saturation' - ] - -AHA_ATTRIBUTES = [*AHA_RO_ATTRIBUTES, - *AHA_WO_ATTRIBUTES, - *AHA_RW_ATTRIBUTES] - -AVM_RW_ATTRIBUTES = ['wlanconfig', - 'tam', - 'deflection_enable'] - -CALL_MONITOR_ATTRIBUTES_GEN = ['call_event', - 'call_direction'] - -CALL_MONITOR_ATTRIBUTES_IN = ['is_call_incoming', - 'last_caller_incoming', - 'last_call_date_incoming', - 'call_event_incoming', - 'last_number_incoming', - 'last_called_number_incoming'] - -CALL_MONITOR_ATTRIBUTES_OUT = ['is_call_outgoing', - 'last_caller_outgoing', - 'last_call_date_outgoing', - 'call_event_outgoing', - 'last_number_outgoing', - 'last_called_number_outgoing'] - -CALL_MONITOR_ATTRIBUTES_TRIGGER = ['monitor_trigger'] - -CALL_MONITOR_ATTRIBUTES = [*CALL_MONITOR_ATTRIBUTES_GEN, - *CALL_MONITOR_ATTRIBUTES_IN, - *CALL_MONITOR_ATTRIBUTES_OUT, - *CALL_MONITOR_ATTRIBUTES_TRIGGER] - -CALL_DURATION_ATTRIBUTES = ['call_duration_incoming', - 'call_duration_outgoing'] - -WAN_CONNECTION_ATTRIBUTES = ['wan_connection_status', - 'wan_connection_error', - 'wan_is_connected', - 'wan_uptime', - 'wan_ip'] - -TAM_ATTRIBUTES = ['tam', - 'tam_name', - 'tam_new_message_number', - 'tam_old_message_number', - 'tam_total_message_number'] - -WLAN_CONFIG_ATTRIBUTES = ['wlanconfig', - 'wlanconfig_ssid', - 'wlan_guest_time_remaining', - 'wlan_associates', - 'wps_active', - 'wps_status', - 'wps_mode'] - -WLAN_ATTRIBUTES = ['wlan_total_associates'] - -WAN_COMMON_INTERFACE_ATTRIBUTES = ['wan_total_packets_sent', - 'wan_total_packets_received', - 'wan_current_packets_sent', - 'wan_current_packets_received', - 'wan_total_bytes_sent', - 'wan_total_bytes_received', - 'wan_current_bytes_sent', - 'wan_current_bytes_received', - 'wan_link'] - -FRITZ_DEVICE_ATTRIBUTES = ['uptime', - 'software_version', - 'hardware_version', - 'serial_number'] - -HOST_ATTRIBUTE = ['network_device', - 'hosts_count', - 'hosts_info', - 'mesh_topology'] - -HOST_CHILD_ATTRIBUTES = ['device_ip', - 'device_connection_type', - 'device_hostname', - 'connection_status'] - -HOST_ATTRIBUTES = [*HOST_ATTRIBUTE, - *HOST_CHILD_ATTRIBUTES] - -DEFLECTION_ATTRIBUTES = ['deflections_details', - 'deflection_enable', - 'deflection_type', - 'deflection_number', - 'deflection_to_number', - 'deflection_mode', - 'deflection_outgoing', - 'deflection_phonebook_id'] - -WAN_DSL_INTERFACE_ATTRIBUTES = ['wan_upstream', - 'wan_downstream'] - -HOMEAUTO_RO_ATTRIBUTES = ['hkr_device', - 'set_temperature', - 'temperature', - 'set_temperature_reduced', - 'set_temperature_comfort', - 'firmware_version'] - -HOMEAUTO_RW_ATTRIBUTES = ['aha_device'] - -HOMEAUTO_ATTRIBUTES = [*HOMEAUTO_RO_ATTRIBUTES, - *HOMEAUTO_RW_ATTRIBUTES] - -MYFRITZ_ATTRIBUTES = ['myfritz_status'] - -DEPRECATED_ATTRIBUTES = ['temperature', - 'set_temperature_reduced', - 'set_temperature_comfort', - 'firmware_version', - 'aha_device', - 'hkr_device'] - -TR064_ATTRIBUTES = [*WAN_CONNECTION_ATTRIBUTES, - *TAM_ATTRIBUTES, - *WLAN_CONFIG_ATTRIBUTES, - *WLAN_ATTRIBUTES, - *WAN_COMMON_INTERFACE_ATTRIBUTES, - *FRITZ_DEVICE_ATTRIBUTES, - *HOST_ATTRIBUTES, - *DEFLECTION_ATTRIBUTES, - *WAN_DSL_INTERFACE_ATTRIBUTES, - *HOMEAUTO_ATTRIBUTES, - *MYFRITZ_ATTRIBUTES, - *AVM_RW_ATTRIBUTES] + + +def _get_attributes(sub_dict: dict) -> list: + attributes = [] + for avm_data_type in item_attributes.AVM_DATA_TYPES: + if sub_dict.items() <= item_attributes.AVM_DATA_TYPES[avm_data_type].items(): + attributes.append(avm_data_type) + return attributes + + +ALL_ATTRIBUTES_SUPPORTED_BY_REPEATER = _get_attributes({'supported_by_repeater': True}) + + +ALL_ATTRIBUTES_WRITEABLE = _get_attributes({'access': 'wo'}) + _get_attributes({'access': 'rw'}) + + +DEPRECATED_ATTRIBUTES = _get_attributes({'deprecated': True}) + + +AHA_ATTRIBUTES = _get_attributes({'interface': 'aha'}) + + +AHA_RO_ATTRIBUTES = _get_attributes({'interface': 'aha', 'access': 'ro'}) + + +AHA_WO_ATTRIBUTES = _get_attributes({'interface': 'aha', 'access': 'wo'}) + + +AHA_RW_ATTRIBUTES = _get_attributes({'interface': 'aha', 'access': 'rw'}) + + +TR064_ATTRIBUTES = _get_attributes({'interface': 'tr064'}) + + +AVM_RW_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'access': 'rw'}) + + +CALL_MONITOR_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'call_monitor'}) + + +CALL_MONITOR_ATTRIBUTES_TRIGGER = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'trigger'}) + + +CALL_MONITOR_ATTRIBUTES_GEN = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic'}) + + +CALL_MONITOR_ATTRIBUTES_IN = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in'}) + + +CALL_MONITOR_ATTRIBUTES_OUT = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out'}) + + +CALL_MONITOR_ATTRIBUTES_DURATION = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'duration'}) + + +WAN_CONNECTION_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection'}) + + +WAN_COMMON_INTERFACE_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface'}) + + +WAN_DSL_INTERFACE_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wan', 'sub_group': 'dsl_interface'}) + + +TAM_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'tam'}) + + +WLAN_CONFIG_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wlan_config'}) + + +WLAN_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wlan'}) + + +FRITZ_DEVICE_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'fritz_device'}) + + +HOST_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'host'}) + + +HOST_ATTRIBUTES_GEN = _get_attributes({'interface': 'tr064', 'group': 'host', 'sub_group': None}) + + +HOST_ATTRIBUTES_CHILD = _get_attributes({'interface': 'tr064', 'group': 'host', 'sub_group': 'child'}) + + +DEFLECTION_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'deflection'}) + + +HOMEAUTO_RO_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'homeauto', 'access': 'ro'}) + + +HOMEAUTO_RW_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'homeauto', 'access': 'rw'}) + + +HOMEAUTO_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'homeauto'}) + + +MYFRITZ_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'myfritz'}) diff --git a/avm/item_attributes.py b/avm/item_attributes.py new file mode 100644 index 000000000..658d26d55 --- /dev/null +++ b/avm/item_attributes.py @@ -0,0 +1,157 @@ +# !/usr/bin/env python +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Copyright 2023 Michael Wenzel +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# AVM for SmartHomeNG. https://github.com/smarthomeNG// +# +# This plugin is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this plugin. If not, see . +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +AVM_DATA_TYPES = { + # 'avm_data_type': {'interface': 'tr064', 'group': '', 'sub_group': None, 'access': '', 'type': '', 'deprecated': False, 'supported_by_repeater': False, 'description': ''}, + 'uptime': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Laufzeit des Fritzdevice in Sekunden'}, + 'software_version': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Serialnummer des Fritzdevice'}, + 'hardware_version': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Software Version'}, + 'serial_number': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Hardware Version'}, + 'myfritz_status': {'interface': 'tr064', 'group': 'myfritz', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'MyFritz Status (an/aus)'}, + 'call_direction': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Richtung des letzten Anrufes'}, + 'call_event': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status des letzten Anrufes'}, + 'monitor_trigger': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'trigger', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Monitortrigger'}, + 'is_call_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingehender Anruf erkannt'}, + 'last_caller_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Letzter Anrufer'}, + 'last_call_date_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitpunkt des letzten eingehenden Anrufs'}, + 'call_event_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status des letzten eingehenden Anrufs'}, + 'last_number_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Nummer des letzten eingehenden Anrufes'}, + 'last_called_number_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Angerufene Nummer des letzten eingehenden Anrufs'}, + 'is_call_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Ausgehender Anruf erkannt'}, + 'last_caller_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Letzter angerufener Kontakt'}, + 'last_call_date_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitpunkt des letzten ausgehenden Anrufs'}, + 'call_event_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status des letzten ausgehenden Anrufs'}, + 'last_number_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Nummer des letzten ausgehenden Anrufes'}, + 'last_called_number_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Letzte verwendete Telefonnummer für ausgehenden Anruf'}, + 'call_duration_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'duration', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Dauer des eingehenden Anrufs'}, + 'call_duration_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'duration', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Dauer des ausgehenden Anrufs'}, + 'tam': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'TAM an/aus'}, + 'tam_name': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Name des TAM'}, + 'tam_new_message_number': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Anzahl der alten Nachrichten'}, + 'tam_old_message_number': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Anzahl der neuen Nachrichten'}, + 'tam_total_message_number': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gesamtanzahl der Nachrichten'}, + 'wan_connection_status': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindungsstatus'}, + 'wan_connection_error': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindungsfehler'}, + 'wan_is_connected': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung aktiv'}, + 'wan_uptime': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindungszeit'}, + 'wan_ip': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN IP Adresse'}, + 'wan_upstream': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'dsl_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Upstream Datenmenge'}, + 'wan_downstream': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'dsl_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Downstream Datenmenge'}, + 'wan_total_packets_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt versendeter Pakete'}, + 'wan_total_packets_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt empfangener Pakete'}, + 'wan_current_packets_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuell versendeter Pakete'}, + 'wan_current_packets_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuell empfangener Pakete'}, + 'wan_total_bytes_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt versendeter Bytes'}, + 'wan_total_bytes_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt empfangener Bytes'}, + 'wan_current_bytes_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuelle Bitrate Senden'}, + 'wan_current_bytes_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuelle Bitrate Empfangen'}, + 'wan_link': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Link'}, + 'wlanconfig': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WLAN An/Aus'}, + 'wlanconfig_ssid': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WLAN SSID'}, + 'wlan_guest_time_remaining': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbleibende Zeit, bis zum automatischen Abschalten des Gäste-WLAN'}, + 'wlan_associates': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Anzahl der verbundenen Geräte im jeweiligen WLAN'}, + 'wps_active': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Schaltet WPS für das entsprechende WlAN an / aus'}, + 'wps_status': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WPS Status des entsprechenden WlAN'}, + 'wps_mode': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WPS Modus des entsprechenden WlAN'}, + 'wlan_total_associates': {'interface': 'tr064', 'group': 'wlan', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Anzahl der verbundenen Geräte im WLAN'}, + 'hosts_count': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Anzahl der Hosts'}, + 'hosts_info': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Informationen über die Hosts'}, + 'mesh_topology': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Topologie des Mesh'}, + 'network_device': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus // Defines Network device via MAC-Adresse'}, + 'device_ip': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Geräte-IP (Muss Child von "network_device" sein'}, + 'device_connection_type': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungstyp (Muss Child von "network_device" sein'}, + 'device_hostname': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Gerätename (Muss Child von "network_device" sein'}, + 'connection_status': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, + 'number_of_deflections': {'interface': 'tr064', 'group': 'deflection', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Anzahl der eingestellten Rufumleitungen'}, + 'deflections_details': {'interface': 'tr064', 'group': 'deflection', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Details zu allen Rufumleitung (als dict)'}, + 'deflection_details': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Details zur Rufumleitung (als dict); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item'}, + 'deflection_enable': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Rufumleitung Status an/aus; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, + 'deflection_type': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Type der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, + 'deflection_number': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Telefonnummer, die umgeleitet wird; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, + 'deflection_to_number': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zielrufnummer der Umleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, + 'deflection_mode': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Modus der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, + 'deflection_outgoing': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Outgoing der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, + 'deflection_phonebook_id': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Phonebook_ID der Zielrufnummer (Only valid if Type==fromPB); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, + 'aha_device': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': True, 'supported_by_repeater': False, 'description': 'Steckdose schalten; siehe "switch_state"'}, + 'hkr_device': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': True, 'supported_by_repeater': False, 'description': 'Status des HKR (OPEN; CLOSED; TEMP)'}, + 'set_temperature': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "target_temperature"'}, + 'temperature': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "current_temperature"'}, + 'set_temperature_reduced': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "temperature_reduced"'}, + 'set_temperature_comfort': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "temperature_comfort"'}, + 'firmware_version': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "fw_version"'}, + 'device_id': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Geräte -ID'}, + 'manufacturer': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hersteller'}, + 'product_name': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Produktname'}, + 'fw_version': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Firmware Version'}, + 'connected': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Verbindungsstatus'}, + 'device_name': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gerätename'}, + 'tx_busy': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Verbindung aktiv'}, + 'device_functions': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'list', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Im Gerät vorhandene Funktionen'}, + 'set_target_temperature': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Soll-Temperatur Setzen'}, + 'target_temperature': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'rw', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Soll-Temperatur (Status und Setzen)'}, + 'current_temperature': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Ist-Temperatur'}, + 'temperature_reduced': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingestellte reduzierte Temperatur'}, + 'temperature_comfort': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingestellte Komfort-Temperatur'}, + 'temperature_offset': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingestellter Temperatur-Offset'}, + 'set_window_open': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Window Open" Funktionen Setzen'}, + 'window_open': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Window Open" Funktion (Status und Setzen)'}, + 'windowopenactiveendtime': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitliches Ende der "Window Open" Funktion'}, + 'set_hkr_boost': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Boost" Funktion Setzen'}, + 'hkr_boost': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Boost" Funktion (Status aund Setzen)'}, + 'boost_active': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': True, 'supported_by_repeater': False, 'description': 'Status der "Boost" Funktion'}, + 'boostactiveendtime': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitliches Ende der "Boost" Funktion'}, + 'summer_active': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status der "Sommer" Funktion'}, + 'holiday_active': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status der "Holiday" Funktion'}, + 'battery_low': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Battery low" Status'}, + 'battery_level': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Batterie-Status in %'}, + 'lock': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Tastensperre über UI/API aktiv'}, + 'device_lock': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Tastensperre direkt am Gerät ein'}, + 'errorcode': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Fehlercodes die der HKR liefert'}, + 'set_simpleonoff': {'interface': 'aha', 'group': 'simpleonoff', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gerät/Aktor/Lampe an-/ausschalten'}, + 'simpleonoff': {'interface': 'aha', 'group': 'simpleonoff', 'sub_group': None, 'access': 'wr', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gerät/Aktor/Lampe (Status und Setzen)'}, + 'set_level': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0 bis 255 Setzen'}, + 'level': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0 bis 255 (Setzen & Status)'}, + 'set_levelpercentage': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0% bis 100% Setzen'}, + 'levelpercentage': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0% bis 100% (Setzen & Status)'}, + 'set_hue': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hue Setzen'}, + 'hue': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hue (Status und Setzen)'}, + 'set_saturation': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Saturation Setzen'}, + 'saturation': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Saturation (Status und Setzen)'}, + 'set_colortemperature': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Farbtemperatur Setzen'}, + 'colortemperature': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Farbtemperatur (Status und Setzen)'}, + 'unmapped_hue': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hue (Status und Setzen)'}, + 'unmapped_saturation': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Saturation (Status und Setzen)'}, + 'color_mode': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Aktueller Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode)'}, + 'supported_color_mode': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Unterstützer Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode)'}, + 'fullcolorsupport': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Lampe unterstützt setunmappedcolor'}, + 'mapped': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'von den Colordefaults abweichend zugeordneter HueSaturation-Wert gesetzt'}, + 'switch_state': {'interface': 'aha', 'group': 'switch', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Schaltzustand Steckdose (Status und Setzen)'}, + 'switch_mode': {'interface': 'aha', 'group': 'switch', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitschaltung oder manuell schalten'}, + 'switch_toggle': {'interface': 'aha', 'group': 'switch', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Schaltzustand umschalten (toggle)'}, + 'power': {'interface': 'aha', 'group': 'powermeter', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Leistung in W (Aktualisierung alle 2 min)'}, + 'energy': {'interface': 'aha', 'group': 'powermeter', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'absoluter Verbrauch seit Inbetriebnahme in Wh'}, + 'voltage': {'interface': 'aha', 'group': 'powermeter', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Spannung in V (Aktualisierung alle 2 min)'}, + 'humidity': {'interface': 'aha', 'group': 'humidity', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Relative Luftfeuchtigkeit in % (FD440)'}, + 'alert_state': {'interface': 'aha', 'group': 'alarm', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'letzter übermittelter Alarmzustand'}, + 'blind_mode': {'interface': 'aha', 'group': 'blind', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'automatische Zeitschaltung oder manuell fahren'}, + 'endpositionsset': {'interface': 'aha', 'group': 'blind', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'ist die Endlage für das Rollo konfiguriert'}, +} + From 045d1eb587483bbfd25a068f73dda5b5ca973487 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 20 Mar 2023 17:47:13 +0100 Subject: [PATCH 117/178] AVM-Plugin: - bump to 2.0.2 - define details of avm_data_type as dict for better maintainability in separate file and import it - import item_attributes.py and create attribute lists - improve item attribute lists - make logging info more clear --- avm/plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avm/plugin.yaml b/avm/plugin.yaml index f60588f70..2966fcbcf 100644 --- a/avm/plugin.yaml +++ b/avm/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: http://smarthomeng.de/user/plugins/avm/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin - version: 2.0.1 # Plugin version (must match the version specified in __init__.py) + version: 2.0.2 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.8 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) # py_minversion: 3.6 # minimum Python version to use for this plugin From 4fd8fe21af5258ae970804120061d70afe3780b2 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 20 Mar 2023 17:53:39 +0100 Subject: [PATCH 118/178] AVM-Plugin: - bump to 2.0.2 - remove dead code from Class Fritzdevice since TR064 client has been put into separate files --- avm/__init__.py | 342 ------------------------------------------------ 1 file changed, 342 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index c3c9f5b56..01413065b 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -30,7 +30,6 @@ import time from abc import ABC from enum import IntFlag -from io import BytesIO from json.decoder import JSONDecodeError from typing import Dict from typing import Union @@ -38,7 +37,6 @@ import lxml.etree as ET import requests -from requests.auth import HTTPDigestAuth from requests.packages import urllib3 from lib.model.smartplugin import SmartPlugin @@ -52,8 +50,6 @@ class AVM(SmartPlugin): """ PLUGIN_VERSION = '2.0.2' - # ToDo: Clean dead code in Fritzdevice - # ToDo: check setting of level to 0 if simpleonoff is off -> Status: Implemented, need to be tested # ToDo: FritzHome.handle_updated_item: implement 'saturation' # ToDo: FritzHome.handle_updated_item: implement 'unmapped_hue' @@ -475,12 +471,6 @@ class FritzDevice: FRITZ_L2TPV3_FILE = "l2tpv3.xml" FRITZ_FBOX_DESC_FILE = "fboxdesc.xml" - IGD_DEVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:device-1-0'} - IGD_SERVICE_NAMESPACE = {'': 'urn:schemas-upnp-org:service-1-0'} - TR064_DEVICE_NAMESPACE = {'': 'urn:dslforum-org:device-1-0'} - TR064_SERVICE_NAMESPACE = {'': 'urn:dslforum-org:service-1-0'} - TR064_CONTROL_NAMESPACE = {'': 'urn:dslforum-org:control-1-0'} - def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, plugin_instance=None): """ Init class FritzDevice @@ -511,7 +501,6 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc # get client objects try: - # self.client = FritzDevice.Client(self.username, self.password, self.verify, base_url=self._build_url(), description_file=self.FRITZ_TR64_DESC_FILE, plugin_instance=plugin_instance) self.client = FritzDevice.Tr064_Client(username=self.username, password=self.password, base_url=self._build_url(), description_file=self.FRITZ_TR64_DESC_FILE, verify=self.verify) except Exception as e: self.logger.error(f"Init TR064 Client for {self.FRITZ_TR64_DESC_FILE} caused error {e!r}.") @@ -523,7 +512,6 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc # init client for InternetGatewayDevice try: - # self.client_igd = FritzDevice.Client(self.username, self.password, self.verify, base_url=self._build_url(), description_file=self.FRITZ_IGD_DESC_FILE, plugin_instance=plugin_instance) self.client_igd = FritzDevice.Tr064_Client(username=self.username, password=self.password, base_url=self._build_url(), description_file=self.FRITZ_IGD_DESC_FILE, verify=self.verify) except Exception as e: self.logger.error(f"Init TR064 Client for {self.FRITZ_IGD_DESC_FILE} caused error {e!r}.") @@ -1705,336 +1693,6 @@ def get_mesh_topology(self) -> Union[dict, None]: return mesh - class Client: - """TR-064 client. - - Description file provided by FritzDevice will be read. All DEVICES, SERVICES, ACTIONS definied in that desciption file can be used. - Format is: Client.DeviceName.ServiceName.ActionName(Arguments, ...) like Client.LANDevice.Hosts.GetGenericHostEntry - Capital and lowercase letters must match with - - If a service is offered multiple times, actions can be accessed by the zero-based square bracket operator: - Client.DeviceName.ServiceName[1].ActionName(Arguments, ...) - - If services, actions or arguments contain a minus sign -, it must be replaced with an underscore _. - - # Devices / Services / Actions - InternetGatewayDevice - DeviceInfo - GetInfo - SetProvisioningCode - GetDeviceLog - GetSecurityPort - DeviceConfig - Layer3Forwarding - LANConfigSecurity - ManagementServer - Time - UserInterface - X_AVM-DE_Storage - X_AVM-DE_WebDAVClient - X_AVM-DE_UPnP - X_AVM-DE_Speedtest - X_AVM-DE_RemoteAccess - X_AVM-DE_MyFritz - X_VoIP - X_AVM-DE_OnTel - X_AVM-DE_Dect - X_AVM-DE_TAM - X_AVM-DE_AppSetup - X_AVM-DE_Homeauto - X_AVM-DE_Homeplug - X_AVM-DE_Filelinks - X_AVM-DE_Auth - X_AVM-DE_HostFilter - LANDevice - WLANConfiguration - Hosts - LANEthernetInterfaceConfig - LANHostConfigManagement - WANDevice - WANCommonInterfaceConfig - WANDSLInterfaceConfig - WANConnectionDevice - WANDSLLinkConfig - WANEthernetLinkConfig - WANPPPConnection - WANIPConnection - - :param str username: Username with access to router. - :param str password: Passwort to access router. - :param str base_url: URL to router. - :param str description_file: Description File to be read - :param plugin_instance: instance of Plugin - """ - def __init__(self, username, password, verify, base_url='https://192.168.178.1:49443', description_file='tr64desc.xml', plugin_instance=None): - - # handle plugin instance - self._plugin_instance = plugin_instance - self.logger = self._plugin_instance.logger - - self.base_url = base_url - self.auth = HTTPDigestAuth(username, password) - self.verify = verify - self.description_file = description_file - - self.devices = {} - self.logger.debug(f"Init Client for description at url={base_url} with description_file={self.description_file} and plugin_instance={plugin_instance}") - - def __getattr__(self, name): - if name not in self.devices: - self._fetch_devices(self.description_file) - - if name in self.devices: - return self.devices[name] - - def _fetch_devices(self, description_file): - """ - Fetch device description. - """ - self.logger.debug(f"_fetch_devices called for description file={description_file}") - - request = requests.get(f'{self.base_url}/{description_file}', verify=self.verify) - - if description_file == 'igddesc.xml': - namespaces = FritzDevice.IGD_DEVICE_NAMESPACE - else: - namespaces = FritzDevice.TR064_DEVICE_NAMESPACE - - if request.status_code == 200: - xml = ET.parse(BytesIO(request.content)) - - for device in xml.findall('.//device', namespaces=namespaces): - name = device.findtext('deviceType', namespaces=namespaces).split(':')[-2] - if name not in self.devices: - self.devices[name] = FritzDevice.Device(device, self.auth, self.verify, self.base_url, description_file) - - self.logger.debug(f"Client: {self.description_file} devices={self.devices}") - - class Device: - """ - TR-064 device. - - :param lxml.etree.Element xml: XML device element - :param HTTPBasicAuthHandler auth: HTTPBasicAuthHandler object, e.g. HTTPDigestAuth - :param str base_url: URL to router. - """ - def __init__(self, xml, auth, verify, base_url, description_file): - # init logger - self.logger = logging.getLogger(__name__) - self.services = {} - self.verify = verify - - if description_file == 'igddesc.xml': - namespaces = FritzDevice.IGD_DEVICE_NAMESPACE - else: - namespaces = FritzDevice.TR064_DEVICE_NAMESPACE - - for service in xml.findall('./serviceList/service', namespaces=namespaces): - service_type = service.findtext('serviceType', namespaces=namespaces) - service_id = service.findtext('serviceId', namespaces=namespaces) - control_url = service.findtext('controlURL', namespaces=namespaces) - event_sub_url = service.findtext('eventSubURL', namespaces=namespaces) - scpdurl = service.findtext('SCPDURL', namespaces=namespaces) - - name = service_type.split(':')[-2].replace('-', '_') - if name not in self.services: - self.services[name] = FritzDevice.ServiceList() - - self.services[name].append( - FritzDevice.Service( - auth, - self.verify, - base_url, - service_type, - service_id, - scpdurl, - control_url, - event_sub_url, - description_file - ) - ) - - def __getattr__(self, name: str): - if name in self.services: - return self.services[name] - - raise RuntimeError("TR064 unknown service exception") - - class ServiceList(list): - """ - Service list. - """ - def __getattr__(self, name: str): - """Direct access to first list entry if brackets omit.""" - return self[0].__getattr__(name) - - def __getitem__(self, index): - """Override bracket operator to return TR-064 exception.""" - if len(self) > index: - return super().__getitem__(index) - - class Service: - """ - TR-064 service. - """ - - def __init__(self, auth, verify, base_url, service_type, service_id, scpdurl, control_url, event_sub_url, description_file): - # init logger - self.logger = logging.getLogger(__name__) - - self.auth = auth - self.verify = verify - self.base_url = base_url - self.service_type = service_type - self.service_id = service_id - self.scpdurl = scpdurl - self.control_url = control_url - self.event_sub_url = event_sub_url - self.actions = {} - self.description_file = description_file - - if description_file == 'igddesc.xml': - self.namespaces = FritzDevice.IGD_SERVICE_NAMESPACE - else: - self.namespaces = FritzDevice.TR064_SERVICE_NAMESPACE - - def __getattr__(self, name: str): - if name not in self.actions: - self._fetch_actions(self.scpdurl) - - if name in self.actions: - return self.actions[name] - - def _fetch_actions(self, scpdurl: str): - """ - Fetch action Actions. - """ - - request = requests.get(f'{self.base_url}{scpdurl}', verify=self.verify) - - # self.logger.warning(f"Service _fetch_actions request={request.content}") - - if request.status_code == 200: - xml = ET.parse(BytesIO(request.content)) - - for action in xml.findall('./actionList/action', namespaces=self.namespaces): - name = action.findtext('name', namespaces=self.namespaces) - canonical_name = name.replace('-', '_') - self.actions[canonical_name] = FritzDevice.Action( - action, - self.auth, - self.base_url, - name, - self.service_type, - self.service_id, - self.control_url, - self.verify, - self.namespaces - ) - - class Action: - """ - TR-064 action. - - :param lxml.etree.Element xml: XML action element - :param HTTPBasicAuthHandler auth: HTTPBasicAuthHandler object, e.g. HTTPDigestAuth - :param str base_url: URL to router. - :param str name: Action name - :param str service_type: Service type - :param str service_id: Service ID - :param str control_url: Control URL - """ - - def __init__(self, xml, auth, base_url, name, service_type, service_id, control_url, verify, namespaces): - - # init logger - self.logger = logging.getLogger(__name__) - - self.auth = auth - self.verify = verify - self.base_url = base_url - self.name = name - self.service_type = service_type - self.service_id = service_id - self.control_url = control_url - - ET.register_namespace('s', 'http://schemas.xmlsoap.org/soap/envelope/') - ET.register_namespace('h', 'http://soap-authentication.org/digest/2001/10/') - - self.headers = {'content-type': 'text/xml; charset="utf-8"'} - self.envelope = ET.Element( - '{http://schemas.xmlsoap.org/soap/envelope/}Envelope', - attrib={ - '{http://schemas.xmlsoap.org/soap/envelope/}encodingStyle': - 'http://schemas.xmlsoap.org/soap/encoding/'}) - self.body = ET.SubElement(self.envelope, '{http://schemas.xmlsoap.org/soap/envelope/}Body') - - self.in_arguments = {} - self.out_arguments = {} - - for argument in xml.findall('./argumentList/argument', namespaces=namespaces): - name = argument.findtext('name', namespaces=namespaces) - direction = argument.findtext('direction', namespaces=namespaces) - - if direction == 'in': - self.in_arguments[name.replace('-', '_')] = name - - if direction == 'out': - self.out_arguments[name] = name.replace('-', '_') - - def __call__(self, **kwargs): - missing_arguments = self.in_arguments.keys() - kwargs.keys() - if missing_arguments: - self.logger.warning('Missing argument(s) \'' + "', '".join(missing_arguments) + '\'') - - unknown_arguments = kwargs.keys() - self.in_arguments.keys() - if unknown_arguments: - self.logger.warning('Unknown argument(s) \'' + "', '".join(unknown_arguments) + '\'') - - # Add SOAP action to header - self.headers['soapaction'] = f'"{self.service_type}#{self.name}"' - ET.register_namespace('u', self.service_type) - - # Prepare body for request - self.body.clear() - action = ET.SubElement(self.body, f'{{{self.service_type}}}{self.name}') - for key in kwargs: - arg = ET.SubElement(action, self.in_arguments[key]) - arg.text = str(kwargs[key]) - - # soap._InitChallenge(header) - data = ET.tostring(self.envelope, encoding='utf-8', xml_declaration=True).decode() - request = requests.post(f'{self.base_url}{self.control_url}', - headers=self.headers, - auth=self.auth, - data=data, - verify=self.verify) - if request.status_code != 200: - xml = ET.parse(BytesIO(request.content)) - try: - error_code = int(xml.find(f".//{{{FritzDevice.TR064_CONTROL_NAMESPACE['']}}}errorCode").text) - except Exception: - error_code = None - pass - # self.logger.debug(f"status_code={request.status_code}, error_code={error_code.text}") - return error_code if error_code is not None else request.status_code - - # Translate response and prepare dict - xml = ET.parse(BytesIO(request.content)) - response = FritzDevice.AttributeDict() - for arg in list(xml.find(f".//{{{self.service_type}}}{self.name}Response")): - name = self.out_arguments[arg.tag] - response[name] = arg.text - return response - - class AttributeDict(dict): - """ - Direct access dict entries like attributes. - """ - - def __getattr__(self, name): - return self[name] - class FritzHome: """ From 35af81fef95aa5fd3d015a51b5c701547d847373 Mon Sep 17 00:00:00 2001 From: msinn Date: Mon, 20 Mar 2023 18:45:38 +0100 Subject: [PATCH 119/178] uzsu: Changed requirement for numpy to 'numpy>=1.23.4' for Python >= 3.8 and to 'numpy==1.21.4' for Python 3.7 --- uzsu/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uzsu/requirements.txt b/uzsu/requirements.txt index 9df08d1fb..3e2a79000 100755 --- a/uzsu/requirements.txt +++ b/uzsu/requirements.txt @@ -1,4 +1,5 @@ -numpy>=1.23.4 +numpy==1.21.4;python_version=='3.7' +numpy>=1.23.4;python_version>='3.8' scipy>=1.1.0,<=1.3.0;python_version<'3.7' scipy>=1.2.0,<=1.7.3;python_version=='3.7' #scipy>=1.5.0,<=1.8.1;python_version>'3.7' From b5dfdf68a430d9c85b924e26d7051a56aa621a38 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 20 Mar 2023 21:24:07 +0100 Subject: [PATCH 120/178] AVM-Plugin: - improve log infos - update item_attributes.py / Sync with supported attributes by plugin - filter items, which are not support by repeater --- avm/__init__.py | 25 +++++++++++++++++-------- avm/item_attributes.py | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index 01413065b..088db8696 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -523,37 +523,46 @@ def register_item(self, item, avm_data_type: str, avm_data_cycle: int): """ index = None + # if fritz device is repeater and avm_data_type is not supported by repeater, return + if self.is_repeater and avm_data_type not in ALL_ATTRIBUTES_SUPPORTED_BY_REPEATER: + self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, which is not supported by Repeaters; Item will be ignored.") + return + # handle wlan items if avm_data_type in WLAN_CONFIG_ATTRIBUTES: index = self._get_wlan_index(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_wlan_index' found; append to list.") + self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_wlan_index' found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_wlan_index' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_wlan_index' is not defined; Item will be ignored.") + return # handle network_device related items elif avm_data_type in HOST_ATTRIBUTES: index = self._get_mac(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_mac' found; append to list.") + self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_mac' found; append to list.") else: - self.logger.warning("Item {item.id()} with avm attribute found, but 'avm_mac' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_mac' is not defined; Item will be ignored.") + return # handle tam related items elif avm_data_type in TAM_ATTRIBUTES: index = self._get_tam_index(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") + self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_tam_index' found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_tam_index' is not defined; Item will be ignored.") + return # handle deflection related items elif avm_data_type in DEFLECTION_ATTRIBUTES: index = self._get_deflection_index(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute and defined 'avm_tam_index' found; append to list.") + self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_tam_index' found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_tam_index' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_tam_index' is not defined; Item will be ignored.") + return # register item self._items[item] = (avm_data_type, index, avm_data_cycle, int(time.time())) diff --git a/avm/item_attributes.py b/avm/item_attributes.py index 658d26d55..c2e19d2d1 100644 --- a/avm/item_attributes.py +++ b/avm/item_attributes.py @@ -25,6 +25,14 @@ 'software_version': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Serialnummer des Fritzdevice'}, 'hardware_version': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Software Version'}, 'serial_number': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Hardware Version'}, + 'manufacturer': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Hersteller'}, + 'product_class': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Produktklasse'}, + 'manufacturer_oui': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Hersteller OUI'}, + 'model_name': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Modelname'}, + 'description': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Modelbeschreibung'}, + 'device_log': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Geräte Log'}, + 'security_port': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Security Port'}, + 'reboot': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Startet das Gerät neu'}, 'myfritz_status': {'interface': 'tr064', 'group': 'myfritz', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'MyFritz Status (an/aus)'}, 'call_direction': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Richtung des letzten Anrufes'}, 'call_event': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status des letzten Anrufes'}, @@ -75,11 +83,16 @@ 'hosts_count': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Anzahl der Hosts'}, 'hosts_info': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Informationen über die Hosts'}, 'mesh_topology': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Topologie des Mesh'}, - 'network_device': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus // Defines Network device via MAC-Adresse'}, + 'number_of_hosts': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, + 'hosts_url': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, + 'mesh_url': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, + 'network_device': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus // Defines Network device via MAC-Adresse'}, 'device_ip': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Geräte-IP (Muss Child von "network_device" sein'}, 'device_connection_type': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungstyp (Muss Child von "network_device" sein'}, 'device_hostname': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Gerätename (Muss Child von "network_device" sein'}, 'connection_status': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, + 'is_host_active': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, + 'host_info': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, 'number_of_deflections': {'interface': 'tr064', 'group': 'deflection', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Anzahl der eingestellten Rufumleitungen'}, 'deflections_details': {'interface': 'tr064', 'group': 'deflection', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Details zu allen Rufumleitung (als dict)'}, 'deflection_details': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Details zur Rufumleitung (als dict); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item'}, From 4032b5533e06328202846ded0d17028f18c9e269 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Tue, 21 Mar 2023 06:59:15 +0100 Subject: [PATCH 121/178] removed gitignore --- avm/.gitignore | 129 ------------------------------------------------- 1 file changed, 129 deletions(-) delete mode 100644 avm/.gitignore diff --git a/avm/.gitignore b/avm/.gitignore deleted file mode 100644 index b6e47617d..000000000 --- a/avm/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ From dcedc62ec70c3b1ef7aff1639a1c22c465f15513 Mon Sep 17 00:00:00 2001 From: msinn Date: Tue, 21 Mar 2023 13:36:54 +0100 Subject: [PATCH 122/178] Workflows: Update unittests.yml --- .github/workflows/unittests.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index b76dbc939..b61eb1ffb 100755 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -29,6 +29,19 @@ jobs: run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >>$GITHUB_OUTPUT id: extract_branch + - name: Workflow Information + run: | + echo github.event_name ${{ github.event_name }} + echo github.workflow ${{ github.workflow }} + echo github.action_repository ${{ github.action_repository }} + echo github.actor ${{ github.actor }} + echo github.ref_name ${{ github.ref_name }} + echo github.ref ${{ github.ref }} + echo github.base_ref '${{ github.base_ref }}' + echo github.head_ref '${{ github.head_ref }}' + echo github.pull_request.base.ref '${{ github.pull_request.base.ref }}' + echo steps.extract_branch_new.outputs.branch '${{ steps.extract_branch_new.outputs.branch }}' + - name: Checkout core from branch '${{steps.extract_branch.outputs.branch}}' (for push) if: github.event_name != 'pull_request' uses: actions/checkout@v3 From f3ad1ce149a5b9cb949d78f8947488b0af7af767 Mon Sep 17 00:00:00 2001 From: msinn Date: Tue, 21 Mar 2023 13:41:05 +0100 Subject: [PATCH 123/178] Workflows: Update unittests.yml --- .github/workflows/unittests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index b61eb1ffb..b200e52a4 100755 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -40,7 +40,7 @@ jobs: echo github.base_ref '${{ github.base_ref }}' echo github.head_ref '${{ github.head_ref }}' echo github.pull_request.base.ref '${{ github.pull_request.base.ref }}' - echo steps.extract_branch_new.outputs.branch '${{ steps.extract_branch_new.outputs.branch }}' + echo steps.extract_branch.outputs.branch '${{ steps.extract_branch.outputs.branch }}' - name: Checkout core from branch '${{steps.extract_branch.outputs.branch}}' (for push) if: github.event_name != 'pull_request' From 4f433ea913ac28966c9d224696b79f68b81af022 Mon Sep 17 00:00:00 2001 From: aschwith Date: Tue, 21 Mar 2023 17:18:30 +0100 Subject: [PATCH 124/178] avm: Fix for RGB lightbulb (fritzdect 500): set on/off state to off and dim level to 0 if device is not connected (same behavior as in plugin version 1.6.8) --- avm/__init__.py | 13 ++++--------- avm/plugin.yaml | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index 7eb452291..f185bdcaa 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -49,7 +49,7 @@ class AVM(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ - PLUGIN_VERSION = '2.0.1' + PLUGIN_VERSION = '2.0.2' # ToDo: Clean dead code in Fritzdevice # Todo: check setting of level to 0 if simpleonoff is off -> Status: Implemented, need to be tested @@ -3609,6 +3609,7 @@ class FritzhomeDeviceSwitchable(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) if self.connected is False: + self.simpleonoff = False return if self.has_switchable: @@ -3649,6 +3650,8 @@ class FritzhomeDeviceDimmable(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) if self.connected is False: + self.level = 0 + self.levelpercentage = 0 return if self.has_level: @@ -3667,14 +3670,6 @@ def _update_level_from_node(self, node): self.levelpercentage = self.get_node_value_as_int(levelcontrol_element, "levelpercentage") # Set Level to zero for consistency, if light is off: - # try: - # onoff = bool(self._fritz.get_state(self.ain)) - # if onoff is False: - # self.level = 0 - # self.levelpercentage = 0 - # except AttributeError: - # pass - state_element = node.find("simpleonoff") if state_element is not None: simpleonoff = self.get_node_value_as_int_as_bool(state_element, "state") diff --git a/avm/plugin.yaml b/avm/plugin.yaml index f60588f70..2966fcbcf 100644 --- a/avm/plugin.yaml +++ b/avm/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: http://smarthomeng.de/user/plugins/avm/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/934835-avm-plugin - version: 2.0.1 # Plugin version (must match the version specified in __init__.py) + version: 2.0.2 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.8 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) # py_minversion: 3.6 # minimum Python version to use for this plugin From 2698847783905d82a37702b51e20e23451f06ae4 Mon Sep 17 00:00:00 2001 From: aschwith Date: Tue, 21 Mar 2023 17:20:08 +0100 Subject: [PATCH 125/178] enocean: Catch exception that occurs, if Tx Enocean item is defined without tx_id_offset attribute. Output error log in this case. --- enocean/__init__.py | 6 +++++- enocean/plugin.yaml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/enocean/__init__.py b/enocean/__init__.py index 178047b47..1363441a9 100755 --- a/enocean/__init__.py +++ b/enocean/__init__.py @@ -166,7 +166,7 @@ class EnOcean(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.3.7" + PLUGIN_VERSION = "1.3.8" def __init__(self, sh, *args, **kwargs): @@ -576,6 +576,10 @@ def parse_item(self, item): if 'enocean_tx_eep' in item.conf: self.logger.debug(f"TX eep found in item {item._name}") + if not 'enocean_tx_id_offset' in item.conf: + self.logger.error(f"TX eep found for item {item._name} but no tx id offset specified.") + return + tx_offset = item.conf['enocean_tx_id_offset'] if not (tx_offset in self._used_tx_offsets): self._used_tx_offsets.append(tx_offset) diff --git a/enocean/plugin.yaml b/enocean/plugin.yaml index 2d56f16cd..a020510a8 100755 --- a/enocean/plugin.yaml +++ b/enocean/plugin.yaml @@ -16,7 +16,7 @@ plugin: # url of the support thread support: https://knx-user-forum.de/forum/supportforen/smarthome-py/26542-featurewunsch-enocean-plugin/page13 - version: 1.3.7 # Plugin version + version: 1.3.8 # Plugin version sh_minversion: 1.3 # minimum shNG version to use this plugin #sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance From 4fdb6806252b6555041bab0f034ad7f12390ee91 Mon Sep 17 00:00:00 2001 From: msinn Date: Wed, 22 Mar 2023 09:10:13 +0100 Subject: [PATCH 126/178] smartvisu: Removed parameter 'protocol_over_reverseproxy' --- smartvisu/__init__.py | 3 ++- smartvisu/plugin.yaml | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/smartvisu/__init__.py b/smartvisu/__init__.py index 7f8ba643a..c9c9582ba 100755 --- a/smartvisu/__init__.py +++ b/smartvisu/__init__.py @@ -76,7 +76,8 @@ def __init__(self, sh): self._handle_widgets = self.get_parameter_value('handle_widgets') self._create_masteritem_file = self.get_parameter_value('create_masteritem_file') self.list_deprecated_warnings = self.get_parameter_value('list_deprecated_warnings') - self.protocol_over_reverseproxy = self.get_parameter_value('protocol_over_reverseproxy') + self.protocol_over_reverseproxy = False + #self.protocol_over_reverseproxy = self.get_parameter_value('protocol_over_reverseproxy') self.smartvisu_version = self.get_smartvisu_version() if self.smartvisu_version == '': diff --git a/smartvisu/plugin.yaml b/smartvisu/plugin.yaml index 831651299..27866e326 100755 --- a/smartvisu/plugin.yaml +++ b/smartvisu/plugin.yaml @@ -89,12 +89,12 @@ parameters: de: 'Sollen Deprecated- bzw. Removed Warnungen und Fehler einzeln geloggt werden?' en: 'Should individual Deprecated/Removed warnings and errors be logged?' - protocol_over_reverseproxy: - type: bool - default: False - description: - de: 'Clients greifen über einen Reverse Proxy auf SmartHomeNG zu' - en: 'Clients access SmartHomeNG via a reverse proxy' +# protocol_over_reverseproxy: +# type: bool +# default: False +# description: +# de: 'Clients greifen über einen Reverse Proxy auf SmartHomeNG zu' +# en: 'Clients access SmartHomeNG via a reverse proxy' item_attributes: # Definition of item attributes defined by this plugin From 8e410caff134b16dc50025ee6922268fcf5a575b Mon Sep 17 00:00:00 2001 From: gruberth Date: Wed, 22 Mar 2023 12:27:07 +0100 Subject: [PATCH 127/178] husky2: Catch exception on plugin shutdown, bump to new aioautomower --- husky2/__init__.py | 9 +++------ husky2/plugin.yaml | 2 +- husky2/requirements.txt | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/husky2/__init__.py b/husky2/__init__.py index 89f33023a..8c30ec124 100755 --- a/husky2/__init__.py +++ b/husky2/__init__.py @@ -55,14 +55,10 @@ async def refresh_token(self): else: session._LOGGER.debug("Refresh access token doing relogin") self.shLogger.debug("Refresh access token doing relogin") - #await self.close() - #self.shLogger.debug("Closed old session") await asyncio.sleep(5) await self.logincc(self.client_secret) self.shLogger.debug("Logged in successfully") await asyncio.sleep(5) - #await self.connect() - #self.shLogger.debug("Connected successfully") class Husky2(SmartPlugin): @@ -75,7 +71,7 @@ class properties and methods (class variables and class functions) are already available! """ - PLUGIN_VERSION = '2.1.0' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release + PLUGIN_VERSION = '2.1.1' ITEM_INFO = "husky_info" ITEM_CONTROL = "husky_control" @@ -318,7 +314,8 @@ def huky2Thread(self): self.asyncLoop.run_until_complete(task) except CancelledError: pass - + except AttributeError: + self.logger.debug("No other running async Tasks to cancel") except Exception as e: self.logger.warning(f"husky2_thread: finally *2 - Exception {e}") try: diff --git a/husky2/plugin.yaml b/husky2/plugin.yaml index a3b5eb0a4..bed222c30 100755 --- a/husky2/plugin.yaml +++ b/husky2/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://smarthomeng.github.io/smarthome/plugins/husky2/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1764058-support-thread - version: 2.1.0 # Plugin version (must match the version specified in __init__.py) + version: 2.1.1 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.8 # minimum shNG version to use this plugin multi_instance: false # plugin supports multi instance restartable: unknown diff --git a/husky2/requirements.txt b/husky2/requirements.txt index 3628d36b5..cdea8ffc8 100755 --- a/husky2/requirements.txt +++ b/husky2/requirements.txt @@ -1 +1 @@ -aioautomower==2022.9.0 \ No newline at end of file +aioautomower==2023.3.0 \ No newline at end of file From f39789dd54095442491e313141257871da1eb4ae Mon Sep 17 00:00:00 2001 From: msinn Date: Wed, 22 Mar 2023 19:38:30 +0100 Subject: [PATCH 128/178] onewire: Added a "sleep-time" for testing to improve sensor reading for parasite powered sensors --- onewire/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/onewire/__init__.py b/onewire/__init__.py index d7b2c0d96..640478f2a 100755 --- a/onewire/__init__.py +++ b/onewire/__init__.py @@ -307,6 +307,7 @@ def _io_cycle(self): self.logger.warning(f"_io_cycle: 'raise' {self._ios[addr][key]['readerrors']}. problem connecting to {addr}-{key}, error: {e}") raise except Exception as e: + self.stopevent.wait(0.5) self._ios[addr][key]['readerrors'] = self._ios[addr][key].get('readerrors', 0) + 1 if self._ios[addr][key]['readerrors'] % self.warn_after == 0: self.logger.warning(f"_io_cycle: {self._ios[addr][key]['readerrors']}. problem reading {addr}-{key}, error: {e}") @@ -391,7 +392,7 @@ def _sensor_cycle(self): start = time.time() for addr in self._sensors: if not self.alive: - self.logger.debug(f"Self not alive (sensor={addr})") + self.logger.debug(f"'self' not alive (sensor={addr})") break for key in self._sensors[addr]: path = self._sensors[addr][key]['path'] @@ -407,6 +408,7 @@ def _sensor_cycle(self): self.logger.error(f"reading {addr} gives error value 85.") continue except Exception as e: + self.stopevent.wait(0.5) self._sensors[addr][key]['readerrors'] = self._sensors[addr][key].get('readerrors', 0) + 1 if self._sensors[addr][key]['readerrors'] % self.warn_after == 0: self.logger.warning(f"_sensor_cycle: {self._sensors[addr][key]['readerrors']}. problem reading {addr}-{key}, error: {e}") From 113503ffc0705ed57d78bb9bdafcf8060871c683 Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 23 Mar 2023 09:39:47 +0100 Subject: [PATCH 129/178] smartvisu: Removed old documentation link from metadata --- smartvisu/plugin.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartvisu/plugin.yaml b/smartvisu/plugin.yaml index 27866e326..c233ea076 100755 --- a/smartvisu/plugin.yaml +++ b/smartvisu/plugin.yaml @@ -8,8 +8,8 @@ plugin: maintainer: msinn tester: wvhn state: ready -# keywords: iot xyz - documentation: http://smarthomeng.de/user/plugins/visu_smartvisu/user_doc.html + #keywords: iot xyz + #documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1586800-support-thread-für-das-smartvisu-plugin version: 1.8.10 # Plugin version From cbc3fa6693ab2f35d19fe5643cecebec3c731eb0 Mon Sep 17 00:00:00 2001 From: Hasenradball Date: Thu, 23 Mar 2023 14:10:53 +0100 Subject: [PATCH 130/178] small improvements in comments and status info --- enocean/eep_parser.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/enocean/eep_parser.py b/enocean/eep_parser.py index 6d959ef1a..e017f7014 100755 --- a/enocean/eep_parser.py +++ b/enocean/eep_parser.py @@ -275,7 +275,7 @@ def _parse_eep_A5_30_03(self, payload, status): if not (payload[3] == 0x0F): self.logger.error("EEP A5_30_03 not according to spec.") return results - # Data_byte2 = Temperatur 0..40C (255..0) + # Data_byte2 = Temperatur 0...40 °C (255...0) results['TEMP'] = 40 - (payload[1]/255*40) # Data_byte1 = 0x0F = Alarm, 0x1F = kein Alarm results['ALARM'] = (payload[2] == 0x0F) @@ -452,6 +452,8 @@ def _parse_eep_F6_0G_03(self, payload, status): B: status of the shutter actor (command) ''' self.logger.debug("Processing F6_0G_03: shutter actor") + self.logger.debug("payload = [{}]".format(', '.join(['0x%02X' % b for b in payload]))) + self.logger.debug("status: {}".format(status)) results = {} if (payload[0] == 0x70): results['POSITION'] = 0 @@ -460,9 +462,10 @@ def _parse_eep_F6_0G_03(self, payload, status): results['POSITION'] = 255 results['B'] = 0 elif (payload[0] == 0x01): - results['STATUS'] = 'Start movin up' + results['STATUS'] = 'Start moving up' results['B'] = 1 elif (payload[0] == 0x02): - results['STATUS'] = 'Start movin down' + results['STATUS'] = 'Start moving down' results['B'] = 2 + self.logger.debug('parse_eep_F6_0G_03 returns: {}'.format(results)) return results From 55183edebe1a66025ffe56c11b0c24fe23e28d46 Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 23 Mar 2023 18:26:12 +0100 Subject: [PATCH 131/178] Div. plugins: Changed readme.md class_path/class_name -> plugin_name --- apcups/README.md | 3 +-- appletv/README.md | 3 +-- artnet/README.md | 3 +-- asterisk/README.md | 3 +-- bose_soundtouch/README.md | 3 +-- buderus/README.md | 3 +-- cli/README.md | 3 +-- co2meter/README.md | 3 +-- comfoair/README.md | 3 +-- dashbutton/README.md | 5 ++--- datalog/README.md | 3 +-- dlms/README.md | 3 +-- dlms/user_doc.rst | 3 +-- dmx/README.md | 3 +-- dmx/user_doc.rst | 3 +-- drexelundweiss/README.md | 3 +-- mlgw/README.md | 5 ++--- onewire/__init__.py | 4 +++- raumfeld_ng/README.md | 5 ++--- wettercom/README.md | 5 ++--- wunderground/README_OLD.md | 3 +-- xmpp/README.md | 3 +-- 22 files changed, 28 insertions(+), 47 deletions(-) diff --git a/apcups/README.md b/apcups/README.md index 5e6e74ba9..4be911631 100755 --- a/apcups/README.md +++ b/apcups/README.md @@ -25,8 +25,7 @@ Add the following lines to activate the plugin: ```yaml ApcUps: - class_name: APCUPS - class_path: plugins.apcups + plugin_name: apcups host: localhost port: 3551 ``` diff --git a/appletv/README.md b/appletv/README.md index 7c3dc2736..88ba3c835 100755 --- a/appletv/README.md +++ b/appletv/README.md @@ -23,8 +23,7 @@ This plugin is designed to work with SHNG v1.5. It runs on current develop versi ```yaml appletv: - class_name: AppleTV - class_path: plugins.appletv + plugin_name: appletv #instance: wohnzimmer #ip: 192.168.2.103 #login_id: 00000000-0580-3568-6c73-86bd9b834320 diff --git a/artnet/README.md b/artnet/README.md index 596d66ca6..bdf79d60d 100755 --- a/artnet/README.md +++ b/artnet/README.md @@ -18,8 +18,7 @@ For specifications of the Art-Net look at https://art-net.org.uk/resources/art-n ```yaml artnet1: - class_name: ArtNet - class_path: plugins.artnet + plugin_name: artnet artnet_universe: 0 artnet_net: 0 artnet_subnet: 0 diff --git a/asterisk/README.md b/asterisk/README.md index 9b3baa777..199a098aa 100755 --- a/asterisk/README.md +++ b/asterisk/README.md @@ -14,8 +14,7 @@ The plugin needs the username and password of the AMI and a IP and port address ```yaml ast: - class_name: Asterisk - class_path: plugins.asterisk + plugin_name: asterisk username: admin password: secret host: 127.0.0.1 # default diff --git a/bose_soundtouch/README.md b/bose_soundtouch/README.md index ea4ebc886..a9eaae7d1 100755 --- a/bose_soundtouch/README.md +++ b/bose_soundtouch/README.md @@ -53,8 +53,7 @@ The following example can be used to setup a device: ```yaml bose_soundtouch: - class_name: BoseSoundtouch - class_path: plugins.bose_soundtouch + plugin_name: bose_soundtouch ip: 192.168.2.28 ``` diff --git a/buderus/README.md b/buderus/README.md index 4b6352ff3..6d042df04 100755 --- a/buderus/README.md +++ b/buderus/README.md @@ -49,8 +49,7 @@ The following example can be used to setup a device: ```yaml buderus: - class_name: Buderus - class_path: plugins.buderus + plugin_name: buderus host: 192.168.2.28 key: 90ad52660ce1234551234579d89e25b70b5331ce0e82c5fd1254a317574ec807 ``` diff --git a/cli/README.md b/cli/README.md index ea0f7a853..e797cac3a 100755 --- a/cli/README.md +++ b/cli/README.md @@ -6,8 +6,7 @@ ``` cli: - class_name: CLI - class_path: plugins.cli + plugin_name: cli # ip = 127.0.0.1 # port = 2323 # update = false diff --git a/co2meter/README.md b/co2meter/README.md index d6d3453a8..dc01ee83a 100755 --- a/co2meter/README.md +++ b/co2meter/README.md @@ -18,8 +18,7 @@ The code was adapted from the CO2Meter project Copyright 2017 by Michael Heinema ### plugin.yaml ```yaml co2meter: - class_name: CO2Meter - class_path: plugins.co2meter + plugin_name: co2meter device: '/dev/hidraw0' time_sleep: 5 ``` diff --git a/comfoair/README.md b/comfoair/README.md index 64678f799..2c7255805 100755 --- a/comfoair/README.md +++ b/comfoair/README.md @@ -23,8 +23,7 @@ This plugin has no requirements or dependencies. ``` comfoair: - class_name: ComfoAir - class_path: plugins.comfoair + plugin_name: comfoair kwltype: comfoair350 # Currently supported: comfoair350 and comfoair500 host: 192.168.123.6 # Provide host and port if you want to use TCP connection (for a TCP to serial converter) port: 5555 # Port diff --git a/dashbutton/README.md b/dashbutton/README.md index 170de4101..0e94f927e 100755 --- a/dashbutton/README.md +++ b/dashbutton/README.md @@ -1,4 +1,4 @@ -# Amazon Dashbutton Plugin +# Dashbutton Plugin ## Setup your Amazon Dashbutton @@ -44,8 +44,7 @@ Activate plugin via plugin.yaml: ```yaml dashbutton: - class_name: Dashbutton - class_path: plugins.dashbutton + plugin_name: dashbutton ``` ### Item attributes diff --git a/datalog/README.md b/datalog/README.md index 157243e9d..b59876944 100755 --- a/datalog/README.md +++ b/datalog/README.md @@ -18,8 +18,7 @@ The plugin can be configured using the following settings: ``` datalog: - class_name: DataLog - class_path: plugins.datalog + plugin_name: datalog # path: var/log/data # filepatterns: # - default:{log}-{year}-{month}-{day}.csv diff --git a/dlms/README.md b/dlms/README.md index 032e5a049..81e18e603 100755 --- a/dlms/README.md +++ b/dlms/README.md @@ -42,8 +42,7 @@ The main module will use the ``manufacturer.yaml`` if it's existing to output mo ``` dlms: - class_name: DLMS - class_path: plugins.dlms + plugin_name: dlms serialport: /dev/dlms0 update_cycle: 900 # SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"10c4\", ATTRS{idProduct}==\"ea60\", ATTRS{serial}==\"0092C9FE\", MODE=\"0666\", GROUP=\"dialout\", SYMLINK+=\"dlms0\" diff --git a/dlms/user_doc.rst b/dlms/user_doc.rst index 8636d5a45..93e077c7c 100755 --- a/dlms/user_doc.rst +++ b/dlms/user_doc.rst @@ -78,8 +78,7 @@ Beispiele für die plugin.yaml .. code:: yaml dlms: - class_name: DLMS - class_path: plugins.dlms + plugin_name: dlms serialport: /dev/dlms0 update_cycle: 900 diff --git a/dmx/README.md b/dmx/README.md index 8613467bc..ff892ff6b 100755 --- a/dmx/README.md +++ b/dmx/README.md @@ -16,8 +16,7 @@ A requirements file is provided to easy the installation. ```yaml dmx: - class_name: DMX - class_path: plugins.dmx + plugin_name: dmx serialport: /dev/usbtty... # interface = nanodmx ``` diff --git a/dmx/user_doc.rst b/dmx/user_doc.rst index 12f3d9c3f..770723718 100755 --- a/dmx/user_doc.rst +++ b/dmx/user_doc.rst @@ -30,8 +30,7 @@ plugin.yaml .. code :: yaml dmx: - class_name: DMX - class_path: plugins.dmx + plugin_name: dmx serialport: /dev/usbtty... # interface = nanodmx diff --git a/drexelundweiss/README.md b/drexelundweiss/README.md index df56e911d..766cb3a3a 100755 --- a/drexelundweiss/README.md +++ b/drexelundweiss/README.md @@ -54,8 +54,7 @@ The plugin detects the connected device type automatically: ```yaml DuW: - class_name: DuW - class_path: plugins.drexelundweiss + plugin_name: drexelundweiss tty: /dev/ttyUSB0 # Busmonitor: 1 # LU_ID: 130 diff --git a/mlgw/README.md b/mlgw/README.md index e7eec9ba0..4fbceae4a 100755 --- a/mlgw/README.md +++ b/mlgw/README.md @@ -1,4 +1,4 @@ -# Bang & Olufsen Masterlink Gateway +# mlgw Plugin - Bang & Olufsen Masterlink Gateway ## Changelog @@ -43,8 +43,7 @@ This plugin need a Bang & Olufsen Masterlink Gateway and can connect to it via T ```yaml mlgw: - class_name: Mlgw - class_path: plugins.mlgw + plugin_name: mlgw host: mlgw.local # port: 9000 # username: mlgw diff --git a/onewire/__init__.py b/onewire/__init__.py index 640478f2a..baee429bc 100755 --- a/onewire/__init__.py +++ b/onewire/__init__.py @@ -307,7 +307,8 @@ def _io_cycle(self): self.logger.warning(f"_io_cycle: 'raise' {self._ios[addr][key]['readerrors']}. problem connecting to {addr}-{key}, error: {e}") raise except Exception as e: - self.stopevent.wait(0.5) + time.sleep(0.5) + # self.stopevent.wait(0.5) self._ios[addr][key]['readerrors'] = self._ios[addr][key].get('readerrors', 0) + 1 if self._ios[addr][key]['readerrors'] % self.warn_after == 0: self.logger.warning(f"_io_cycle: {self._ios[addr][key]['readerrors']}. problem reading {addr}-{key}, error: {e}") @@ -408,6 +409,7 @@ def _sensor_cycle(self): self.logger.error(f"reading {addr} gives error value 85.") continue except Exception as e: + # time.sleep(0.5) self.stopevent.wait(0.5) self._sensors[addr][key]['readerrors'] = self._sensors[addr][key].get('readerrors', 0) + 1 if self._sensors[addr][key]['readerrors'] % self.warn_after == 0: diff --git a/raumfeld_ng/README.md b/raumfeld_ng/README.md index d387e03ef..9321a4135 100755 --- a/raumfeld_ng/README.md +++ b/raumfeld_ng/README.md @@ -1,4 +1,4 @@ -# Sample Plugin <- put the name of your plugin here +# raumfeld_ng Plugin #### Version 1.x.y @@ -26,8 +26,7 @@ The plugin needs a running node-raumserver instance on the same or another host. ```yaml raumfeld_ng: - class_name: raumfeld_ng - class_path: plugins.raumfeld_ng + plugin_name: raumfeld_ng rf_HostIP: '127.0.0.1' rf_HostPort: '8080' ``` diff --git a/wettercom/README.md b/wettercom/README.md index 16f7a3198..c4018e8f5 100755 --- a/wettercom/README.md +++ b/wettercom/README.md @@ -1,4 +1,4 @@ -# wetter.com +# wettercom Plugin ## Requirements @@ -11,8 +11,7 @@ wetter.com account with project, recommended: 3 days, all data transmitted ```yaml wettercom: - class_name: wettercom - class_path: plugins.wettercom + plugin_name: wettercom # apikey: # project: ``` diff --git a/wunderground/README_OLD.md b/wunderground/README_OLD.md index 747977d72..0eb0935eb 100755 --- a/wunderground/README_OLD.md +++ b/wunderground/README_OLD.md @@ -52,8 +52,7 @@ You can configure multiple instances of the wunderground plugin to collect data ```yaml # for etc/plugin.yaml configuration file: weather_somewhere: - class_name: Wunderground - class_path: plugins.wunderground + plugin_name: wunderground apikey: xxxxyyyyxxxxyyyy # language: de location: Germany/Hamburg diff --git a/xmpp/README.md b/xmpp/README.md index 8d537b3f9..fc365a0e8 100755 --- a/xmpp/README.md +++ b/xmpp/README.md @@ -52,8 +52,7 @@ loggers: ```yaml xmpp: - class_name: XMPP - class_path: plugins.xmpp + plugin_name: xmpp jid: 'user account eg skender@somexmppserver.com' password: your xmpp server password #server: 127.0.0.1:5222 From 69c204ac06dd8bf9f251839b7ec0d0b3b272a1d8 Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 23 Mar 2023 19:38:13 +0100 Subject: [PATCH 132/178] onewire: Added wait parameter for sensor read when using parasitic power --- onewire/__init__.py | 15 ++++++++------- onewire/plugin.yaml | 11 ++++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/onewire/__init__.py b/onewire/__init__.py index baee429bc..fe72c6124 100755 --- a/onewire/__init__.py +++ b/onewire/__init__.py @@ -43,7 +43,7 @@ class OneWire(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.9.2' + PLUGIN_VERSION = '1.9.3' _flip = {0: '1', False: '1', 1: '0', True: '0', '0': True, '1': False} @@ -124,6 +124,7 @@ def __init__(self, sh, *args, **kwargs ): self.read_alias_definitions() self._io_wait = self.get_parameter_value('io_wait') + self._parasitic_power_wait = self.get_parameter_value('parasitic_power_wait') self._button_wait = self.get_parameter_value('button_wait') self._cycle = self.get_parameter_value('cycle') self.log_counter_cycle_time = self.get_parameter_value('log_counter_cycle_time') @@ -307,8 +308,8 @@ def _io_cycle(self): self.logger.warning(f"_io_cycle: 'raise' {self._ios[addr][key]['readerrors']}. problem connecting to {addr}-{key}, error: {e}") raise except Exception as e: - time.sleep(0.5) - # self.stopevent.wait(0.5) + # time.sleep(self._parasitic_power_wait) + self.stopevent.wait(self._parasitic_power_wait) self._ios[addr][key]['readerrors'] = self._ios[addr][key].get('readerrors', 0) + 1 if self._ios[addr][key]['readerrors'] % self.warn_after == 0: self.logger.warning(f"_io_cycle: {self._ios[addr][key]['readerrors']}. problem reading {addr}-{key}, error: {e}") @@ -352,8 +353,8 @@ def _ibutton_cycle(self): try: entries = self.owbase.dir(path) except Exception: - #time.sleep(0.5) - self.stopevent.wait(0.5) + #time.sleep(self._parasitic_power_wait) + self.stopevent.wait(self._parasitic_power_wait) error = True continue for entry in entries: @@ -409,8 +410,8 @@ def _sensor_cycle(self): self.logger.error(f"reading {addr} gives error value 85.") continue except Exception as e: - # time.sleep(0.5) - self.stopevent.wait(0.5) + # time.sleep(self._parasitic_power_wait) + self.stopevent.wait(self._parasitic_power_wait) self._sensors[addr][key]['readerrors'] = self._sensors[addr][key].get('readerrors', 0) + 1 if self._sensors[addr][key]['readerrors'] % self.warn_after == 0: self.logger.warning(f"_sensor_cycle: {self._sensors[addr][key]['readerrors']}. problem reading {addr}-{key}, error: {e}") diff --git a/onewire/plugin.yaml b/onewire/plugin.yaml index fa66bb55b..519da5f1b 100755 --- a/onewire/plugin.yaml +++ b/onewire/plugin.yaml @@ -11,7 +11,7 @@ plugin: keywords: 1wire onewire dallas ibutton sensor temperature humidity documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1493319-support-thread-zum-onewire-plugin - version: 1.9.2 # Plugin version + version: 1.9.3 # Plugin version sh_minversion: 1.9.3.5 # minimum shNG version to use this plugin multi_instance: True restartable: True @@ -113,6 +113,15 @@ parameters: en: 'Time period between two requests of 1-wire I/O chip.' fr: 'Délai entre deux demandes de puce 1-wire I/O.' + parasitic_power_wait: + type: num + default: 0.5 + valid_min: 0.1 + valid_max: 1.5 + description: + de: 'Wartezeit in Sekunden, um pei parasitärer Spannungsversorgung der Sensoren die Busspannung zu regenerieren' + en: 'Waiting time in seconds to regenerate the bus voltage, if sensors are operated using parasitic power' + log_counter_io_loop_time: type: num default: 10 From 2c92b98cf00da7c5edb63dff4a2ade04796ae913 Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 23 Mar 2023 19:58:52 +0100 Subject: [PATCH 133/178] Div. plugins: Changed readme.md class_path/class_name -> plugin_name --- bose_soundtouch/README.md | 2 - buderus/README.md | 2 - datalog/README.md | 3 +- easymeter/README.md | 3 +- ecmd/README.md | 5 +- elro/README.md | 3 +- enigma2/README.md | 6 +-- eta_pu/README.md | 5 +- harmony/README.md | 3 +- helios/README.md | 5 +- helios_tcp/user_doc.rst | 107 +++++++++++++++++++------------------- iaqstick/README.md | 5 +- indego/README.md | 5 +- indego4shng/README.md | 6 +-- influxdata/README.md | 3 +- influxdb/README.md | 5 +- join/README.md | 5 +- jvcproj/README.md | 5 +- kathrein/README.md | 5 +- kostal/README.md | 5 +- logo/README.md | 6 +-- luxtronic2/README.md | 5 +- memlog/README.md | 5 +- miflora/README.md | 5 +- milight/README.md | 5 +- mvg_live/README.md | 5 +- network/user_doc.rst | 3 +- 27 files changed, 95 insertions(+), 127 deletions(-) diff --git a/bose_soundtouch/README.md b/bose_soundtouch/README.md index a9eaae7d1..c4747e611 100755 --- a/bose_soundtouch/README.md +++ b/bose_soundtouch/README.md @@ -43,8 +43,6 @@ The plugin can be configured with the following parameters: | Parameter | Description | Required | ------------- | ------------- | ------------- | -| class_name | Must be set to `BoseSoundtouch` | Yes | -| class_path | Must be set to `plugins.bose_soundtouch` | Yes | | ip | IP address of Bose Soundtouch system. e.g. `192.168.2.28` | Yes | | port | Port of Bose Soundtouch system. e.g. `8090` | - | | cycle_time | Bose Soundtouch system will we queried every X seconds. e.g. `10` | - | diff --git a/buderus/README.md b/buderus/README.md index 6d042df04..bc0132969 100755 --- a/buderus/README.md +++ b/buderus/README.md @@ -39,8 +39,6 @@ The plugin can be configured with the following parameters: | Parameter | Description | Required | ------------- | ------------- | ------------- | -| class_name | Must be set to `Buderus` | Yes | -| class_path | Must be set to `plugins.buderus` | Yes | | host | IP address of the KM200 gateway. e.g. `192.168.2.28` | Yes | | key | Security key which must be created beforehand from your device password (printed on the KM200) and your user defined password (set in the EasyControl App): https://ssl-account.com/km200.andreashahn.info/ | Yes | | cycle_time | Information will be fetched from KM200 every X seconds. Defaults to 900, meaning an update will be pulled every 15 minutes. | - | diff --git a/datalog/README.md b/datalog/README.md index b59876944..12cbf395f 100755 --- a/datalog/README.md +++ b/datalog/README.md @@ -55,8 +55,7 @@ Example: ```yaml datalog: - class_name = DataLog - class_path = plugins.datalog + plugin_name = datalog filepatterns = default:{log}-{year}-{month}-{day}.csv | custom:{log}-{year}-{month}-{day}.txt logpatterns = csv:{time};{item};{value}\n ``` diff --git a/easymeter/README.md b/easymeter/README.md index 0e303c01b..66e82b6ce 100755 --- a/easymeter/README.md +++ b/easymeter/README.md @@ -30,8 +30,7 @@ If you like, you can also give the serial port a descriptive name with this. ```yaml easymeter: - class_name: easymeter - class_path: plugins.easymeter + plugin_name: easymeter ``` Parameter for serial device are currently set to fix 9600/7E1. diff --git a/ecmd/README.md b/ecmd/README.md index c006f67f5..46b88fb03 100755 --- a/ecmd/README.md +++ b/ecmd/README.md @@ -1,4 +1,4 @@ -# ECMD +# ecmd ## Requirements @@ -23,8 +23,7 @@ You can specify the host ip of your ethersex device. ```yaml ecmd: - class_name: ECMD - class_path: plugins.ecmd + plugin_name: ecmd host: 10.10.10.10 # port: 2701 ``` diff --git a/elro/README.md b/elro/README.md index a0d7f0213..f8204adba 100755 --- a/elro/README.md +++ b/elro/README.md @@ -20,8 +20,7 @@ You have to just simply copy the following into your plugin.yaml file. The ip-ad ```yaml elro: - class_name: Elro - class_path: plugins.elro + plugin_name: elro ``` ### items.yaml diff --git a/enigma2/README.md b/enigma2/README.md index 4f3d8b6be..06b3be1e6 100755 --- a/enigma2/README.md +++ b/enigma2/README.md @@ -26,8 +26,7 @@ The samples use multi-instance feature of SmartHomeNG. ```yaml vusolo4k: - class_name: Enigma2 - class_path: plugins.enigma2 + plugin_name: enigma2 host: xxx.xxx.xxx.xxx port: 81 # 81 for "vu"-boxes, it may be port 80 for a dreambox cycle: 240 @@ -37,8 +36,7 @@ vusolo4k: instance: vusolo4k vusolo2: - class_name: Enigma2 - class_path: plugins.enigma2 + plugin_name: enigma2 host: xxx.xxx.xxx.xxx port: 81 # 81 for "vu"-boxes, it may be port 80 for a dreambox cycle: 240 diff --git a/eta_pu/README.md b/eta_pu/README.md index ad1ce2095..efc58cca7 100755 --- a/eta_pu/README.md +++ b/eta_pu/README.md @@ -1,4 +1,4 @@ -# ETA Pellet Unit PU +# eta_pu - ETA Pellet Unit PU ## Requirements @@ -12,8 +12,7 @@ ```yaml eta_pu: - class_name: ETA_PU - class_path: plugins.eta_pu + plugin_name: eta_pu address: 192.168.179.15 port: 8080 setpath: /user/vars diff --git a/harmony/README.md b/harmony/README.md index cb0ea22a5..f525b9334 100755 --- a/harmony/README.md +++ b/harmony/README.md @@ -103,8 +103,7 @@ for a Harmony Hub activity the activity id. ```yaml harmony: - class_name: Harmony - class_path: plugins.harmony + plugin_name: harmony harmony_ip: 192.168.178.78 #harmony_port: 5222 # [default: 5222, int] #sleekxmpp_debug: false #[default:false, bool] diff --git a/helios/README.md b/helios/README.md index 686188a7a..7a9d0fb2e 100755 --- a/helios/README.md +++ b/helios/README.md @@ -1,4 +1,4 @@ -# Helios ECx00Pro / Vallox xx SE Plugin +# helios - Helios ECx00Pro / Vallox xx SE Plugin Detailed documentation can be found on the Wiki: https://github.com/Tom-Bom-badil/helios/wiki @@ -17,8 +17,7 @@ Add the following lines to ``plugin.yaml``: ```yaml helios: - class_name: helios - class_path: plugins.helios + plugin_name: helios tty: /dev/ttyUSB0 # put your serial port here (usually /dev/ttyUSB0 or /dev/ttyAMA0) cycle: 60 # update interval in seconds; ex-default: 300 ``` diff --git a/helios_tcp/user_doc.rst b/helios_tcp/user_doc.rst index 64b262013..1750add48 100755 --- a/helios_tcp/user_doc.rst +++ b/helios_tcp/user_doc.rst @@ -12,14 +12,13 @@ Die Lüftungsanlage ... - ... ist mit dem lokalen Netzwerk verbunden - ... hat "Modbus" unter Konfiguration --> Gerät aktiviert - + Einrichtung ----------- pymodbus3 muss installiert sein. Weiterhin ist das Plugin in der plugin.yml zu aktivieren:: helios_tcp: - class_name: HeliosTCP - class_path: plugins.helios_tcp + plugin_name: helios_tcp helios_ip: < IP-Adresse des Lüftungsgeräts > update_cycle: < Abstand in Sekunden, nachdem die Werte aktualisiert werden sollen > @@ -42,58 +41,58 @@ Die folgenden Variablen stehen zur Verfügung und können abgefragt werden. Einige Werte stehen je nach Systemkonfiguration nicht in jeder Lüftungsanlage zur Verfügung. ======================== ============================================== ======== ========== ===== ===== -Variable Beschreibung Datentyp Schreibbar Min Max +Variable Beschreibung Datentyp Schreibbar Min Max ======================== ============================================== ======== ========== ===== ===== -outside_temp Temperatur Außenluft float -exhaust_temp Temperatur Abluft float -inside_temp Temperatur Fortluft float -incoming_temp Temperatur Zuluft float -pre_heating_temp VHZ Kanalfüler (-Außenluft- T5) float -post_heating_temp NHZ Kanalfühler (-Zuluft- T6) float -post_heating_reflux_temp NHZ Rücklauffühler (-Warmwasser-Register- T7) float -error_count Anzahl der Fehler int -warning_count Anzahl der Warnungenint int -info_count Anzahl der Infos int -fan_in_rpm Zuluft rpm int -fan_out_rpm Abluft rpm int -internal_humidity Interner Luftfeuchtigkeitsfühler int -sensor1_humidity Externer Fühler KWL-FTF Feuchte 1 int -sensor2_humidity Externer Fühler KWL-FTF Feuchte 2 int -sensor3_humidity Externer Fühler KWL-FTF Feuchte 3 int -sensor4_humidity Externer Fühler KWL-FTF Feuchte 4 int -sensor5_humidity Externer Fühler KWL-FTF Feuchte 5 int -sensor6_humidity Externer Fühler KWL-FTF Feuchte 6 int -sensor7_humidity Externer Fühler KWL-FTF Feuchte 7 int -sensor8_humidity Externer Fühler KWL-FTF Feuchte 8 int -sensor1_temperature Externer Fühler KWL-FTF Temp 1 float -sensor2_temperature Externer Fühler KWL-FTF Temp 2 float -sensor3_temperature Externer Fühler KWL-FTF Temp 3 float -sensor4_temperature Externer Fühler KWL-FTF Temp 4 float -sensor5_temperature Externer Fühler KWL-FTF Temp 5 float -sensor6_temperature Externer Fühler KWL-FTF Temp 6 float -sensor7_temperature Externer Fühler KWL-FTF Temp 7 float -sensor8_temperature Externer Fühler KWL-FTF Temp 8 float -sensor1_co2 Externer Fühler KWL-CO2 1 float -sensor2_co2 Externer Fühler KWL-CO2 2 float -sensor3_co2 Externer Fühler KWL-CO2 3 float -sensor4_co2 Externer Fühler KWL-CO2 4 float -sensor5_co2 Externer Fühler KWL-CO2 5 float -sensor6_co2 Externer Fühler KWL-CO2 6 float -sensor7_co2 Externer Fühler KWL-CO2 7 float -sensor8_co2 Externer Fühler KWL-CO2 8 float -sensor1_voc Externer Fühler KWL-VOC 1 float -sensor2_voc Externer Fühler KWL-VOC 2 float -sensor3_voc Externer Fühler KWL-VOC 3 float -sensor4_voc Externer Fühler KWL-VOC 4 float -sensor5_voc Externer Fühler KWL-VOC 5 float -sensor6_voc Externer Fühler KWL-VOC 6 float -sensor7_voc Externer Fühler KWL-VOC 7 float -sensor8_voc Externer Fühler KWL-VOC 8 float -filter_remaining Restlaufzeit int -boost_remaining Partybetrieb Restzeit int -sleep_remaining Ruhebetrieb Restzeit int -fan_level_percent Prozentuale Lüfterstufe int -bypass_open Bypass geöffnet bool +outside_temp Temperatur Außenluft float +exhaust_temp Temperatur Abluft float +inside_temp Temperatur Fortluft float +incoming_temp Temperatur Zuluft float +pre_heating_temp VHZ Kanalfüler (-Außenluft- T5) float +post_heating_temp NHZ Kanalfühler (-Zuluft- T6) float +post_heating_reflux_temp NHZ Rücklauffühler (-Warmwasser-Register- T7) float +error_count Anzahl der Fehler int +warning_count Anzahl der Warnungenint int +info_count Anzahl der Infos int +fan_in_rpm Zuluft rpm int +fan_out_rpm Abluft rpm int +internal_humidity Interner Luftfeuchtigkeitsfühler int +sensor1_humidity Externer Fühler KWL-FTF Feuchte 1 int +sensor2_humidity Externer Fühler KWL-FTF Feuchte 2 int +sensor3_humidity Externer Fühler KWL-FTF Feuchte 3 int +sensor4_humidity Externer Fühler KWL-FTF Feuchte 4 int +sensor5_humidity Externer Fühler KWL-FTF Feuchte 5 int +sensor6_humidity Externer Fühler KWL-FTF Feuchte 6 int +sensor7_humidity Externer Fühler KWL-FTF Feuchte 7 int +sensor8_humidity Externer Fühler KWL-FTF Feuchte 8 int +sensor1_temperature Externer Fühler KWL-FTF Temp 1 float +sensor2_temperature Externer Fühler KWL-FTF Temp 2 float +sensor3_temperature Externer Fühler KWL-FTF Temp 3 float +sensor4_temperature Externer Fühler KWL-FTF Temp 4 float +sensor5_temperature Externer Fühler KWL-FTF Temp 5 float +sensor6_temperature Externer Fühler KWL-FTF Temp 6 float +sensor7_temperature Externer Fühler KWL-FTF Temp 7 float +sensor8_temperature Externer Fühler KWL-FTF Temp 8 float +sensor1_co2 Externer Fühler KWL-CO2 1 float +sensor2_co2 Externer Fühler KWL-CO2 2 float +sensor3_co2 Externer Fühler KWL-CO2 3 float +sensor4_co2 Externer Fühler KWL-CO2 4 float +sensor5_co2 Externer Fühler KWL-CO2 5 float +sensor6_co2 Externer Fühler KWL-CO2 6 float +sensor7_co2 Externer Fühler KWL-CO2 7 float +sensor8_co2 Externer Fühler KWL-CO2 8 float +sensor1_voc Externer Fühler KWL-VOC 1 float +sensor2_voc Externer Fühler KWL-VOC 2 float +sensor3_voc Externer Fühler KWL-VOC 3 float +sensor4_voc Externer Fühler KWL-VOC 4 float +sensor5_voc Externer Fühler KWL-VOC 5 float +sensor6_voc Externer Fühler KWL-VOC 6 float +sensor7_voc Externer Fühler KWL-VOC 7 float +sensor8_voc Externer Fühler KWL-VOC 8 float +filter_remaining Restlaufzeit int +boost_remaining Partybetrieb Restzeit int +sleep_remaining Ruhebetrieb Restzeit int +fan_level_percent Prozentuale Lüfterstufe int +bypass_open Bypass geöffnet bool humidity_control_status Feuchte-Steuerung Status int X 0 2 humidity_control_target Feuchte-Steuerung Sollwert int X 20 80 co2_control_status CO2-Steuerung Status int X 0 2 diff --git a/iaqstick/README.md b/iaqstick/README.md index c684b184b..8478ead8c 100755 --- a/iaqstick/README.md +++ b/iaqstick/README.md @@ -1,4 +1,4 @@ -# iAQ Stick +# iaqstick ## Requirements @@ -28,8 +28,7 @@ udevadm trigger ```yaml iaqstick: - class_name: iAQ_Stick - class_path: plugins.iaqstick + plugin_name: iaqstick # update_cycle: 10 ``` diff --git a/indego/README.md b/indego/README.md index 1206818bd..ec8a390d5 100755 --- a/indego/README.md +++ b/indego/README.md @@ -1,4 +1,4 @@ -# Indego Plugin +# indego Plugin #### Version 1.x.y @@ -60,8 +60,7 @@ Please refer to the documentation generated from plugin.yaml metadata. ```yaml MyIndego: - class_name: Indego - class_path: plugins.indego + plugin_name: indego user: 'NUTZERNAME' # -> you need to use the name that you used on your Indego App password: 'PASSWORT' # -> you need to use the password that you used on your Indego App cycle: 30 # frequency of when how often the status is updated, is working without a problem with 30seconds, not sure when the server will start to be annoyed by your requests diff --git a/indego4shng/README.md b/indego4shng/README.md index 005e693c4..3421940c5 100755 --- a/indego4shng/README.md +++ b/indego4shng/README.md @@ -109,8 +109,7 @@ Bei den "Kleinen" gibt es folgende Einschränkungen: folgende Einträge werden in der "./etc/plugin.yaml" benötigt. -* `plugin_name: Indego4shNG`: fix "Indego4shNG" -* `class_path: plugins.indego4shng`: fix "plugins.indego4shng" +* `plugin_name: indego4shng`: fix "indego4shng" * `path_2_weather_pics: XXXXXXX`: ist der Pfad zu den Bilder des Wetter-Widgets. (default ="/smartvisu/lib/weather/pics/") * `img_pfad: XXXXXXX`: ist der Pfad unter dem die Gartenkarte gespeichert wird. @@ -129,8 +128,7 @@ Beispiel: ```yaml Indego4shNG: - plugin_name: Indego4shNG - class_path: plugins.indego4shng + plugin_name: indego4shng path_2_weather_pics: /smartvisu/lib/weather/pics/ img_pfad: /tmp/garden.svg indego_credentials: diff --git a/influxdata/README.md b/influxdata/README.md index b1874c47c..001157ea7 100755 --- a/influxdata/README.md +++ b/influxdata/README.md @@ -48,8 +48,7 @@ For more information on buffers and how to setup high performance UDP listener s ```yaml influxdata: - class_name: InfluxData - class_path: plugins.influxdata + plugin_name: influxdata # influx_host = localhost # influx_port = 8089 influx_keyword: influx diff --git a/influxdb/README.md b/influxdb/README.md index 954f76ebd..91f05d339 100755 --- a/influxdb/README.md +++ b/influxdb/README.md @@ -1,4 +1,4 @@ -# Influxdb +# influxdb ## Logging to InfluxDB over UDP or HTTP @@ -43,8 +43,7 @@ you can setup global tags and fields (JSON encoded) ```yaml influxdb: - class_name: InfluxDB - class_path: plugins.influxdb + plugin_name: influxdb # host: localhost # udp_port: 8089 # keyword: influxdb diff --git a/join/README.md b/join/README.md index a2b2c9a7e..82d54611c 100755 --- a/join/README.md +++ b/join/README.md @@ -1,4 +1,4 @@ -# Join +# join Version 1.0 @@ -21,8 +21,7 @@ sudo pip3 install requests --upgrade ```yaml join: - class_name: Join - class_path: plugins.join + plugin_name: join device_id: api_key: ``` diff --git a/jvcproj/README.md b/jvcproj/README.md index 405ec0f32..6df5e38e3 100755 --- a/jvcproj/README.md +++ b/jvcproj/README.md @@ -1,4 +1,4 @@ -# JVC D-ILA Control +# jvcproj - JVC D-ILA Control With this plugin you can control JVC D-ILA projectors over TCP by using the "JVC External Control Command Communication Specification" and transfer gammatables generated with jvcprojectortools. @@ -17,8 +17,7 @@ Please use [this thread for support, questions, feedback etc.](https://knx-user- ```yaml jvcproj: - class_name: JVC_DILA_Control - class_path: plugins.jvcproj + plugin_name: jvcproj host: 1.1.1.1 # host address of the projector gammaconf_dir: ... # optional, location gamma table configuration files ``` diff --git a/kathrein/README.md b/kathrein/README.md index a67c71ca5..a976220f5 100755 --- a/kathrein/README.md +++ b/kathrein/README.md @@ -1,4 +1,4 @@ -# Kathrein +# kathrein ## Requirements This plugin has no requirements or dependencies. @@ -13,8 +13,7 @@ The webinterface needs to be implemented ```yaml kathrein: - class_name: Kathrein - class_path: plugins.kathrein + plugin_name: kathrein host: 192.168.0.149 # port: 9000 # kathreinid: 1 diff --git a/kostal/README.md b/kostal/README.md index 787bc9482..901e2bf4e 100755 --- a/kostal/README.md +++ b/kostal/README.md @@ -1,4 +1,4 @@ -# KOSTAL +# kostal ### Version: 1.3.1.2 @@ -55,8 +55,7 @@ The plugin can be configured like this: ```yaml Kostal_PV: - class_name: Kostal - class_path: plugins.kostal + plugin_name: kostal ip: 192.168.1.21 user: pvserver passwd: pvwr diff --git a/logo/README.md b/logo/README.md index f491798af..991c1bffd 100755 --- a/logo/README.md +++ b/logo/README.md @@ -33,16 +33,14 @@ Sample configuration file for two instances of the logo plugin. ```yaml logo1: - class_name: LOGO - class_path: plugins.logo + plugin_name: logo host: 10.10.10.99 instance: logo1 # port: 102 # io_wait: 5 # version: 0BA8 logo2: - class_name: LOGO - class_path: plugins.logo + plugin_name: logo host: 10.10.10.100 version: 0BA8 instance: logo2 diff --git a/luxtronic2/README.md b/luxtronic2/README.md index 5edbabdd7..9887e76b7 100755 --- a/luxtronic2/README.md +++ b/luxtronic2/README.md @@ -1,4 +1,4 @@ -# Luxtronic2 +# luxtronic2 ## Requirements This plugin has no requirements or dependencies. @@ -9,8 +9,7 @@ This plugin has no requirements or dependencies. ```yaml luxtronic2: - class_name: Luxtronic2 - class_path: plugins.luxtronic2 + plugin_name: luxtronic2 host: 192.168.0.123 # port: 8888 ``` diff --git a/memlog/README.md b/memlog/README.md index 6e3840738..7dcf8c289 100755 --- a/memlog/README.md +++ b/memlog/README.md @@ -1,4 +1,4 @@ -# MemLog +# memlog This plugins can be used to create in-memory logs which can be used by items or other plugins. @@ -15,8 +15,7 @@ Use the plugin configuration to configure the in-memory logs. ``` memlog: - class_name: MemLog - class_path: plugins.memlog + plugin_name: memlog name: alert mappings: - time diff --git a/miflora/README.md b/miflora/README.md index 2501d9dd6..39c701251 100755 --- a/miflora/README.md +++ b/miflora/README.md @@ -1,4 +1,4 @@ -# Miflora +# miflora ## Requirements This plugin requires lib miflora in version 0.4 or above. You can install this lib with: @@ -26,8 +26,7 @@ Forum thread to the plugin: https://knx-user-forum.de/forum/supportforen/smartho ```yaml miflora: - class_name: Miflora - class_path: miflora + plugin_name: miflora bt_library: bluepy bt_addr: C4:7C:7E:21:F3:2B cycle: 300 diff --git a/milight/README.md b/milight/README.md index 505a69267..eec974109 100755 --- a/milight/README.md +++ b/milight/README.md @@ -1,4 +1,4 @@ -# Milight +# milight #### Version 1.6.0 @@ -41,8 +41,7 @@ Typical configuration ```yaml milight: - class_name: milight - class_path = plugins.milight + plugin_name: milight #udp_ip: 192.168.123.147 #udp_port: 8899 #bri: yes diff --git a/mvg_live/README.md b/mvg_live/README.md index 1bda22c8c..b8b9b21bb 100755 --- a/mvg_live/README.md +++ b/mvg_live/README.md @@ -1,4 +1,4 @@ -# MVG Live +# mvg_live - MVG Live ## Requirements This plugin requires lib PyMVGLive. You can install this lib with: @@ -19,8 +19,7 @@ Forum thread to the plugin: https://knx-user-forum.de/forum/supportforen/smartho ```yaml mvg_live: - class_name: MVG_Live - class_path: plugins.mvg_live + plugin_name: mvg_live ``` ### items.yaml diff --git a/network/user_doc.rst b/network/user_doc.rst index 00f6452ae..d9baef67a 100755 --- a/network/user_doc.rst +++ b/network/user_doc.rst @@ -28,8 +28,7 @@ plugin.yaml .. code:: yaml nw: - class_name: Network - class_path: plugins.network + plugin_name: network # ip: 0.0.0.0 # port: 2727 tcp: yes From feabcc4199e2bd7248f2763397fa53e25f844b24 Mon Sep 17 00:00:00 2001 From: gruberth Date: Thu, 23 Mar 2023 20:32:43 +0100 Subject: [PATCH 134/178] husky2: Uopdated webif --- husky2/__init__.py | 8 +- husky2/webif/templates/index.html | 314 ++++++++++++++++++------------ 2 files changed, 199 insertions(+), 123 deletions(-) diff --git a/husky2/__init__.py b/husky2/__init__.py index 8c30ec124..1809dafe9 100755 --- a/husky2/__init__.py +++ b/husky2/__init__.py @@ -729,7 +729,7 @@ def getTimedeltas(self): for data in self.mowerTimestamp.get_list(): min = int((now - (data / 1000.0)) / 60.0) sec = int((now - (data / 1000.0)) % 60.0) - deltas.append(f"{min}:{sec}") + deltas.append(f"{min}:{sec:02d}") return deltas def getErrormessages(self): @@ -821,9 +821,11 @@ def index(self, reload=None): """ tmpl = self.tplenv.get_template('index.html') + items_count = len(self.plugin._items_control) + len(self.plugin._items_state) + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) - return tmpl.render(p=self.plugin, device_count=self.plugin.mowerCount, items_control=self.plugin._items_control, - items_state=self.plugin._items_state) + return tmpl.render(p=self.plugin, device_count=self.plugin.mowerCount, items_count=items_count, + items_control=self.plugin._items_control, items_state=self.plugin._items_state) @cherrypy.expose def mower_park(self): diff --git a/husky2/webif/templates/index.html b/husky2/webif/templates/index.html index 356ba5f30..79f69ddb1 100755 --- a/husky2/webif/templates/index.html +++ b/husky2/webif/templates/index.html @@ -1,11 +1,102 @@ {% extends "base_plugin.html" %} {% set logo_frame = false %} -{% set items_count = items_control|length + items_state|length %} + + + + + +{% block pluginstyles %} + +{% endblock pluginstyles %} + + +{% block pluginscripts %} + + + + +{% endblock pluginscripts %} + {% block headtable %} - - + +
+ {% if p.getToken() %} @@ -29,17 +120,7 @@ -{% block buttons %} -{% if 1==2 %} - - -{% endif %} -{% endblock %} - - {% set tabcount = 2 %} @@ -48,7 +129,7 @@ Set the tab that will be visible on start, if another tab that 1 is wanted (1 - 3) --> {% if item_count==0 %} -{% set start_tab = 1 %} + {% set start_tab = 2 %} {% endif %} @@ -57,7 +138,9 @@ --> {% set tab1title = "" ~ p.get_shortname() ~ " Geräte (" ~ device_count ~ ")" %} {% block bodytab1 %} -
+
+ +
Token
@@ -210,121 +293,112 @@ - - -
-

{{ p.translate("History") }}

- - - - - - - - - - - - - {% for stamp in p.getTimestamps() %} - - - - {% if p.getErrormessages()[loop.index0] == p.translate("No error") %} - - {% else %} - - {% endif %} - - - - {% endfor %} - - -
{{ p.translate("Timestamp") }}{{ p.translate("Timedelta min:sec") }}{{ p.translate("Activity") }}{{ p.translate("Battery") }}{{ p.translate("Coordinates") }}
{{ stamp }}{{ p.getTimedeltas()[loop.index0] }}{{ p.translate(p.getActivities()[loop.index0]) + ", " + - p.translate(p.getErrormessages()[loop.index0]) }}{{ p.translate(p.getActivities()[loop.index0]) + ", " + - p.translate(p.getErrormessages()[loop.index0]) }}{{ p.getBatterypercents()[loop.index0] }} %{{ p.getLongitudes()[loop.index0] }}, {{ p.getLatitudes()[loop.index0] }}
-
- +

{{ p.translate("History") }}

+

#CXOdxXF={QekG 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

#CXOdxXF={QekG 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

+ + + + + + + + + + + + + + + + {% for stamp in p.getTimestamps() %} + + + + + + {% if p.getErrormessages()[loop.index0] == p.translate("No error") %} + + {% else %} + + {% endif %} + + + + {% endfor %} + +
{{ p.translate("Timestamp") }}{{ p.translate("Timedelta min:sec") }}{{ p.translate("Activity") }}{{ p.translate("Battery") }}{{ p.translate("Coordinates") }}
{{ stamp }}{{ p.getTimedeltas()[loop.index0] }}{{ p.translate(p.getActivities()[loop.index0]) + ", " + + p.translate(p.getErrormessages()[loop.index0]) }}{{ p.translate(p.getActivities()[loop.index0]) + ", " + + p.translate(p.getErrormessages()[loop.index0]) }}{{ p.getBatterypercents()[loop.index0] }} %{{ p.getLongitudes()[loop.index0] }}, {{ p.getLatitudes()[loop.index0] }}
- {% endblock bodytab1 %} - {% set tab2title = "" ~ p.get_shortname() ~ " Items (" ~ items_count ~ ")" %} {% block bodytab2 %} -
- +

Control Items

- - - - - - - - - - {% for key in items_control %} - {% for item in items_control[key] %} - - - - - - {% endfor %} - {% endfor %} - -
{{ p.translate("Key") }}{{ p.translate("Description") }}{{ p.translate("Path") }}
{{ key }}{{ item }}{{ item._path }}
- - + + + + + + + + + + + + + {% for key in items_control %} + {% for item in items_control[key] %} + + + + + + + + {% endfor %} + {% endfor %} + +
{{ p.translate("Key") }}{{ p.translate("Description") }}{{ p.translate("Path") }}
{{ key }}{{ item.property.name }}{{ item.property.path }}
+

State Items

- - - - - - - - - - - - - {% for key in items_state %} - {% for item in items_state[key] %} - - - - - - - - - {% endfor %} - {% endfor %} - -
{{ p.translate("Key") }}{{ p.translate("Path") }}{{ p.translate("Value") }}{{ p.translate("Last Update") }}{{ p.translate("Last Change") }}
{{ key }}{{ item._path }}{{ item() }}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
+ + + + + + + + + + + + + + {% for key in items_state %} + {% for item in items_state[key] %} + + + + + + {% if item.property.value is iterable and (item.property.value is not string and item.property.value is not mapping) %} + + {% else %} + + {% endif %} + + + + {% endfor %} + {% endfor %} + +
{{ p.translate("Key") }}{{ p.translate("Path") }}{{ p.translate("Value") }}{{ p.translate("Last Update") }}{{ p.translate("Last Change") }}
{{ key }}{{ item.property.path }}---{{ p.translate(item.property.value)}}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{% endblock bodytab2 %} - -{% block pluginscripts %} - -{% endblock pluginscripts %} - From a7f32b7877a991265b0598b375bfdccee3e7f405 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Fri, 24 Mar 2023 06:37:03 +0100 Subject: [PATCH 135/178] AVM-Plugin: - bugfix --- avm/.gitignore | 129 --- avm/__init__.py | 1598 +++++++++++++++----------------- avm/item_attributes.py | 180 +--- avm/webif/__init__.py | 12 +- avm/webif/templates/index.html | 48 +- 5 files changed, 806 insertions(+), 1161 deletions(-) delete mode 100644 avm/.gitignore diff --git a/avm/.gitignore b/avm/.gitignore deleted file mode 100644 index b6e47617d..000000000 --- a/avm/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ diff --git a/avm/__init__.py b/avm/__init__.py index 088db8696..33f958aaa 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -28,20 +28,78 @@ import socket import threading import time +import functools + from abc import ABC from enum import IntFlag from json.decoder import JSONDecodeError from typing import Dict from typing import Union from xml.etree import ElementTree - import lxml.etree as ET import requests from requests.packages import urllib3 from lib.model.smartplugin import SmartPlugin from .webif import WebInterface -from . import item_attributes +from .item_attributes import \ + ALL_ATTRIBUTES_SUPPORTED_BY_REPEATER, ALL_ATTRIBUTES_WRITEABLE, AHA_ATTRIBUTES, \ + TR064_ATTRIBUTES, CALL_MONITOR_ATTRIBUTES, CALL_MONITOR_ATTRIBUTES_TRIGGER, \ + CALL_MONITOR_ATTRIBUTES_GEN, CALL_MONITOR_ATTRIBUTES_IN, CALL_MONITOR_ATTRIBUTES_OUT, \ + CALL_MONITOR_ATTRIBUTES_DURATION, TAM_ATTRIBUTES, WLAN_CONFIG_ATTRIBUTES, \ + HOST_ATTRIBUTES_CHILD, DEFLECTION_ATTRIBUTES, ALL_ATTRIBUTES_WRITEONLY + + +def NoAttributeError(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except AttributeError: + pass + return wrapper + + +def NoKeyOrAttributeError(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except (KeyError, AttributeError): + pass + return wrapper + + +def to_str(arg) -> str: + if arg is not None: + return str(arg) + else: + return '' + + +def to_int(arg) -> int: + try: + return int(arg) + except (ValueError, TypeError): + return 0 + + +def walk_nodes(root, nodes: list): + data = root + for atype, arg in nodes: + if atype == 'attr': + data = getattr(data, arg) + elif atype == 'sub': + data = data[arg] + elif atype == 'arg': + if arg is None: + data = data() + else: + if isinstance(arg, dict): + data = data(**arg) + else: + data = data(arg) + return data class AVM(SmartPlugin): @@ -50,15 +108,12 @@ class AVM(SmartPlugin): """ PLUGIN_VERSION = '2.0.2' - # ToDo: check setting of level to 0 if simpleonoff is off -> Status: Implemented, need to be tested # ToDo: FritzHome.handle_updated_item: implement 'saturation' # ToDo: FritzHome.handle_updated_item: implement 'unmapped_hue' # ToDo: FritzHome.handle_updated_item: implement 'unmapped_saturation' # ToDo: FritzHome.handle_updated_item: implement 'hsv' # ToDo: FritzHome.handle_updated_item: implement 'hs' - # ToDo: Update to new smartplugin methods to prepare for plugin being restartable - def __init__(self, sh): """Initializes the plugin.""" # Call init code of parent class (SmartPlugin) @@ -80,7 +135,6 @@ def __init__(self, sh): self._call_monitor = self.get_parameter_value('call_monitor') self._aha_http_interface = self.get_parameter_value('avm_home_automation') self._cycle = self.get_parameter_value('cycle') - self.log_entries = self.get_parameter_value('log_entries') self.alive = False ssl = self.get_parameter_value('ssl') if ssl and not _verify: @@ -125,12 +179,12 @@ def run(self): """ self.logger.debug("Run method called") if self.fritz_device is not None: - self.create_cyclic_scheduler(target='tr064', items=self.fritz_device.item_dict, fct=self.fritz_device.cyclic_item_update, offset=2) + self.create_cyclic_scheduler(target='tr064', items=self.fritz_device.items, fct=self.fritz_device.cyclic_item_update, offset=2) self.fritz_device.cyclic_item_update(read_all=True) if self._aha_http_interface and self.fritz_device is not None and self.fritz_device.is_fritzbox: # add scheduler for updating items - self.create_cyclic_scheduler(target='aha', items=self.fritz_home.item_dict, fct=self.fritz_home.cyclic_item_update, offset=4) + self.create_cyclic_scheduler(target='aha', items=self.fritz_home.items, fct=self.fritz_home.cyclic_item_update, offset=4) self.fritz_home.cyclic_item_update(read_all=True) # add scheduler for checking validity of session id self.scheduler_add('check_sid', self.fritz_home.check_sid, prio=5, cycle=900, offset=30) @@ -172,37 +226,58 @@ def parse_item(self, item): # get avm_data_type and avm_data_cycle avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') - avm_data_cycle = self.get_iattr_value(item.conf, 'avm_data_cycle') if self.has_iattr(item.conf, 'avm_data_cycle') else self._cycle - avm_data_cycle = 30 if 1 < avm_data_cycle < 29 else avm_data_cycle + avm_data_cycle = self.get_iattr_value(item.conf, 'avm_data_cycle') + if avm_data_cycle is None: + avm_data_cycle = self._cycle + if 0 < avm_data_cycle < 30: + avm_data_cycle = 30 + + # define item_config + item_config = {'avm_data_type': avm_data_type, 'avm_data_cycle': avm_data_cycle, 'next_update': time.time()} # handle items specific to call monitor if avm_data_type in CALL_MONITOR_ATTRIBUTES: if self.monitoring_service: - self.monitoring_service.register_item(item, avm_data_type) + self.monitoring_service.register_item(item, item_config) else: self.logger.warning(f"Items with avm attribute {avm_data_type!r} found, which needs Call-Monitoring-Service. This is not available/enabled for that plugin; Item will be ignored.") # handle smarthome items using aha-interface (new) elif avm_data_type in AHA_ATTRIBUTES: if self.fritz_home: - self.fritz_home.register_item(item, avm_data_type, avm_data_cycle) + self.fritz_home.register_item(item, item_config) else: self.logger.warning(f"Items with avm attribute {avm_data_type!r} found, which needs aha-http-interface. This is not available/enabled for that plugin; Item will be ignored.") # handle items updated by tr-064 interface elif avm_data_type in TR064_ATTRIBUTES: if self.fritz_device: - self.fritz_device.register_item(item, avm_data_type, avm_data_cycle) + self.fritz_device.register_item(item, item_config) else: self.logger.warning(f"Items with avm attribute {avm_data_type!r} found, which needs tr064 interface. This is not available/enabled; Item will be ignored.") # handle anything else else: - self.logger.warning(f"Item={item.id()} has unknown avm_data_type {avm_data_type!r}. Item will be ignored.") + self.logger.warning(f"Item={item.path()} has unknown avm_data_type {avm_data_type!r}. Item will be ignored.") # items which can be changed outside the plugin context if avm_data_type in ALL_ATTRIBUTES_WRITEABLE: return self.update_item + def unparse_item(self, item): + """ remove item bindings from plugin """ + super().unparse_item(item) + + # handle items specific to call monitor + if self.monitoring_service: + self.monitoring_service.unregister_item(item) + + if self.fritz_home: + self.fritz_home.unregister_item(item) + + # handle items updated by tr-064 interface + if self.fritz_device: + self.fritz_device.unregister_item(item) + def update_item(self, item, caller=None, source=None, dest=None): """ Item has been updated @@ -219,27 +294,27 @@ def update_item(self, item, caller=None, source=None, dest=None): if self.alive and caller != self.get_fullname(): # get avm_data_type - avm_data_type = self.get_iattr_value(item.conf, 'avm_data_type') + avm_data_type = to_str(self.get_iattr_value(item.conf, 'avm_data_type')) - self.logger.info(f"Updated item: {item.property.path} with avm_data_type={avm_data_type} item has been changed outside this plugin from caller={caller}") + self.logger.info(f"Updated item: {item.path()} with avm_data_type={avm_data_type} item has been changed outside this plugin from caller={caller}") - readafterwrite = None + readafterwrite = 0 if self.has_iattr(item.conf, 'avm_read_after_write'): - readafterwrite = int(self.get_iattr_value(item.conf, 'avm_read_after_write')) + readafterwrite = to_int(self.get_iattr_value(item.conf, 'avm_read_after_write')) if self.debug_log: - self.logger.debug(f'Attempting read after write for item: {item.id()}, avm_data_type: {avm_data_type}, delay: {readafterwrite}s') + self.logger.debug(f'Attempting read after write for item: {item.path()}, avm_data_type: {avm_data_type}, delay: {readafterwrite}s') # handle items updated by tr-064 interface if avm_data_type in TR064_ATTRIBUTES: if self.debug_log: - self.logger.debug(f"Updated item={item.id()} with avm_data_type={avm_data_type} identified as part of 'TR064_ATTRIBUTES'") + self.logger.debug(f"Updated item={item.path()} with avm_data_type={avm_data_type} identified as part of 'TR064_ATTRIBUTES'") self.fritz_device.handle_updated_item(item, avm_data_type, readafterwrite) # handle items updated by AHA_ATTRIBUTES elif avm_data_type in AHA_ATTRIBUTES: if self.fritz_home: if self.debug_log: - self.logger.debug(f"Updated item={item.id()} with avm_data_type={avm_data_type} identified as part of 'AHA_ATTRIBUTES'") + self.logger.debug(f"Updated item={item.path()} with avm_data_type={avm_data_type} identified as part of 'AHA_ATTRIBUTES'") self.fritz_home.handle_updated_item(item, avm_data_type, readafterwrite) else: self.logger.warning(f"AVM Homeautomation Interface not activated or not available. Update for {avm_data_type} will not be executed.") @@ -249,7 +324,7 @@ def create_cyclic_scheduler(self, target: str, items: dict, fct, offset: int): # find the shortest cycle shortestcycle = -1 for item in items: - item_cycle = items[item][2] + item_cycle = items[item]['avm_data_cycle'] if item_cycle != 0 and (shortestcycle == -1 or item_cycle < shortestcycle): shortestcycle = item_cycle @@ -269,166 +344,109 @@ def create_cyclic_scheduler(self, target: str, items: dict, fct, offset: int): @property def log_level(self): - try: - return self.logger.getEffectiveLevel() - except AttributeError: - pass + return self.logger.getEffectiveLevel() def monitoring_service_connect(self): - try: - self.monitoring_service.connect() - except AttributeError: - pass + self.monitoring_service.connect() def monitoring_service_disconnect(self): - try: - self.monitoring_service.disconnect() - except AttributeError: - pass + self.monitoring_service.disconnect() + @NoAttributeError def start_call(self, phone_number): - try: - return self.fritz_device.start_call(phone_number) - except AttributeError: - pass + return self.fritz_device.start_call(phone_number) + @NoAttributeError def cancel_call(self): - try: - return self.fritz_device.cancel_call() - except AttributeError: - pass + return self.fritz_device.cancel_call() + @NoAttributeError def get_call_origin(self): - try: - return self.fritz_device.get_call_origin() - except AttributeError: - pass + return self.fritz_device.get_call_origin() + @NoAttributeError def set_call_origin(self, phone_name: str): - try: - return self.fritz_device.set_call_origin(phone_name) - except AttributeError: - pass + return self.fritz_device.set_call_origin(phone_name) + @NoAttributeError def get_calllist(self): - try: - return self.fritz_device.get_calllist_from_cache() - except AttributeError: - pass + return self.fritz_device.get_calllist_from_cache() + @NoAttributeError def get_phone_name(self, index: int = 1): - try: - return self.fritz_device.get_phone_name(index) - except AttributeError: - pass + return self.fritz_device.get_phone_name(index) + @NoAttributeError def get_phone_numbers_by_name(self, name: str = '', phonebook_id: int = 0): - try: - return self.fritz_device.get_phone_numbers_by_name(name, phonebook_id) - except AttributeError: - pass + return self.fritz_device.get_phone_numbers_by_name(name, phonebook_id) + @NoAttributeError def get_contact_name_by_phone_number(self, phone_number: str = '', phonebook_id: int = 0): - try: - return self.fritz_device.get_phone_numbers_by_name(phone_number, phonebook_id) - except AttributeError: - pass + return self.fritz_device.get_phone_numbers_by_name(phone_number, phonebook_id) + @NoAttributeError def get_device_log_from_lua(self): - try: - return self.fritz_home.get_device_log_from_lua() - except AttributeError: - pass + return self.fritz_home.get_device_log_from_lua() + @NoAttributeError def get_device_log_from_lua_separated(self): - try: - return self.fritz_home.get_device_log_from_lua_separated() - except AttributeError: - pass + return self.fritz_home.get_device_log_from_lua_separated() + @NoAttributeError def get_device_log_from_tr064(self): - try: - return self.fritz_device.get_device_log_from_tr064() - except AttributeError: - pass + return self.fritz_device.get_device_log_from_tr064() + @NoAttributeError def get_host_details(self, index: int): - try: - return self.fritz_device.get_host_details(index) - except AttributeError: - pass + return self.fritz_device.get_host_details(index) + @NoAttributeError def get_hosts(self, only_active: bool = False): - try: - return self.fritz_device.get_hosts(only_active) - except AttributeError: - pass + return self.fritz_device.get_hosts(only_active) + @NoAttributeError def get_hosts_dict(self): - try: - return self.fritz_device.get_hosts_dict() - except AttributeError: - pass + return self.fritz_device.get_hosts_dict() + @NoAttributeError def get_mesh_topology(self): - try: - return self.fritz_device.get_mesh_topology() - except AttributeError: - pass + return self.fritz_device.get_mesh_topology() + @NoAttributeError def is_host_active(self, mac_address: str): - try: - return self.fritz_device.is_host_active(mac_address) - except AttributeError: - pass + return self.fritz_device.is_host_active(mac_address) + @NoAttributeError def reboot(self): - try: - return self.fritz_device.reboot() - except AttributeError: - pass + return self.fritz_device.reboot() + @NoAttributeError def reconnect(self): - try: - return self.fritz_device.reconnect() - except AttributeError: - pass + return self.fritz_device.reconnect() + @NoAttributeError def wol(self, mac_address: str): - try: - return self.fritz_device.wol(mac_address) - except AttributeError: - pass + return self.fritz_device.wol(mac_address) + @NoAttributeError def get_number_of_deflections(self): - try: - return self.fritz_device.get_number_of_deflections() - except AttributeError: - pass + return self.fritz_device.get_number_of_deflections() + @NoAttributeError def get_deflection(self, deflection_id: int = 0): - try: - return self.fritz_device.get_deflection(deflection_id) - except AttributeError: - pass + return self.fritz_device.get_deflection(deflection_id) + @NoAttributeError def get_deflections(self): - try: - return self.fritz_device.get_deflections() - except AttributeError: - pass + return self.fritz_device.get_deflections() + @NoAttributeError def set_deflection_enable(self, deflection_id: int = 0, new_enable: bool = False): - try: - return self.fritz_device.set_deflection(deflection_id, new_enable) - except AttributeError: - pass + return self.fritz_device.set_deflection(deflection_id, new_enable) + @NoAttributeError def set_tam(self, tam_index: int = 0, new_enable: bool = False): - try: - return self.fritz_device.set_tam(tam_index, new_enable) - except AttributeError: - pass + return self.fritz_device.set_tam(tam_index, new_enable) class FritzDevice: @@ -471,6 +489,8 @@ class FritzDevice: FRITZ_L2TPV3_FILE = "l2tpv3.xml" FRITZ_FBOX_DESC_FILE = "fboxdesc.xml" + ERROR_COUNT_TO_BE_BLACKLISTED = 2 + def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, plugin_instance=None): """ Init class FritzDevice @@ -490,9 +510,7 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self._data_cache = {} self._calllist_cache = [] self._timeout = 10 - self._items = {} - self._item_blacklist = [] - self._item_errordict = {} + self.items = {} self._session = requests.Session() self.connected = False self.default_connection_service = None @@ -506,7 +524,7 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self.logger.error(f"Init TR064 Client for {self.FRITZ_TR64_DESC_FILE} caused error {e!r}.") else: self.connected = True - if self.is_fritzbox: + if self.is_fritzbox(): # get GetDefaultConnectionService self.default_connection_service = self._get_default_connection_service() @@ -517,74 +535,85 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self.logger.error(f"Init TR064 Client for {self.FRITZ_IGD_DESC_FILE} caused error {e!r}.") pass - def register_item(self, item, avm_data_type: str, avm_data_cycle: int): + def register_item(self, item, item_config: dict): """ Parsed items valid for that class will be registered """ index = None + avm_data_type = item_config['avm_data_type'] # if fritz device is repeater and avm_data_type is not supported by repeater, return - if self.is_repeater and avm_data_type not in ALL_ATTRIBUTES_SUPPORTED_BY_REPEATER: - self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, which is not supported by Repeaters; Item will be ignored.") + if self.is_repeater() and avm_data_type not in ALL_ATTRIBUTES_SUPPORTED_BY_REPEATER: + self.logger.warning(f"Item {item.path()} with avm attribute {avm_data_type!r} found, which is not supported by Repeaters; Item will be ignored.") return # handle wlan items if avm_data_type in WLAN_CONFIG_ATTRIBUTES: index = self._get_wlan_index(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_wlan_index' found; append to list.") + self.logger.debug(f"Item {item.path()} with avm device attribute {avm_data_type!r} and defined 'avm_wlan_index' with {index!r} found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_wlan_index' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.path()} with avm attribute {avm_data_type!r} found, but 'avm_wlan_index' is not defined; Item will be ignored.") return - # handle network_device related items - elif avm_data_type in HOST_ATTRIBUTES: + # handle network_device / host child related items + elif avm_data_type in HOST_ATTRIBUTES_CHILD: index = self._get_mac(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_mac' found; append to list.") + self.logger.debug(f"Item {item.path()} with avm device attribute {avm_data_type!r} and defined 'avm_mac' with {index!r} found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_mac' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.path()} with avm attribute {avm_data_type!r} found, but 'avm_mac' is not defined; Item will be ignored.") return # handle tam related items elif avm_data_type in TAM_ATTRIBUTES: index = self._get_tam_index(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_tam_index' found; append to list.") + self.logger.debug(f"Item {item.path()} with avm device attribute {avm_data_type!r} and defined 'avm_tam_index' with {index!r} found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_tam_index' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.path()} with avm attribute {avm_data_type!r} found, but 'avm_tam_index' is not defined; Item will be ignored.") return # handle deflection related items elif avm_data_type in DEFLECTION_ATTRIBUTES: index = self._get_deflection_index(item) if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute {avm_data_type!r} and defined 'avm_tam_index' found; append to list.") + self.logger.debug(f"Item {item.path()} with avm device attribute {avm_data_type!r} and defined 'avm_tam_index' with {index!r} found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute {avm_data_type!r} found, but 'avm_tam_index' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.path()} with avm attribute {avm_data_type!r} found, but 'avm_tam_index' is not defined; Item will be ignored.") return + # update item config + item_config.update({'interface': 'tr064', 'index': index, 'error_count': 0}) + # register item - self._items[item] = (avm_data_type, index, avm_data_cycle, int(time.time())) + self.items[item] = item_config + + def unregister_item(self, item): + """ remove item from instance """ + try: + del self.items[item] + except KeyError: + pass def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): """Updated Item will be processed and value communicated to AVM Device""" # get index - _index = self._items[item][1] + _index = self.items[item]['index'] # to be set value to_be_set_value = item() # define command per avm_data_type - _dispatcher = {'wlanconfig': (self.set_wlan, ('set_wlan', f"NewEnable={int(to_be_set_value)}", _index)), - 'wps_active': (self.set_wps, ('set_wps', f"NewX_AVM_DE_WPSEnable={int(to_be_set_value)}", _index)), - 'tam': (self.set_tam, ('set_tam', f"NewIndex={_index}, NewEnable={int(to_be_set_value)}", None)), + _dispatcher = {'wlanconfig': (self.set_wlan, ('set_wlan', f"NewEnable={int(to_be_set_value)}", _index)), + 'wps_active': (self.set_wps, ('set_wps', f"NewX_AVM_DE_WPSEnable={int(to_be_set_value)}", _index)), + 'tam': (self.set_tam, ('set_tam', f"NewIndex={_index}, NewEnable={int(to_be_set_value)}", None)), 'deflection_enable': (self.set_deflection, ('set_deflection', f"NewDeflectionId={_index}, NewEnable={int(to_be_set_value)}", None)), } # do logging if self.debug_log: - self.logger.debug(f"Item {item.id()} with avm_data_type={avm_data_type} has changed for index {_index}; New value={to_be_set_value}") + self.logger.debug(f"Item {item.path()} with avm_data_type={avm_data_type} has changed for index {_index}; New value={to_be_set_value}") # call setting method cmd, args, index = _dispatcher[avm_data_type][1] @@ -600,7 +629,7 @@ def _read_after_write(self, item, avm_data_type, _index, delay, to_be_set_value) """read the new item value and compares with to_be_set_value, update item to confirm correct value""" # do logging if self.debug_log: - self.logger.warning(f"_readafterwrite called with: item={item.id()}, avm_data_type={avm_data_type}, index={_index}; delay={delay}, to_be_set_value={to_be_set_value}") + self.logger.warning(f"_readafterwrite called with: item={item.path()}, avm_data_type={avm_data_type}, index={_index}; delay={delay}, to_be_set_value={to_be_set_value}") # sleep time.sleep(delay) @@ -613,10 +642,10 @@ def _read_after_write(self, item, avm_data_type, _index, delay, to_be_set_value) # do logging if current_value != to_be_set_value: - self.logger.warning(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") + self.logger.warning(f"Setting AVM Device defined in Item={item.path()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") else: if self.debug_log: - self.logger.debug(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") + self.logger.debug(f"Setting AVM Device defined in Item={item.path()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") def _build_url(self) -> str: """ @@ -637,7 +666,7 @@ def _get_wlan_index(self, item): return wlan index for given item """ wlan_index = None - for i in range(2): + for _ in range(2): attribute = 'avm_wlan_index' wlan_index = self._plugin_instance.get_iattr_value(item.conf, attribute) @@ -650,7 +679,7 @@ def _get_wlan_index(self, item): wlan_index = int(wlan_index) - 1 if not 0 <= wlan_index <= 2: wlan_index = None - self.logger.warning(f"Attribute 'avm_wlan_index' for item {item.id()} not in valid range 1-3.") + self.logger.warning(f"Attribute 'avm_wlan_index' for item {item.path()} not in valid range 1-3.") return wlan_index @@ -659,7 +688,7 @@ def _get_tam_index(self, item): return tam index for given item """ tam_index = None - for i in range(2): + for _ in range(2): attribute = 'avm_tam_index' tam_index = self._plugin_instance.get_iattr_value(item.conf, attribute) @@ -672,7 +701,7 @@ def _get_tam_index(self, item): tam_index = int(tam_index) - 1 if not 0 <= tam_index <= 4: tam_index = None - self.logger.warning(f"Attribute 'avm_tam_index' for item {item.id()} not in valid range 1-5.") + self.logger.warning(f"Attribute 'avm_tam_index' for item {item.path()} not in valid range 1-5.") return tam_index @@ -681,7 +710,7 @@ def _get_deflection_index(self, item): return deflection index for given item """ deflection_index = None - for i in range(2): + for _ in range(2): attribute = 'avm_deflection_index' deflection_index = self._plugin_instance.get_iattr_value(item.conf, attribute) @@ -694,16 +723,16 @@ def _get_deflection_index(self, item): deflection_index = int(deflection_index) - 1 if not 0 <= deflection_index <= 31: deflection_index = None - self.logger.warning(f"Attribute 'avm_deflection_index' for item {item.id()} not in valid range 1-5.") + self.logger.warning(f"Attribute 'avm_deflection_index' for item {item.path()} not in valid range 1-5.") return deflection_index - def _get_mac(self, item) -> str: + def _get_mac(self, item) -> Union[str, None]: """ return mac for given item """ mac = None - for i in range(2): + for _ in range(2): attribute = 'avm_mac' mac = self._plugin_instance.get_iattr_value(item.conf, attribute) @@ -712,7 +741,7 @@ def _get_mac(self, item) -> str: else: item = item.return_parent() - return str(mac) + return mac def _get_default_connection_service(self): @@ -727,63 +756,47 @@ def _get_default_connection_service(self): elif 'IP' in _default_connection_service: return 'IP' - # -------------------------------------- - # Properties of FritzDevice - # -------------------------------------- - - @property - def item_dict(self): - return self._items - - @property def item_list(self): - return list(self._items.keys()) + return list(self.items.keys()) - @property def manufacturer_name(self): return self._poll_fritz_device('manufacturer') - @property def manufacturer_oui(self): return self._poll_fritz_device('manufacturer_oui') - @property def model_name(self): return self._poll_fritz_device('model_name') - @property def product_class(self): return self._poll_fritz_device('product_class') - @property def desciption(self): return self._poll_fritz_device('description') - @property def safe_port(self): return self._poll_fritz_device('security_port') - @property def is_fritzbox(self): try: - # return True if 'box' in self.product_class.lower() else False - return True if 'box' in self.model_name.lower() else False + return 'box' in self.model_name().lower() except AttributeError as e: - self.logger.error(f'Could now find out if {self.product_class} represents a Fritzbox. Error {e!r} occurred.') + self.logger.error(f'Could now find out if {self.product_class()} represents a Fritzbox. Error {e!r} occurred.') return False - @property def is_repeater(self): try: - return True if 'Repeater' in self.product_class.lower() else False + return 'repeater' in self.product_class().lower() except AttributeError as e: - self.logger.error(f'Could now find out if {self.product_class} represents a Repeater. Error {e!r} occurred.') + self.logger.error(f'Could now find out if {self.product_class()} represents a Repeater. Error {e!r} occurred.') return False - @property def wlan_devices_count(self): wlan_devices = self.get_wlan_devices() - return wlan_devices.get('TotalAssociations') if wlan_devices else 0 + if wlan_devices: + return wlan_devices.get('TotalAssociations') + else: + return 0 # ---------------------------------- # Update methods @@ -795,48 +808,48 @@ def cyclic_item_update(self, read_all: bool = False): return current_time = int(time.time()) - _item_errorlist = [] # iterate over items and get data - for item in self._items: - avm_data_type = self._items[item][0] - index = self._items[item][1] - cycle = self._items[item][2] - next_time = self._items[item][3] - - # check it item is on blacklist - if item in self._item_blacklist: - self.logger.info(f"Item={item.id()} is blacklisted due to exceptions in former update cycles. Item will be ignored.") + for item in self.items: + + # get item config + item_config = self.items[item] + avm_data_type = item_config['avm_data_type'] + index = item_config['index'] + cycle = item_config['avm_data_cycle'] + next_time = item_config['next_update'] + error_count = item_config['error_count'] + + # check if item is blacklisted + if error_count >= self.ERROR_COUNT_TO_BE_BLACKLISTED: + self.logger.info(f"Item {item.path()} is blacklisted due to exceptions in former update cycles. Item will be ignored.") continue # read items with cycle == 0 just at init - if read_all is False and cycle == 0: - # self.logger.debug(f"Item={item.id()} just read at init. No further update.") + if not read_all and cycle == 0: + self.logger.debug(f"Item {item.path()} just read at init. No further update.") continue # check if item is already due - if next_time > current_time: - # self.logger.debug(f"Item={item.id()} is not due, yet.") + if next_time > current_time and not read_all: + self.logger.debug(f"Item {item.path()} is not due yet.") continue # check, if client_igd exists when avm_data_type startswith 'wan_current' are due if avm_data_type.startswith('wan_current') and self.client_igd is None: + self.logger.debug(f"Skipping item {item} with wan_current and no client_igd") continue - self.logger.info(f"Item={item.id()} with avm_data_type={avm_data_type} and index={index} will be updated") - - # set new due date - lst = list(self._items[item]) - lst[3] = current_time + cycle - self._items[item] = tuple(lst) + self.logger.info(f"Item={item.path()} with avm_data_type={avm_data_type} and index={index} will be updated") # get data and set item value - if self._update_item_value(item, avm_data_type, index) is False: - self.logger.debug(f"{item.id()} caused error, append item to _item_errorlist") - _item_errorlist.append(item) + if not self._update_item_value(item, avm_data_type, index): + error_count += 1 + self.logger.debug(f"{item.path()} caused error. New error_count: {error_count}. Item will be blacklisted after more than 2 errors.") + item_config.update({'error_count': error_count}) - # create / update item blacklist - self.create_item_blacklist(_item_errorlist) + # set next due date + self.items[item].update({'next_update': current_time + cycle}) # clear data cache dict after update cycle self._clear_data_cache() @@ -955,24 +968,24 @@ def _poll_fritz_device(self, avm_data_type: str, index=None, enforce_read: bool } link2 = { - 'tam_total_message_number': 'self.get_tam_message_count(index, "total")', - 'tam_new_message_number': 'self.get_tam_message_count(index, "new")', - 'tam_old_message_number': 'self.get_tam_message_count(index, "old")', - 'wlan_total_associates': 'self.wlan_devices_count', - 'hosts_info': 'self.get_hosts_dict()', - 'hosts_count': 'self.get_hosts_count()', - 'mesh_topology': 'self.get_mesh_topology()', + 'tam_total_message_number': ('get_tam_message_count', {'count_type': 'total'}), + 'tam_new_message_number': ('get_tam_message_count', {'count_type': 'new'}), + 'tam_old_message_number': ('get_tam_message_count', {'count_type': 'old'}), + 'wlan_total_associates': ('wlan_devices_count', None), + 'hosts_info': ('get_hosts_dict', None), + 'hosts_count': ('get_hosts_count', None), + 'mesh_topology': ('get_mesh_topology', None) } # turn data to True of string is as listed str_to_bool = { - 'wan_is_connected': 'Connected', - 'wan_link': 'Up', - 'wps_active': 'active', - 'wlanconfig': '1', - 'tam': '1', + 'wan_is_connected': 'Connected', + 'wan_link': 'Up', + 'wps_active': 'active', + 'wlanconfig': '1', + 'tam': '1', 'deflection_enable': '1', - 'myfritz_status': '1' + 'myfritz_status': '1' } # Update link dict depending on connection type @@ -982,7 +995,9 @@ def _poll_fritz_device(self, avm_data_type: str, index=None, enforce_read: bool link.update(link_ip) # define client - client = 'client' if not avm_data_type.startswith('wan_current') else 'client_igd' + client = 'client' + if avm_data_type.startswith('wan_current'): + client = 'client_igd' # self.logger.debug(f"_poll_fritz_device: {avm_data_type=}, {index=}, {enforce_read=} with {client=}") @@ -994,7 +1009,17 @@ def _poll_fritz_device(self, avm_data_type: str, index=None, enforce_read: bool return data = self._poll_data(client, device, service, action, in_arg, out_arg, index, enforce_read) elif avm_data_type in link2: - data = eval(link2[avm_data_type]) + attr, arg = link2[avm_data_type] + if not arg and index is not None: + arg = {'index': index} + data = getattr(self, attr)(**arg) + elif arg and index is not None: + arg.update({'index': index}) + data = getattr(self, attr)(**arg) + elif arg and index is None: + data = getattr(self, attr)(**arg) + else: + data = getattr(self, attr)() else: return @@ -1011,29 +1036,29 @@ def _poll_data(self, client: str, device: str, service: str, action: str, in_arg """ # self.logger.debug(f"_get_update_data called with device={device}, service={service}, action={action}, in_argument={in_argument}, out_argument={out_argument}, in_argument_value={in_argument_value}, enforce_read={enforce_read}") - data = None - data_string = None + data = self + data_args = [] cache_dict_key = f"{device}_{service}_{action}_{in_argument}_{in_argument_value}" # create data_string for polling data from tr064 client if in_argument is None: - data_string = f"self.{client}.{device}.{service}.{action}()" - elif in_argument is not None and in_argument_value is not None: + data_args = [('attr', client), ('attr', device), ('attr', service), ('attr', action), ('arg', None)] + elif in_argument_value is not None: if service.lower().startswith('wlan'): - data_string = f"self.{client}.{device}.{service}[{in_argument_value}].{action}()" + data_args = [('attr', client), ('attr', device), ('attr', service), ('sub', in_argument_value), ('attr', action), ('arg', None)] else: - data_string = f"self.{client}.{device}.{service}.{action}({in_argument}='{in_argument_value}')" + data_args = [('attr', client), ('attr', device), ('attr', service), ('attr', action), ('arg', {in_argument: in_argument_value})] - if data_string is None: + if not data_args: return # poll data from tr064 client if cache_dict_key not in self._data_cache or enforce_read: try: - data = eval(data_string) + data = walk_nodes(self, data_args) except Exception as e: self.logger.warning(f"Poll data from TR064 Client caused Error '{e}'") - pass + return else: self._data_cache[cache_dict_key] = data else: @@ -1041,13 +1066,18 @@ def _poll_data(self, client: str, device: str, service: str, action: str, in_arg # return data if data is None: - self.logger.info(f"No data for {data_string!r} received.") + self.logger.info(f"No data for {cache_dict_key!r} received.") return elif isinstance(data, int) and 99 < data < 1000: self.logger.info(f"Response was ErrorCode: {data} '{self.ERROR_CODES.get(data, None)}' for self.{client}.{device}.{service}.{action}()") return data + elif out_argument: + try: + return data.get(out_argument) + except Exception as e: + self.logger.warning(f"Poll data from TR064 Client caused Error '{e}'") else: - return data if not out_argument else data.get(out_argument) + return data def _set_fritz_device(self, avm_data_type: str, args=None, wlan_index=None): """Set AVM Device based on avm_data_type and args""" @@ -1055,18 +1085,18 @@ def _set_fritz_device(self, avm_data_type: str, args=None, wlan_index=None): self.logger.debug(f"_set_fritz_device called: avm_data_type={avm_data_type}, args={args}, wlan_index={wlan_index}") link = { - 'set_call_origin': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialSetConfig'), - 'set_tam': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'SetEnable'), - 'set_aha_device': ('InternetGatewayDevice', 'X_AVM_DE_Homeauto', 'SetSwitch'), - 'set_deflection': ('InternetGatewayDevice', 'X_AVM_DE_OnTel', 'SetDeflectionEnable'), - 'set_wlan': ('LANDevice', 'WLANConfiguration', 'SetEnable'), - 'set_wps': ('LANDevice', 'WLANConfiguration', 'X_AVM_DE_SetWPSEnable'), - 'reboot': ('InternetGatewayDevice', 'DeviceConfig', 'Reboot'), - 'wol': ('LanDevice', 'Hosts', 'X_AVM_DE_GetAutoWakeOnLANByMACAddress'), - 'reconnect_ppp': ('WANConnectionDevice', 'WANPPPConnection', 'ForceTermination'), - 'reconnect_ipp': ('WANConnectionDevice', 'WANIPPConnection', 'ForceTermination'), - 'start_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialNumber'), - 'cancel_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialHangup'), + 'set_call_origin': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialSetConfig'), + 'set_tam': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'SetEnable'), + 'set_aha_device': ('InternetGatewayDevice', 'X_AVM_DE_Homeauto', 'SetSwitch'), + 'set_deflection': ('InternetGatewayDevice', 'X_AVM_DE_OnTel', 'SetDeflectionEnable'), + 'set_wlan': ('LANDevice', 'WLANConfiguration', 'SetEnable'), + 'set_wps': ('LANDevice', 'WLANConfiguration', 'X_AVM_DE_SetWPSEnable'), + 'reboot': ('InternetGatewayDevice', 'DeviceConfig', 'Reboot'), + 'wol': ('LanDevice', 'Hosts', 'X_AVM_DE_GetAutoWakeOnLANByMACAddress'), + 'reconnect_ppp': ('WANConnectionDevice', 'WANPPPConnection', 'ForceTermination'), + 'reconnect_ipp': ('WANConnectionDevice', 'WANIPPConnection', 'ForceTermination'), + 'start_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialNumber'), + 'cancel_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialHangup'), } if avm_data_type not in link: @@ -1077,30 +1107,33 @@ def _set_fritz_device(self, avm_data_type: str, args=None, wlan_index=None): self.logger.debug(f"avm_data_type={avm_data_type} -> {device}.{service}.{action}") if service.lower().startswith('wlan'): - wlan_index = "" if wlan_index is None else wlan_index - cmd = f"self.client.{device}.{service}[{wlan_index}].{action}({args})" + if wlan_index is None: + return + cmd_args = [('attr', 'client'), ('attr', device), ('attr', service), ('sub', 'wlan_index'), ('attr', action), ('arg', args)] elif args is None: - cmd = f"self.client.{device}.{service}.{action}()" + cmd_args = [('attr', 'client'), ('attr', device), ('attr', service), ('attr', action), ('arg', None)] else: - cmd = f"self.client.{device}.{service}.{action}({args})" + cmd_args = [('attr', 'client'), ('attr', device), ('attr', service), ('attr', action), ('arg', args)] - response = eval(cmd) + response = walk_nodes(self, cmd_args) if self.debug_log: - self.logger.debug(f"response={response} for cmd={cmd}.") + self.logger.debug(f"response={response} for cmd={cmd_args}.") # return response if response is None: - self.logger.info(f"No response for cmd={cmd} received.") + self.logger.info(f"No response for cmd={cmd_args} received.") + return elif isinstance(response, int) and 99 < response < 1000: - self.logger.info(f"Response was ErrorCode: {response} '{self.ERROR_CODES.get(response, None)}' for cmd={cmd}") + self.logger.info(f"Response was ErrorCode: {response} '{self.ERROR_CODES.get(response, None)}' for cmd={cmd_args}") + return + return response def _clear_data_cache(self): """ Clears _data_cache dict and put needed content back """ - self._data_cache.clear() def _request(self, url: str, timeout: int, verify: bool): @@ -1114,72 +1147,20 @@ def _request(self, url: str, timeout: int, verify: bool): self.logger.error(f"Request to URL={url} failed with {request.status_code}") request.raise_for_status() - @staticmethod - def _request_response_to_xml(request): - """ - Parse request response to element object - """ - root = ET.fromstring(request.content) - return root - - def _lxml_element_to_dict(self, node): - """Parse lxml Element to dictionary""" - result = {} - - for element in node.iterchildren(): - # Remove namespace prefix - key = element.tag.split('}')[1] if '}' in element.tag else element.tag - - # Process element as tree element if the inner XML contains non-whitespace content - if element.text and element.text.strip(): - value = element.text - else: - value = self._lxml_element_to_dict(element) - if key in result: - - if type(result[key]) is list: - result[key].append(value) - else: - tempvalue = result[key].copy() - result[key] = [tempvalue, value] - else: - result[key] = value - return result - - def update_item_errordict(self, item_errorlist: list): - """ - Get list of item where an error during last data polling cycle occurred and feed dict of error items to count number of error per item. - """ - for item in item_errorlist: - if item not in self._item_errordict: - self._item_errordict[item] = 1 - else: - self._item_errordict[item] += 1 - - self.logger.debug(f"_item_errordict={self._item_errordict}") - - def create_item_blacklist(self, item_errorlist: list, errorcount: int = 2): - """ - Get list of item where an error during last data polling cycle occurred, call update of item errordict and generate item blacklist. - Item blacklist will be ignored for further update cycles. - """ - self.update_item_errordict(item_errorlist) - - blacklist = set(self._item_blacklist) - for item in self._item_errordict: - if self._item_errordict[item] >= errorcount: - blacklist.add(item) - self.logger.info(f"Item {item} will be blacklisted, since it caused an error {errorcount} times during update. Item will not be updated until restart or blacklist is cleaned via WebIF") - self._item_blacklist = list(blacklist) - self.logger.debug(f"_item_blacklist={self._item_blacklist}") - def reset_item_blacklist(self): """ - Clean/reset Item errordict and blacklist + Clean/reset item blacklist """ - self._item_errordict.clear() - self._item_blacklist.clear() - self.logger.info(f"Item Blacklist reset. item_blacklist={self._item_blacklist}") + for item in self.items: + self.items[item]['error_count'] = 0 + self.logger.info(f"Item Blacklist reset. item_blacklist={self.get_tr064_items_blacklisted()}") + + def get_tr064_items_blacklisted(self) -> list: + item_list = [] + for item in self.items: + if self.items[item].get('error_count', 0) >= self.ERROR_COUNT_TO_BE_BLACKLISTED: + item_list.append(item) + return item_list # ---------------------------------- # Fritz Device methods, reboot, wol, reconnect @@ -1201,12 +1182,16 @@ def reconnect(self): Uses: http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanipconnSCPD.pdf http://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wanpppconnSCPD.pdf """ - if 'PPP' in self.default_connection_service: - # self.client.WANConnectionDevice.WANPPPConnection.ForceTermination() - return self._set_fritz_device('reconnect_ppp') - else: - # self.client.WANConnectionDevice.WANIPPConnection.ForceTermination() - return self._set_fritz_device('reconnect_ipp') + # default_connection_service can be None, e.g. for repeaters + try: + if 'PPP' in self.default_connection_service: + # self.client.WANConnectionDevice.WANPPPConnection.ForceTermination() + return self._set_fritz_device('reconnect_ppp') + except TypeError: + pass + + # self.client.WANConnectionDevice.WANIPPConnection.ForceTermination() + return self._set_fritz_device('reconnect_ipp') def wol(self, mac_address: str): """ @@ -1232,7 +1217,7 @@ def get_call_origin(self): # phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_DialGetConfig()['NewX_AVM_DE_PhoneName'] phone_name = self._poll_fritz_device('call_origin', enforce_read=True) - if phone_name is None: + if not phone_name: self.logger.error("No call origin available.") return phone_name @@ -1248,7 +1233,7 @@ def get_phone_name(self, index: int = 1): # phone_name = self.client.InternetGatewayDevice.X_VoIP.X_AVM_DE_GetPhonePort()['NewX_AVM_DE_PhoneName'] phone_name = self._poll_fritz_device('phone_name', index, enforce_read=True) - if phone_name is None: + if not phone_name: self.logger.error(f"No phone name available at provided index {index}") return phone_name @@ -1289,12 +1274,12 @@ def get_contact_name_by_phone_number(self, phone_number: str = '', phonebook_id: phone_number = phone_number.strip('#') # phonebook_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetPhonebook(NewPhonebookID=phonebook_id)['NewPhonebookURL'] - phonebook_url = self._poll_fritz_device('phonebook_url', phonebook_id, enforce_read=True) + phonebook_url = to_str(self._poll_fritz_device('phonebook_url', phonebook_id, enforce_read=True)) if not phonebook_url: - return "" + return '' - phonebooks = self._request_response_to_xml(self._request(phonebook_url, self._timeout, self.verify)) - if phonebooks is not None: + phonebooks = request_response_to_xml(self._request(phonebook_url, self._timeout, self.verify)) + if phonebooks: for phonebook in phonebooks.iter('phonebook'): for contact in phonebook.iter('contact'): for number in contact.findall('.//number'): @@ -1304,7 +1289,8 @@ def get_contact_name_by_phone_number(self, phone_number: str = '', phonebook_id: return contact.find('.//realName').text else: self.logger.error("Phonebook not available on the FritzDevice") - return "" + + return '' def get_phone_numbers_by_name(self, name: str = '', phonebook_id: int = 0) -> dict: """Get phone number from phone book by contact""" @@ -1312,12 +1298,12 @@ def get_phone_numbers_by_name(self, name: str = '', phonebook_id: int = 0) -> di result_numbers = {} # phonebook_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetPhonebook(NewPhonebookID=phonebook_id)['NewPhonebookURL'] - phonebook_url = self._poll_fritz_device('phonebook_url', phonebook_id, enforce_read=True) + phonebook_url = to_str(self._poll_fritz_device('phonebook_url', phonebook_id, enforce_read=True)) if not phonebook_url: return {} - phonebooks = self._request_response_to_xml(self._request(phonebook_url, self._timeout, self.verify)) - if phonebooks is not None: + phonebooks = request_response_to_xml(self._request(phonebook_url, self._timeout, self.verify)) + if phonebooks: for phonebook in phonebooks.iter('phonebook'): for contact in phonebook.iter('contact'): for real_name in contact.findall('.//realName'): @@ -1338,19 +1324,16 @@ def get_calllist_from_cache(self) -> list: """returns the cached calllist when all items are initialized. The filter set by plugin.yaml is applied.""" if not self._calllist_cache: self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) - elif len(self._calllist_cache) == 0: - self._calllist_cache = self.get_calllist(self._call_monitor_incoming_filter) return self._calllist_cache def get_calllist(self, filter_incoming: str = '') -> list: """request calllist from AVM Device""" - # calllist_url = self.client.InternetGatewayDevice.X_AVM_DE_OnTel.GetCallList() - calllist_url = self._poll_fritz_device('calllist_url', enforce_read=True) + calllist_url = to_str(self._poll_fritz_device('calllist_url', enforce_read=True)) if not calllist_url: return [] - calllist = self._request_response_to_xml(self._request(calllist_url, self._timeout, self.verify)) + calllist = request_response_to_xml(self._request(calllist_url, self._timeout, self.verify)) if calllist is not None: result_entries = [] @@ -1384,7 +1367,9 @@ def get_calllist(self, filter_incoming: str = '') -> list: # ---------------------------------- # logs methods # ---------------------------------- - def get_device_log_from_tr064(self) -> Union[list, str, dict]: +# TODO: rewrite to -> list[str]? +# --> das ist Bestandscode und diente zur Anzeige von "Fließtext" in der Visu + def get_device_log_from_tr064(self): """ uses: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/deviceinfoSCPD.pdf @@ -1423,8 +1408,8 @@ def set_wlan(self, wlan_index: int, new_enable: bool = False): def set_wlan_time_remaining(self, wlan_index: int): """look for item and set time remaining""" - for item in self._items: # search for guest time remaining item. - if self._items[item][0] == 'wlan_guest_time_remaining' and self._items[item][1] == wlan_index: + for item in self.items: # search for guest time remaining item. + if self.items[item][0] == 'wlan_guest_time_remaining' and self.items[item][1] == wlan_index: data = self._poll_fritz_device('wlan_guest_time_remaining', wlan_index, enforce_read=True) if data is not None: item(data, self._plugin_instance.get_fullname()) @@ -1449,11 +1434,11 @@ def get_wlan_devices(self, wlan_index: int = 0): return url = f"{self._build_url()}{wlandevice_url}" - wlandevices_xml = self._request_response_to_xml(self._request(url, self._timeout, self.verify)) + wlandevices_xml = request_response_to_xml(self._request(url, self._timeout, self.verify)) if wlandevices_xml is None: return - return self._lxml_element_to_dict(wlandevices_xml) + return lxml_element_to_dict(wlandevices_xml) def set_wps(self, wlan_index: int, wps_enable: bool = False): """ @@ -1478,7 +1463,7 @@ def get_wps(self, wlan_index: int): status = self._poll_fritz_device('wps_status', wlan_index, enforce_read=True) - return True if status == 'active' else False + return status == 'active' # ---------------------------------- # tam methods @@ -1508,14 +1493,14 @@ def get_tam_list(self, tam_index: int = 0): uses: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_tam.pdf """ # tamlist_url = self.client.InternetGatewayDevice.X_AVM_DE_TAM.GetMessageList(NewIndex=tam_index)['NewURL'] - tamlist_url = self._poll_fritz_device('tamlist_url', tam_index, enforce_read=True) + tamlist_url = to_str(self._poll_fritz_device('tamlist_url', tam_index, enforce_read=True)) if tamlist_url: - return self._request_response_to_xml(self._request(tamlist_url, self._timeout, self.verify)) + return request_response_to_xml(self._request(tamlist_url, self._timeout, self.verify)) - def get_tam_message_count(self, tam_index: int = 0, number=None): + def get_tam_message_count(self, index: int = 0, count_type: str = ''): """Return count to tam messages""" - tam_list = self.get_tam_list(tam_index) + tam_list = self.get_tam_list(index) total_count = 0 new_count = 0 @@ -1528,11 +1513,11 @@ def get_tam_message_count(self, tam_index: int = 0, number=None): if new.text.strip() == '1': new_count += 1 - if number == 'total': + if count_type == 'total': return total_count - elif number == 'new': + elif count_type == 'new': return new_count - elif number == 'old': + elif count_type == 'old': return total_count - new_count else: return total_count, new_count @@ -1543,7 +1528,9 @@ def get_tam_message_count(self, tam_index: int = 0, number=None): def set_aha_device(self, ain: str = '', set_switch: bool = False): """Set AHA-Device via TR-064 protocol""" # SwitchState: OFF, ON, TOGGLE, UNDEFINED - switch_state = "ON" if set_switch is True else "OFF" + switch_state = "OFF" + if set_switch: + switch_state = "ON" # self.client.InternetGatewayDevice.X_AVM_DE_Homeauto.SetSwitch(NewAIN=ain, NewSwitchState=switch_state) return self._set_fritz_device('set_aha_device', f"NewAIN={ain}, NewSwitchState='{switch_state}'") @@ -1592,7 +1579,7 @@ def is_host_active(self, mac_address: str) -> bool: :return: True or False, depending if the host is active on the FritzDevice """ # is_active = self.client.LANDevice.Hosts.GetSpecificHostEntry(NewMACAddress=mac_address)['NewActive'] - return bool(int(self._poll_fritz_device('is_host_active', mac_address, enforce_read=True))) + return bool(self._poll_fritz_device('is_host_active', mac_address, enforce_read=True)) def get_hosts(self, only_active: bool = False) -> list: """ @@ -1604,14 +1591,14 @@ def get_hosts(self, only_active: bool = False) -> list: :return: Array host dicts (see get_host_details) """ # number_of_hosts = int(self.client.LANDevice.Hosts.GetHostNumberOfEntries()['NewHostNumberOfEntries']) - number_of_hosts = int(self._poll_fritz_device('number_of_hosts', enforce_read=True)) + number_of_hosts = to_int(self._poll_fritz_device('number_of_hosts', enforce_read=True)) hosts = [] for i in range(1, number_of_hosts): host = self.get_host_details(i) - if host is None: + if not host: continue - if not only_active or (only_active and host['is_active']): + if not only_active or host['is_active']: hosts.append(host) return hosts @@ -1639,8 +1626,8 @@ def get_host_details(self, index: int): 'ip_address': host_info.get('NewIPAddress'), 'address_source': host_info.get('NewAddressSource'), 'mac_address': host_info.get('NewMACAddress'), - 'is_active': bool(int(host_info.get('NewActive'))), - 'lease_time_remaining': int(host_info.get('NewLeaseTimeRemaining')) + 'is_active': bool(host_info.get('NewActive')), + 'lease_time_remaining': to_int(host_info.get('NewLeaseTimeRemaining')) } return host @@ -1649,14 +1636,12 @@ def get_hosts_dict(self) -> Union[dict, None]: # hosts_url = self.client.LANDevice.Hosts.X_AVM_DE_GetHostListPath()['NewX_AVM_DE_HostListPath'] hosts_url = self._poll_fritz_device('hosts_url', enforce_read=True) - if hosts_url is None: + if not hosts_url: return - url = f"{self._build_url()}{hosts_url}" + url = self._build_url() + str(hosts_url) - hosts_xml = self._request_response_to_xml(self._request(url, self._timeout, self.verify)) - if hosts_xml is None: - return {} + hosts_xml = request_response_to_xml(self._request(url, self._timeout, self.verify)) hosts_dict = {} for item in hosts_xml: @@ -1667,8 +1652,10 @@ def get_hosts_dict(self) -> Union[dict, None]: index = int(attr.text) key = str(attr.tag) value = str(attr.text) - key = key[9:] if key.startswith('X_AVM-DE_') else key - value = int(value) if value.isdigit() else value + if key.startswith('X_AVM-DE_'): + key = key[9:] + if value.isdigit(): + value = int(value) if key in ['Active', 'Guest', 'Disallow', 'UpdateAvailable', 'VPN']: value = bool(value) host_dict[key] = value @@ -1677,20 +1664,22 @@ def get_hosts_dict(self) -> Union[dict, None]: return hosts_dict - def get_hosts_count(self): + def get_hosts_count(self) -> int: """Returns count of hosts""" - - return len(self.get_hosts_dict()) + try: + return len(self.get_hosts_dict()) + except TypeError: + return 0 def get_mesh_topology(self) -> Union[dict, None]: """Get mesh topology information as dict""" # mesh_url = self.client.LANDevice.Hosts.X_AVM_DE_GetMeshListPath()['NewX_AVM_DE_MeshListPath'] mesh_url = self._poll_fritz_device('mesh_url', enforce_read=True) - if mesh_url is None: + if not mesh_url: return - url = f"{self._build_url()}{mesh_url}" + url = self._build_url() + str(mesh_url) mesh_response = self._request(url, self._timeout, self.verify) if mesh_response: @@ -1698,7 +1687,6 @@ def get_mesh_topology(self) -> Union[dict, None]: mesh = mesh_response.json() except Exception: mesh = None - pass return mesh @@ -1735,7 +1723,7 @@ def __init__(self, host, ssl, verify, user, password, _log_entry_count, plugin_i self._devices: Dict[str, FritzHome.FritzhomeDevice] = {} self._templates: Dict[str, FritzHome.FritzhomeTemplate] = {} self._logged_in = False - self._items = dict() + self.items = dict() self._aha_devices = dict() self._session = requests.Session() self.connected = False @@ -1744,19 +1732,30 @@ def __init__(self, host, ssl, verify, user, password, _log_entry_count, plugin_i # Login self.login() - def register_item(self, item, avm_data_type: str, avm_data_cycle: int): + def register_item(self, item, item_config: dict): """ Parsed items valid fpr that class will be registered """ # handle aha items index = self._get_item_ain(item) - if index is not None: - self.logger.debug(f"Item {item.id()} with avm device attribute and defined avm_ain={index} found; append to list.") + if index: + self.logger.debug(f"Item {item.path()} with avm device attribute and defined avm_ain={index} found; append to list.") else: - self.logger.warning(f"Item {item.id()} with avm attribute found, but 'avm_ain' is not defined; Item will be ignored.") + self.logger.warning(f"Item {item.path()} with avm attribute found, but 'avm_ain' is not defined; Item will be ignored.") + return + + # update item config + item_config.update({'interface': 'aha', 'index': index}) # register item - self._items[item] = (avm_data_type, index, avm_data_cycle, int(time.time())) + self.items[item] = item_config + + def unregister_item(self, item): + """ remove item from instance """ + try: + del self.items[item] + except KeyError: + pass def cyclic_item_update(self, read_all: bool = False): """ @@ -1775,27 +1774,28 @@ def cyclic_item_update(self, read_all: bool = False): current_time = int(time.time()) # iterate over items and get data - for item in self._items: - # get avm_data_type and ain - avm_data_type = self._items[item][0] - ain = self._items[item][1] - cycle = self._items[item][2] - next_time = self._items[item][3] + for item in self.items: + # get item config + item_config = self.items[item] + avm_data_type = item_config['avm_data_type'] + ain = item_config['index'] + cycle = item_config['avm_data_cycle'] + next_time = item_config['next_update'] # Just read items with cycle == 0 at init - if read_all is False and cycle == 0: - # self.logger.debug(f"Item={item.id()} just read at init. No further update.") + if not read_all and not cycle: + # self.logger.debug(f"Item={item.path()} just read at init. No further update.") continue # check if item is already due if next_time > current_time: - # self.logger.debug(f"Item={item.id()} is not due, yet.") + # self.logger.debug(f"Item={item.path()} is not due, yet.") continue - self.logger.info(f"Item={item.id()} with avm_data_type={avm_data_type} and ain={ain} will be updated") + self.logger.info(f"Item={item.path()} with avm_data_type={avm_data_type} and ain={ain} will be updated") # Attributes that are write-only commands with no corresponding read commands are excluded from status updates via update black list: - update_black_list = ['switch_toggle'] + update_black_list = ALL_ATTRIBUTES_WRITEONLY if avm_data_type in update_black_list: self.logger.info(f"avm_data_type '{avm_data_type}' is in update blacklist. Item will not be updated") continue @@ -1807,16 +1807,14 @@ def cyclic_item_update(self, read_all: bool = False): # get value value = self.get_value_by_ain_and_avm_data_type(ain, avm_data_type) if value is None: - self.logger.debug(f'Value for attribute={avm_data_type} at device with AIN={ain} to set Item={item.id()} is not available/None.') + self.logger.debug(f'Value for attribute={avm_data_type} at device with AIN={ain} to set Item={item.path()} is not available/None.') continue # set item item(value, self._plugin_instance.get_fullname()) - # set new due date - lst = list(self._items[item]) - lst[3] = current_time + cycle - self._items[item] = tuple(lst) + # set next due date + self.items[item].update({'next_update': current_time + cycle}) def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): """ @@ -1836,26 +1834,24 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): } # get AIN - _ain = self._items[item][1] + _ain = self.items[item]['index'] # adapt avm_data_type by removing 'set_' if avm_data_type.startswith('set_'): - avm_data_type = avm_data_type[len('set_'):] + avm_data_type = avm_data_type[4:] # logs message for upcoming/limited functionality - if avm_data_type == 'hue': + if avm_data_type == 'hue' or avm_data_type == 'saturation': # Full RGB hue will be supported by Fritzbox approximately from Q2 2022 on: # Currently, only use default RGB colors that are supported by default (getcolordefaults) # These default colors have given saturation values. self.logger.info("Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") - elif avm_data_type == 'saturation': - self.logger.info("Full RGB hue will be supported by Fritzbox approximately from Q2 2022. Limited functionality.") # Call set method per avm_data_type to_be_set_value = item() try: _dispatcher[avm_data_type][0](_ain, to_be_set_value) - except Exception: + except KeyError: self.logger.error(f"{avm_data_type} is not defined to be updated.") # handle readafterwrite @@ -1864,15 +1860,16 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): time.sleep(wait) try: set_value = _dispatcher[avm_data_type][1](_ain) - except Exception: + # only handle avm_data_type not present in _dispatcher + except KeyError: self.logger.error(f"{avm_data_type} is not defined to be read.") else: item(set_value, self._plugin_instance.get_fullname()) if set_value != to_be_set_value: - self.logger.warning(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") + self.logger.warning(f"Setting AVM Device defined in Item={item.path()} with avm_data_type={avm_data_type} to value={to_be_set_value} FAILED!") else: if self.debug_log: - self.logger.debug(f"Setting AVM Device defined in Item={item.id()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") + self.logger.debug(f"Setting AVM Device defined in Item={item.path()} with avm_data_type={avm_data_type} to value={to_be_set_value} successful!") def get_value_by_ain_and_avm_data_type(self, ain, avm_data_type): """ @@ -1900,14 +1897,14 @@ def _get_item_ain(self, item) -> Union[str, None]: for i in range(2): attribute = 'ain' ain_device = self._plugin_instance.get_iattr_value(lookup_item.conf, attribute) - if ain_device is not None: + if ain_device: break else: lookup_item = lookup_item.return_parent() if ain_device: # deprecated warning for attribute 'ain' - self.logger.warning(f"Item {item.id()} uses deprecated 'ain' attribute. Please consider to switch to 'avm_ain'.") + self.logger.warning(f"Item {item.path()} uses deprecated 'ain' attribute. Please consider to switch to 'avm_ain'.") else: lookup_item = item for i in range(2): @@ -1919,18 +1916,13 @@ def _get_item_ain(self, item) -> Union[str, None]: lookup_item = lookup_item.return_parent() if ain_device is None: - self.logger.error(f'Device AIN for {item.id()} is not defined or instance not given') + self.logger.error(f'Device AIN for {item.path()} is not defined or instance not given') return None return ain_device - @property - def item_dict(self): - return self._items - - @property def item_list(self): - return list(self._items.keys()) + return list(self.items.keys()) def _request(self, url: str, params=None, timeout: int = 10, result: str = 'text'): """ @@ -1981,10 +1973,10 @@ def _login_request(self, username=None, challenge_response=None): if challenge_response: params["response"] = challenge_response plain = self._request(url, params) - dom = ElementTree.fromstring(plain) + dom = ElementTree.fromstring(to_str(plain)) sid = dom.findtext("SID") challenge = dom.findtext("Challenge") - blocktime = int(dom.findtext("BlockTime")) + blocktime = to_int(dom.findtext("BlockTime")) return sid, challenge, blocktime @@ -2049,7 +2041,7 @@ def _aha_request(self, cmd, ain=None, param=None, rf='str'): return if rf == 'bool': - return bool(int(plain)) + return bool(plain) elif rf == 'str': return str(plain) elif rf == 'int': @@ -2103,7 +2095,7 @@ def check_sid(self): url = self._get_prefixed_host() + self.LOGIN_ROUTE params = {"sid": self._sid} plain = self._request(url, params) - dom = ElementTree.fromstring(plain) + dom = ElementTree.fromstring(to_str(plain)) sid = dom.findtext("SID") if sid == "0000000000000000": @@ -2122,7 +2114,7 @@ def _get_prefixed_host(self): - (unencrypted) """ host = self.host - if not host.startswith("https://") or not host.startswith("http://"): + if not host.startswith("https://") and not host.startswith("http://"): if self.ssl: host = "https://" + host else: @@ -2161,7 +2153,7 @@ def _get_listinfo_elements(self, entity_type): if plain is None: return self.last_request = plain - dom = ElementTree.fromstring(plain) + dom = ElementTree.fromstring(to_str(plain)) return dom.findall(entity_type) def get_device_elements(self): @@ -2175,9 +2167,12 @@ 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 + try: + for element in elements: + if element.attrib["identifier"] == ain: + return element + except TypeError: + pass return None def get_devices(self): @@ -2199,10 +2194,7 @@ def get_device_by_ain(self, ain): """ Return a device specified by the AIN. """ - try: - return self.get_devices_as_dict()[ain] - except AttributeError: - pass + return self.get_devices_as_dict().get(ain) def get_device_present(self, ain): """ @@ -2256,7 +2248,10 @@ def get_switch_power(self, ain): Get the switch power consumption in W. """ value = self._aha_request("getswitchpower", ain=ain, rf='int') - return None if value is None else (value / 1000) # value in 0.001W + try: + return value / 1000 # value in 0.001W + except TypeError: + pass def get_switch_energy(self, ain): """ @@ -2271,13 +2266,16 @@ def get_temperature(self, ain): Get the device temperature sensor value. """ value = self._aha_request("gettemperature", ain=ain, rf='int') - return None if value is None else (value / 10.0) + try: + return value / 10.0 + except TypeError: + pass def _get_temperature(self, ain, name): """ Get temperature raw value """ - plain = self._aha_request(name, ain=ain, rf='int') + plain = to_int(self._aha_request(name, ain=ain, rf='int')) return (plain - 16) / 2 + 8 def get_target_temperature(self, ain): @@ -2303,39 +2301,47 @@ def set_window_open(self, ain, seconds): """ Set windows open. """ + endtimestamp = -1 if isinstance(seconds, bool): - seconds = 12 * 60 * 60 if seconds else 0 - endtimestamp = int(time.time() + seconds) - - self._aha_request("sethkrwindowopen", ain=ain, param={'endtimestamp': endtimestamp}) - + endtimestamp = 0 + if seconds: + endtimestamp = int(time.time() + 43200) + elif isinstance(seconds, int): + endtimestamp = 0 + if seconds > 0: + endtimestamp = int(time.time() + seconds) + if endtimestamp >= 0: + self._aha_request("sethkrwindowopen", ain=ain, param={'endtimestamp': endtimestamp}) + + @NoAttributeError def get_window_open(self, ain): """ Get windows open. """ - try: - return self.get_devices_as_dict()[ain].window_open - except AttributeError: - pass + return self.get_devices_as_dict()[ain].window_open def set_boost(self, ain, seconds): """ Set the thermostate to boost. """ + endtimestamp = -1 if isinstance(seconds, bool): - seconds = 12 * 60 * 60 if seconds else 0 - endtimestamp = int(time.time() + seconds) - - self._aha_request("sethkrboost", ain=ain, param={'endtimestamp': endtimestamp}) - + endtimestamp = 0 + if seconds: + endtimestamp = int(time.time() + 43200) + elif isinstance(seconds, int): + endtimestamp = 0 + if seconds > 0: + endtimestamp = int(time.time() + seconds) + if endtimestamp >= 0: + self._aha_request("sethkrboost", ain=ain, param={'endtimestamp': endtimestamp}) + + @NoKeyOrAttributeError def get_boost(self, ain): """ Get boost status. """ - try: - return self.get_devices_as_dict()[ain].hkr_boost - except AttributeError: - pass + return self.get_devices_as_dict()[ain].hkr_boost def get_comfort_temperature(self, ain): """ @@ -2379,19 +2385,17 @@ def set_state(self, ain, state): """ Set the switch/actuator/lightbulb to a state. """ - if bool(state): + if state: self.set_state_on(ain) else: self.set_state_off(ain) + @NoKeyOrAttributeError def get_state(self, ain): """ Get the switch/actuator/lightbulb to a state. """ - try: - return self.get_devices_as_dict()[ain].switch_state - except AttributeError: - pass + return self.get_devices_as_dict()[ain].switch_state # Level/Dimmer-related commands @@ -2406,14 +2410,12 @@ def set_level(self, ain, level): self._aha_request("setlevel", ain=ain, param={'level': int(level)}) + @NoKeyOrAttributeError def get_level(self, ain): """ get level/brightness/height in interval [0,255]. """ - try: - return self.get_devices_as_dict()[ain].level - except AttributeError: - pass + return self.get_devices_as_dict()[ain].level def set_level_percentage(self, ain, level): """ @@ -2422,14 +2424,12 @@ def set_level_percentage(self, ain, level): # Scale percentage to [0,255] interval self.set_level(ain, int(level * 2.55)) + @NoKeyOrAttributeError def get_level_percentage(self, ain): """ get level/brightness/height in interval [0,100]. """ - try: - return self.get_devices_as_dict()[ain].levelpercentage - except AttributeError: - pass + return self.get_devices_as_dict()[ain].levelpercentage # Color-related commands @@ -2438,7 +2438,7 @@ def _get_colordefaults(self, ain): Get colour defaults """ plain = self._aha_request("getcolordefaults", ain=ain) - return ElementTree.fromstring(plain) + return ElementTree.fromstring(to_str(plain)) def get_colors(self, ain): """ @@ -2523,14 +2523,12 @@ def set_color_discrete(self, ain, hue, duration=0): return self._aha_request("setcolor", ain=ain, param=param, rf='bool') + @NoKeyOrAttributeError def get_hue(self, ain): """ Get Hue value. """ - try: - return self.get_devices_as_dict()[ain].hue - except AttributeError: - pass + return self.get_devices_as_dict()[ain].hue def get_color_temps(self, ain): """ @@ -2554,14 +2552,12 @@ def set_color_temp(self, ain, temperature, duration=1): } self._aha_request("setcolortemperature", ain=ain, param=params) + @NoKeyOrAttributeError def get_color_temp(self, ain): """ Get color temperature. """ - try: - return self.get_devices_as_dict()[ain].colortemperature - except AttributeError: - pass + return self.get_devices_as_dict()[ain].colortemperature # Template-related commands @@ -2573,14 +2569,17 @@ def update_templates(self): if self._templates is None: self._templates = {} - for element in self.get_template_elements(): - if element.attrib["identifier"] in self._templates.keys(): - self.logger.info(f"Updating already existing Template {element.attrib['identifier']}") - self._templates[element.attrib["identifier"]]._update_from_node(element) - else: - self.logger.info("Adding new Template " + element.attrib["identifier"]) - template = FritzHome.FritzhomeTemplate(self, node=element) - self._templates[template.ain] = template + try: + for element in self.get_template_elements(): + if element.attrib["identifier"] in self._templates.keys(): + self.logger.info(f"Updating already existing Template {element.attrib['identifier']}") + self._templates[element.attrib["identifier"]]._update_from_node(element) + else: + self.logger.info("Adding new Template " + element.attrib["identifier"]) + template = FritzHome.FritzhomeTemplate(self, node=element) + self._templates[template.ain] = template + except TypeError: + pass return True def get_template_elements(self): @@ -2603,14 +2602,12 @@ def get_templates_as_dict(self): self.update_templates() return self._templates + @NoAttributeError def get_template_by_ain(self, ain): """ Return a template specified by the AIN. """ - try: - return self.get_templates_as_dict()[ain] - except AttributeError: - pass + return self.get_templates_as_dict()[ain] def apply_template(self, ain): """ @@ -2633,7 +2630,10 @@ def get_device_log_from_lua(self): params = {"sid": self._sid} # get data - data = self._request(url, params, result='json')['mq_log'] + try: + data = self._request(url, params, result='json')['mq_log'] + except KeyError: + data = '' # cut data if needed if self._log_entry_count: @@ -2719,7 +2719,11 @@ def __init__(self, fritz=None, node=None): self._functionsbitmask = None self.device_functions = [] - if fritz is not None: +# TODO: wenn fritz is none, ist dann überhaupt Funktionalität gegeben? ggf. abbrechen? +# --> Abbrechen ist glaube ich besser. + if not fritz: + raise RuntimeError(f'passed object fritz is type {type(fritz)}, not type FritzHome. Aborting.') + else: self._fritz = fritz if node is not None: self._update_from_node(node) @@ -2773,31 +2777,6 @@ def _update_device_functions(self): if self._has_feature(FritzHome.FritzhomeDeviceFeatures.HUM_SENSOR): self.device_functions.append('humidity_sensor') - # XML Helpers - @staticmethod - def get_node_value(elem, node): - return elem.findtext(node) - - def get_node_value_as_int(self, elem, node) -> int: - value = self.get_node_value(elem, node) - return None if value is None else (int(value)) - - def get_node_value_as_int_as_bool(self, elem, node) -> bool: - value = self.get_node_value_as_int(elem, node) - return None if value is None else (bool(value)) - - def get_temp_from_node(self, elem, node) -> float: - value = self.get_node_value_as_int(elem, node) - return None if value is None else (float(value) / 2) - - def get_node_value_as_float_1000(self, elem, node) -> float: - value = self.get_node_value_as_int(elem, node) - return None if value is None else (float(value) / 1000) - - def get_node_value_as_float_10(self, elem, node) -> float: - value = self.get_node_value_as_int(elem, node) - return None if value is None else (float(value) / 10) - class FritzhomeTemplate(FritzhomeEntityBase): """The Fritzhome Template class.""" @@ -2864,16 +2843,16 @@ def _update_from_node(self, node): self.product_name = node.attrib["productname"] self.device_name = node.findtext("name") - self.connected = self.get_node_value_as_int_as_bool(node, "present") - self.tx_busy = self.get_node_value_as_int_as_bool(node, "txbusy") + self.connected = get_node_value_as_int_as_bool(node, "present") + self.tx_busy = get_node_value_as_int_as_bool(node, "txbusy") try: - self.battery_low = self.get_node_value_as_int_as_bool(node, "batterylow") + self.battery_low = get_node_value_as_int_as_bool(node, "batterylow") except AttributeError: pass try: - self.battery_level = self.get_node_value_as_int(node, "battery") + self.battery_level = get_node_value_as_int(node, "battery") except AttributeError: pass @@ -2890,13 +2869,12 @@ class FritzhomeDeviceAlarm(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_alarm: + if self.has_alarm(): self._update_alarm_from_node(node) - @property def has_alarm(self): """Check if the device has alarm function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.ALARM) @@ -2904,8 +2882,8 @@ def has_alarm(self): def _update_alarm_from_node(self, node): alarm_element = node.find("alert") if alarm_element is not None: - self.alert_state = self.get_node_value_as_int_as_bool(alarm_element, "state") - self.last_alert_chgtimestamp = self.get_node_value_as_int(alarm_element, "lastalertchgtimestamp") + self.alert_state = get_node_value_as_int_as_bool(alarm_element, "state") + self.last_alert_chgtimestamp = get_node_value_as_int(alarm_element, "lastalertchgtimestamp") class FritzhomeDeviceButton(FritzhomeDeviceBase): """The Fritzhome Device class.""" @@ -2915,13 +2893,12 @@ class FritzhomeDeviceButton(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_button: + if self.has_button(): self._update_button_from_node(node) - @property def has_button(self): """Check if the device has button function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.BUTTON) @@ -2934,12 +2911,12 @@ def _update_button_from_node(self, node): self.buttons[button.button_identifier] = button try: - self.battery_low = self.get_node_value_as_int_as_bool(node, "batterylow") + self.battery_low = get_node_value_as_int_as_bool(node, "batterylow") except AttributeError: pass try: - self.battery_level = self.get_node_value_as_int(node, "battery") + self.battery_level = get_node_value_as_int(node, "battery") except AttributeError: pass @@ -2969,8 +2946,8 @@ def _update_from_node(self, node): self.button_identifier = node.attrib["button_identifier"] self.button_id = node.attrib["id"] - self.button_name = self.get_node_value(node, "name") - self.last_pressed = self.get_node_value(node, "lastpressedtimestamp") + self.button_name = get_node_value(node, "name") + self.last_pressed = get_node_value(node, "lastpressedtimestamp") @staticmethod def get_node_value(elem, node): @@ -2981,7 +2958,7 @@ class FritzhomeDeviceLightBulb(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return @property @@ -2999,7 +2976,7 @@ class FritzhomeDevicePowermeter(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return if self.has_powermeter: @@ -3014,14 +2991,14 @@ def _update_powermeter_from_node(self, node): powermeter_element = node.find("powermeter") if powermeter_element is not None: - self.power = self.get_node_value_as_float_1000(powermeter_element, "power") # raw value in 0.001W - self.voltage = self.get_node_value_as_float_1000(powermeter_element, "voltage") # raw value in 0.001V - self.energy = self.get_node_value_as_int(powermeter_element, "energy") # raw value in 1Wh + self.power = get_node_value_as_float_1000(powermeter_element, "power") # raw value in 0.001W + self.voltage = get_node_value_as_float_1000(powermeter_element, "voltage") # raw value in 0.001V + self.energy = get_node_value_as_int(powermeter_element, "energy") # raw value in 1Wh - if self.power is not None and self.voltage is not None and self.voltage > 0: - self.current = (round(self.power / self.voltage), 1) # value in A + if self.power and self.voltage: + self.current = round(self.power / self.voltage, 1) # value in A else: - self.current = None + self.current = 0.0 def get_switch_power(self): """The switch state.""" @@ -3036,10 +3013,9 @@ class FritzhomeDeviceRepeater(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - @property def has_repeater(self): """Check if the device has repeater function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.DECT_REPEATER) @@ -3054,13 +3030,12 @@ class FritzhomeDeviceSwitch(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_switch: + if self.has_switch(): self._update_switch_from_node(node) - @property def has_switch(self): """Check if the device has switch function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.SWITCH) @@ -3069,10 +3044,10 @@ def _update_switch_from_node(self, node): switch_element = node.find("switch") if switch_element is not None: - self.switch_state = self.get_node_value_as_int_as_bool(switch_element, "state") - self.switch_mode = self.get_node_value(switch_element, "mode") - self.lock = self.get_node_value_as_int_as_bool(switch_element, "lock") - self.device_lock = self.get_node_value_as_int_as_bool(switch_element, "devicelock") + self.switch_state = get_node_value_as_int_as_bool(switch_element, "state") + self.switch_mode = get_node_value(switch_element, "mode") + self.lock = get_node_value_as_int_as_bool(switch_element, "lock") + self.device_lock = get_node_value_as_int_as_bool(switch_element, "devicelock") def get_switch_state(self): """Get the switch state.""" @@ -3098,13 +3073,12 @@ class FritzhomeDeviceTemperature(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_temperature_sensor: + if self.has_temperature_sensor(): self._update_temperature_from_node(node) - @property def has_temperature_sensor(self): """Check if the device has temperature function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.TEMPERATURE) @@ -3112,8 +3086,8 @@ def has_temperature_sensor(self): def _update_temperature_from_node(self, node): temperature_element = node.find("temperature") if temperature_element is not None: - self.temperature_offset = self.get_node_value_as_float_10(temperature_element, "offset") # value in 0.1 °C - self.current_temperature = self.get_node_value_as_float_10(temperature_element, "celsius") # value in 0.1 °C + self.temperature_offset = get_node_value_as_float_10(temperature_element, "offset") # value in 0.1 °C + self.current_temperature = get_node_value_as_float_10(temperature_element, "celsius") # value in 0.1 °C def get_temperature(self): """Get the device temperature value.""" @@ -3144,13 +3118,12 @@ class FritzhomeDeviceThermostat(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_thermostat: + if self.has_thermostat(): self._update_hkr_from_node(node) - @property def has_thermostat(self): """Check if the device has thermostat function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.THERMOSTAT) @@ -3158,28 +3131,28 @@ def has_thermostat(self): def _update_hkr_from_node(self, node): hkr_element = node.find("hkr") if hkr_element is not None: - self.current_temperature = self.get_temp_from_node(hkr_element, "tist") - self.target_temperature = self.get_temp_from_node(hkr_element, "tsoll") - self.temperature_comfort = self.get_temp_from_node(hkr_element, "komfort") - self.temperature_reduced = self.get_temp_from_node(hkr_element, "absenk") - self.battery_low = self.get_node_value_as_int_as_bool(hkr_element, "batterylow") - self.battery_level = self.get_node_value_as_int(hkr_element, "battery") - self.window_open = self.get_node_value_as_int_as_bool(hkr_element, "windowopenactiv") - self.windowopenactiveendtime = self.get_node_value_as_int(hkr_element, "windowopenactiveendtime") - self.hkr_boost = self.get_node_value_as_int_as_bool(hkr_element, "boostactive") - self.boostactiveendtime = self.get_node_value_as_int(hkr_element, "boostactiveendtime") - self.adaptiveHeatingActive = self.get_node_value_as_int_as_bool(hkr_element, "adaptiveHeatingActive") - self.adaptiveHeatingRunning = self.get_node_value_as_int(hkr_element, "adaptiveHeatingRunning") - self.holiday_active = self.get_node_value_as_int_as_bool(hkr_element, "holidayactive") - self.summer_active = self.get_node_value_as_int_as_bool(hkr_element, "summeractive") - self.lock = self.get_node_value_as_int_as_bool(hkr_element, "lock") - self.device_lock = self.get_node_value_as_int_as_bool(hkr_element, "devicelock") - self.errorcode = self.get_node_value_as_int(hkr_element, "errorcode") + self.current_temperature = get_temp_from_node(hkr_element, "tist") + self.target_temperature = get_temp_from_node(hkr_element, "tsoll") + self.temperature_comfort = get_temp_from_node(hkr_element, "komfort") + self.temperature_reduced = get_temp_from_node(hkr_element, "absenk") + self.battery_low = get_node_value_as_int_as_bool(hkr_element, "batterylow") + self.battery_level = get_node_value_as_int(hkr_element, "battery") + self.window_open = get_node_value_as_int_as_bool(hkr_element, "windowopenactiv") + self.windowopenactiveendtime = get_node_value_as_int(hkr_element, "windowopenactiveendtime") + self.hkr_boost = get_node_value_as_int_as_bool(hkr_element, "boostactive") + self.boostactiveendtime = get_node_value_as_int(hkr_element, "boostactiveendtime") + self.adaptiveHeatingActive = get_node_value_as_int_as_bool(hkr_element, "adaptiveHeatingActive") + self.adaptiveHeatingRunning = get_node_value_as_int(hkr_element, "adaptiveHeatingRunning") + self.holiday_active = get_node_value_as_int_as_bool(hkr_element, "holidayactive") + self.summer_active = get_node_value_as_int_as_bool(hkr_element, "summeractive") + self.lock = get_node_value_as_int_as_bool(hkr_element, "lock") + self.device_lock = get_node_value_as_int_as_bool(hkr_element, "devicelock") + self.errorcode = get_node_value_as_int(hkr_element, "errorcode") nextchange_element = hkr_element.find("nextchange") - if nextchange_element is not None: - self.nextchange_endperiod = self.get_node_value_as_int(nextchange_element, "endperiod") - self.nextchange_temperature = self.get_temp_from_node(nextchange_element, "tchange") + if nextchange_element: + self.nextchange_endperiod = get_node_value_as_int(nextchange_element, "endperiod") + self.nextchange_temperature = get_temp_from_node(nextchange_element, "tchange") def get_temperature(self): """Get the device temperature value.""" @@ -3211,29 +3184,25 @@ def get_eco_temperature(self): def get_hkr_state(self): """Get the thermostate state.""" - try: - return {126.5: "off", - 127.0: "on", - self.temperature_reduced: "eco", - self.temperature_comfort: "comfort", - }[self.target_temperature] - except KeyError: - return "manual" + return {126.5: "off", + 127.0: "on", + self.temperature_reduced: "eco", + self.temperature_comfort: "comfort", + }.get(self.target_temperature, "manual") def set_hkr_state(self, state): """Set the state of the thermostat. Possible values for state are: 'on', 'off', 'comfort', 'eco'. """ - try: - value = {"off": 0, - "on": 100, - "eco": self.temperature_reduced, - "comfort": self.temperature_comfort, - }[state] - except KeyError: - return + value = {"off": 0, + "on": 100, + "eco": self.temperature_reduced, + "comfort": self.temperature_comfort, + }.get(state) - self.set_target_temperature(value) + # check for None as value can be 0 + if value is not None: + self.set_target_temperature(value) class FritzhomeDeviceBlind(FritzhomeDeviceBase): """The Fritzhome Device class.""" @@ -3243,13 +3212,12 @@ class FritzhomeDeviceBlind(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_blind: + if self.has_blind(): self._update_blind_from_node(node) - @property def has_blind(self): """Check if the device has blind function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.BLIND) @@ -3258,9 +3226,9 @@ def _update_blind_from_node(self, node): blind_element = node.find("blind") - if blind_element is not None: - self.endpositionsset = self.get_node_value_as_int_as_bool(blind_element, "endpositionsset") - self.blind_mode = self.get_node_value(blind_element, "mode") + if blind_element: + self.endpositionsset = get_node_value_as_int_as_bool(blind_element, "endpositionsset") + self.blind_mode = get_node_value(blind_element, "mode") def set_blind_open(self): """Open the blind.""" @@ -3281,13 +3249,13 @@ class FritzhomeDeviceSwitchable(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: + self.simpleonoff = False return - if self.has_switchable: + if self.has_switchable(): self._update_switchable_from_node(node) - @property def has_switchable(self): """Check if the device has switch function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.SWITCHABLE) @@ -3295,8 +3263,8 @@ def has_switchable(self): def _update_switchable_from_node(self, node): state_element = node.find("simpleonoff") - if state_element is not None: - self.simpleonoff = self.get_node_value_as_int_as_bool(state_element, "state") + if state_element: + self.simpleonoff = get_node_value_as_int_as_bool(state_element, "state") def set_state_off(self): """Switch light bulb off.""" @@ -3321,13 +3289,14 @@ class FritzhomeDeviceDimmable(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: + self.level = 0 + self.levelpercentage = 0 return - if self.has_level: + if self.has_level(): self._update_level_from_node(node) - @property def has_level(self): """Check if the device has dimmer function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.LEVEL) @@ -3336,22 +3305,13 @@ def _update_level_from_node(self, node): levelcontrol_element = node.find("levelcontrol") if levelcontrol_element is not None: - self.level = self.get_node_value_as_int(levelcontrol_element, "level") - self.levelpercentage = self.get_node_value_as_int(levelcontrol_element, "levelpercentage") - - # Set Level to zero for consistency, if light is off: - # try: - # onoff = bool(self._fritz.get_state(self.ain)) - # if onoff is False: - # self.level = 0 - # self.levelpercentage = 0 - # except AttributeError: - # pass + self.level = get_node_value_as_int(levelcontrol_element, "level") + self.levelpercentage = get_node_value_as_int(levelcontrol_element, "levelpercentage") state_element = node.find("simpleonoff") if state_element is not None: - simpleonoff = self.get_node_value_as_int_as_bool(state_element, "state") - if simpleonoff is False: + simpleonoff = get_node_value_as_int_as_bool(state_element, "state") + if not simpleonoff: self.level = 0 self.levelpercentage = 0 @@ -3371,13 +3331,12 @@ class FritzhomeDeviceColor(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_color: + if self.has_color(): self._update_color_from_node(node) - @property def has_color(self): """Check if the device has LightBulb function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.COLOR) @@ -3385,7 +3344,7 @@ def has_color(self): def _update_color_from_node(self, node): colorcontrol_element = node.find("colorcontrol") - if colorcontrol_element is not None: + if colorcontrol_element: try: self.color_mode = int(colorcontrol_element.attrib.get("current_mode")) @@ -3397,61 +3356,34 @@ def _update_color_from_node(self, node): except ValueError: pass - try: - self.fullcolorsupport = bool(colorcontrol_element.attrib.get("fullcolorsupport")) - except ValueError: - pass - - try: - self.mapped = bool(colorcontrol_element.attrib.get("mapped")) - except ValueError: - pass - - try: - self.hue = self.get_node_value_as_int(colorcontrol_element, "hue") - except ValueError: - self.hue = 0 - - try: - self.saturation = self.get_node_value_as_int(colorcontrol_element, "saturation") - except ValueError: - self.saturation = 0 - - try: - self.unmapped_hue = self.get_node_value_as_int(colorcontrol_element, "unmapped_hue") - except ValueError: - self.unmapped_hue = 0 - - try: - self.unmapped_saturation = self.get_node_value_as_int(colorcontrol_element, "unmapped_saturation") - except ValueError: - self.unmapped_saturation = 0 - - try: - self.colortemperature = self.get_node_value_as_int(colorcontrol_element, "temperature") - except ValueError: - self.colortemperature = 0 + self.fullcolorsupport = bool(colorcontrol_element.attrib.get("fullcolorsupport")) + self.mapped = bool(colorcontrol_element.attrib.get("mapped")) + self.hue = get_node_value_as_int(colorcontrol_element, "hue") + self.saturation = get_node_value_as_int(colorcontrol_element, "saturation") + self.unmapped_hue = get_node_value_as_int(colorcontrol_element, "unmapped_hue") + self.unmapped_saturation = get_node_value_as_int(colorcontrol_element, "unmapped_saturation") + self.colortemperature = get_node_value_as_int(colorcontrol_element, "temperature") def get_colors(self): """Get the supported colors.""" - if self.has_color: + if self.has_color(): return self._fritz.get_colors(self.ain) else: return {} def set_color(self, hsv, duration=0): """Set HSV color.""" - if self.has_color: + if self.has_color(): self._fritz.set_color(self.ain, hsv, duration, True) def set_unmapped_color(self, hsv, duration=0): """Set unmapped HSV color (Free color selection).""" - if self.has_color: + if self.has_color(): self._fritz.set_color(self.ain, hsv, duration, False) def get_color_temps(self): """Get the supported color temperatures energy.""" - if self.has_color: + if self.has_color(): return self._fritz.get_color_temps(self.ain) else: return [] @@ -3468,21 +3400,20 @@ class FritzhomeDeviceHumidity(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_humidity_sensor: + if self.has_humidity_sensor(): self._update_humidity_from_node(node) - @property def has_humidity_sensor(self): """Check if the device has humidity function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.HUM_SENSOR) def _update_humidity_from_node(self, node): humidity_element = node.find("humidity") - if humidity_element is not None: - self.humidity = self.get_node_value_as_int(humidity_element, "rel_humidity") + if humidity_element: + self.humidity = get_node_value_as_int(humidity_element, "rel_humidity") class FritzhomeDeviceHanFunUnit(FritzhomeDeviceBase): """The Fritzhome Device class.""" @@ -3493,34 +3424,22 @@ class FritzhomeDeviceHanFunUnit(FritzhomeDeviceBase): def _update_from_node(self, node): super()._update_from_node(node) - if self.connected is False: + if not self.connected: return - if self.has_han_fun_unit: + if self.has_han_fun_unit(): self._update_han_fun_unit_from_node(node) - @property def has_han_fun_unit(self): """Check if the device has humidity function.""" return self._has_feature(FritzHome.FritzhomeDeviceFeatures.HANFUN) def _update_han_fun_unit_from_node(self, node): hanfun_element = node.find("etsiunitinfo>") - if hanfun_element is not None: - try: - self.etsideviceid = self.get_node_value(hanfun_element, "etsideviceid") - except ValueError: - pass - - try: - self.unittype = self.get_node_value_as_int(hanfun_element, "unittype>") - except ValueError: - pass - - try: - self.interfaces = self.get_node_value_as_int(hanfun_element, "interfaces>") - except ValueError: - pass + if hanfun_element: + self.etsideviceid = get_node_value(hanfun_element, "etsideviceid") + self.unittype = get_node_value_as_int(hanfun_element, "unittype>") + self.interfaces = get_node_value_as_int(hanfun_element, "interfaces>") class FritzhomeDevice( FritzhomeDeviceAlarm, @@ -3563,12 +3482,7 @@ def __init__(self, host, port, callback, call_monitor_incoming_filter, plugin_in self._call_monitor_incoming_filter = call_monitor_incoming_filter self._callback = callback - self._items = dict() # more general items for the call monitor - self._trigger_items = dict() # items which can be used to trigger sth, e.g. a logic - self._items_incoming = dict() # items for incoming calls - self._items_outgoing = dict() # items for outgoing calls - self._duration_item_in = dict() # item counting the incoming call duration - self._duration_item_out = dict() # item counting the outgoing call duration + self.items = dict() # item dict self._call_active = dict() self._listen_active = False self._call_active['incoming'] = False @@ -3610,6 +3524,7 @@ def disconnect(self): self._listen_active = False self._stop_counter('incoming') self._stop_counter('outgoing') + try: self._listen_thread.join(1) except Exception: @@ -3627,40 +3542,52 @@ def reconnect(self): self.disconnect() self.connect() - def register_item(self, item, avm_data_type: str): + def register_item(self, item, item_config: dict): """ Registers an item to the CallMonitoringService :param item: item to register - :param avm_data_type: avm_data_type of item to be registered + :param item_config: item config dict of item to be registered """ + avm_data_type = item_config['avm_data_type'] + # handle CALL_MONITOR_ATTRIBUTES_IN if avm_data_type in CALL_MONITOR_ATTRIBUTES_IN: - self._items_incoming[item] = (avm_data_type, None) + item_config.update({'monitor_item_type': 'incoming'}) elif avm_data_type in CALL_MONITOR_ATTRIBUTES_OUT: - self._items_outgoing[item] = (avm_data_type, None) + item_config.update({'monitor_item_type': 'outgoing'}) elif avm_data_type in CALL_MONITOR_ATTRIBUTES_GEN: - self._items[item] = (avm_data_type, None) + item_config.update({'monitor_item_type': 'generic'}) elif avm_data_type in CALL_MONITOR_ATTRIBUTES_TRIGGER: avm_incoming_allowed = self._plugin_instance.get_iattr_value(item.conf, 'avm_incoming_allowed') avm_target_number = self._plugin_instance.get_iattr_value(item.conf, 'avm_target_number') if not avm_incoming_allowed or not avm_target_number: - self.logger.error(f"For Trigger-item={item.id()} both 'avm_incoming_allowed' and 'avm_target_number' must be specified as attributes. Item will be ignored.") + self.logger.error(f"For Trigger-item={item.path()} both 'avm_incoming_allowed' and 'avm_target_number' must be specified as attributes. Item will be ignored.") else: - self._trigger_items[item] = (avm_data_type, avm_incoming_allowed, avm_target_number) + item_config.update({'monitor_item_type': 'trigger', 'avm_incoming_allowed': avm_incoming_allowed, 'avm_target_number': avm_target_number}) elif avm_data_type in CALL_MONITOR_ATTRIBUTES_DURATION: if avm_data_type == 'call_duration_incoming': - self._duration_item_in[item] = (avm_data_type, None) + item_config.update({'monitor_item_type': 'duration_in'}) else: - self._duration_item_out[item] = (avm_data_type, None) + item_config.update({'monitor_item_type': 'duration_out'}) else: - self._items[item] = (avm_data_type, None) + item_config.update({'monitor_item_type': 'generic'}) + + # register item + self.items[item] = item_config + + def unregister_item(self, item): + """ remove item from instance """ + try: + del self.items[item] + except KeyError: + pass def set_callmonitor_item_values_initially(self): """ @@ -3668,11 +3595,11 @@ def set_callmonitor_item_values_initially(self): """ _calllist = self._plugin_instance.get_calllist() - if _calllist is None: + if not _calllist: return - for item in self._items_incoming: - avm_data_type = self._items_incoming[item][0] + for item in self.items: + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'last_caller_incoming': for element in _calllist: @@ -3712,10 +3639,7 @@ def set_callmonitor_item_values_initially(self): elif avm_data_type == 'is_call_incoming': item(False, self._plugin_instance.get_fullname()) - for item in self._items_outgoing: - avm_data_type = self._items_outgoing[item][0] - - if avm_data_type == 'last_caller_outgoing': + elif avm_data_type == 'last_caller_outgoing': for element in _calllist: if element['Type'] in ['3', '4']: if 'Name' in element: @@ -3753,10 +3677,7 @@ def set_callmonitor_item_values_initially(self): elif avm_data_type == 'is_call_outgoing': item(False, self._plugin_instance.get_fullname()) - for item in self._items: - avm_data_type = self._items[item][0] - - if avm_data_type == 'call_event': + elif avm_data_type == 'call_event': item('disconnect', self._plugin_instance.get_fullname()) elif avm_data_type == 'call_direction': @@ -3768,9 +3689,7 @@ def set_callmonitor_item_values_initially(self): item('outgoing', self._plugin_instance.get_fullname()) break - for item in self._duration_item_in: - avm_data_type = self._duration_item_in[item][0] - if avm_data_type == 'call_duration_incoming': + elif avm_data_type == 'call_duration_incoming': for element in _calllist: if element['Type'] in ['1', '2']: duration = element['Duration'] @@ -3778,9 +3697,7 @@ def set_callmonitor_item_values_initially(self): item(duration, self._plugin_instance.get_fullname()) break - for item in self._duration_item_out: - avm_data_type = self._duration_item_out[item][0] - if avm_data_type == 'call_duration_outgoing': + elif avm_data_type == 'call_duration_outgoing': for element in _calllist: if element['Type'] in ['3', '4']: duration = element['Duration'] @@ -3788,38 +3705,45 @@ def set_callmonitor_item_values_initially(self): item(duration, self._plugin_instance.get_fullname()) break - @property def item_list(self): - return list(self._items.keys()) + return list(self.items.keys()) - @property - def trigger_item_list(self): - return list(self._trigger_items.keys()) + def item_list_gen(self) -> list: + return self._get_item_list({'monitor_item_type': 'generic'}) - @property - def item_incoming_list(self): - return list(self._items_incoming.keys()) + def item_list_incoming(self) -> list: + return self._get_item_list({'monitor_item_type': 'incoming'}) - @property - def item_outgoing_list(self): - return list(self._items_outgoing.keys()) + def item_list_outgoing(self) -> list: + return self._get_item_list({'monitor_item_type': 'outgoing'}) - @property - def item_all_list(self): - return self.item_list + self.trigger_item_list + self.item_incoming_list + self.item_outgoing_list + def item_list_trigger(self) -> list: + return self._get_item_list({'monitor_item_type': 'trigger'}) - @property - def item_all_dict(self): - return {**self._items, **self._trigger_items, **self._items_incoming, **self._items_outgoing} + def duration_item_in(self): + item_list = self._get_item_list({'monitor_item_type': 'duration_in'}) + if item_list: + return item_list[0] + + def duration_item_out(self): + item_list = self._get_item_list({'monitor_item_type': 'duration_out'}) + if item_list: + return item_list[0] + + def _get_item_list(self, sub_dict: dict) -> list: + item_list = [] + for item in self.items: + if sub_dict.items() <= self.items[item].items(): + item_list.append(item) + return item_list - @property def item_count_total(self): """ Returns number of added items (all items of MonitoringService service) :return: number of items hold by the MonitoringService """ - return len(self._items) + len(self._trigger_items) + len(self._items_incoming) + len(self._items_outgoing) + len(self._duration_item_in) + len(self._duration_item_out) + return len(self.items) def _listen(self, recv_buffer: int = 4096): """ @@ -3877,10 +3801,9 @@ def _count_duration_incoming(self): """ self._call_active['incoming'] = True while self._call_active['incoming']: - if self._duration_item_in is not None: + if self.duration_item_in(): duration = time.time() - self._call_connect_timestamp - duration_item = list(self._duration_item_in.keys())[0] - duration_item(int(duration), self._plugin_instance.get_fullname()) + self.duration_item_in()(int(duration), self._plugin_instance.get_fullname()) time.sleep(1) def _count_duration_outgoing(self): @@ -3889,10 +3812,9 @@ def _count_duration_outgoing(self): """ self._call_active['outgoing'] = True while self._call_active['outgoing']: - if self._duration_item_out is not None: + if self.duration_item_out(): duration = time.time() - self._call_connect_timestamp - duration_item = list(self._duration_item_out.keys())[0] - duration_item(int(duration), self._plugin_instance.get_fullname()) + self.duration_item_out()(int(duration), self._plugin_instance.get_fullname()) time.sleep(1) def _parse_line(self, line: str): @@ -3932,8 +3854,8 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st self.logger.debug(f"_trigger: Event={event}, Call from={call_from}, Call to={call_to}, Time={dt}, CallID={callid}, Branch={branch}") # set generic item value - for item in self._items: - avm_data_type = self._items[item][0] + for item in self.item_list_gen(): + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'call_event': item(event.lower(), self._plugin_instance.get_fullname()) if avm_data_type == 'call_direction': @@ -3945,10 +3867,10 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st # handle incoming call if event == 'RING': # process "trigger items" - for trigger_item in self._trigger_items: - avm_data_type = self._trigger_items[trigger_item][0] - avm_incoming_allowed = self._trigger_items[trigger_item][1] - avm_target_number = self._trigger_items[trigger_item][2] + for trigger_item in self.item_list_trigger(): + avm_data_type = self.items[trigger_item]['avm_data_type'] + avm_incoming_allowed = self.items[trigger_item]['avm_incoming_allowed'] + avm_target_number = self.items[trigger_item]['avm_target_number'] trigger_item(0, self._plugin_instance.get_fullname()) if self.debug_log: self.logger.debug(f"{avm_data_type} {call_from} {call_to}") @@ -3961,21 +3883,20 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st self._call_incoming_cid = callid # reset duration for incoming calls - if self._duration_item_in is not None: - duration_item = list(self._duration_item_in.keys())[0] - duration_item(0, self._plugin_instance.get_fullname()) + if self.duration_item_in(): + self.duration_item_in()(0, self._plugin_instance.get_fullname()) # process items specific to incoming calls - for item in self._items_incoming: - avm_data_type = self._items_incoming[item][0] + for item in self.item_list_incoming(): + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'is_call_incoming': if self.debug_log: self.logger.debug("Setting is_call_incoming: True") item(True, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_caller_incoming': - if call_from != '' and call_from is not None: + if call_from: name = self._callback(call_from) - if name != '' and name is not None: + if name: item(name, self._plugin_instance.get_fullname()) else: item(call_from, self._plugin_instance.get_fullname()) @@ -4004,18 +3925,17 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st self._call_outgoing_cid = callid # reset duration for outgoing calls - if self._duration_item_out is not None: - duration_item = list(self._duration_item_out.keys())[0] - duration_item(0, self._plugin_instance.get_fullname()) + if self.duration_item_out(): + self.duration_item_out()(0, self._plugin_instance.get_fullname()) # process items specific to outgoing calls - for item in self._items_outgoing: - avm_data_type = self._items_outgoing[item][0] + for item in self.item_list_outgoing(): + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'is_call_outgoing': item(True, self._plugin_instance.get_fullname()) elif avm_data_type == 'last_caller_outgoing': name = self._callback(call_to) - if name != '' and name is not None: + if name: item(name, self._plugin_instance.get_fullname()) else: item(call_to, self._plugin_instance.get_fullname()) @@ -4032,23 +3952,24 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st elif event == 'CONNECT': # handle OUTGOING calls if callid == self._call_outgoing_cid: - if self._duration_item_out is not None: # start counter thread only if duration item set and call is outgoing + if self.duration_item_out() is not None: # start counter thread only if duration item set and call is outgoing self._stop_counter('outgoing') # stop potential running counter for parallel (older) outgoing call self._start_counter(dt, 'outgoing') - for item in self._items_outgoing: - avm_data_type = self._items_outgoing[item][0] + for item in self.item_list_outgoing(): + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'call_event_outgoing': item(event.lower(), self._plugin_instance.get_fullname()) + break # handle INCOMING calls elif callid == self._call_incoming_cid: - if self._duration_item_in is not None: # start counter thread only if duration item set and call is incoming + if self.duration_item_in() is not None: # start counter thread only if duration item set and call is incoming self._stop_counter('incoming') # stop potential running counter for parallel (older) incoming call if self.debug_log: self.logger.debug("Starting Counter for Call Time") self._start_counter(dt, 'incoming') - for item in self._items_incoming: - avm_data_type = self._items_incoming[item][0] + for item in self.item_list_incoming(): + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'call_event_incoming': if self.debug_log: self.logger.debug(f"Setting call_event_incoming: {event.lower()}") @@ -4058,20 +3979,20 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st elif event == 'DISCONNECT': # handle OUTGOING calls if callid == self._call_outgoing_cid: - for item in self._items_outgoing: - avm_data_type = self._items_outgoing[item][0] + for item in self.item_list_outgoing(): + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'call_event_outgoing': item(event.lower(), self._plugin_instance.get_fullname()) elif avm_data_type == 'is_call_outgoing': item(False, self._plugin_instance.get_fullname()) - if self._duration_item_out is not None: # stop counter threads + if self.duration_item_out() is not None: # stop counter threads self._stop_counter('outgoing') self._call_outgoing_cid = None # handle INCOMING calls elif callid == self._call_incoming_cid: - for item in self._items_incoming: - avm_data_type = self._items_incoming[item][0] + for item in self.item_list_incoming(): + avm_data_type = self.items[item]['avm_data_type'] if avm_data_type == 'call_event_incoming': if self.debug_log: self.logger.debug(f"Setting call_event_incoming: {event.lower()}") @@ -4080,111 +4001,76 @@ def _trigger(self, call_from: str, call_to: str, dt: str, callid: str, event: st if self.debug_log: self.logger.debug("Setting is_call_incoming: False") item(False, self._plugin_instance.get_fullname()) - if self._duration_item_in is not None: # stop counter threads + if self.duration_item_in() is not None: # stop counter threads if self.debug_log: self.logger.debug("Stopping Counter for Call Time") self._stop_counter('incoming') self._call_incoming_cid = None -""" -Definition of attribute value groups of avm_data_type -""" - - -def _get_attributes(sub_dict: dict) -> list: - attributes = [] - for avm_data_type in item_attributes.AVM_DATA_TYPES: - if sub_dict.items() <= item_attributes.AVM_DATA_TYPES[avm_data_type].items(): - attributes.append(avm_data_type) - return attributes - - -ALL_ATTRIBUTES_SUPPORTED_BY_REPEATER = _get_attributes({'supported_by_repeater': True}) - - -ALL_ATTRIBUTES_WRITEABLE = _get_attributes({'access': 'wo'}) + _get_attributes({'access': 'rw'}) - - -DEPRECATED_ATTRIBUTES = _get_attributes({'deprecated': True}) - - -AHA_ATTRIBUTES = _get_attributes({'interface': 'aha'}) - - -AHA_RO_ATTRIBUTES = _get_attributes({'interface': 'aha', 'access': 'ro'}) - - -AHA_WO_ATTRIBUTES = _get_attributes({'interface': 'aha', 'access': 'wo'}) - - -AHA_RW_ATTRIBUTES = _get_attributes({'interface': 'aha', 'access': 'rw'}) - - -TR064_ATTRIBUTES = _get_attributes({'interface': 'tr064'}) - - -AVM_RW_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'access': 'rw'}) - - -CALL_MONITOR_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'call_monitor'}) - - -CALL_MONITOR_ATTRIBUTES_TRIGGER = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'trigger'}) - - -CALL_MONITOR_ATTRIBUTES_GEN = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic'}) - - -CALL_MONITOR_ATTRIBUTES_IN = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in'}) - - -CALL_MONITOR_ATTRIBUTES_OUT = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out'}) - - -CALL_MONITOR_ATTRIBUTES_DURATION = _get_attributes({'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'duration'}) - - -WAN_CONNECTION_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection'}) - - -WAN_COMMON_INTERFACE_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface'}) - - -WAN_DSL_INTERFACE_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wan', 'sub_group': 'dsl_interface'}) - - -TAM_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'tam'}) - - -WLAN_CONFIG_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wlan_config'}) - - -WLAN_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'wlan'}) +# +# static XML helpers +# +def get_node_value(elem, node): + return elem.findtext(node) -FRITZ_DEVICE_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'fritz_device'}) +def get_node_value_as_int(elem, node) -> int: + value = get_node_value(elem, node) + try: + return int(value) + except TypeError: + return 0 -HOST_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'host'}) +def get_node_value_as_int_as_bool(elem, node) -> bool: + value = get_node_value_as_int(elem, node) + return bool(value) -HOST_ATTRIBUTES_GEN = _get_attributes({'interface': 'tr064', 'group': 'host', 'sub_group': None}) +def get_temp_from_node(elem, node) -> float: + value = get_node_value_as_int(elem, node) + return float(value) / 2 -HOST_ATTRIBUTES_CHILD = _get_attributes({'interface': 'tr064', 'group': 'host', 'sub_group': 'child'}) +def get_node_value_as_float_1000(elem, node) -> float: + value = get_node_value_as_int(elem, node) + return float(value) / 1000 -DEFLECTION_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'deflection'}) +def get_node_value_as_float_10(elem, node) -> float: + value = get_node_value_as_int(elem, node) + return float(value) / 10 -HOMEAUTO_RO_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'homeauto', 'access': 'ro'}) +def lxml_element_to_dict(node): + """Parse lxml Element to dictionary""" + result = {} -HOMEAUTO_RW_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'homeauto', 'access': 'rw'}) + for element in node.iterchildren(): + # Remove namespace prefix + key = element.tag.split('}')[1] if '}' in element.tag else element.tag + # Process element as tree element if the inner XML contains non-whitespace content + if element.text and element.text.strip(): + value = element.text + else: + value = lxml_element_to_dict(element) + if key in result: -HOMEAUTO_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'homeauto'}) + if type(result[key]) is list: + result[key].append(value) + else: + tempvalue = result[key].copy() + result[key] = [tempvalue, value] + else: + result[key] = value + return result -MYFRITZ_ATTRIBUTES = _get_attributes({'interface': 'tr064', 'group': 'myfritz'}) +def request_response_to_xml(request): + """ + Parse request response to element object + """ + return ET.fromstring(request.content) diff --git a/avm/item_attributes.py b/avm/item_attributes.py index c2e19d2d1..db2cdb477 100644 --- a/avm/item_attributes.py +++ b/avm/item_attributes.py @@ -19,152 +19,34 @@ # along with this plugin. If not, see . # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -AVM_DATA_TYPES = { - # 'avm_data_type': {'interface': 'tr064', 'group': '', 'sub_group': None, 'access': '', 'type': '', 'deprecated': False, 'supported_by_repeater': False, 'description': ''}, - 'uptime': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Laufzeit des Fritzdevice in Sekunden'}, - 'software_version': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Serialnummer des Fritzdevice'}, - 'hardware_version': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Software Version'}, - 'serial_number': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Hardware Version'}, - 'manufacturer': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Hersteller'}, - 'product_class': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Produktklasse'}, - 'manufacturer_oui': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Hersteller OUI'}, - 'model_name': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Modelname'}, - 'description': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Modelbeschreibung'}, - 'device_log': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Geräte Log'}, - 'security_port': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Security Port'}, - 'reboot': {'interface': 'tr064', 'group': 'fritz_device', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Startet das Gerät neu'}, - 'myfritz_status': {'interface': 'tr064', 'group': 'myfritz', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'MyFritz Status (an/aus)'}, - 'call_direction': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Richtung des letzten Anrufes'}, - 'call_event': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'generic', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status des letzten Anrufes'}, - 'monitor_trigger': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'trigger', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Monitortrigger'}, - 'is_call_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingehender Anruf erkannt'}, - 'last_caller_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Letzter Anrufer'}, - 'last_call_date_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitpunkt des letzten eingehenden Anrufs'}, - 'call_event_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status des letzten eingehenden Anrufs'}, - 'last_number_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Nummer des letzten eingehenden Anrufes'}, - 'last_called_number_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'in', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Angerufene Nummer des letzten eingehenden Anrufs'}, - 'is_call_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Ausgehender Anruf erkannt'}, - 'last_caller_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Letzter angerufener Kontakt'}, - 'last_call_date_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitpunkt des letzten ausgehenden Anrufs'}, - 'call_event_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status des letzten ausgehenden Anrufs'}, - 'last_number_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Nummer des letzten ausgehenden Anrufes'}, - 'last_called_number_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'out', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Letzte verwendete Telefonnummer für ausgehenden Anruf'}, - 'call_duration_incoming': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'duration', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Dauer des eingehenden Anrufs'}, - 'call_duration_outgoing': {'interface': 'tr064', 'group': 'call_monitor', 'sub_group': 'duration', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Dauer des ausgehenden Anrufs'}, - 'tam': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'TAM an/aus'}, - 'tam_name': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Name des TAM'}, - 'tam_new_message_number': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Anzahl der alten Nachrichten'}, - 'tam_old_message_number': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Anzahl der neuen Nachrichten'}, - 'tam_total_message_number': {'interface': 'tr064', 'group': 'tam', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gesamtanzahl der Nachrichten'}, - 'wan_connection_status': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindungsstatus'}, - 'wan_connection_error': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindungsfehler'}, - 'wan_is_connected': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung aktiv'}, - 'wan_uptime': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindungszeit'}, - 'wan_ip': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'connection', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN IP Adresse'}, - 'wan_upstream': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'dsl_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Upstream Datenmenge'}, - 'wan_downstream': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'dsl_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Downstream Datenmenge'}, - 'wan_total_packets_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt versendeter Pakete'}, - 'wan_total_packets_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt empfangener Pakete'}, - 'wan_current_packets_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuell versendeter Pakete'}, - 'wan_current_packets_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuell empfangener Pakete'}, - 'wan_total_bytes_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt versendeter Bytes'}, - 'wan_total_bytes_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl insgesamt empfangener Bytes'}, - 'wan_current_bytes_sent': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuelle Bitrate Senden'}, - 'wan_current_bytes_received': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Verbindung-Anzahl aktuelle Bitrate Empfangen'}, - 'wan_link': {'interface': 'tr064', 'group': 'wan', 'sub_group': 'common_interface', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'WAN Link'}, - 'wlanconfig': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WLAN An/Aus'}, - 'wlanconfig_ssid': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WLAN SSID'}, - 'wlan_guest_time_remaining': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbleibende Zeit, bis zum automatischen Abschalten des Gäste-WLAN'}, - 'wlan_associates': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Anzahl der verbundenen Geräte im jeweiligen WLAN'}, - 'wps_active': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Schaltet WPS für das entsprechende WlAN an / aus'}, - 'wps_status': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WPS Status des entsprechenden WlAN'}, - 'wps_mode': {'interface': 'tr064', 'group': 'wlan_config', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'WPS Modus des entsprechenden WlAN'}, - 'wlan_total_associates': {'interface': 'tr064', 'group': 'wlan', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Anzahl der verbundenen Geräte im WLAN'}, - 'hosts_count': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Anzahl der Hosts'}, - 'hosts_info': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Informationen über die Hosts'}, - 'mesh_topology': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Topologie des Mesh'}, - 'number_of_hosts': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, - 'hosts_url': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, - 'mesh_url': {'interface': 'tr064', 'group': 'host', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, - 'network_device': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus // Defines Network device via MAC-Adresse'}, - 'device_ip': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Geräte-IP (Muss Child von "network_device" sein'}, - 'device_connection_type': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungstyp (Muss Child von "network_device" sein'}, - 'device_hostname': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Gerätename (Muss Child von "network_device" sein'}, - 'connection_status': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, - 'is_host_active': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, - 'host_info': {'interface': 'tr064', 'group': 'host', 'sub_group': 'child', 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': True, 'description': 'Verbindungsstatus (Muss Child von "network_device" sein'}, - 'number_of_deflections': {'interface': 'tr064', 'group': 'deflection', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Anzahl der eingestellten Rufumleitungen'}, - 'deflections_details': {'interface': 'tr064', 'group': 'deflection', 'sub_group': None, 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Details zu allen Rufumleitung (als dict)'}, - 'deflection_details': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'dict', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Details zur Rufumleitung (als dict); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item'}, - 'deflection_enable': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Rufumleitung Status an/aus; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, - 'deflection_type': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Type der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, - 'deflection_number': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Telefonnummer, die umgeleitet wird; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, - 'deflection_to_number': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zielrufnummer der Umleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, - 'deflection_mode': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Modus der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, - 'deflection_outgoing': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Outgoing der Rufumleitung; Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, - 'deflection_phonebook_id': {'interface': 'tr064', 'group': 'deflection', 'sub_group': 'single', 'access': 'ro', 'type': 'str', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Phonebook_ID der Zielrufnummer (Only valid if Type==fromPB); Angabe der Rufumleitung mit Parameter "avm_deflection_index" im Item bzw Parent-Item'}, - 'aha_device': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': True, 'supported_by_repeater': False, 'description': 'Steckdose schalten; siehe "switch_state"'}, - 'hkr_device': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': True, 'supported_by_repeater': False, 'description': 'Status des HKR (OPEN; CLOSED; TEMP)'}, - 'set_temperature': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "target_temperature"'}, - 'temperature': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "current_temperature"'}, - 'set_temperature_reduced': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "temperature_reduced"'}, - 'set_temperature_comfort': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'num', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "temperature_comfort"'}, - 'firmware_version': {'interface': 'tr064', 'group': 'homeauto', 'sub_group': None, 'access': 'ro', 'type': 'str', 'deprecated': True, 'supported_by_repeater': False, 'description': 'siehe "fw_version"'}, - 'device_id': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Geräte -ID'}, - 'manufacturer': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hersteller'}, - 'product_name': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Produktname'}, - 'fw_version': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Firmware Version'}, - 'connected': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Verbindungsstatus'}, - 'device_name': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gerätename'}, - 'tx_busy': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Verbindung aktiv'}, - 'device_functions': {'interface': 'aha', 'group': 'device', 'sub_group': None, 'access': 'ro', 'type': 'list', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Im Gerät vorhandene Funktionen'}, - 'set_target_temperature': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Soll-Temperatur Setzen'}, - 'target_temperature': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'rw', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Soll-Temperatur (Status und Setzen)'}, - 'current_temperature': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Ist-Temperatur'}, - 'temperature_reduced': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingestellte reduzierte Temperatur'}, - 'temperature_comfort': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingestellte Komfort-Temperatur'}, - 'temperature_offset': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Eingestellter Temperatur-Offset'}, - 'set_window_open': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Window Open" Funktionen Setzen'}, - 'window_open': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Window Open" Funktion (Status und Setzen)'}, - 'windowopenactiveendtime': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitliches Ende der "Window Open" Funktion'}, - 'set_hkr_boost': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Boost" Funktion Setzen'}, - 'hkr_boost': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Boost" Funktion (Status aund Setzen)'}, - 'boost_active': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': True, 'supported_by_repeater': False, 'description': 'Status der "Boost" Funktion'}, - 'boostactiveendtime': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitliches Ende der "Boost" Funktion'}, - 'summer_active': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status der "Sommer" Funktion'}, - 'holiday_active': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Status der "Holiday" Funktion'}, - 'battery_low': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': '"Battery low" Status'}, - 'battery_level': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Batterie-Status in %'}, - 'lock': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Tastensperre über UI/API aktiv'}, - 'device_lock': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Tastensperre direkt am Gerät ein'}, - 'errorcode': {'interface': 'aha', 'group': 'hkr', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Fehlercodes die der HKR liefert'}, - 'set_simpleonoff': {'interface': 'aha', 'group': 'simpleonoff', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gerät/Aktor/Lampe an-/ausschalten'}, - 'simpleonoff': {'interface': 'aha', 'group': 'simpleonoff', 'sub_group': None, 'access': 'wr', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Gerät/Aktor/Lampe (Status und Setzen)'}, - 'set_level': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0 bis 255 Setzen'}, - 'level': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0 bis 255 (Setzen & Status)'}, - 'set_levelpercentage': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0% bis 100% Setzen'}, - 'levelpercentage': {'interface': 'aha', 'group': 'level', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Level/Niveau von 0% bis 100% (Setzen & Status)'}, - 'set_hue': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hue Setzen'}, - 'hue': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hue (Status und Setzen)'}, - 'set_saturation': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Saturation Setzen'}, - 'saturation': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Saturation (Status und Setzen)'}, - 'set_colortemperature': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wo', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Farbtemperatur Setzen'}, - 'colortemperature': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Farbtemperatur (Status und Setzen)'}, - 'unmapped_hue': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Hue (Status und Setzen)'}, - 'unmapped_saturation': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'wr', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Saturation (Status und Setzen)'}, - 'color_mode': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Aktueller Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode)'}, - 'supported_color_mode': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Unterstützer Farbmodus (1-HueSaturation-Mode; 4-Farbtemperatur-Mode)'}, - 'fullcolorsupport': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Lampe unterstützt setunmappedcolor'}, - 'mapped': {'interface': 'aha', 'group': 'color', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'von den Colordefaults abweichend zugeordneter HueSaturation-Wert gesetzt'}, - 'switch_state': {'interface': 'aha', 'group': 'switch', 'sub_group': None, 'access': 'rw', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Schaltzustand Steckdose (Status und Setzen)'}, - 'switch_mode': {'interface': 'aha', 'group': 'switch', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Zeitschaltung oder manuell schalten'}, - 'switch_toggle': {'interface': 'aha', 'group': 'switch', 'sub_group': None, 'access': 'wo', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Schaltzustand umschalten (toggle)'}, - 'power': {'interface': 'aha', 'group': 'powermeter', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Leistung in W (Aktualisierung alle 2 min)'}, - 'energy': {'interface': 'aha', 'group': 'powermeter', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'absoluter Verbrauch seit Inbetriebnahme in Wh'}, - 'voltage': {'interface': 'aha', 'group': 'powermeter', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Spannung in V (Aktualisierung alle 2 min)'}, - 'humidity': {'interface': 'aha', 'group': 'humidity', 'sub_group': None, 'access': 'ro', 'type': 'num ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'Relative Luftfeuchtigkeit in % (FD440)'}, - 'alert_state': {'interface': 'aha', 'group': 'alarm', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'letzter übermittelter Alarmzustand'}, - 'blind_mode': {'interface': 'aha', 'group': 'blind', 'sub_group': None, 'access': 'ro', 'type': 'str ', 'deprecated': False, 'supported_by_repeater': False, 'description': 'automatische Zeitschaltung oder manuell fahren'}, - 'endpositionsset': {'interface': 'aha', 'group': 'blind', 'sub_group': None, 'access': 'ro', 'type': 'bool', 'deprecated': False, 'supported_by_repeater': False, 'description': 'ist die Endlage für das Rollo konfiguriert'}, -} - +ALL_ATTRIBUTES_SUPPORTED_BY_REPEATER = ['uptime', 'software_version', 'hardware_version', 'serial_number', 'manufacturer', 'product_class', 'manufacturer_oui', 'model_name', 'description', 'device_log', 'security_port', 'reboot', 'wlanconfig', 'wlanconfig_ssid', 'wlan_guest_time_remaining', 'wlan_associates', 'wps_active', 'wps_status', 'wps_mode', 'wlan_total_associates', 'hosts_count', 'hosts_info', 'mesh_topology', 'number_of_hosts', 'hosts_url', 'mesh_url', 'network_device', 'device_ip', 'device_connection_type', 'device_hostname', 'connection_status', 'is_host_active', 'host_info'] +ALL_ATTRIBUTES_WRITEABLE = ['reboot', 'set_target_temperature', 'set_window_open', 'set_hkr_boost', 'set_simpleonoff', 'set_level', 'set_levelpercentage', 'set_hue', 'set_saturation', 'set_colortemperature', 'switch_toggle', 'tam', 'wlanconfig', 'wps_active', 'deflection_enable', 'aha_device', 'target_temperature', 'window_open', 'hkr_boost', 'switch_state'] +ALL_ATTRIBUTES_WRITEONLY = ['set_target_temperature', 'set_window_open', 'set_hkr_boost', 'set_simpleonoff', 'set_level', 'set_levelpercentage', 'set_hue', 'set_saturation', 'set_colortemperature', 'switch_toggle'] +DEPRECATED_ATTRIBUTES = ['aha_device', 'hkr_device', 'set_temperature', 'temperature', 'set_temperature_reduced', 'set_temperature_comfort', 'firmware_version', 'boost_active'] +AHA_ATTRIBUTES = ['device_id', 'manufacturer', 'product_name', 'fw_version', 'connected', 'device_name', 'tx_busy', 'device_functions', 'set_target_temperature', 'target_temperature', 'current_temperature', 'temperature_reduced', 'temperature_comfort', 'temperature_offset', 'set_window_open', 'window_open', 'windowopenactiveendtime', 'set_hkr_boost', 'hkr_boost', 'boost_active', 'boostactiveendtime', 'summer_active', 'holiday_active', 'battery_low', 'battery_level', 'lock', 'device_lock', 'errorcode', 'set_simpleonoff', 'simpleonoff', 'set_level', 'level', 'set_levelpercentage', 'levelpercentage', 'set_hue', 'hue', 'set_saturation', 'saturation', 'set_colortemperature', 'colortemperature', 'unmapped_hue', 'unmapped_saturation', 'color_mode', 'supported_color_mode', 'fullcolorsupport', 'mapped', 'switch_state', 'switch_mode', 'switch_toggle', 'power', 'energy', 'voltage', 'humidity', 'alert_state', 'blind_mode', 'endpositionsset'] +AHA_RO_ATTRIBUTES = ['device_id', 'manufacturer', 'product_name', 'fw_version', 'connected', 'device_name', 'tx_busy', 'device_functions', 'current_temperature', 'temperature_reduced', 'temperature_comfort', 'temperature_offset', 'windowopenactiveendtime', 'boost_active', 'boostactiveendtime', 'summer_active', 'holiday_active', 'battery_low', 'battery_level', 'lock', 'device_lock', 'errorcode', 'color_mode', 'supported_color_mode', 'fullcolorsupport', 'mapped', 'switch_mode', 'power', 'energy', 'voltage', 'humidity', 'alert_state', 'blind_mode', 'endpositionsset'] +AHA_WO_ATTRIBUTES = ['set_target_temperature', 'set_window_open', 'set_hkr_boost', 'set_simpleonoff', 'set_level', 'set_levelpercentage', 'set_hue', 'set_saturation', 'set_colortemperature', 'switch_toggle'] +AHA_RW_ATTRIBUTES = ['target_temperature', 'window_open', 'hkr_boost', 'switch_state'] +TR064_ATTRIBUTES = ['uptime', 'software_version', 'hardware_version', 'serial_number', 'manufacturer', 'product_class', 'manufacturer_oui', 'model_name', 'description', 'device_log', 'security_port', 'reboot', 'myfritz_status', 'call_direction', 'call_event', 'monitor_trigger', 'is_call_incoming', 'last_caller_incoming', 'last_call_date_incoming', 'call_event_incoming', 'last_number_incoming', 'last_called_number_incoming', 'is_call_outgoing', 'last_caller_outgoing', 'last_call_date_outgoing', 'call_event_outgoing', 'last_number_outgoing', 'last_called_number_outgoing', 'call_duration_incoming', 'call_duration_outgoing', 'tam', 'tam_name', 'tam_new_message_number', 'tam_old_message_number', 'tam_total_message_number', 'wan_connection_status', 'wan_connection_error', 'wan_is_connected', 'wan_uptime', 'wan_ip', 'wan_upstream', 'wan_downstream', 'wan_total_packets_sent', 'wan_total_packets_received', 'wan_current_packets_sent', 'wan_current_packets_received', 'wan_total_bytes_sent', 'wan_total_bytes_received', 'wan_current_bytes_sent', 'wan_current_bytes_received', 'wan_link', 'wlanconfig', 'wlanconfig_ssid', 'wlan_guest_time_remaining', 'wlan_associates', 'wps_active', 'wps_status', 'wps_mode', 'wlan_total_associates', 'hosts_count', 'hosts_info', 'mesh_topology', 'number_of_hosts', 'hosts_url', 'mesh_url', 'network_device', 'device_ip', 'device_connection_type', 'device_hostname', 'connection_status', 'is_host_active', 'host_info', 'number_of_deflections', 'deflections_details', 'deflection_details', 'deflection_enable', 'deflection_type', 'deflection_number', 'deflection_to_number', 'deflection_mode', 'deflection_outgoing', 'deflection_phonebook_id', 'aha_device', 'hkr_device', 'set_temperature', 'temperature', 'set_temperature_reduced', 'set_temperature_comfort', 'firmware_version'] +AVM_RW_ATTRIBUTES = ['tam', 'wlanconfig', 'wps_active', 'deflection_enable', 'aha_device'] +CALL_MONITOR_ATTRIBUTES = ['call_direction', 'call_event', 'monitor_trigger', 'is_call_incoming', 'last_caller_incoming', 'last_call_date_incoming', 'call_event_incoming', 'last_number_incoming', 'last_called_number_incoming', 'is_call_outgoing', 'last_caller_outgoing', 'last_call_date_outgoing', 'call_event_outgoing', 'last_number_outgoing', 'last_called_number_outgoing', 'call_duration_incoming', 'call_duration_outgoing'] +CALL_MONITOR_ATTRIBUTES_TRIGGER = ['monitor_trigger'] +CALL_MONITOR_ATTRIBUTES_GEN = ['call_direction', 'call_event'] +CALL_MONITOR_ATTRIBUTES_IN = ['is_call_incoming', 'last_caller_incoming', 'last_call_date_incoming', 'call_event_incoming', 'last_number_incoming', 'last_called_number_incoming'] +CALL_MONITOR_ATTRIBUTES_OUT = ['is_call_outgoing', 'last_caller_outgoing', 'last_call_date_outgoing', 'call_event_outgoing', 'last_number_outgoing', 'last_called_number_outgoing'] +CALL_MONITOR_ATTRIBUTES_DURATION = ['call_duration_incoming', 'call_duration_outgoing'] +WAN_CONNECTION_ATTRIBUTES = ['wan_connection_status', 'wan_connection_error', 'wan_is_connected', 'wan_uptime', 'wan_ip'] +WAN_COMMON_INTERFACE_ATTRIBUTES = ['wan_total_packets_sent', 'wan_total_packets_received', 'wan_current_packets_sent', 'wan_current_packets_received', 'wan_total_bytes_sent', 'wan_total_bytes_received', 'wan_current_bytes_sent', 'wan_current_bytes_received', 'wan_link'] +WAN_DSL_INTERFACE_ATTRIBUTES = ['wan_upstream', 'wan_downstream'] +TAM_ATTRIBUTES = ['tam', 'tam_name', 'tam_new_message_number', 'tam_old_message_number', 'tam_total_message_number'] +WLAN_CONFIG_ATTRIBUTES = ['wlanconfig', 'wlanconfig_ssid', 'wlan_guest_time_remaining', 'wlan_associates', 'wps_active', 'wps_status', 'wps_mode'] +WLAN_ATTRIBUTES = ['wlan_total_associates'] +FRITZ_DEVICE_ATTRIBUTES = ['uptime', 'software_version', 'hardware_version', 'serial_number', 'manufacturer', 'product_class', 'manufacturer_oui', 'model_name', 'description', 'device_log', 'security_port', 'reboot'] +HOST_ATTRIBUTES = ['host_info'] # host index needed +HOST_ATTRIBUTES_CHILD = ['network_device', 'device_ip', 'device_connection_type', 'device_hostname', 'connection_status', 'is_host_active'] # avm_mac needed +HOSTS_ATTRIBUTES = ['hosts_count', 'hosts_info', 'mesh_topology', 'number_of_hosts', 'hosts_url', 'mesh_url'] # no index needed +DEFLECTION_ATTRIBUTES = ['number_of_deflections', 'deflections_details', 'deflection_details', 'deflection_enable', 'deflection_type', 'deflection_number', 'deflection_to_number', 'deflection_mode', 'deflection_outgoing', 'deflection_phonebook_id'] +HOMEAUTO_RO_ATTRIBUTES = ['hkr_device', 'set_temperature', 'temperature', 'set_temperature_reduced', 'set_temperature_comfort', 'firmware_version'] +HOMEAUTO_RW_ATTRIBUTES = ['aha_device'] +HOMEAUTO_ATTRIBUTES = ['aha_device', 'hkr_device', 'set_temperature', 'temperature', 'set_temperature_reduced', 'set_temperature_comfort', 'firmware_version'] +MYFRITZ_ATTRIBUTES = ['myfritz_status'] \ No newline at end of file diff --git a/avm/webif/__init__.py b/avm/webif/__init__.py index 6e34cfa6b..e871af7ca 100644 --- a/avm/webif/__init__.py +++ b/avm/webif/__init__.py @@ -61,14 +61,14 @@ def index(self, reload=None, action=None): """ if self.plugin.fritz_device: - tr064_items = self.plugin.fritz_device.item_list + tr064_items = self.plugin.fritz_device.item_list() tr064_item_count = len(tr064_items) else: tr064_items = None tr064_item_count = None if self.plugin.fritz_home: - aha_items = self.plugin.fritz_home.item_list + aha_items = self.plugin.fritz_home.item_list() aha_item_count = len(aha_items) logentries = self.plugin.get_device_log_from_lua_separated() else: @@ -77,7 +77,7 @@ def index(self, reload=None, action=None): logentries = None if self.plugin.monitoring_service: - call_monitor_items = self.plugin.monitoring_service.item_all_list + call_monitor_items = self.plugin.monitoring_service.item_list() call_monitor_item_count = len(call_monitor_items) else: call_monitor_items = None @@ -116,7 +116,7 @@ def get_data_html(self, dataSet=None): data = dict() if self.plugin.monitoring_service: data['call_monitor'] = {} - for item in self.plugin.monitoring_service.item_all_list: + for item in self.plugin.monitoring_service.item_list(): data['call_monitor'][item.id()] = {} data['call_monitor'][item.id()]['value'] = item() data['call_monitor'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') @@ -124,7 +124,7 @@ def get_data_html(self, dataSet=None): if self.plugin.fritz_device: data['tr064_items'] = {} - for item in self.plugin.fritz_device.item_list: + for item in self.plugin.fritz_device.item_list(): data['tr064_items'][item.id()] = {} data['tr064_items'][item.id()]['value'] = item() data['tr064_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') @@ -132,7 +132,7 @@ def get_data_html(self, dataSet=None): if self.plugin.fritz_home: data['aha_items'] = {} - for item in self.plugin.fritz_home.item_list: + for item in self.plugin.fritz_home.item_list(): data['aha_items'][item.id()] = {} data['aha_items'][item.id()]['value'] = item() data['aha_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') diff --git a/avm/webif/templates/index.html b/avm/webif/templates/index.html index 146078187..bb6374e78 100644 --- a/avm/webif/templates/index.html +++ b/avm/webif/templates/index.html @@ -259,12 +259,13 @@ {% if tr064_items %} {% for item in tr064_items %} + {% set item_config = p.fritz_device.items[item] %} {{ item.id() }} {{ item.property.type }} - {{ p.fritz_device.item_dict[item][0] }} - {{ p.fritz_device.item_dict[item][2] }} + {{ item_config[0] }} + {{ item_config[2] }} {{ item.property.value }} {{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }} {{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }} @@ -295,12 +296,13 @@ {% if aha_items %} {% for item in aha_items %} + {% set item_config = p.fritz_home.items[item] %} {{ item.id() }} {{ item.property.type }} - {{ p.fritz_home.item_dict[item][0] }} - {{ p.fritz_home.item_dict[item][2] }} + {{ item_config[0] }} + {{ item_config[2] }} {{ item.property.value }} {{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }} {{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }} @@ -327,13 +329,13 @@ {% if p.fritz_home %} - {% for device in p.fritz_home.get_devices() %} + {% set devices = p.fritz_home.get_devices() %} + {% for device in devices %} {{ device.ain }} - {{ p.fritz_home._devices[device.ain].__dict__ }} - + {{ p.fritz_home.get_device_by_ain(device.ain).__dict__ }} {% endfor %} {% endif %} @@ -465,29 +467,33 @@ - {{ "fritz_device._items" }} - {{ p.fritz_device._items }} + {{ "fritz_device.items" }} + {{ p.fritz_device.items }} - {{ "fritz_device._item_blacklist" }} - {{ p.fritz_device._item_blacklist }} + {{ "fritz_device.get_tr064_items_blacklisted" }} + {{ p.fritz_device.get_tr064_items_blacklisted() }} + {% if p._fritz_home %} - {{ "fritz_home._items" }} - {{ p.fritz_home._items }} + {{ "fritz_home.items" }} + {{ p.fritz_home.items }} {{ "self._data_cache" }} {{ p.fritz_device._data_cache }} - - {{ "monitoring_service._items_incoming" }} - {{ p.monitoring_service._items_incoming }} - - - {{ "monitoring_service._items_outgoing" }} - {{ p.monitoring_service._items_outgoing }} - + {% endif %} + {% if p._call_monitor %} + + {{ "monitoring_service.item_list_incoming()" }} + {{ p.monitoring_service.item_list_incoming() }} + + + {{ "monitoring_service.item_list_outgoing()" }} + {{ p.monitoring_service.item_list_outgoing() }} + + {% endif %} VF{!F;rc3qRO#cu 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 z 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

ZxObDUfJ*Hnon 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

DW`B?b8Q=N~2YO`i=?~`9tHrgtedO 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. +# +######################################################################### + +import datetime +import time +import os +import json + +from lib.item import Items +from lib.model.smartplugin import SmartPluginWebIf + + +# ------------------------------------------ +# Webinterface of the plugin +# ------------------------------------------ + +import cherrypy +import csv +from jinja2 import Environment, FileSystemLoader + + +class WebInterface(SmartPluginWebIf): + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + self.logger = plugin.logger + self.webif_dir = webif_dir + self.plugin = plugin + self.items = Items.get_instance() + + self.tplenv = self.init_template_environment() + + + @cherrypy.expose + def index(self, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + tmpl = self.tplenv.get_template('index.html') + pagelength = self.plugin.get_parameter_value('webif_pagelength') + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) + return tmpl.render(p=self.plugin, + webif_pagelength=pagelength, + item_count=len(self.plugin.get_item_list('lirc', True)), + items=self.plugin.get_item_list('lirc', True)) + + + @cherrypy.expose + def get_data_html(self, dataSet=None): + """ + Return data to update the webpage + + For the standard update mechanism of the web interface, the dataSet to return the data for is None + + :param dataSet: Dataset for which the data should be returned (standard: None) + :return: dict with the data needed to update the web page. + """ + if dataSet is None: + # get the new data + data = {'response': self.plugin._responseStr, 'items': {}} + for item in self.plugin.get_item_list('lirc', True): + data['items'].update({item.id(): {'last_update': item.property.last_update.strftime('%d.%m.%Y %H:%M:%S'), 'last_change': item.property.last_change.strftime('%d.%m.%Y %H:%M:%S')}}) + try: + self.logger.error(f"data for webif: {data}") + return json.dumps(data) + except Exception as e: + self.logger.error(f"get_data_html exception: {e}") + return {} + + @cherrypy.expose + def submit(self, item=None): + result = None + if item is not None: + item = self.plugin.items.return_item(item) + self.logger.debug(f"Sending remote signal for {item} via web interface") + result = self.plugin.update_item(item, caller=None, source='Web Interface', dest=None) + + if result is not None: + # JSON zurücksenden + cherrypy.response.headers['Content-Type'] = 'application/json' + self.logger.debug(f"Result for web interface: {result}") + return json.dumps(result).encode('utf-8') diff --git a/lirc/webif/static/img/readme.txt b/lirc/webif/static/img/readme.txt deleted file mode 100755 index 1a7c55eef..000000000 --- a/lirc/webif/static/img/readme.txt +++ /dev/null @@ -1,6 +0,0 @@ -This directory is for storing images that are used by the web interface. - -If you want to have your own logo on the top of the web interface, store it here and name it plugin_logo.. - -Extension can be png, svg or jpg - diff --git a/lirc/webif/templates/index.html b/lirc/webif/templates/index.html new file mode 100755 index 000000000..05db5474d --- /dev/null +++ b/lirc/webif/templates/index.html @@ -0,0 +1,165 @@ +{% extends "base_plugin.html" %} +{% set update_interval = 2000 %} +{% block pluginstyles %} + +{% endblock pluginstyles %} +{% block pluginscripts %} + + + +{% endblock pluginscripts %} +{% set logo_frame = false %} + +{% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} +{% set tabcount = 1 %} + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _('Host') }}{{ p._host }}{{ _('Port') }}{{ p._port }}
{{ _('Version') }}{{ p._lircd_version }}{{ _('Verbindung') }}{{ p._lirc_server_alive }}
{{ _('Autoreconnect') }}{{ p._autoreconnect }}{% if p._autoreconnect == true %}{{ _('Reconnect Details') }}{% endif %}{% if p._autoreconnect == true %}{{ _('Retries') }}: {{ p._connect_retries }}, + {{ _('Cycle') }}: {{ p._connect_cycle }}{% endif %}
{{ _('Letzte Antwort') }}{{ p._responseStr }}
+{% endblock headtable %} + +{% block bodytab1 %} + + + {% for item in items %} + + + + + + + + + + {% endfor %} + +
{{ item.property.path }} + {{ p.get_iattr_value(item.conf, 'lirc_remote') }}{{ p.get_iattr_value(item.conf, 'lirc_key') }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
+
+ +
+{% endblock bodytab1 %} From 3d4f2f70398ed40a6d2e9ca6e947bb1555c31702 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 24 Mar 2023 10:41:42 +0100 Subject: [PATCH 138/178] lirc plugin: fix previously introduced duplicate key in locale.yaml --- lirc/locale.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/lirc/locale.yaml b/lirc/locale.yaml index 91d45311c..b3aed7dc8 100755 --- a/lirc/locale.yaml +++ b/lirc/locale.yaml @@ -8,7 +8,6 @@ plugin_translations: 'Autoreconnect': {'de': '=', 'en': '='} 'Reconnect Details': {'de': '=', 'en': '='} 'Retries': {'de': '=', 'en': '='} - 'Cycle': {'de': '=', 'en': '='} 'Letzte Antwort': {'de': '=', 'en': 'Last Response'} 'Senden': {'de': '=', 'en': 'Send'} 'Item': {'de': '=', 'en': '='} From cc7c9571c6cd88e4a80400715d4802232c946a05 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 24 Mar 2023 10:44:55 +0100 Subject: [PATCH 139/178] lirc plugin: remove debug/error log entry --- lirc/webif/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lirc/webif/__init__.py b/lirc/webif/__init__.py index 4088b981a..17676604b 100755 --- a/lirc/webif/__init__.py +++ b/lirc/webif/__init__.py @@ -95,7 +95,6 @@ def get_data_html(self, dataSet=None): for item in self.plugin.get_item_list('lirc', True): data['items'].update({item.id(): {'last_update': item.property.last_update.strftime('%d.%m.%Y %H:%M:%S'), 'last_change': item.property.last_change.strftime('%d.%m.%Y %H:%M:%S')}}) try: - self.logger.error(f"data for webif: {data}") return json.dumps(data) except Exception as e: self.logger.error(f"get_data_html exception: {e}") From e7463d6f74ad5c10464cb7c0540c37fae0b59ac4 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sat, 25 Mar 2023 08:11:27 +0100 Subject: [PATCH 140/178] AVM Plugin: - Plugin Code cleanup --- avm/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index 33f958aaa..9cba79ccd 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -867,7 +867,7 @@ def _update_item_value(self, item, avm_data_type: str, index: str) -> bool: self.logger.debug(f"Value for item={item} is None.") return False elif isinstance(data, int) and data in self.ERROR_CODES: - self.logger.error(f"Error {data} '{self.ERROR_CODES.get(data, None)}' occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") + self.logger.warning(f"Error {data} '{self.ERROR_CODES.get(data, None)}' occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") return False else: item(data, self._plugin_instance.get_fullname()) @@ -1704,7 +1704,7 @@ class FritzHome: HOMEAUTO_ROUTE = '/webservices/homeautoswitch.lua' INTERNET_STATUS_ROUTE = '/internet/inetstat_monitor.lua?sid=' - def __init__(self, host, ssl, verify, user, password, _log_entry_count, plugin_instance): + def __init__(self, host, ssl, verify, user, password, log_entry_count, plugin_instance): """ Init the Class FritzHome """ @@ -1723,11 +1723,11 @@ def __init__(self, host, ssl, verify, user, password, _log_entry_count, plugin_i self._devices: Dict[str, FritzHome.FritzhomeDevice] = {} self._templates: Dict[str, FritzHome.FritzhomeTemplate] = {} self._logged_in = False - self.items = dict() - self._aha_devices = dict() self._session = requests.Session() + self.items = dict() self.connected = False - self._log_entry_count = _log_entry_count + self.last_request = None + self.log_entry_count = log_entry_count # Login self.login() @@ -2152,7 +2152,7 @@ def _get_listinfo_elements(self, entity_type): if plain is None: return - self.last_request = plain + self.last_request = to_str(plain) dom = ElementTree.fromstring(to_str(plain)) return dom.findall(entity_type) @@ -2636,8 +2636,8 @@ def get_device_log_from_lua(self): data = '' # cut data if needed - if self._log_entry_count: - data = data[:self._log_entry_count] + if self.log_entry_count: + data = data[:self.log_entry_count] # bring data to needed format newlog = [] @@ -2668,8 +2668,8 @@ def get_device_log_from_lua_separated(self): if isinstance(data, dict): data = data.get('mq_log') if data is not None: - if self._log_entry_count: - data = data[:self._log_entry_count] + if self.log_entry_count: + data = data[:self.log_entry_count] data_formated = [] for entry in data: From 1d4a79d3da4256ee33acaf4cab725f9d9adb6716 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sat, 25 Mar 2023 08:12:16 +0100 Subject: [PATCH 141/178] AVM Plugin: - WebIF Update --- avm/webif/__init__.py | 4 ++- avm/webif/templates/index.html | 62 ++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/avm/webif/__init__.py b/avm/webif/__init__.py index e871af7ca..2b0095786 100644 --- a/avm/webif/__init__.py +++ b/avm/webif/__init__.py @@ -7,7 +7,7 @@ # https://www.smarthomeNG.de # https://knx-user-forum.de/forum/supportforen/smarthome-py # -# Part of AVM2 Plugin +# Part of AVM Plugin # # SmartHomeNG is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -129,6 +129,7 @@ def get_data_html(self, dataSet=None): data['tr064_items'][item.id()]['value'] = item() data['tr064_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') data['tr064_items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + data['tr064_items_blacklistet'] = self.plugin.fritz_device.get_tr064_items_blacklisted() if self.plugin.fritz_home: data['aha_items'] = {} @@ -137,6 +138,7 @@ def get_data_html(self, dataSet=None): data['aha_items'][item.id()]['value'] = item() data['aha_items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') data['aha_items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + data['aha_last_request'] = self.plugin.fritz_home.last_request data['maintenance'] = True if self.plugin.log_level <= 20 else False diff --git a/avm/webif/templates/index.html b/avm/webif/templates/index.html index bb6374e78..34a009faf 100644 --- a/avm/webif/templates/index.html +++ b/avm/webif/templates/index.html @@ -1,7 +1,7 @@ {% extends "base_plugin.html" %} {% set logo_frame = false %} -{% if tr064_items %} - {% set update_interval = [(((10 * (tr064_items | length)) / 1000) | round | int) * 1000, 5000]|max %} +{% if tr064_item_count > 0 %} + {% set update_interval = [(((10 * (tr064_item_count)) / 1000) | round | int) * 1000, 5000]|max %} {% else %} {% set update_interval = 5 %} {% endif %} @@ -104,6 +104,11 @@ pageResize: resize } ); console.log("Init smarthome_devicetable for page length -1, pageResize: " + resize); + maintenance_table = $('#maintenance_table').DataTable( { + pageLength: -1, + pageResize: resize + } ); + console.log("Init maintenance_table for page length -1, pageResize: " + resize); } catch (e) { console.log("Datatable JS not loaded, showing standard table without reorder option " +e); @@ -131,6 +136,9 @@ shngInsertText (item+'_call_last_update', objResponse['call_monitor'][item]['last_update'], 'call_monitortable'); shngInsertText (item+'_call_last_change', objResponse['call_monitor'][item]['last_change'], 'call_monitortable'); } + shngInsertText ('tr064_blacklisted', objResponse['tr064_items_blacklistet'], 'maintenance_table', 5); + shngInsertText ('aha_last_request', objResponse['aha_last_request'], 'maintenance_table', 5); + } } @@ -458,56 +466,52 @@ {% endif %} {% if maintenance %} - +
- - + + + + + {% if p.fritz_device %} - + + - - + + + - {% if p._fritz_home %} + {% endif %} + {% if p.fritz_home %} - + + - - + + + {% endif %} - {% if p._call_monitor %} + {% if p.monitoring_service %} - + + - + + {% endif %} -
{{ 'Befehl' }}{{ 'Ergebnis' }}
{{ "fritz_device.items" }}{{ "01 TR064 Items @ Fritz_Device" }} {{ p.fritz_device.items }}
{{ "fritz_device.get_tr064_items_blacklisted" }}{{ p.fritz_device.get_tr064_items_blacklisted() }}{{ "02 TR064 Blacklisted Items @ Fritz_Device " }}{{ p.fritz_device.get_tr064_items_blacklisted() }}
{{ "fritz_home.items" }}{{ "03 AHA Items @ Fritz_Home" }} {{ p.fritz_home.items }}
{{ "self._data_cache" }}{{ p.fritz_device._data_cache }}{{ " 04 AHA LastRequest XML" }}{{ p.fritz_home.last_request | string }}
{{ "monitoring_service.item_list_incoming()" }}{{ " 05 CM Items @ CallMonitor Incoming" }} {{ p.monitoring_service.item_list_incoming() }}
{{ "monitoring_service.item_list_outgoing()" }}{{ " 06 CM Items @ CallMonitor Outgoing" }} {{ p.monitoring_service.item_list_outgoing() }}
{% endif %} From 0d79488e4ced0cda0d38411474183dc292fc5081 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sat, 25 Mar 2023 14:02:30 +0100 Subject: [PATCH 142/178] AVM Plugin: - Bugfix AHA Button - Bugfix is_fritzbox() --- avm/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index 9cba79ccd..d0f8f0b9a 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -182,7 +182,7 @@ def run(self): self.create_cyclic_scheduler(target='tr064', items=self.fritz_device.items, fct=self.fritz_device.cyclic_item_update, offset=2) self.fritz_device.cyclic_item_update(read_all=True) - if self._aha_http_interface and self.fritz_device is not None and self.fritz_device.is_fritzbox: + if self._aha_http_interface and self.fritz_device is not None and self.fritz_device.is_fritzbox(): # add scheduler for updating items self.create_cyclic_scheduler(target='aha', items=self.fritz_home.items, fct=self.fritz_home.cyclic_item_update, offset=4) self.fritz_home.cyclic_item_update(read_all=True) @@ -2943,7 +2943,7 @@ def __init__(self, node=None): def _update_from_node(self, node): - self.button_identifier = node.attrib["button_identifier"] + self.button_identifier = node.attrib["identifier"] self.button_id = node.attrib["id"] self.button_name = get_node_value(node, "name") From f054112462f54e02d5a7653c6ce9ea3fc51bf1b4 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sat, 25 Mar 2023 14:03:34 +0100 Subject: [PATCH 143/178] AVM Plugin: - WebIF: Show typ of host (Fritz!Box or Fritz!Repeater) --- avm/webif/templates/index.html | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/avm/webif/templates/index.html b/avm/webif/templates/index.html index 34a009faf..e7198a4e8 100644 --- a/avm/webif/templates/index.html +++ b/avm/webif/templates/index.html @@ -231,7 +231,17 @@ {{ p.get_parameter_value_for_display('password') }} - {{ _('Host') }} + + {{ _('Host') }} + {% if p.fritz_device.is_fritzbox() %} + {{ _(' is Fritz!Box') }} + {% elif p.fritz_device.is_repeater() %} + {{ _(' is Fritz!Repeater') }} + {% else %} + {{ _(' -') }} + {% endif %} + +
{{ p.fritz_device.host }} {{ _('Port') }} {{ p.fritz_device.port }} {% if p.fritz_device.ssl %}(HTTPS){% endif %} From 3095bbf73553e81dd8411598153c504fbdd3bb90 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sun, 26 Mar 2023 01:34:42 +0100 Subject: [PATCH 144/178] AVM Plugin: - Bugfix in _set_fritz_device --- avm/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index d0f8f0b9a..43fc86cf4 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -1036,7 +1036,6 @@ def _poll_data(self, client: str, device: str, service: str, action: str, in_arg """ # self.logger.debug(f"_get_update_data called with device={device}, service={service}, action={action}, in_argument={in_argument}, out_argument={out_argument}, in_argument_value={in_argument_value}, enforce_read={enforce_read}") - data = self data_args = [] cache_dict_key = f"{device}_{service}_{action}_{in_argument}_{in_argument_value}" @@ -1109,7 +1108,7 @@ def _set_fritz_device(self, avm_data_type: str, args=None, wlan_index=None): if service.lower().startswith('wlan'): if wlan_index is None: return - cmd_args = [('attr', 'client'), ('attr', device), ('attr', service), ('sub', 'wlan_index'), ('attr', action), ('arg', args)] + cmd_args = [('attr', 'client'), ('attr', device), ('attr', service), ('sub', wlan_index), ('attr', action), ('arg', args)] elif args is None: cmd_args = [('attr', 'client'), ('attr', device), ('attr', service), ('attr', action), ('arg', None)] else: From af0fa3ca64f480539a1d0048ce71ebd7920dbff9 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Sun, 26 Mar 2023 14:52:50 +0200 Subject: [PATCH 145/178] AVM Plugin: - Bugfix in tr064\config.py to mitigate 'empty namespace prefix must be passed as None, not the empty string' --- avm/__init__.py | 8 +++----- avm/tr064/config.py | 12 ++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index 43fc86cf4..52cb3dc44 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -144,7 +144,7 @@ def __init__(self, sh): try: self.fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, _call_monitor_incoming_filter, self) except Exception as e: - self.logger.warning(f"Error {e!r} establishing connection to Fritzdevice via TR064-Interface.") + self.logger.warning(f"Error '{e!r}' establishing connection to Fritzdevice via TR064-Interface.") self.fritz_device = None else: self.logger.debug("Connection to FritzDevice established.") @@ -153,7 +153,7 @@ def __init__(self, sh): try: self.fritz_home = FritzHome(_host, ssl, _verify, _username, _passwort, _log_entry_count, self) except Exception as e: - self.logger.warning(f"Error {e!r} establishing connection to Fritzdevice via AHA-HTTP-Interface.") + self.logger.warning(f"Error '{e!r}' establishing connection to Fritzdevice via AHA-HTTP-Interface.") self.fritz_home = None else: self.logger.debug("Connection to FritzDevice via AHA-HTTP-Interface established.") @@ -163,7 +163,7 @@ def __init__(self, sh): try: self.monitoring_service = Callmonitor(_host, 1012, self.fritz_device.get_contact_name_by_phone_number, _call_monitor_incoming_filter, self) except Exception as e: - self.logger.warning(f"Error {e!r} establishing connection to Fritzdevice CallMonitor.") + self.logger.warning(f"Error '{e!r}' establishing connection to Fritzdevice CallMonitor.") self.monitoring_service = None else: self.logger.debug("Connection to FritzDevice CallMonitor established.") @@ -2718,8 +2718,6 @@ def __init__(self, fritz=None, node=None): self._functionsbitmask = None self.device_functions = [] -# TODO: wenn fritz is none, ist dann überhaupt Funktionalität gegeben? ggf. abbrechen? -# --> Abbrechen ist glaube ich besser. if not fritz: raise RuntimeError(f'passed object fritz is type {type(fritz)}, not type FritzHome. Aborting.') else: diff --git a/avm/tr064/config.py b/avm/tr064/config.py index 76d59aeee..0f3733bbf 100644 --- a/avm/tr064/config.py +++ b/avm/tr064/config.py @@ -10,25 +10,25 @@ ] TR064_DEVICE_NAMESPACE = { - '': 'urn:dslforum-org:device-1-0' + None: 'urn:dslforum-org:device-1-0' } TR064_SERVICE_NAMESPACE = { - '': 'urn:dslforum-org:service-1-0' + None: 'urn:dslforum-org:service-1-0' } TR064_CONTROL_NAMESPACE = { - '': 'urn:dslforum-org:control-1-0' + None: 'urn:dslforum-org:control-1-0' } IGD_DEVICE_NAMESPACE = { - '': 'urn:schemas-upnp-org:device-1-0' + None: 'urn:schemas-upnp-org:device-1-0' } IGD_SERVICE_NAMESPACE = { - '': 'urn:schemas-upnp-org:service-1-0' + None: 'urn:schemas-upnp-org:service-1-0' } IGD_CONTROL_NAMESPACE = { - '': 'urn:schemas-upnp-org:service-1-0' + None: 'urn:schemas-upnp-org:service-1-0' } From 44f058cffe24f0e6eed7585f3b393ef2add957c4 Mon Sep 17 00:00:00 2001 From: msinn Date: Sun, 26 Mar 2023 17:47:35 +0200 Subject: [PATCH 146/178] piratewthr: Bugfix --- piratewthr/__init__.py | 4 ++-- piratewthr/plugin.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/piratewthr/__init__.py b/piratewthr/__init__.py index 234eeb443..d8e860fdb 100755 --- a/piratewthr/__init__.py +++ b/piratewthr/__init__.py @@ -37,7 +37,7 @@ class PirateWeather(SmartPlugin): - PLUGIN_VERSION = "1.1.0" + PLUGIN_VERSION = "1.1.1" # https://api.pirateweather.net/forecast/[apikey]/[latitude],[longitude] _base_url = 'https://api.pirateweather.net/forecast/' @@ -230,7 +230,7 @@ def get_forecast(self): hour.update({'date': date_entry, 'weekday': day_entry, 'hour': hour_entry, 'icon_visu': self.map_icon(hour['icon'])}) if json_obj['daily'].get(date_key) is None: json_obj['daily'].update({date_key: {}}) - elif json_obj['daily'][date_key].get('hours') is None: + if json_obj['daily'][date_key].get('hours') is None: json_obj['daily'][date_key].update({'hours': {}}) json_obj['daily'][date_key]['hours'].update(OrderedDict({hour_entry: hour})) json_obj['hourly'].update(OrderedDict({'hour{}'.format(number): hour})) diff --git a/piratewthr/plugin.yaml b/piratewthr/plugin.yaml index 87dd91539..79507d926 100755 --- a/piratewthr/plugin.yaml +++ b/piratewthr/plugin.yaml @@ -11,7 +11,7 @@ plugin: keywords: weather sun wind rain precipitation #documentation: '' support: 'https://knx-user-forum.de/forum/supportforen/smarthome-py/1852685' - version: 1.1.0 # Plugin version + version: 1.1.1 # Plugin version sh_minversion: 1.9.3.4 # minimum shNG version to use this plugin #sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True # plugin supports multi instance From 1aa459ed955686c4cb4735dd28bac4e10e865fe1 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 27 Mar 2023 10:02:50 +0200 Subject: [PATCH 147/178] smartvisu plugin: fix some issues with datatables init, clearing and ordering options --- smartvisu/webif/templates/index.html | 103 +++++++++++++-------------- 1 file changed, 48 insertions(+), 55 deletions(-) diff --git a/smartvisu/webif/templates/index.html b/smartvisu/webif/templates/index.html index 38e6a6be3..4fb8c9413 100755 --- a/smartvisu/webif/templates/index.html +++ b/smartvisu/webif/templates/index.html @@ -5,7 +5,7 @@ {% set update_interval = 10000 %} {% set reload_button = false %} - +{% set initial_update = true %} {% block pluginstyles %} {% endblock pluginstyles %} {% set tab1title = "" ~ p.get_shortname() ~ " " ~ _('Clients') ~ " (" ~ clients|length ~ ")" %} {% block bodytab1 %} - - - -
-
- {% endblock %} @@ -258,70 +286,35 @@ --> {% set tab2title = "" ~ p.get_shortname() ~ " " ~ _('Items') ~ " (" ~ items|length ~ ")" %} {% block bodytab2 %} -
-
- - - - - - - - - + +
{{ _('Item') }}{{ _('Typ') }}{{ _('Wert') }}{{ _('Visu Zugriff') }}
+ {% for item in items %} - + {% endfor %} +
{{ item._path }} {{ item._type }} {{ item() }} {{ item.conf['visu_acl'] }}
-
-
-{% endblock bodytab2 %} - - {% set tab3title = "" ~ p.get_shortname() ~ " " ~ _('Logiken') ~ " (" ~ logics|length ~ ")" %} {% block bodytab3 %} -
-
- - - - - - - - - +
{{ _('Logik') }}{{ _('Status') }}{{ _('Visu Zugriff') }}
+ {% for logic in logics %} - + - {% endfor %} +
{{ logic.name }} {% if logic.enabled %}{{ _('aktiv') }}{% else %}{{ _('nicht aktiv') }}{% endif %} {% if logic.visu_access %}{{ _('erlaubt') }}{% else %}{{ _('nicht erlaubt') }}{% endif %}
{% endblock bodytab3 %} - - - -{% set tab4title = "" ~ p.get_shortname() ~ " " ~ _('Clients') ~ " (" ~ clients|length ~ ")" %} - -{% block bodytab4 %} - -{% endblock bodytab4 %} From d55ddf4236b7071ac0b8d9f5782f3b508c93addb Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 27 Mar 2023 10:33:51 +0200 Subject: [PATCH 148/178] AVM Plugin: - update requirements.txt to have lxml in 4.9.2 - update tr064/config.py to have receive correct error numbers from device - introduce new plugin-parameter 'tr064_item_blacklist' and update code accordingly - update item even if poll result is None - Update WebIF - code cleanup --- avm/__init__.py | 30 ++++++++++++++---------------- avm/plugin.yaml | 6 ++++++ avm/requirements.txt | 2 +- avm/tr064/config.py | 12 ++++++------ 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index 52cb3dc44..a563cad85 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -132,6 +132,7 @@ def __init__(self, sh): _passwort = self.get_parameter_value('password') _call_monitor_incoming_filter = self.get_parameter_value('call_monitor_incoming_filter') _log_entry_count = self.get_parameter_value('log_entry_count') + _use_tr064_backlist = self.get_parameter_value('tr064_item_blacklist') self._call_monitor = self.get_parameter_value('call_monitor') self._aha_http_interface = self.get_parameter_value('avm_home_automation') self._cycle = self.get_parameter_value('cycle') @@ -142,7 +143,7 @@ def __init__(self, sh): # init FritzDevice try: - self.fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, _call_monitor_incoming_filter, self) + self.fritz_device = FritzDevice(_host, _port, ssl, _verify, _username, _passwort, _call_monitor_incoming_filter, _use_tr064_backlist, self) except Exception as e: self.logger.warning(f"Error '{e!r}' establishing connection to Fritzdevice via TR064-Interface.") self.fritz_device = None @@ -491,7 +492,7 @@ class FritzDevice: ERROR_COUNT_TO_BE_BLACKLISTED = 2 - def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, plugin_instance=None): + def __init__(self, host, port, ssl, verify, username, password, call_monitor_incoming_filter, use_tr064_backlist, plugin_instance=None): """ Init class FritzDevice """ @@ -506,6 +507,7 @@ def __init__(self, host, port, ssl, verify, username, password, call_monitor_inc self.verify = verify self.username = username self.password = password + self.use_tr064_blacklist = use_tr064_backlist self._call_monitor_incoming_filter = call_monitor_incoming_filter self._data_cache = {} self._calllist_cache = [] @@ -843,7 +845,8 @@ def cyclic_item_update(self, read_all: bool = False): self.logger.info(f"Item={item.path()} with avm_data_type={avm_data_type} and index={index} will be updated") # get data and set item value - if not self._update_item_value(item, avm_data_type, index): + + if not self._update_item_value(item, avm_data_type, index) and self.use_tr064_blacklist: error_count += 1 self.logger.debug(f"{item.path()} caused error. New error_count: {error_count}. Item will be blacklisted after more than 2 errors.") item_config.update({'error_count': error_count}) @@ -863,10 +866,7 @@ def _update_item_value(self, item, avm_data_type: str, index: str) -> bool: self.logger.error(f"Error {e!r} occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") return False - if data is None: - self.logger.debug(f"Value for item={item} is None.") - return False - elif isinstance(data, int) and data in self.ERROR_CODES: + if isinstance(data, int) and data in self.ERROR_CODES: self.logger.warning(f"Error {data} '{self.ERROR_CODES.get(data, None)}' occurred during update of item={item} with avm_data_type={avm_data_type} and index={index}. Check item configuration regarding supported/activated function of AVM device. ") return False else: @@ -999,8 +999,6 @@ def _poll_fritz_device(self, avm_data_type: str, index=None, enforce_read: bool if avm_data_type.startswith('wan_current'): client = 'client_igd' - # self.logger.debug(f"_poll_fritz_device: {avm_data_type=}, {index=}, {enforce_read=} with {client=}") - # check if avm_data_type is linked and gather data if avm_data_type in link: device, service, action, in_arg, out_arg = link[avm_data_type] @@ -1064,11 +1062,8 @@ def _poll_data(self, client: str, device: str, service: str, action: str, in_arg data = self._data_cache.get(cache_dict_key) # return data - if data is None: - self.logger.info(f"No data for {cache_dict_key!r} received.") - return - elif isinstance(data, int) and 99 < data < 1000: - self.logger.info(f"Response was ErrorCode: {data} '{self.ERROR_CODES.get(data, None)}' for self.{client}.{device}.{service}.{action}()") + if isinstance(data, int) and 99 < data < 1000: + self.logger.info(f"Response was ErrorCode: {data} '{self.ERROR_CODES.get(data, 'unknown')}' for self.{client}.{device}.{service}.{action}()") return data elif out_argument: try: @@ -2662,9 +2657,12 @@ def get_device_log_from_lua_separated(self): url = self._get_prefixed_host() + self.LOG_SEPARATE_ROUTE params = {"sid": self._sid} - data = self._request(url, params, result='json') + try: + data = self._request(url, params, result='json') + except Exception: + return - if isinstance(data, dict): + if data and isinstance(data, dict): data = data.get('mq_log') if data is not None: if self.log_entry_count: diff --git a/avm/plugin.yaml b/avm/plugin.yaml index 2966fcbcf..cf3460070 100644 --- a/avm/plugin.yaml +++ b/avm/plugin.yaml @@ -90,6 +90,12 @@ parameters: description: de: '(optional) Anzahl der Log-Messages, die verarbeitet/bereitgestellt werden. 0 = alle' en: '(optional) Amount of Log-Messages, witch will be displayed. 0 = all' + tr064_item_blacklist: + type: bool + default: False + description: + de: '(optional) Wenn aktiv, werden TR064 Items, deren Abfrageergebnis 2x zu einen Fehler geführt hat, werden blacklisted und anschließend nicht mehr abgefragt.' + en: '(optional) If active, TR064 Items for which data polling resulted in errors, will be blacklisted and excluded from update cycle' item_attributes: # Definition of item attributes defined by this plugin diff --git a/avm/requirements.txt b/avm/requirements.txt index 6b9849bad..ad2e259bc 100644 --- a/avm/requirements.txt +++ b/avm/requirements.txt @@ -1,2 +1,2 @@ requests -lxml \ No newline at end of file +lxml==4.9.2 \ No newline at end of file diff --git a/avm/tr064/config.py b/avm/tr064/config.py index 0f3733bbf..76d59aeee 100644 --- a/avm/tr064/config.py +++ b/avm/tr064/config.py @@ -10,25 +10,25 @@ ] TR064_DEVICE_NAMESPACE = { - None: 'urn:dslforum-org:device-1-0' + '': 'urn:dslforum-org:device-1-0' } TR064_SERVICE_NAMESPACE = { - None: 'urn:dslforum-org:service-1-0' + '': 'urn:dslforum-org:service-1-0' } TR064_CONTROL_NAMESPACE = { - None: 'urn:dslforum-org:control-1-0' + '': 'urn:dslforum-org:control-1-0' } IGD_DEVICE_NAMESPACE = { - None: 'urn:schemas-upnp-org:device-1-0' + '': 'urn:schemas-upnp-org:device-1-0' } IGD_SERVICE_NAMESPACE = { - None: 'urn:schemas-upnp-org:service-1-0' + '': 'urn:schemas-upnp-org:service-1-0' } IGD_CONTROL_NAMESPACE = { - None: 'urn:schemas-upnp-org:service-1-0' + '': 'urn:schemas-upnp-org:service-1-0' } From f0146a2466fb075cea338ab237b6db3c417d8200 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 27 Mar 2023 10:41:13 +0200 Subject: [PATCH 149/178] AVM Plugin: - Update WebIF --- avm/webif/templates/index.html | 391 ++++++++++++++++----------------- 1 file changed, 189 insertions(+), 202 deletions(-) diff --git a/avm/webif/templates/index.html b/avm/webif/templates/index.html index e7198a4e8..c71bd30f2 100644 --- a/avm/webif/templates/index.html +++ b/avm/webif/templates/index.html @@ -260,186 +260,174 @@ {% block bodytab1 %} -
- - - - - - - - - - - - - - - {% if tr064_items %} - {% for item in tr064_items %} - {% set item_config = p.fritz_device.items[item] %} - - - - - - - - - - - {% endfor %} - {% endif %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Cycle') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item.id() }}{{ item.property.type }}{{ item_config[0] }}{{ item_config[2] }}{{ item.property.value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
+ + + + + + + + + + + + + + + {% if tr064_items %} + {% for item in tr064_items %} + {% set item_config = p.fritz_device.items[item] %} + + + + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Cycle') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item.id() }}{{ item.property.type }}{{ item_config[0] }}{{ item_config[2] }}{{ item.property.value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{% endblock %} {% block bodytab2 %} -
- - - - - - - - - - - - - - - {% if aha_items %} - {% for item in aha_items %} - {% set item_config = p.fritz_home.items[item] %} - - - - - - - - - - - {% endfor %} - {% endif %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Cycle') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item.id() }}{{ item.property.type }}{{ item_config[0] }}{{ item_config[2] }}{{ item.property.value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
+ + + + + + + + + + + + + + + {% if aha_items %} + {% for item in aha_items %} + {% set item_config = p.fritz_home.items[item] %} + + + + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Cycle') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item.id() }}{{ item.property.type }}{{ item_config[0] }}{{ item_config[2] }}{{ item.property.value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{% endblock %} {% block bodytab3 %} -
- - - - - - - - - - - - {% if p.fritz_home %} - {% set devices = p.fritz_home.get_devices() %} - {% for device in devices %} - - - - - - {% endfor %} - {% endif %} - -
{{ 'Device AIN' }}{{ '' }}{{ 'Device Details (dict)' }}
{{ device.ain }} - {{ p.fritz_home.get_device_by_ain(device.ain).__dict__ }}
-
+ + + + + + + + + + + + {% if p.fritz_home %} + {% set devices = p.fritz_home.get_devices() %} + {% for device in devices %} + + + + + + {% endfor %} + {% endif %} + +
{{ 'Device AIN' }}{{ '' }}{{ 'Device Details (dict)' }}
{{ device.ain }} + {{ p.fritz_home.get_device_by_ain(device.ain).__dict__ }}
{% endblock %} {% block bodytab4 %} -
- - - - - - - - - - - - - - {% if call_monitor_items %} - {% for item in call_monitor_items %} - {% set item_id = item.id() %} - {% if p.get_instance_name() %} - {% set instance_key = "avm_data_type@"+p.get_instance_name() %} - {% else %} - {% set instance_key = "avm_data_type" %} - {% endif %} - - - - - - - - - - {% endfor %} - {% endif %} - -
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key]}}{{ item.property.value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
+ + + + + + + + + + + + + + {% if call_monitor_items %} + {% for item in call_monitor_items %} + {% set item_id = item.id() %} + {% if p.get_instance_name() %} + {% set instance_key = "avm_data_type@"+p.get_instance_name() %} + {% else %} + {% set instance_key = "avm_data_type" %} + {% endif %} + + + + + + + + + + {% endfor %} + {% endif %} + +
{{ _('Pfad') }}{{ _('Typ') }}{{ _('AVM Datentyp') }}{{ _('Wert') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item.conf[instance_key]}}{{ item.property.value }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{% endblock %} {% block bodytab5 %} -
- - - - - - - - - - - - {% if logentries %} - {% for logentry in logentries%} - - - - - - - - {% endfor %} - {% endif %} - -
{{ 'Datum/Uhrzeit' }}{{ 'Meldung' }}{{ 'Typ' }}{{ 'Kategorie' }}
{{ logentry[0] }}{{ logentry[1] }} - - {{ logentry[2] }} - - {{ _('cat_'+logentry[3]|string) }}
- -
+ + + + + + + + + + + + {% if logentries %} + {% for logentry in logentries%} + + + + + + + + {% endfor %} + {% endif %} + +
{{ 'Datum/Uhrzeit' }}{{ 'Meldung' }}{{ 'Typ' }}{{ 'Kategorie' }}
{{ logentry[0] }}{{ logentry[1] }} + + {{ logentry[2] }} + + {{ _('cat_'+logentry[3]|string) }}
{% endblock %} {% block bodytab6 %} -
{% if not maintenance %} {% for function, dict in p.metadata.plugin_functions.items() %}
@@ -476,54 +464,53 @@ {% endif %} {% if maintenance %} - - - - - - - - +
+ + + + + + + - - {% if p.fritz_device %} - - - - - - - - - - - {% endif %} - {% if p.fritz_home %} - - - - - - - - - - - {% endif %} - {% if p.monitoring_service %} + + {% if p.fritz_device %} + + + + + - - + + + {% endif %} + {% if p.fritz_home %} - - + + - {% endif %} - -
{{ "01 TR064 Items @ Fritz_Device" }}{{ p.fritz_device.items }}
{{ "02 TR064 Blacklisted Items @ Fritz_Device " }}{{ p.fritz_device.get_tr064_items_blacklisted() }}
{{ "03 AHA Items @ Fritz_Home" }}{{ p.fritz_home.items }}
{{ " 04 AHA LastRequest XML" }}{{ p.fritz_home.last_request | string }}
{{ "01 TR064 Items @ Fritz_Device" }}{{ p.fritz_device.items }}
{{ " 05 CM Items @ CallMonitor Incoming" }}{{ p.monitoring_service.item_list_incoming() }}{{ "02 TR064 Blacklisted Items @ Fritz_Device " }}{{ p.fritz_device.get_tr064_items_blacklisted() }}
{{ " 06 CM Items @ CallMonitor Outgoing" }}{{ p.monitoring_service.item_list_outgoing() }}{{ "03 AHA Items @ Fritz_Home" }}{{ p.fritz_home.items }}
+ + + {{ " 04 AHA LastRequest XML" }} + {{ p.fritz_home.last_request | string }} + + {% endif %} + {% if p.monitoring_service %} + + + {{ " 05 CM Items @ CallMonitor Incoming" }} + {{ p.monitoring_service.item_list_incoming() }} + + + + {{ " 06 CM Items @ CallMonitor Outgoing" }} + {{ p.monitoring_service.item_list_outgoing() }} + + {% endif %} + + {% endif %} -
{% endblock %} From a49600122510540f5a31143acbd2dcdbb28a2b98 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 27 Mar 2023 10:56:26 +0200 Subject: [PATCH 150/178] AVM Plugin: - correct typo in plugin.yaml --- avm/plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avm/plugin.yaml b/avm/plugin.yaml index cf3460070..00737b2e9 100644 --- a/avm/plugin.yaml +++ b/avm/plugin.yaml @@ -94,7 +94,7 @@ parameters: type: bool default: False description: - de: '(optional) Wenn aktiv, werden TR064 Items, deren Abfrageergebnis 2x zu einen Fehler geführt hat, werden blacklisted und anschließend nicht mehr abgefragt.' + de: '(optional) Wenn aktiv, werden TR064 Items, deren Abfrageergebnis 2x zu einen Fehler geführt hat, blacklisted und anschließend nicht mehr abgefragt.' en: '(optional) If active, TR064 Items for which data polling resulted in errors, will be blacklisted and excluded from update cycle' item_attributes: From c903b71f82dd0b335e131dbc70c9f8ed20197a40 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Mon, 27 Mar 2023 11:42:33 +0200 Subject: [PATCH 151/178] AVM Plugin: - update get_device_log_from_lua_separated - update get_device_log_from_lua --- avm/__init__.py | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index a563cad85..04f0dd7f5 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -2625,25 +2625,28 @@ def get_device_log_from_lua(self): # get data try: - data = self._request(url, params, result='json')['mq_log'] - except KeyError: - data = '' - - # cut data if needed - if self.log_entry_count: - data = data[:self.log_entry_count] - - # bring data to needed format - newlog = [] - for text, typ, cat in data: - l_date = text[:8] - l_time = text[9:17] - l_text = text[18:] - l_cat = int(cat) - l_type = int(typ) - l_ts = int(datetime.datetime.timestamp(datetime.datetime.strptime(text[:17], '%d.%m.%y %H:%M:%S'))) - newlog.append([l_text, l_type, l_cat, l_ts, l_date, l_time]) - return newlog + data = self._request(url, params, result='json') + except JSONDecodeError: + return + + if isinstance(data, dict): + data = data.get('mq_log') + if data and isinstance(data, list): + # cut data if needed + if self.log_entry_count: + data = data[:self.log_entry_count] + + # bring data to needed format + newlog = [] + for text, typ, cat in data: + l_date = text[:8] + l_time = text[9:17] + l_text = text[18:] + l_cat = int(cat) + l_type = int(typ) + l_ts = int(datetime.datetime.timestamp(datetime.datetime.strptime(text[:17], '%d.%m.%y %H:%M:%S'))) + newlog.append([l_text, l_type, l_cat, l_ts, l_date, l_time]) + return newlog def get_device_log_from_lua_separated(self): """ @@ -2659,15 +2662,16 @@ def get_device_log_from_lua_separated(self): try: data = self._request(url, params, result='json') - except Exception: + except JSONDecodeError: return - if data and isinstance(data, dict): + if isinstance(data, dict): data = data.get('mq_log') - if data is not None: + if data and isinstance(data, list): if self.log_entry_count: data = data[:self.log_entry_count] + # bring data to needed format data_formated = [] for entry in data: dt = datetime.datetime.strptime(f"{entry[0]} {entry[1]}", '%d.%m.%y %H:%M:%S').strftime('%d.%m.%Y %H:%M:%S') From 524752b46778d7432bec20daeb1fc0b7b091b7a0 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 27 Mar 2023 17:50:36 +0200 Subject: [PATCH 152/178] ical plugin: if no dtend is found, this is logged as an info instead of warning. --- ical/__init__.py | 4 ++-- ical/plugin.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ical/__init__.py b/ical/__init__.py index 21f41dc5b..6dd1b11ac 100755 --- a/ical/__init__.py +++ b/ical/__init__.py @@ -33,7 +33,7 @@ class iCal(SmartPlugin): - PLUGIN_VERSION = "1.6.1" + PLUGIN_VERSION = "1.6.2" ALLOW_MULTIINSTANCE = False DAYS = ("MO", "TU", "WE", "TH", "FR", "SA", "SU") FREQ = ("YEARLY", "MONTHLY", "WEEKLY", "DAILY", "HOURLY", "MINUTELY", "SECONDLY") @@ -290,7 +290,7 @@ def _parse_ical(self, ical, ics, prio): self.logger.warning("problem parsing {0} no DTSTART for UID: {1}".format(ics, event['UID'])) continue if 'DTEND' not in event: - self.logger.warning("Warning in parsing {0} no DTEND for UID: {1}. Setting DTEND from DTSTART".format(ics, event['UID'])) + self.logger.info("Warning in parsing {0} no DTEND for UID: {1}. Setting DTEND from DTSTART".format(ics, event['UID'])) # Set end to start time: event['DTEND'] = event['DTSTART'] continue diff --git a/ical/plugin.yaml b/ical/plugin.yaml index 44ac58dfe..ceb552767 100755 --- a/ical/plugin.yaml +++ b/ical/plugin.yaml @@ -13,14 +13,14 @@ plugin: You can use offline files and online feeds. ' - maintainer: cmalo (mknx) - tester: onkelandy + maintainer: onkelandy, cmalo (mknx) + tester: ? state: ready keywords: ical ics calendar # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1352089-support-thread-zum-ical-plugin - version: 1.6.1 # Plugin version + version: 1.6.2 # Plugin version sh_minversion: 1.9.0 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: false # plugin supports multi instance From 7da3cc7b92436eff74bce177d9cdb543cbcb0849 Mon Sep 17 00:00:00 2001 From: aschwith Date: Mon, 27 Mar 2023 18:09:29 +0200 Subject: [PATCH 153/178] sonos: fix major bug introduced with PR #699, causing the av_transport to stop working after some time. Upgraded to SoCo version 0.29.1. --- sonos/__init__.py | 433 ++++++++++++++++++++++------------- sonos/plugin.yaml | 37 ++- sonos/soco/__init__.py | 2 +- sonos/soco/events.py | 4 +- sonos/soco/events_asyncio.py | 1 + sonos/soco/events_twisted.py | 1 - sonos/soco/utils.py | 1 - 7 files changed, 310 insertions(+), 169 deletions(-) diff --git a/sonos/__init__.py b/sonos/__init__.py index 3b36f3a8e..deb39814b 100755 --- a/sonos/__init__.py +++ b/sonos/__init__.py @@ -194,27 +194,73 @@ def __init__(self, endpoint, service, logger, threadName): self._service = service self._endpoint = endpoint self._event = None - self._signal = None + self._signal = threading.Event() self.logger = logger self._threadName = threadName def subscribe(self): - self.logger.debug(f"start subscribe for endpoint {self._endpoint}") + self.logger.dbglow(f"start subscribe for endpoint {self._endpoint}") + if 'eventAvTransport' in self._threadName: + self.logger.dbghigh(f"subscribe(): endpoint av envent detected. Enabling debugging logs") + debug = 1 + else: + debug = 0 + + if debug: + self.logger.dbghigh(f"subscribe(): start for endpoint {self._endpoint}") + with self._lock: - self._signal = threading.Event() + if debug: + self.logger.dbghigh(f"subscribe(): clearing signal Event for endpoint {self._endpoint}") + self._signal.clear() + + # Check if signal was cleared correctly: + if self._signal.is_set(): + self.logger.error(f"subscribe(): Event could not be cleared correctly for service {self._service}") + else: + self.logger.dbghigh(f"subscribe(): Event cleared successfully. Thread can be started for service {self._service}") + try: - # self._event = self._service.subscribe(auto_renew=True) self._event = self._service.subscribe(auto_renew=False) + # No benefits of automatic renew could be observed. + # self._event = self._service.subscribe(auto_renew=True) + except Exception as e: - self.logger.warning(f"Exception in subscribe(): {e}") + self.logger.error(f"Exception in subscribe(): {e}") if self._event: - self._event.auto_renew_fail = renew_error_callback - self._thread = threading.Thread(target=self._endpoint, name=self._threadName, args=(self,)) - self._thread.setDaemon(True) - self._thread.start() + if debug: + self.logger.dbghigh(f"subscribe(): event valid, starting new thread for endpoint {self._endpoint}") + + try: + self._event.auto_renew_fail = renew_error_callback + self._thread = threading.Thread(target=self._endpoint, name=self._threadName, args=(self,)) + self._thread.setDaemon(True) + self._thread.start() + self.logger.debug(f"start subscribe finished successfully") + if not self._thread.is_alive(): + self.logger.error("Critical error in subscribe method: Thread could not be startet and is not alive.") + else: + if debug: + self.logger.dbghigh(f"Debug subscribe: Thread startet successfully for service {self._service}") + + except Exception as e: + self.logger.error(f"Exception in subscribe() at point b: {e}") + + else: + self.logger.error(f"subscribe(): Error in subscribe for endpoint {self._endpoint}: self._event not valid") + if debug: + self.logger.dbghigh(f"subscribe() {self._endpoint}: lock released. Self._service is {self._service}") def unsubscribe(self): - self.logger.debug(f"start unsubscribe for endpoint {self._endpoint}") + self.logger.dbglow(f"unsubscribe(): start for endpoint {self._endpoint}") + if 'eventAvTransport' in self._threadName: + self.logger.dbghigh(f"unsubscribe: endpoint av envent detected. Enabling debugging logs") + debug = 1 + else: + debug = 0 + if debug: + self.logger.dbghigh(f"unsubscribe(): start for endpoint {self._endpoint}") + with self._lock: if self._event: # try to unsubscribe first @@ -224,13 +270,42 @@ def unsubscribe(self): self.logger.warning(f"Exception in unsubscribe(): {e}") self._signal.set() if self._thread: - self.logger.debug("Preparing to terminate thread") + self.logger.dbglow("Preparing to terminate thread") + if debug: + self.logger.dbghigh(f"unsubscribe(): Preparing to terminate thread for endpoint {self._endpoint}") self._thread.join(2) + if debug: + self.logger.dbghigh(f"unsubscribe(): Thread joined for endpoint {self._endpoint}") + if not self._thread.is_alive(): - self.logger.debug("Thread killed") + self.logger.dbglow("Thread killed for enpoint {self._endpoint}") + if debug: + self.logger.dbghigh(f"Thread killed for endpoint {self._endpoint}") + else: - self.logger.warning("Thread is still alive") + self.logger.error("unsubscibe(): Error, thread is still alive") + self._thread = None self.logger.info(f"Event {self._endpoint} unsubscribed and thread terminated") + if debug: + self.logger.dbghigh(f"unsubscribe(): Event {self._endpoint} unsubscribed and thread terminated") + else: + if debug: + self.logger.warning(f"unsubscribe(): {self._endpoint}: self._event not valid") + if debug: + self.logger.dbghigh(f"unsubscribe(): {self._endpoint}: lock released") + + + @property + def eventSignalIsSet(self): + if self._signal: + return self._signal.is_set() + return False + + @property + def subscriptionThreadIsActive(self): + if self._thread: + return self._thread.is_alive() + return False @property def signal(self): @@ -415,6 +490,7 @@ def dispose(self): def subscribe_base_events(self): if not self._soco: + self.logger.error("Error in subscribe_base_events: self._soco not valid.") return self.logger.debug("Start subscribe base event fct") self.zone_subscription.unsubscribe() @@ -432,6 +508,11 @@ def subscribe_base_events(self): self.render_subscription.unsubscribe() self.render_subscription.subscribe() + # Important note: + # av event is not subscribed here because it has special handling in function zone group event. + pass + + def refresh_static_properties(self) -> None: """ This function is called by the plugins discover function. This is typically called every 180sec. @@ -500,7 +581,7 @@ def _rendering_control_event(self, sub_handler: SubscriptionHandler) -> None: self.night_mode = event.variables['night_mode'] if 'dialog_mode' in event.variables: self.dialog_mode = event.variables['dialog_mode'] - self.logger.debug(f"{self.uid}: event variables: {event.variables}") + self.logger.debug(f"rendering_control_event: {self.uid}: event variables: {event.variables}") sub_handler.event.events.task_done() del event except Empty: @@ -618,146 +699,161 @@ def _av_transport_event(self, sub_handler: SubscriptionHandler) -> None: AV event handling :param sub_handler: SubscriptionHandler for the av transport event """ - try: - self.logger.debug(f"_av_transport_event: {self.uid}: av transport event handler active.") - while not sub_handler.signal.wait(1): - try: - event = sub_handler.event.events.get(timeout=0.5) - self._av_transport_event = event + if sub_handler is None: + self.logger.error(f"_av_transport_event: SubscriptionHandler is None.") - # set streaming type - if self.soco.is_playing_line_in: + self.logger.dbghigh(f"_av_transport_event: {self.uid}: av transport event handler active.") + while not sub_handler.signal.wait(1): + self.logger.dbgmed(f"_av_transport_event: {self.uid}: start try") + + try: + event = sub_handler.event.events.get(timeout=0.5) + except Empty: + #self.logger.dbglow(f"av_transport_event: got empty exception, which is normal") + pass + except Exception as e: + self.logger.error(f"_av_tranport_event: Exception during events.get(): {e}") + else: + + self.logger.dbghigh(f"_av_transport_event: {self.uid}: received event") + + # set streaming type + try: + is_playing_line_in = self.soco.is_playing_line_in + is_playing_tv = self.soco.is_playing_tv + is_playing_radio = self.soco.is_playing_radio + except Exception as e: + self.logger.error(f"_av_tranport_event: Exception during soco.get functions: {e}") + else: + if is_playing_line_in: self.streamtype = "line_in" - elif self.soco.is_playing_tv: + elif is_playing_tv: self.streamtype = "tv" - elif self.soco.is_playing_radio: + elif is_playing_radio: self.streamtype = "radio" else: self.streamtype = "music" - if 'transport_state' in event.variables: - transport_state = event.variables['transport_state'] - if transport_state: - self.handle_transport_state(transport_state) - if 'current_crossfade_mode' in event.variables: - self.cross_fade = bool(event.variables['current_crossfade_mode']) - if 'sleep_timer_generation' in event.variables: - if int(event.variables['sleep_timer_generation']) > 0: - self.snooze = self.get_snooze() + if 'transport_state' in event.variables: + transport_state = event.variables['transport_state'] + if transport_state: + self.handle_transport_state(transport_state) + if 'current_crossfade_mode' in event.variables: + self.cross_fade = bool(event.variables['current_crossfade_mode']) + if 'sleep_timer_generation' in event.variables: + if int(event.variables['sleep_timer_generation']) > 0: + self.snooze = self.get_snooze() + else: + self.snooze = 0 + if 'current_play_mode' in event.variables: + self.play_mode = event.variables['current_play_mode'] + if 'current_track_uri' in event.variables: + track_uri = event.variables['current_track_uri'] + if re.match(r'^x-rincon:RINCON_', track_uri) is not None: + # slave call, set uri to the coordinator track uri + if self._check_property(): + self.track_uri = sonos_speaker[self.coordinator].track_uri else: - self.snooze = 0 - if 'current_play_mode' in event.variables: - self.play_mode = event.variables['current_play_mode'] - if 'current_track_uri' in event.variables: - track_uri = event.variables['current_track_uri'] - if re.match(r'^x-rincon:RINCON_', track_uri) is not None: - # slave call, set uri to the coordinator track uri - if self._check_property(): - self.track_uri = sonos_speaker[self.coordinator].track_uri - else: - self.track_uri = '' + self.track_uri = '' + else: + self.track_uri = track_uri + # empty track is a trigger to reset some other props + if not self.track_uri: + self.track_artist = '' + self.track_album = '' + self.track_album_art = '' + self.track_title = '' + self.radio_show = '' + self.radio_station = '' + if 'current_track' in event.variables: + self.current_track = event.variables['current_track'] + else: + self.current_track = 0 + if 'number_of_tracks' in event.variables: + self.number_of_tracks = event.variables['number_of_tracks'] + else: + self.number_of_tracks = 0 + if 'current_track_duration' in event.variables: + self.current_track_duration = event.variables['current_track_duration'] + else: + self.current_track_duration = '' + + # don't do an else here: these value won't always be updated + if 'current_transport_actions' in event.variables: + self.current_transport_actions = event.variables['current_transport_actions'] + if 'current_valid_play_modes' in event.variables: + self.current_valid_play_modes = event.variables['current_valid_play_modes'] + if 'current_track_meta_data' in event.variables: + if event.variables['current_track_meta_data']: + # we have some different data structures, handle it + if isinstance(event.variables['current_track_meta_data'], DidlMusicTrack): + metadata = event.variables['current_track_meta_data'].__dict__ + elif isinstance(event.variables['current_track_meta_data'], DidlItem): + metadata = event.variables['current_track_meta_data'].__dict__ + else: + metadata = event.variables['current_track_meta_data'].metadata + if 'creator' in metadata: + self.track_artist = metadata['creator'] else: - self.track_uri = track_uri - # empty track is a trigger to reset some other props - if not self.track_uri: self.track_artist = '' - self.track_album = '' - self.track_album_art = '' + if 'title' in metadata: + # ignore x-sonos-api-stream: radio played, title seems wrong + if re.match(r"^x-sonosapi-stream:", metadata['title']) is None: + self.track_title = metadata['title'] + else: self.track_title = '' - self.radio_show = '' - self.radio_station = '' - - if 'current_track' in event.variables: - self.current_track = event.variables['current_track'] - else: - self.current_track = 0 - if 'number_of_tracks' in event.variables: - self.number_of_tracks = event.variables['number_of_tracks'] - else: - self.number_of_tracks = 0 - if 'current_track_duration' in event.variables: - self.current_track_duration = event.variables['current_track_duration'] - else: - self.current_track_duration = '' - - # don't do an else here: these value won't always be updated - if 'current_transport_actions' in event.variables: - self.current_transport_actions = event.variables['current_transport_actions'] - if 'current_valid_play_modes' in event.variables: - self.current_valid_play_modes = event.variables['current_valid_play_modes'] - - if 'current_track_meta_data' in event.variables: - if event.variables['current_track_meta_data']: - # we have some different data structures, handle it - if isinstance(event.variables['current_track_meta_data'], DidlMusicTrack): - metadata = event.variables['current_track_meta_data'].__dict__ - elif isinstance(event.variables['current_track_meta_data'], DidlItem): - metadata = event.variables['current_track_meta_data'].__dict__ - else: - metadata = event.variables['current_track_meta_data'].metadata - if 'creator' in metadata: - self.track_artist = metadata['creator'] - else: - self.track_artist = '' - if 'title' in metadata: - # ignore x-sonos-api-stream: radio played, title seems wrong - if re.match(r"^x-sonosapi-stream:", metadata['title']) is None: - self.track_title = metadata['title'] - else: - self.track_title = '' - if 'album' in metadata: - self.track_album = metadata['album'] - else: - self.track_album = '' - if 'album_art_uri' in metadata: - cover_url = metadata['album_art_uri'] - if not cover_url.startswith(('http:', 'https:')): - self.track_album_art = 'http://' + self.soco.ip_address + ':1400' + cover_url - else: - self.track_album_art = cover_url + if 'album' in metadata: + self.track_album = metadata['album'] + else: + self.track_album = '' + if 'album_art_uri' in metadata: + cover_url = metadata['album_art_uri'] + if not cover_url.startswith(('http:', 'https:')): + self.track_album_art = 'http://' + self.soco.ip_address + ':1400' + cover_url else: - self.track_album_art = '' + self.track_album_art = cover_url + else: + self.track_album_art = '' - if 'stream_content' in metadata: - stream_content = metadata['stream_content'].title() - if not stream_content.lower() in \ - ['zpstr_buffering', 'zpstr_connecting', 'x-sonosapi-stream']: - self.stream_content = stream_content - else: - self.stream_content = "" + if 'stream_content' in metadata: + stream_content = metadata['stream_content'].title() + if not stream_content.lower() in \ + ['zpstr_buffering', 'zpstr_connecting', 'x-sonosapi-stream']: + self.stream_content = stream_content else: - self.stream_content = '' - if 'radio_show' in metadata: - radio_show = metadata['radio_show'] - if radio_show: - radio_show = radio_show.split(',p', 1) - if len(radio_show) > 1: - self.radio_show = radio_show[0] - else: - self.radio_show = '' + self.stream_content = "" + else: + self.stream_content = '' + if 'radio_show' in metadata: + radio_show = metadata['radio_show'] + if radio_show: + radio_show = radio_show.split(',p', 1) + if len(radio_show) > 1: + self.radio_show = radio_show[0] else: self.radio_show = '' + else: + self.radio_show = '' - if self.streamtype == 'radio': - # we need the title from 'enqueued_transport_uri_meta_data' - if 'enqueued_transport_uri_meta_data' in event.variables: - radio_metadata = event.variables['enqueued_transport_uri_meta_data'] - if isinstance(radio_metadata, str): - radio_station = radio_metadata[radio_metadata.find('') + 10:radio_metadata.find('')] - elif hasattr(radio_metadata, 'title'): - radio_station = str(radio_metadata.title) - else: - radio_station = "" - self.radio_station = radio_station - else: - self.radio_station = '' + if self.streamtype == 'radio': + # we need the title from 'enqueued_transport_uri_meta_data' + if 'enqueued_transport_uri_meta_data' in event.variables: + radio_metadata = event.variables['enqueued_transport_uri_meta_data'] + if isinstance(radio_metadata, str): + radio_station = radio_metadata[radio_metadata.find('') + 10:radio_metadata.find('')] + elif hasattr(radio_metadata, 'title'): + radio_station = str(radio_metadata.title) + else: + radio_station = "" + self.radio_station = radio_station + else: + self.radio_station = '' + + sub_handler.event.events.task_done() + del event + self.logger.dbghigh(f"av_transport_event() for {self.uid}: task_done()") - sub_handler.event.events.task_done() - del event - except Empty: - pass - except Exception as ex: - self.logger.error(f"_av_transport_event: Error {ex} occurred.") + self.logger.dbghigh(f"av_transport_event(): {self.uid}: while loop terminated.") def _check_property(self): if not self.is_initialized: @@ -1320,7 +1416,7 @@ def zone_group_members(self, value: list) -> None: :param value: list with uids to set as group members """ if not isinstance(value, list): - self.logger.warning(f"zone_group_members: {self.uid}: value={value} for setter zone_group_members must be type of list.") + self.logger.error(f"zone_group_members: {self.uid}: value={value} for setter zone_group_members must be type of list.") return self._members = value @@ -1332,19 +1428,31 @@ def zone_group_members(self, value: list) -> None: if self.is_coordinator: for member in self._zone_group_members: - self.logger.debug(f"****zone_group_members: {member=}") + self.logger.dbglow(f"****zone_group_members: {member=}") if member is not self: try: - self.logger.debug(f"Unsubscribe av event for uid '{self.uid}' in fct zone_group_members") + self.logger.dbghigh(f"zone_group_members(): Unsubscribe av event for uid '{self.uid}' in fct zone_group_members") member.av_subscription.unsubscribe() except Exception as e: - self.logger.info(f"Unsubscribe av event for uid '{self.uid}' in fct zone_group_members caused error {e}") + self.logger.warning(f"Unsubscribe av event for uid '{self.uid}' in fct zone_group_members caused error {e}") pass else: - # Why are the member speakers un- and subscribed again? - self.logger.debug(f"Un/Subscribe av event for uid '{self.uid}' in fct zone_group_members") - member.av_subscription.unsubscribe() - member.av_subscription.subscribe() + # Register AV event for coordinator speakers: + #self.logger.dbglow(f"Un/Subscribe av event for uid '{self.uid}' in fct zone_group_members") + + active = member.av_subscription.subscriptionThreadIsActive + is_subscribed = member.av_subscription.is_subscribed + self.logger.dbghigh(f"zone_group_members(): Subscribe av event for uid '{self.uid}': Status before measure: AV Thread is {active}, subscription is {is_subscribed}, Eventflag: {member.av_subscription.eventSignalIsSet}") + + if active == False: + self.logger.dbghigh(f"zone_group_members: Subscribe av event for uid '{self.uid}' because thread is not active") + #member.av_subscription.unsubscribe() + # + # Workaround: + # member.av_subscription.update_endpoint(endpoint=self._av_transport_event) + member.av_subscription.subscribe() + self.logger.dbghigh(f"zone_group_members: Subscribe av event for uid '{self.uid}': Status after measure: AV thread is {member.av_subscription.subscriptionThreadIsActive}, subscription {member.av_subscription.is_subscribed}, Eventflag: {member.av_subscription.eventSignalIsSet}") + @property def streamtype(self) -> str: @@ -2189,6 +2297,7 @@ def play_sonos_radio(self, station_name: str, start: bool = True) -> None: self.logger.warning(msg) return False return True + def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: bool = True) -> tuple: """ @@ -2283,6 +2392,7 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b self.soco.play_uri(uri=uri, meta=metadata, title=the_station.title, start=start, force_radio=True) return True, "" + def play_sharelink(self, url: str, start: bool = True) -> None: """ Plays a sharelink from a given url @@ -2769,7 +2879,7 @@ class Sonos(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ - PLUGIN_VERSION = "1.8.1" + PLUGIN_VERSION = "1.8.2" def __init__(self, sh): """Initializes the plugin.""" @@ -2813,7 +2923,7 @@ def __init__(self, sh): # init TTS if self._tts: if self._init_tts(webservice_ip, webservice_port, local_webservice_path, local_webservice_path_snippet): - self.logger.info(f"TTS successful enabled") + self.logger.info(f"TTS successfully enabled") else: self.logger.info(f"TTS initialisation failed.") @@ -3118,16 +3228,27 @@ def _parse_speaker_ips(self, speaker_ips: list) -> list: # return unique items in list return utils.unique_list(self._speaker_ips) + + def debug_speaker(self, uid): + self.logger.warning(f"debug_speaker: Starting function for uid {uid}") + #sonos_speaker[uid].set_stop() + self.logger.warning(f"debug_speaker: check sonos_speaker[uid].av.subscription: {sonos_speaker[uid].av_subscription}") + # Event objekt is not callable: + #sonos_speaker[uid]._av_transport_event(sonos_speaker[uid].av_subscription) + self.logger.warning(f"debug_speaker: av_subscription: thread active {sonos_speaker[uid].av_subscription.subscriptionThreadIsActive}, eventSignal: {sonos_speaker[uid].av_subscription.eventSignalIsSet}") + + def get_soco_version(self) -> str: """ Get version of used Soco and return it """ - + try: src = io.open('plugins/sonos/soco/__init__.py', encoding='utf-8').read() metadata = dict(re.findall("__([a-z]+)__ = \"([^\"]+)\"", src)) - except Exception: - self.logger.warning(f"Version of used Soco module not available") + except Exception as e: + self.logger.warning(f"Version of used Soco module not available. Exception: {e}") + self.logger.warning(f"DEBUG get socoversion: Current dir: {os.getcwd()}") return '' else: soco_version = metadata['version'] @@ -3452,7 +3573,7 @@ def _discover(self, force: bool = False) -> None: uid = uid.lower() if self._is_speaker_up(uid, zone.ip_address): - self.logger.debug(f"Speaker found: {zone.ip_address}, {uid}") + self.logger.dbglow(f"Speaker found: {zone.ip_address}, {uid}") online_speaker_count = online_speaker_count + 1 if uid in sonos_speaker: try: @@ -3461,11 +3582,11 @@ def _discover(self, force: bool = False) -> None: self.logger.warning(f"Exception in discover -> sonos_speaker[uid].soco: {e}") else: if zone is not zone_compare: - self.logger.debug(f"zone is not in speaker list, yet. Adding and subscribing zone {zone}.") + self.logger.dbghigh(f"zone is not in speaker list, yet. Adding and subscribing zone {zone}.") sonos_speaker[uid].soco = zone sonos_speaker[uid].subscribe_base_events() else: - self.logger.debug(f"SoCo instance {zone} already initiated, skipping.") + self.logger.dbglow(f"SoCo instance {zone} already initiated, skipping.") # The following check subscriptions functions triggers an unsubscribe/subscribe. However, this causes # a massive memory leak increasing with every check_subscription call. # self.logger.debug("checking subscriptions") @@ -3481,15 +3602,15 @@ def _discover(self, force: bool = False) -> None: else: # Speaker is not online. Disposing... if sonos_speaker[uid].soco is not None: - self.logger.debug(f"Disposing offline speaker: {zone.ip_address}, {uid}") + self.logger.dbghigh(f"Disposing offline speaker: {zone.ip_address}, {uid}") sonos_speaker[uid].dispose() else: - self.logger.debug(f"Ignoring offline speaker: {zone.ip_address}, {uid}") + self.logger.info(f"Ignoring offline speaker: {zone.ip_address}, {uid}") sonos_speaker[uid].is_initialized = False if uid in sonos_speaker: - self.logger.debug(f"setting {zone.ip_address}, uid {uid} to handled speaker") + self.logger.dbglow(f"setting {zone.ip_address}, uid {uid} to handled speaker") handled_speaker[uid] = sonos_speaker[uid] else: self.logger.debug(f"ip {zone.ip_address}, uid {uid} is not in sonos_speaker") @@ -3497,7 +3618,7 @@ def _discover(self, force: bool = False) -> None: # dispose every speaker that was not found for uid in set(sonos_speaker.keys()) - set(handled_speaker.keys()): if sonos_speaker[uid].soco is not None: - self.logger.debug(f"Removing undiscovered speaker: {sonos_speaker[uid].ip_address}, {uid}") + self.logger.warning(f"Removing/disposing undiscovered speaker: {sonos_speaker[uid].ip_address}, {uid}") sonos_speaker[uid].dispose() # Extract number of online speakers: diff --git a/sonos/plugin.yaml b/sonos/plugin.yaml index 3845bbb4e..3f60f0aa7 100755 --- a/sonos/plugin.yaml +++ b/sonos/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://github.com/smarthomeNG/plugins/blob/master/sonos/README.md support: https://knx-user-forum.de/forum/supportforen/smarthome-py/25151-sonos-anbindung - version: 1.8.1 # Plugin version + version: 1.8.2 # Plugin version sh_minversion: 1.5.1 # minimum shNG version to use this plugin py_minversion: 3.8 # minimum Python version to use for this plugin multi_instance: False # plugin supports multi instance @@ -85,6 +85,27 @@ parameters: de: "(optional) Verlängert die Dauer von Snippet Audio Dateien um einen festen Offset in Sekunden." en: "(optional) Extend snippet duration by a fixed offset specified in seconds" + webif_pagelength: + type: int + default: 0 + valid_list: + - -1 + - 0 + - 25 + - 50 + - 100 + description: + de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden. + 0 = automatisch, -1 = alle' + en: 'Amount of items being listed in a web interface table per page by default. + 0 = automatic, -1 = all' + description_long: + de: 'Anzahl an Items, die standardmäßig in einer Web Interface Tabelle pro Seite angezeigt werden.\n + Bei 0 wird die Tabelle automatisch an die Höhe des Browserfensters angepasst.\n + Bei -1 werden alle Tabelleneinträge auf einer Seite angezeigt.' + en: 'Amount of items being listed in a web interface table per page by default.\n + 0 adjusts the table height automatically based on the height of the browser windows.\n + -1 shows all table entries on one page.' item_attributes: sonos_uid: @@ -356,12 +377,12 @@ item_structs: type: num sonos_recv: snooze sonos_send: snooze - + status_light: type: bool sonos_recv: status_light sonos_send: status_light - + join: type: str sonos_send: join @@ -539,17 +560,17 @@ item_structs: type: str sonos_send: play_tts enforce_updates: True - + tts_language: type: str initial_value: de sonos_attrib: tts_language - + tts_volume: type: num initial_value: -1 sonos_attrib: tts_volume - + tts_fade_in: type: bool sonos_attrib: tts_fade_in @@ -558,12 +579,12 @@ item_structs: type: str sonos_send: play_snippet enforce_updates: True - + snippet_volume: type: num initial_value: 25 sonos_attrib: snippet_volume - + snippet_fade_in: type: bool initial_value: True diff --git a/sonos/soco/__init__.py b/sonos/soco/__init__.py index 4c89d7f92..573076afc 100755 --- a/sonos/soco/__init__.py +++ b/sonos/soco/__init__.py @@ -17,7 +17,7 @@ __author__ = "The SoCo-Team " # Please increment the version number and add the suffix "-dev" after # a release, to make it possible to identify in-development code -__version__ = "0.29.0" +__version__ = "0.29.1" __website__ = "https://github.com/SoCo/SoCo" __license__ = "MIT License" diff --git a/sonos/soco/events.py b/sonos/soco/events.py index 16299ff98..6a48b08b6 100755 --- a/sonos/soco/events.py +++ b/sonos/soco/events.py @@ -366,6 +366,8 @@ def run(self): auto_renew_thread = AutoRenewThread( interval, self._auto_renew_thread_flag, self ) + debugName = 'SonosAutoRenewThread' + auto_renew_thread.setName(debugName) auto_renew_thread.start() def _auto_renew_cancel(self): @@ -410,7 +412,6 @@ def _request(self, method, url, headers, success, unconditional=None): # pylint: disable=inconsistent-return-statements def _wrap(self, method, strict, *args, **kwargs): - """This is a wrapper for `Subscription.subscribe`, `Subscription.renew` and `Subscription.unsubscribe` which: @@ -437,7 +438,6 @@ def _wrap(self, method, strict, *args, **kwargs): # A lock is used, because autorenewal occurs in # a thread with self._lock: - try: method(*args, **kwargs) diff --git a/sonos/soco/events_asyncio.py b/sonos/soco/events_asyncio.py index 73cf8c50c..a991a1b90 100755 --- a/sonos/soco/events_asyncio.py +++ b/sonos/soco/events_asyncio.py @@ -281,6 +281,7 @@ async def _async_start(self): async def async_stop(self): """Stop the listener.""" + self.is_running = False if self.site: await self.site.stop() self.site = None diff --git a/sonos/soco/events_twisted.py b/sonos/soco/events_twisted.py index 5271383e3..d5a5749ba 100755 --- a/sonos/soco/events_twisted.py +++ b/sonos/soco/events_twisted.py @@ -383,7 +383,6 @@ def on_success(response): # pylint: disable=missing-docstring return d def _wrap(self, method, strict, *args, **kwargs): - """This is a wrapper for `Subscription.subscribe`, `Subscription.renew` and `Subscription.unsubscribe` which: diff --git a/sonos/soco/utils.py b/sonos/soco/utils.py index 0ddfeffe9..309b682f3 100755 --- a/sonos/soco/utils.py +++ b/sonos/soco/utils.py @@ -154,7 +154,6 @@ def __init__( def __call__(self, deprecated_fn): @functools.wraps(deprecated_fn) def decorated(*args, **kwargs): - message = "Call to deprecated function {}.".format(deprecated_fn.__name__) if self.will_be_removed_in is not None: message += " Will be removed in version {}.".format( From a2207df11216c705f84e661165b6712ad50497e8 Mon Sep 17 00:00:00 2001 From: aschwith Date: Mon, 27 Mar 2023 18:11:23 +0200 Subject: [PATCH 154/178] neato: added additional debug log in case that a command cannot be executed. --- neato/robot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/neato/robot.py b/neato/robot.py index 5297ac43c..ed68aa0df 100755 --- a/neato/robot.py +++ b/neato/robot.py @@ -158,13 +158,13 @@ def robot_command(self, command, arg1 = None, arg2 = None): self.logger.warning(f"Command returned {str(responseJson['result'])}: Retry starting with non-persistent-map") return self.robot_command(command = 'start_non-persistent-map') else: - self.logger.error("Sending command failed. Result: {0}".format(str(responseJson['result']) )) + self.logger.error("Sending command {command} failed. Result: {0}".format(str(responseJson['result']) )) self.logger.error("Debug: send command response: {0}".format(start_cleaning_response.text)) else: if 'message' in responseJson: - self.logger.error("Sending command failed. Message: {0}".format(str(responseJson['message']))) + self.logger.error("Sending command {command} failed. Message: {0}".format(str(responseJson['message']))) if 'error' in responseJson: - self.logger.error("Sending command failed. Error: {0}".format(str(responseJson['error']))) + self.logger.error("Sending command {command} failed. Error: {0}".format(str(responseJson['error']))) # - NOT on Charge BASE return start_cleaning_response From b9be216fe4c48dda34425a1c04da7f742e5857fb Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 28 Mar 2023 22:06:07 +0200 Subject: [PATCH 155/178] AVM Plugin: - bugfix handle_updated_item and _set_fritz_device --- avm/__init__.py | 80 ++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index 04f0dd7f5..e9bb87fa5 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -601,37 +601,37 @@ def unregister_item(self, item): def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): """Updated Item will be processed and value communicated to AVM Device""" # get index - _index = self.items[item]['index'] + index = self.items[item]['index'] # to be set value to_be_set_value = item() # define command per avm_data_type - _dispatcher = {'wlanconfig': (self.set_wlan, ('set_wlan', f"NewEnable={int(to_be_set_value)}", _index)), - 'wps_active': (self.set_wps, ('set_wps', f"NewX_AVM_DE_WPSEnable={int(to_be_set_value)}", _index)), - 'tam': (self.set_tam, ('set_tam', f"NewIndex={_index}, NewEnable={int(to_be_set_value)}", None)), - 'deflection_enable': (self.set_deflection, ('set_deflection', f"NewDeflectionId={_index}, NewEnable={int(to_be_set_value)}", None)), + _dispatcher = {'wlanconfig': ('set_wlan', {'NewEnable': int(to_be_set_value)}, index), + 'wps_active': ('set_wps', {'NewX_AVM_DE_WPSEnable': int(to_be_set_value)}, index), + 'tam': ('set_tam', {'NewIndex': int(index), 'NewEnable': int(to_be_set_value)}, None), + 'deflection_enable': ('set_deflection', {'NewDeflectionId': int(index), 'NewEnable': int(to_be_set_value)}, None), } # do logging if self.debug_log: - self.logger.debug(f"Item {item.path()} with avm_data_type={avm_data_type} has changed for index {_index}; New value={to_be_set_value}") + self.logger.debug(f"Item {item.path()} with avm_data_type={avm_data_type} has changed for index {index}; New value={to_be_set_value}") # call setting method - cmd, args, index = _dispatcher[avm_data_type][1] - response = self._set_fritz_device(cmd, args, index) + cmd, args, wlan_index = _dispatcher[avm_data_type] + self._set_fritz_device(cmd, args, wlan_index) if self.debug_log: - self.logger.debug(f"Setting AVM Device with successful with response={response}.") + self.logger.debug(f"Setting AVM Device with successful.") # handle readafterwrite if readafterwrite: - self._read_after_write(item, avm_data_type, _index, readafterwrite, to_be_set_value) + self._read_after_write(item, avm_data_type, index, readafterwrite, to_be_set_value) def _read_after_write(self, item, avm_data_type, _index, delay, to_be_set_value): """read the new item value and compares with to_be_set_value, update item to confirm correct value""" # do logging if self.debug_log: - self.logger.warning(f"_readafterwrite called with: item={item.path()}, avm_data_type={avm_data_type}, index={_index}; delay={delay}, to_be_set_value={to_be_set_value}") + self.logger.debug(f"_readafterwrite called with: item={item.path()}, avm_data_type={avm_data_type}, index={_index}; delay={delay}, to_be_set_value={to_be_set_value}") # sleep time.sleep(delay) @@ -842,7 +842,7 @@ def cyclic_item_update(self, read_all: bool = False): self.logger.debug(f"Skipping item {item} with wan_current and no client_igd") continue - self.logger.info(f"Item={item.path()} with avm_data_type={avm_data_type} and index={index} will be updated") + self.logger.debug(f"Item={item.path()} with avm_data_type={avm_data_type} and index={index} will be updated") # get data and set item value @@ -1076,21 +1076,21 @@ def _poll_data(self, client: str, device: str, service: str, action: str, in_arg def _set_fritz_device(self, avm_data_type: str, args=None, wlan_index=None): """Set AVM Device based on avm_data_type and args""" if self.debug_log: - self.logger.debug(f"_set_fritz_device called: avm_data_type={avm_data_type}, args={args}, wlan_index={wlan_index}") + self.logger.debug(f"_set_fritz_device called: avm_data_type={avm_data_type}, args={args}, wlan_index{wlan_index}") link = { - 'set_call_origin': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialSetConfig'), - 'set_tam': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'SetEnable'), - 'set_aha_device': ('InternetGatewayDevice', 'X_AVM_DE_Homeauto', 'SetSwitch'), - 'set_deflection': ('InternetGatewayDevice', 'X_AVM_DE_OnTel', 'SetDeflectionEnable'), - 'set_wlan': ('LANDevice', 'WLANConfiguration', 'SetEnable'), - 'set_wps': ('LANDevice', 'WLANConfiguration', 'X_AVM_DE_SetWPSEnable'), - 'reboot': ('InternetGatewayDevice', 'DeviceConfig', 'Reboot'), - 'wol': ('LanDevice', 'Hosts', 'X_AVM_DE_GetAutoWakeOnLANByMACAddress'), - 'reconnect_ppp': ('WANConnectionDevice', 'WANPPPConnection', 'ForceTermination'), - 'reconnect_ipp': ('WANConnectionDevice', 'WANIPPConnection', 'ForceTermination'), - 'start_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialNumber'), - 'cancel_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialHangup'), + 'set_call_origin': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialSetConfig'), + 'set_tam': ('InternetGatewayDevice', 'X_AVM_DE_TAM', 'SetEnable'), + 'set_aha_device': ('InternetGatewayDevice', 'X_AVM_DE_Homeauto', 'SetSwitch'), + 'set_deflection': ('InternetGatewayDevice', 'X_AVM_DE_OnTel', 'SetDeflectionEnable'), + 'set_wlan': ('LANDevice', 'WLANConfiguration', 'SetEnable'), + 'set_wps': ('LANDevice', 'WLANConfiguration', 'X_AVM_DE_SetWPSEnable'), + 'reboot': ('InternetGatewayDevice', 'DeviceConfig', 'Reboot'), + 'wol': ('LanDevice', 'Hosts', 'X_AVM_DE_GetAutoWakeOnLANByMACAddress'), + 'reconnect_ppp': ('WANConnectionDevice', 'WANPPPConnection', 'ForceTermination'), + 'reconnect_ipp': ('WANConnectionDevice', 'WANIPPConnection', 'ForceTermination'), + 'start_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialNumber'), + 'cancel_call': ('InternetGatewayDevice', 'X_VoIP', 'X_AVM_DE_DialHangup'), } if avm_data_type not in link: @@ -1115,14 +1115,14 @@ def _set_fritz_device(self, avm_data_type: str, args=None, wlan_index=None): self.logger.debug(f"response={response} for cmd={cmd_args}.") # return response - if response is None: + if not response: self.logger.info(f"No response for cmd={cmd_args} received.") return elif isinstance(response, int) and 99 < response < 1000: self.logger.info(f"Response was ErrorCode: {response} '{self.ERROR_CODES.get(response, None)}' for cmd={cmd_args}") return - - return response + else: + return response def _clear_data_cache(self): """ @@ -1786,7 +1786,7 @@ def cyclic_item_update(self, read_all: bool = False): # self.logger.debug(f"Item={item.path()} is not due, yet.") continue - self.logger.info(f"Item={item.path()} with avm_data_type={avm_data_type} and ain={ain} will be updated") + self.logger.debug(f"Item={item.path()} with avm_data_type={avm_data_type} and ain={ain} will be updated") # Attributes that are write-only commands with no corresponding read commands are excluded from status updates via update black list: update_black_list = ALL_ATTRIBUTES_WRITEONLY @@ -1815,16 +1815,16 @@ def handle_updated_item(self, item, avm_data_type: str, readafterwrite: int): Updated Item will be processed and value communicated to AVM Device """ # define set method per avm_data_type - _dispatcher = {'window_open': (self.set_window_open, self.get_window_open), + _dispatcher = {'window_open': (self.set_window_open, self.get_window_open), 'target_temperature': (self.set_target_temperature, self.get_target_temperature), - 'hkr_boost': (self.set_boost, self.get_boost), - 'simpleonoff': (self.set_state, self.get_state), - 'level': (self.set_level, self.get_level), - 'levelpercentage': (self.set_level_percentage, self.get_level_percentage), - 'switch_state': (self.set_switch_state, self.get_switch_state), - 'switch_toggle': (self.set_switch_state_toggle, self.get_switch_state), - 'colortemperature': (self.set_color_temp, self.get_color_temp), - 'hue': (self.set_color_discrete, self.get_hue), + 'hkr_boost': (self.set_boost, self.get_boost), + 'simpleonoff': (self.set_state, self.get_state), + 'level': (self.set_level, self.get_level), + 'levelpercentage': (self.set_level_percentage, self.get_level_percentage), + 'switch_state': (self.set_switch_state, self.get_switch_state), + 'switch_toggle': (self.set_switch_state_toggle, self.get_switch_state), + 'colortemperature': (self.set_color_temp, self.get_color_temp), + 'hue': (self.set_color_discrete, self.get_hue), } # get AIN @@ -2122,7 +2122,7 @@ def update_devices(self): Updating AHA Devices respective dictionary """ - self.logger.info("Updating Devices ...") + self.logger.info("Updating AHA Devices ...") elements = self.get_device_elements() if elements is None: @@ -2130,7 +2130,7 @@ def update_devices(self): for element in elements: if element.attrib["identifier"] in self._devices.keys(): - self.logger.info("Updating already existing Device " + element.attrib["identifier"]) + self.logger.debug("Updating already existing Device " + element.attrib["identifier"]) self._devices[element.attrib["identifier"]].update_from_node(element) else: self.logger.info("Adding new Device " + element.attrib["identifier"]) From 4705b0c3db08457fc777ca85dd88091d17077ce8 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Tue, 28 Mar 2023 22:14:50 +0200 Subject: [PATCH 156/178] AVM Plugin: - fix get_phone_numbers_by_name --- avm/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index e9bb87fa5..a1717ba7e 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -1297,7 +1297,7 @@ def get_phone_numbers_by_name(self, name: str = '', phonebook_id: int = 0) -> di return {} phonebooks = request_response_to_xml(self._request(phonebook_url, self._timeout, self.verify)) - if phonebooks: + if phonebooks is not None: for phonebook in phonebooks.iter('phonebook'): for contact in phonebook.iter('contact'): for real_name in contact.findall('.//realName'): @@ -1307,7 +1307,8 @@ def get_phone_numbers_by_name(self, name: str = '', phonebook_id: int = 0) -> di if number.text: result_number_dict = dict() result_number_dict['number'] = number.text.strip() - result_number_dict['type'] = tel_type[number.attrib["type"]] + # result_number_dict['type'] = tel_type[number.attrib["type"]] + result_number_dict['type'] = number.attrib["type"] result_numbers[real_name.text].append(result_number_dict) return result_numbers else: From 8fa51e5fd24432c3138c34c0d7245b8501b7e177 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 29 Mar 2023 09:08:43 +0200 Subject: [PATCH 157/178] AVM Plugin: - catch Tr064 Client Error in _set_fritz_device --- avm/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/avm/__init__.py b/avm/__init__.py index a1717ba7e..1e4e2d1a3 100644 --- a/avm/__init__.py +++ b/avm/__init__.py @@ -1109,20 +1109,20 @@ def _set_fritz_device(self, avm_data_type: str, args=None, wlan_index=None): else: cmd_args = [('attr', 'client'), ('attr', device), ('attr', service), ('attr', action), ('arg', args)] - response = walk_nodes(self, cmd_args) + try: + response = walk_nodes(self, cmd_args) + except Exception as e: + self.logger.warning(f"Set FritzDevice with TR064 Client caused Error '{e}'") + return if self.debug_log: self.logger.debug(f"response={response} for cmd={cmd_args}.") # return response - if not response: - self.logger.info(f"No response for cmd={cmd_args} received.") - return - elif isinstance(response, int) and 99 < response < 1000: + if isinstance(response, int) and 99 < response < 1000: self.logger.info(f"Response was ErrorCode: {response} '{self.ERROR_CODES.get(response, None)}' for cmd={cmd_args}") return - else: - return response + return response def _clear_data_cache(self): """ From 922f47676113af1048602cba379928a64da3d25a Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 29 Mar 2023 09:38:10 +0200 Subject: [PATCH 158/178] =?UTF-8?q?DB=5FADDON=20Plugin=20=20-=20allow=20db?= =?UTF-8?q?=5Faddon=20item=20to=20be=20written=20in=20database=20itself=20?= =?UTF-8?q?=20-=20introduce=20new=20parameter=20to=20configure=20database-?= =?UTF-8?q?item=20explicitly=20=20-=20add=20methods=20to=20calculate=20tag?= =?UTF-8?q?esmitteltemperatur=20=20-=20change=20W=C3=A4rmesumme=20to=20use?= =?UTF-8?q?=20whole=20year=20=20-=20introduce=20Wachstumsgradtage=20=20-?= =?UTF-8?q?=20bugfixing=20=20-=20code=20clean=20up=20=20-=20update=20user?= =?UTF-8?q?=5Fdoc=20=20-=20update=20WebIF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db_addon/__init__.py | 2323 +++++++++++++-------------- db_addon/item_attributes_master.py | 194 +++ db_addon/plugin.yaml | 892 +++++----- db_addon/user_doc.rst | 66 +- db_addon/webif/__init__.py | 2 +- db_addon/webif/templates/index.html | 391 +++-- 6 files changed, 1995 insertions(+), 1873 deletions(-) create mode 100644 db_addon/item_attributes_master.py diff --git a/db_addon/__init__.py b/db_addon/__init__.py index 82d733f47..629331a4c 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -25,14 +25,6 @@ # ######################################################################### -from lib.model.smartplugin import SmartPlugin -from lib.item import Items -from lib.item.item import Item -from lib.shtime import Shtime -from lib.plugin import Plugins -from .webif import WebInterface -import lib.db - import sqlvalidator import datetime import time @@ -42,6 +34,14 @@ from typing import Union import threading +from lib.model.smartplugin import SmartPlugin +from lib.item import Items +from lib.item.item import Item +from lib.shtime import Shtime +from lib.plugin import Plugins +from .webif import WebInterface +import lib.db + DAY = 'day' WEEK = 'week' MONTH = 'month' @@ -53,7 +53,7 @@ class DatabaseAddOn(SmartPlugin): Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items """ - PLUGIN_VERSION = '1.0.0' + PLUGIN_VERSION = '1.1.0' def __init__(self, sh): """ @@ -68,12 +68,12 @@ def __init__(self, sh): self.items = Items.get_instance() self.plugins = Plugins.get_instance() - # define properties // cache dicts + # define cache dicts self.current_values = {} # Dict to hold min and max value of current day / week / month / year for items self.previous_values = {} # Dict to hold value of end of last day / week / month / year for items self.item_cache = {} # Dict to hold item_id, oldest_log_ts and oldest_entry for items - # define properties // database, database connection, working queue and status + # define variables for database, database connection, working queue and status self.item_queue = queue.Queue() # Queue containing all to be executed items self.work_item_queue_thread = None # Working Thread for queue self._db_plugin = None # object if database plugin @@ -86,20 +86,20 @@ def __init__(self, sh): self.alive = None # Is plugin alive? self.startup_finished = False # Startup of Plugin finished self.suspended = False # Is plugin activity suspended - self._active_queue_item = '-' # String holding item path of currently executed item - - # define properties // Debugs + self.active_queue_item: str = '-' # String holding item path of currently executed item + + # define debug logs self.parse_debug = False # Enable / Disable debug logging for method 'parse item' self.execute_debug = False # Enable / Disable debug logging for method 'execute items' self.sql_debug = False # Enable / Disable debug logging for sql stuff self.onchange_debug = False # Enable / Disable debug logging for method 'handle_onchange' self.prepare_debug = False # Enable / Disable debug logging for query preparation - - # define properties // default mysql settings + + # define default mysql settings self.default_connect_timeout = 60 self.default_net_read_timeout = 60 - - # define properties // plugin parameters + + # define variables from plugin parameters self.db_configname = self.get_parameter_value('database_plugin_config') self.startup_run_delay = self.get_parameter_value('startup_run_delay') self.ignore_0 = self.get_parameter_value('ignore_0') @@ -154,6 +154,9 @@ def run(self): self.logger.info(f"Set scheduler for calculating startup-items with delay of {self.startup_run_delay + 3}s to {dt}.") self.scheduler_add('startup', self.execute_startup_items, next=dt) + # update database_items in item config, where path was given + self._update_database_items() + # set plugin to alive self.alive = True @@ -189,40 +192,39 @@ def get_database_item() -> Item: Returns item from shNG config which is an item with database attribut valid for current db_addon item """ - _lookup_item = item + _lookup_item = item.return_parent() - for i in range(3): + for i in range(2): if self.has_iattr(_lookup_item.conf, self.item_attribute_search_str): + self.logger.debug(f"Attribut '{self.item_attribute_search_str}' has been found for item={item.path()} {i + 1} level above item.") return _lookup_item else: - self.logger.debug(f"Attribut '{self.item_attribute_search_str}' has not been found for item={item.path()} {i + 1} level above item.") _lookup_item = _lookup_item.return_parent() - def get_db_addon_item() -> bool: - """ - Returns item from shNG config which is item with db_addon attribut valid for database item - - """ + def has_db_addon_item() -> bool: + """Returns item from shNG config which is item with db_addon attribut valid for database item""" for child in item.return_children(): - if _check_db_addon_fct(child): + if check_db_addon_fct(child): return True for child_child in child.return_children(): - if _check_db_addon_fct(child_child): + if check_db_addon_fct(child_child): return True for child_child_child in child_child.return_children(): - if _check_db_addon_fct(child_child_child): + if check_db_addon_fct(child_child_child): return True return False - def _check_db_addon_fct(check_item) -> bool: + def check_db_addon_fct(check_item) -> bool: + """ + Check if item has db_addon_fct and is onchange + """ if self.has_iattr(check_item.conf, 'db_addon_fct'): - __db_addon_fct = self.get_iattr_value(check_item.conf, 'db_addon_fct').lower() - if onchange_attribute(__db_addon_fct): - self.logger.debug(f"db_addon item for database item {item.id()} found.") + if self.get_iattr_value(check_item.conf, 'db_addon_fct').lower() in ALL_ONCHANGE_ATTRIBUTES: + self.logger.debug(f"db_addon item for database item {item.path()} found.") return True return False @@ -230,153 +232,168 @@ def _check_db_addon_fct(check_item) -> bool: if self.has_iattr(item.conf, 'db_addon_fct'): if self.parse_debug: - self.logger.debug(f"parse item: {item.id()} due to 'db_addon_fct'") + self.logger.debug(f"parse item: {item.path()} due to 'db_addon_fct'") - # get attribute value - _db_addon_fct = self.get_iattr_value(item.conf, 'db_addon_fct').lower() + # get db_addon_fct attribute value + db_addon_fct = self.get_iattr_value(item.conf, 'db_addon_fct').lower() - # get attribute if item should be calculated at plugin startup - _db_addon_startup = self.get_iattr_value(item.conf, 'db_addon_startup') + # get attribute value if item should be calculated at plugin startup + db_addon_startup = bool(self.get_iattr_value(item.conf, 'db_addon_startup')) # get attribute if certain value should be ignored at db query if self.has_iattr(item.conf, 'database_ignore_value'): - _db_addon_ignore_value = self.get_iattr_value(item.conf, 'database_ignore_value') + db_addon_ignore_value = self.get_iattr_value(item.conf, 'database_ignore_value') elif any(x in str(item.id()) for x in self.ignore_0): - _db_addon_ignore_value = 0 + db_addon_ignore_value = 0 else: - _db_addon_ignore_value = None + db_addon_ignore_value = None - # get database item - _database_item = get_database_item() + # get database item and return if not available + database_item_path = self.get_iattr_value(item.conf, 'db_addon_database_item') + if database_item_path is not None: + database_item = database_item_path + else: + database_item = get_database_item() + if database_item is None: + self.logger.warning(f"No database item found for {item.path()}: Item ignored. Maybe you should check instance of database plugin.") + return - # return if no database_item - if _database_item is None: - self.logger.warning(f"No database item found for {item.id()}: Item ignored. Maybe you should check instance of database plugin.") - return + # return if mandatory params for ad_addon_fct not given. + if db_addon_fct in ALL_NEED_PARAMS_ATTRIBUTES and not self.has_iattr(item.conf, 'db_addon_params'): + self.logger.warning(f"Item '{item.path()}' with db_addon_fct={db_addon_fct} ignored, since parameter using 'db_addon_params' not given. Item will be ignored.") + return - # create items configs - item_config_data_dict = {'db_addon': 'function', 'attribute': _db_addon_fct, 'database_item': _database_item, 'ignore_value': _db_addon_ignore_value} - _update_cycle = None + # create standard items config + item_config_data_dict = {'db_addon': 'function', 'db_addon_fct': db_addon_fct, 'database_item': database_item, 'ignore_value': db_addon_ignore_value} + if database_item_path is not None: + item_config_data_dict.update({'database_item_path': True}) + else: + database_item_path = database_item.path() if self.parse_debug: - self.logger.debug(f"Item '{item.id()}' added with db_addon_fct={_db_addon_fct} and database_item={_database_item.id()}") + self.logger.debug(f"Item '{item.path()}' added with db_addon_fct={db_addon_fct} and database_item={database_item_path}") - # handle items with for daily run - if daily_attribute(_db_addon_fct): - _update_cycle = 'daily' + # handle daily items + if db_addon_fct in ALL_DAILY_ATTRIBUTES: + item_config_data_dict.update({'cycle': 'daily'}) - # handle items for weekly - elif weekly_attribute(_db_addon_fct): - _update_cycle = 'weekly' + # handle weekly items + elif db_addon_fct in ALL_WEEKLY_ATTRIBUTES: + item_config_data_dict.update({'cycle': 'weekly'}) - # handle items for monthly run - elif monthly_attribute(_db_addon_fct): - _update_cycle = 'monthly' + # handle monthly items + elif db_addon_fct in ALL_MONTHLY_ATTRIBUTES: + item_config_data_dict.update({'cycle': 'monthly'}) - # handle items for yearly run - elif yearly_attribute(_db_addon_fct): - _update_cycle = 'yearly' + # handle yearly items + elif db_addon_fct in ALL_YEARLY_ATTRIBUTES: + item_config_data_dict.update({'cycle': 'yearly'}) - # handle static items starting with 'general_' - elif _db_addon_fct.startswith('general_'): - _update_cycle = 'static' + # handle static items + elif db_addon_fct in ALL_GEN_ATTRIBUTES: + item_config_data_dict.update({'cycle': 'static'}) - # handle all functions with 'summe' like waermesumme, kaeltesumme, gruenlandtemperatursumme - elif 'summe' in _db_addon_fct: - if not self.has_iattr(item.conf, 'db_addon_params'): - self.logger.warning(f"Item '{item.id()}' with db_addon_fct={_db_addon_fct} ignored, since parameter using 'db_addon_params' not given. Item will be ignored.") - return + # handle on-change items + elif db_addon_fct in ALL_ONCHANGE_ATTRIBUTES: + item_config_data_dict.update({'cycle': 'on-change'}) - _db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) - if _db_addon_params is None or 'year' not in _db_addon_params: - self.logger.info(f"No 'year' for evaluation via 'db_addon_params' of item {item.id()} for function {_db_addon_fct} given. Default with 'current year' will be used.") - _db_addon_params = {} if _db_addon_params is None else _db_addon_params - _db_addon_params.update({'year': 'current'}) - - item_config_data_dict.update({'params': _db_addon_params}) - _update_cycle = 'daily' + # handle all functions with 'summe' like waermesumme, kaeltesumme, gruenlandtemperatursumme + if 'summe' in db_addon_fct: + db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) + if db_addon_params is None or 'year' not in db_addon_params: + self.logger.info(f"No 'year' for evaluation via 'db_addon_params' of item {item.path()} for function {db_addon_fct} given. Default with 'current year' will be used.") + db_addon_params = {'year': 'current'} + item_config_data_dict.update({'params': db_addon_params}) + + # handle wachstumsgradtage function + elif db_addon_fct == 'wachstumsgradtage': + DEFAULT_THRESHOLD = 10 + db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) + if db_addon_params is None or 'threshold' not in db_addon_params: + self.logger.info(f"No 'threshold' for evaluation via 'db_addon_params' of item {item.path()} for function {db_addon_fct} given. Default with {DEFAULT_THRESHOLD} will be used.") + db_addon_params = {'threshold': DEFAULT_THRESHOLD} if db_addon_params is None else db_addon_params + + if not isinstance(db_addon_params['threshold'], int): + threshold = to_int(db_addon_params['threshold']) + db_addon_params['threshold'] = DEFAULT_THRESHOLD if threshold is None else threshold + item_config_data_dict.update({'params': db_addon_params}) # handle tagesmitteltemperatur - elif _db_addon_fct == 'tagesmitteltemperatur': + elif db_addon_fct == 'tagesmitteltemperatur': if not self.has_iattr(item.conf, 'db_addon_params'): - self.logger.warning(f"Item '{item.id()}' with db_addon_fct={_db_addon_fct} ignored, since parameter using 'db_addon_params' not given. Item will be ignored.") + self.logger.warning(f"Item '{item.path()}' with db_addon_fct={db_addon_fct} ignored, since parameter using 'db_addon_params' not given. Item will be ignored.") return - _db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) - item_config_data_dict.update({'params': _db_addon_params}) - _update_cycle = 'daily' + db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) + if db_addon_params is None: + self.logger.warning(f"Error occurred during parsing of item attribute 'db_addon_params' of item {item.path()}. Item will be ignored.") + return + item_config_data_dict.update({'params': db_addon_params}) # handle db_request - elif _db_addon_fct == 'db_request': + elif db_addon_fct == 'db_request': if not self.has_iattr(item.conf, 'db_addon_params'): - self.logger.warning(f"Item '{item.id()}' with db_addon_fct={_db_addon_fct} ignored, since parameter using 'db_addon_params' not given. Item will be ignored") + self.logger.warning(f"Item '{item.path()}' with db_addon_fct={db_addon_fct} ignored, since parameter using 'db_addon_params' not given. Item will be ignored") return - _db_addon_params = self.get_iattr_value(item.conf, 'db_addon_params') - _db_addon_params = params_to_dict(_db_addon_params) - if _db_addon_params is None: - self.logger.warning(f"Error occurred during parsing of item attribute 'db_addon_params' of item {item.id()}. Item will be ignored.") + db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) + if db_addon_params is None: + self.logger.warning(f"Error occurred during parsing of item attribute 'db_addon_params' of item {item.path()}. Item will be ignored.") return if self.parse_debug: - self.logger.debug(f"parse_item: {_db_addon_fct=} for item={item.id()}, {_db_addon_params=}") + self.logger.debug(f"parse_item: {db_addon_fct=} for item={item.path()}, {db_addon_params=}") - if not any(param in _db_addon_params for param in ('func', 'timeframe')): - self.logger.warning(f"Item '{item.id()}' with {_db_addon_fct=} ignored, not all mandatory parameters in {_db_addon_params=} given. Item will be ignored.") + if not any(param in db_addon_params for param in ('func', 'timeframe')): + self.logger.warning(f"Item '{item.path()}' with {db_addon_fct=} ignored, not all mandatory parameters in {db_addon_params=} given. Item will be ignored.") return - item_config_data_dict.update({'params': _db_addon_params}) - _timeframe = _db_addon_params.get('group', None) + TIMEFRAMES_2_UPDATECYCLE = {'day': 'daily', + 'week': 'weekly', + 'month': 'monthly', + 'year': 'yearly'} + + _timeframe = db_addon_params.get('group', None) if not _timeframe: - _timeframe = _db_addon_params.get('timeframe', None) - if _timeframe == 'day': - _update_cycle = 'daily' - elif _timeframe == 'week': - _update_cycle = 'weekly' - elif _timeframe == 'month': - _update_cycle = 'monthly' - elif _timeframe == 'year': - _update_cycle = 'yearly' - else: - self.logger.warning(f"Item '{item.id()}' with {_db_addon_fct=} ignored. Not able to detect update cycle.") + _timeframe = db_addon_params.get('timeframe', None) + update_cycle = TIMEFRAMES_2_UPDATECYCLE.get(_timeframe) + if update_cycle is None: + self.logger.warning(f"Item '{item.path()}' with {db_addon_fct=} ignored. Not able to detect update cycle.") + return - # handle on_change items - elif onchange_attribute(_db_addon_fct): - _update_cycle = 'on-change' + item_config_data_dict.update({'params': db_addon_params, 'cycle': update_cycle}) # debug log item cycle if self.parse_debug: - self.logger.debug(f"Item '{item.id()}' added to be run {_update_cycle}.") + self.logger.debug(f"Item '{item.path()}' added to be run {item_config_data_dict['cycle']}.") - # add item to be run on startup (onchange_items shall not be run at startup, but at first noticed change of item value; therefore remove for list of items to be run at startup) - if (_db_addon_startup and not onchange_attribute(_db_addon_fct)) or (_db_addon_fct.startswith('general_')): + # handle item to be run on startup (onchange_items shall not be run at startup, but at first noticed change of item value; therefore remove for list of items to be run at startup) + if (db_addon_startup and db_addon_fct not in ALL_ONCHANGE_ATTRIBUTES) or db_addon_fct in ALL_GEN_ATTRIBUTES: if self.parse_debug: - self.logger.debug(f"Item '{item.id()}' added to be run on startup") + self.logger.debug(f"Item '{item.path()}' added to be run on startup") item_config_data_dict.update({'startup': True}) else: item_config_data_dict.update({'startup': False}) # add item to plugin item dict self.add_item(item, config_data_dict=item_config_data_dict) - item_config = self.get_item_config(item) - item_config.update({'cycle': _update_cycle}) # handle all items with db_addon_info elif self.has_iattr(item.conf, 'db_addon_info'): if self.parse_debug: - self.logger.debug(f"parse item: {item.id()} due to used item attribute 'db_addon_info'") - self.add_item(item, config_data_dict={'db_addon': 'info', 'attribute': f"info_{self.get_iattr_value(item.conf, 'db_addon_info').lower()}", 'startup': True}) + self.logger.debug(f"parse item: {item.path()} due to used item attribute 'db_addon_info'") + self.add_item(item, config_data_dict={'db_addon': 'info', 'db_addon_fct': f"info_{self.get_iattr_value(item.conf, 'db_addon_info').lower()}", 'database_item': None, 'startup': True}) # handle all items with db_addon_admin elif self.has_iattr(item.conf, 'db_addon_admin'): if self.parse_debug: - self.logger.debug(f"parse item: {item.id()} due to used item attribute 'db_addon_admin'") - self.add_item(item, config_data_dict={'db_addon': 'admin', 'attribute': f"admin_{self.get_iattr_value(item.conf, 'db_addon_admin').lower()}"}) + self.logger.debug(f"parse item: {item.path()} due to used item attribute 'db_addon_admin'") + self.add_item(item, config_data_dict={'db_addon': 'admin', 'db_addon_fct': f"admin_{self.get_iattr_value(item.conf, 'db_addon_admin').lower()}", 'database_item': None}) return self.update_item # Reference to 'update_item' für alle Items mit Attribut 'database', um die on_change Items zu berechnen - elif self.has_iattr(item.conf, self.item_attribute_search_str) and get_db_addon_item(): - self.logger.debug(f"reference to update_item for item '{item}' will be set due to on-change") + elif self.has_iattr(item.conf, self.item_attribute_search_str) and has_db_addon_item(): + self.logger.debug(f"reference to update_item for item '{item.path()}' will be set due to on-change") self.add_item(item, config_data_dict={'db_addon': 'database'}) return self.update_item @@ -394,14 +411,14 @@ def update_item(self, item, caller=None, source=None, dest=None): if self.alive and caller != self.get_shortname(): # handle database items - if item in self._database_items: + if item in self._database_items(): # self.logger.debug(f"update_item was called with item {item.property.path} with value {item()} from caller {caller}, source {source} and dest {dest}") if not self.startup_finished: self.logger.info(f"Handling of 'on-change' is paused for startup. No updated will be processed.") elif self.suspended: self.logger.info(f"Plugin is suspended. No updated will be processed.") else: - self.logger.info(f"+ Updated item '{item.id()}' with value {item()} will be put to queue for processing. {self.item_queue.qsize() + 1} items to do.") + self.logger.info(f"+ Updated item '{item.path()}' with value {item()} will be put to queue for processing. {self.item_queue.qsize() + 1} items to do.") self.item_queue.put((item, item())) # handle admin items @@ -439,8 +456,8 @@ def execute_startup_items(self) -> None: self.logger.debug("execute_startup_items called") if not self.suspended: - self.logger.info(f"{len(self._startup_items)} items will be calculated at startup.") - [self.item_queue.put(i) for i in self._startup_items] + self.logger.info(f"{len(self._startup_items())} items will be calculated at startup.") + [self.item_queue.put(i) for i in self._startup_items()] self.startup_finished = True else: self.logger.info(f"Plugin is suspended. No items will be calculated.") @@ -453,8 +470,8 @@ def execute_static_items(self) -> None: self.logger.debug("execute_static_item called") if not self.suspended: - self.logger.info(f"{len(self._static_items)} items will be calculated.") - [self.item_queue.put(i) for i in self._static_items] + self.logger.info(f"{len(self._static_items())} items will be calculated.") + [self.item_queue.put(i) for i in self._static_items()] else: self.logger.info(f"Plugin is suspended. No items will be calculated.") @@ -466,8 +483,8 @@ def execute_info_items(self) -> None: self.logger.debug("execute_info_items called") if not self.suspended: - self.logger.info(f"{len(self._static_items)} items will be calculated.") - [self.item_queue.put(i) for i in self._static_items] + self.logger.info(f"{len(self._info_items())} items will be calculated.") + [self.item_queue.put(i) for i in self._info_items()] else: self.logger.info(f"Plugin is suspended. No items will be calculated.") @@ -477,35 +494,32 @@ def execute_all_items(self) -> None: """ if not self.suspended: - self.logger.info(f"Values for all {len(self._ondemand_items)} items with 'db_addon_fct' attribute, which are not 'on-change', will be calculated!") - [self.item_queue.put(i) for i in self._ondemand_items] + self.logger.info(f"Values for all {len(self._ondemand_items())} items with 'db_addon_fct' attribute, which are not 'on-change', will be calculated!") + [self.item_queue.put(i) for i in self._ondemand_items()] else: self.logger.info(f"Plugin is suspended. No items will be calculated.") def work_item_queue(self) -> None: """ Handles item queue were all to be executed items were be placed in. - """ - self.logger.info(f"work_item_queue called.") - while self.alive: try: queue_entry = self.item_queue.get(True, 10) - self.logger.info(f"{queue_entry} received.") + self.logger.info(f" Queue Entry: '{queue_entry}' received.") except queue.Empty: - self._active_queue_item = '-' + self.active_queue_item = '-' pass else: if isinstance(queue_entry, tuple): item, value = queue_entry - self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'on-change' item {item.id()} with {value=} will be processed.") - self._active_queue_item = str(item.id()) + self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'on-change' item '{item.path()}' with {value=} will be processed.") + self.active_queue_item = str(item.path()) self.handle_onchange(item, value) else: - self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'on-demand' item {queue_entry.id()} will be processed.") - self._active_queue_item = str(queue_entry.id()) + self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'on-demand' item '{queue_entry.path()}' will be processed.") + self.active_queue_item = str(queue_entry.path()) self.handle_ondemand(queue_entry) def handle_ondemand(self, item: Item) -> None: @@ -517,135 +531,174 @@ def handle_ondemand(self, item: Item) -> None: # set/get parameters item_config = self.get_item_config(item) - _db_addon_fct = item_config['attribute'] - _database_item = item_config.get('database_item') - _ignore_value = item_config.get('ignore_value') - _result = None + db_addon = item_config['db_addon'] + db_addon_fct = item_config['db_addon_fct'] + database_item = item_config['database_item'] + ignore_value = item_config.get('ignore_value') + result = None + self.logger.debug(f"handle_ondemand: Item={item.path()} with {item_config=}") # handle info functions - if _db_addon_fct.startswith('info_'): + if db_addon == 'info': # handle info_db_version - if _db_addon_fct == 'info_db_version': - _result = self._get_db_version() + if db_addon_fct == 'info_db_version': + result = self._get_db_version() + self.logger.debug(f"handle_ondemand: info_db_version {result=}") + else: + self.logger.warning(f"No handling for attribute {db_addon_fct=} for Item {item.path()} defined.") # handle general functions - elif _db_addon_fct.startswith('general_'): + elif db_addon_fct in ALL_GEN_ATTRIBUTES: # handle oldest_value - if _db_addon_fct == 'general_oldest_value': - _result = self._get_oldest_value(_database_item) + if db_addon_fct == 'general_oldest_value': + result = self._get_oldest_value(database_item) # handle oldest_log - elif _db_addon_fct == 'general_oldest_log': - _result = self._get_oldest_log(_database_item) + elif db_addon_fct == 'general_oldest_log': + result = self._get_oldest_log(database_item) - # handle item starting with 'verbrauch_' - elif _db_addon_fct.startswith('verbrauch_'): + else: + self.logger.warning(f"No handling for attribute {db_addon_fct=} for Item {item.path()} defined.") + # handle item starting with 'verbrauch_' + elif db_addon_fct in ALL_VERBRAUCH_ATTRIBUTES: if self.execute_debug: self.logger.debug(f"handle_ondemand: 'verbrauch' detected.") - _result = self._handle_verbrauch(_database_item, _db_addon_fct) + result = self._handle_verbrauch(database_item, db_addon_fct, ignore_value) - if _result and _result < 0: - self.logger.warning(f"Result of item {item.id()} with {_db_addon_fct=} was negative. Something seems to be wrong.") + if result and result < 0: + self.logger.warning(f"Result of item {item.path()} with {db_addon_fct=} was negative. Something seems to be wrong.") # handle item starting with 'zaehlerstand_' of format 'zaehlerstand_timeframe_timedelta' like 'zaehlerstand_woche_minus1' - elif _db_addon_fct.startswith('zaehlerstand_'): - + elif db_addon_fct in ALL_ZAEHLERSTAND_ATTRIBUTES: if self.execute_debug: self.logger.debug(f"handle_ondemand: 'zaehlerstand' detected.") - _result = self._handle_zaehlerstand(_database_item, _db_addon_fct) + result = self._handle_zaehlerstand(database_item, db_addon_fct, ignore_value) # handle item starting with 'minmax_' - elif _db_addon_fct.startswith('minmax_'): - + elif db_addon_fct in ALL_HISTORIE_ATTRIBUTES: if self.execute_debug: self.logger.debug(f"handle_ondemand: 'minmax' detected.") - _result = self._handle_min_max(_database_item, _db_addon_fct, _ignore_value) + result = self._handle_min_max(database_item, db_addon_fct, ignore_value)[0][1] + + # handle item starting with 'tagesmitteltemperatur_' + elif db_addon_fct in ALL_TAGESMITTEL_ATTRIBUTES: + if self.execute_debug: + self.logger.debug(f"handle_ondemand: 'tagesmitteltemperatur' detected.") + + result = self._handle_tagesmitteltemperatur(database_item, db_addon_fct, ignore_value)[0][1] # handle item starting with 'serie_' - elif _db_addon_fct.startswith('serie_'): - _db_addon_params = STD_REQUEST_DICT[_db_addon_fct] - _db_addon_params['item'] = _database_item + elif db_addon_fct in ALL_SERIE_ATTRIBUTES: + if 'minmax' in db_addon_fct: + if self.execute_debug: + self.logger.debug(f"handle_ondemand: 'serie_minmax' detected.") - if self.execute_debug: - self.logger.debug(f"handle_ondemand: 'serie' detected with {_db_addon_params=}") + result = self._handle_min_max(database_item, db_addon_fct, ignore_value) - _result = self._handle_serie(_db_addon_params) + elif 'verbrauch' in db_addon_fct: + if self.execute_debug: + self.logger.debug(f"handle_ondemand: 'serie_verbrauch' detected.") - # handle kaeltesumme - elif _db_addon_fct == 'kaeltesumme': - _db_addon_params = item_config['params'] - _db_addon_params['_database_item'] = item_config['database_item'] + result = self._handle_verbrauch(database_item, db_addon_fct, ignore_value) + + elif 'zaehlerstand' in db_addon_fct: + if self.execute_debug: + self.logger.debug(f"handle_ondemand: 'serie_zaehlerstand' detected.") + + result = self._handle_zaehlerstand(database_item, db_addon_fct, ignore_value) + + elif 'tagesmitteltemperatur' in db_addon_fct: + if self.execute_debug: + self.logger.debug(f"handle_ondemand: 'serie_tagesmittelwert' detected.") + + result = self._handle_tagesmitteltemperatur(database_item, db_addon_fct, ignore_value) + else: + self.logger.warning(f"No handling for attribute {db_addon_fct=} for Item {item.path()} defined.") + # handle kaeltesumme + elif db_addon_fct == 'kaeltesumme': + db_addon_params = item_config.get('params') if self.execute_debug: - self.logger.debug(f"handle_ondemand: {_db_addon_fct=} detected; {_db_addon_params=}") + self.logger.debug(f"handle_ondemand: {db_addon_fct=} detected; {db_addon_params=}") - _result = self._handle_kaeltesumme(**_db_addon_params) + if db_addon_params: + db_addon_params.update({'database_item': item_config['database_item']}) + result = self._handle_kaeltesumme(**db_addon_params) # handle waermesumme - elif _db_addon_fct == 'waermesumme': - _db_addon_params = item_config['params'] - _db_addon_params['_database_item'] = item_config['database_item'] - + elif db_addon_fct == 'waermesumme': + db_addon_params = item_config.get('params') if self.execute_debug: - self.logger.debug(f"handle_ondemand: {_db_addon_fct=} detected; {_db_addon_params=}") + self.logger.debug(f"handle_ondemand: {db_addon_fct=} detected; {db_addon_params=}") - _result = self._handle_waermesumme(**_db_addon_params) + if db_addon_params: + db_addon_params.update({'database_item': item_config['database_item']}) + result = self._handle_waermesumme(**db_addon_params) # handle gruenlandtempsumme - elif _db_addon_fct == 'gruenlandtempsumme': - _db_addon_params = item_config['params'] - _db_addon_params['_database_item'] = item_config['database_item'] + elif db_addon_fct == 'gruenlandtempsumme': + db_addon_params = item_config.get('params') + if self.execute_debug: + self.logger.debug(f"handle_ondemand: {db_addon_fct=} detected; {db_addon_params=}") + if db_addon_params: + db_addon_params.update({'database_item': item_config['database_item']}) + result = self._handle_gruenlandtemperatursumme(**db_addon_params) + + # handle wachstumsgradtage + elif db_addon_fct == 'wachstumsgradtage': + db_addon_params = item_config.get('params') if self.execute_debug: - self.logger.debug(f"handle_ondemand: {_db_addon_fct=} detected; {_db_addon_params=}") + self.logger.debug(f"handle_ondemand: {db_addon_fct=} detected; {db_addon_params}") - _result = self._handle_gruenlandtemperatursumme(**_db_addon_params) + if db_addon_params: + db_addon_params.update({'database_item': item_config['database_item']}) + result = self._handle_wachstumsgradtage(**db_addon_params) # handle tagesmitteltemperatur - elif _db_addon_fct == 'tagesmitteltemperatur': - _db_addon_params = item_config['params'] - _db_addon_params['_database_item'] = item_config['database_item'] - + elif db_addon_fct == 'tagesmitteltemperatur': + db_addon_params = item_config.get('params') if self.execute_debug: - self.logger.debug(f"handle_ondemand: {_db_addon_fct=} detected; {_db_addon_params=}") + self.logger.debug(f"handle_ondemand: {db_addon_fct=} detected; {db_addon_params=}") - _result = self._handle_tagesmitteltemperatur(**_db_addon_params) + if db_addon_params: + result = self._handle_tagesmitteltemperatur(database_item, db_addon_fct, ignore_value, db_addon_params) # handle db_request - elif _db_addon_fct == 'db_request': - _db_addon_params = item_config['params'] - _db_addon_params['îtem'] = item_config['database_item'] - + elif db_addon_fct == 'db_request': + db_addon_params = item_config.get('params') if self.execute_debug: - self.logger.debug(f"handle_ondemand: {_db_addon_fct=} detected with {_db_addon_params=}") + self.logger.debug(f"handle_ondemand: {db_addon_fct=} detected with {db_addon_params=}") - if _db_addon_params.keys() & {'func', 'item', 'timeframe'}: - _result = self._query_item(**_db_addon_params) - else: - self.logger.error(f"Attribute 'db_addon_params' not containing needed params for Item {item.id} with {_db_addon_fct=}.") + if db_addon_params: + db_addon_params.update({'database_item': item_config['database_item']}) + if db_addon_params.keys() & {'func', 'item', 'timeframe'}: + result = self._query_item(**db_addon_params) + else: + self.logger.error(f"Attribute 'db_addon_params' not containing needed params for Item {item.id} with {db_addon_fct=}.") # handle everything else else: - self.logger.warning(f"handle_ondemand: Function '{_db_addon_fct}' for item {item.id()} not defined or found.") + self.logger.warning(f"handle_ondemand: Function '{db_addon_fct}' for item {item.path()} not defined or found.") return # log result if self.execute_debug: - self.logger.debug(f"handle_ondemand: result is {_result} for item '{item.id()}' with '{_db_addon_fct=}'") + self.logger.debug(f"handle_ondemand: result is {result} for item '{item.path()}' with '{db_addon_fct=}'") - if _result is None: + if result is None: self.logger.info(f" Result was None; No item value will be set.") return # set item value and put data into plugin_item_dict - self.logger.info(f" Item value for '{item.id()}' will be set to {_result}") + self.logger.info(f" Item value for '{item.path()}' will be set to {result}") item_config = self.get_item_config(item) - item_config.update({'value': _result}) - item(_result, self.get_shortname()) + item_config.update({'value': result}) + item(result, self.get_shortname()) def handle_onchange(self, updated_item: Item, value: float) -> None: """ @@ -656,7 +709,7 @@ def handle_onchange(self, updated_item: Item, value: float) -> None: """ if self.onchange_debug: - self.logger.debug(f"handle_onchange called with updated_item={updated_item.id()} and value={value}.") + self.logger.debug(f"handle_onchange called with updated_item={updated_item.path()} and value={value}.") relevant_item_list = self.get_item_list('database_item', updated_item) if self.onchange_debug: @@ -665,18 +718,20 @@ def handle_onchange(self, updated_item: Item, value: float) -> None: for item in relevant_item_list: item_config = self.get_item_config(item) _database_item = item_config['database_item'] - _db_addon_fct = item_config['attribute'] - _var = _db_addon_fct.split('_') + _db_addon_fct = item_config['db_addon_fct'] _ignore_value = item_config['ignore_value'] + _var = _db_addon_fct.split('_') # handle minmax on-change items like minmax_heute_max, minmax_heute_min, minmax_woche_max, minmax_woche_min..... if _db_addon_fct.startswith('minmax') and len(_var) == 3 and _var[2] in ['min', 'max']: _timeframe = convert_timeframe(_var[1]) _func = _var[2] _cache_dict = self.current_values[_timeframe] + if not _timeframe: + return if self.onchange_debug: - self.logger.debug(f"handle_onchange: 'minmax' item {updated_item.id()} with {_func=} detected. Check for update of _cache_dicts and item value.") + self.logger.debug(f"handle_onchange: 'minmax' item {updated_item.path()} with {_func=} detected. Check for update of _cache_dicts and item value.") _initial_value = False _new_value = None @@ -684,11 +739,12 @@ def handle_onchange(self, updated_item: Item, value: float) -> None: # make sure, that database item is in cache dict if _database_item not in _cache_dict: _cache_dict[_database_item] = {} - if _cache_dict[_database_item].get(_func, None) is None: - _cached_value = self._query_item(func=_func, item=_database_item, timeframe=_timeframe, start=0, end=0, ignore_value=_ignore_value)[0][1] + if _cache_dict[_database_item].get(_func) is None: + _query_params = {'func': _func, 'item': _database_item, 'timeframe': _timeframe, 'start': 0, 'end': 0, 'ignore_value': _ignore_value} + _cached_value = self._query_item(**_query_params)[0][1] _initial_value = True if self.onchange_debug: - self.logger.debug(f"handle_onchange: Item={updated_item.id()} with _func={_func} and _timeframe={_timeframe} not in cache dict. recent value={_cached_value}.") + self.logger.debug(f"handle_onchange: Item={updated_item.path()} with _func={_func} and _timeframe={_timeframe} not in cache dict. recent value={_cached_value}.") else: _cached_value = _cache_dict[_database_item][_func] @@ -715,159 +771,202 @@ def handle_onchange(self, updated_item: Item, value: float) -> None: if _new_value: _cache_dict[_database_item][_func] = _new_value - self.logger.info(f"Item value for '{item.id()}' with func={_func} will be set to {_new_value}") + self.logger.info(f"Item value for '{item.path()}' with func={_func} will be set to {_new_value}") item_config = self.get_item_config(item) item_config.update({'value': _new_value}) item(_new_value, self.get_shortname()) else: - self.logger.info(f"Received value={value} is not influencing min / max value. Therefore item {item.id()} will not be changed.") + self.logger.info(f"Received value={value} is not influencing min / max value. Therefore item {item.path()} will not be changed.") # handle verbrauch on-change items ending with heute, woche, monat, jahr elif _db_addon_fct.startswith('verbrauch') and len(_var) == 2 and _var[1] in ['heute', 'woche', 'monat', 'jahr']: _timeframe = convert_timeframe(_var[1]) _cache_dict = self.previous_values[_timeframe] + if _timeframe is None: + return # make sure, that database item is in cache dict if _database_item not in _cache_dict: - _cached_value = self._query_item(func='max', item=_database_item, timeframe=_timeframe, start=1, end=1, ignore_value=_ignore_value)[0][1] + _query_params = {'func': 'max', 'item': _database_item, 'timeframe': _timeframe, 'start': 1, 'end': 1, 'ignore_value': _ignore_value} + _cached_value = self._query_item(**_query_params)[0][1] _cache_dict[_database_item] = _cached_value if self.onchange_debug: - self.logger.debug(f"handle_onchange: Item={updated_item.id()} with {_timeframe=} not in cache dict. Value {_cached_value} has been added.") + self.logger.debug(f"handle_onchange: Item={updated_item.path()} with {_timeframe=} not in cache dict. Value {_cached_value} has been added.") else: _cached_value = _cache_dict[_database_item] # calculate value, set item value, put data into plugin_item_dict if _cached_value is not None: _new_value = round(value - _cached_value, 1) - self.logger.info(f"Item value for '{item.id()}' will be set to {_new_value}") + self.logger.info(f"Item value for '{item.path()}' will be set to {_new_value}") item_config = self.get_item_config(item) item_config.update({'value': _new_value}) item(_new_value, self.get_shortname()) else: self.logger.info(f"Value for end of last {_timeframe} not available. No item value will be set.") + def _update_database_items(self): + for item in self._database_item_path_items(): + item_config = self.get_item_config(item) + database_item_path = item_config.get('database_item') + database_item = self.items.return_item(database_item_path) + + if database_item is None: + self.logger.warning(f"Database-Item for Item with config item path for Database-Item {database_item_path!r} not found. Item '{item.path()}' will be removed from plugin.") + self.remove_item(item) + else: + item_config.update({'database_item': database_item}) + @property def log_level(self): return self.logger.getEffectiveLevel() - @property def queue_backlog(self): return self.item_queue.qsize() - @property - def active_queue_item(self): - return self._active_queue_item - - @property def db_version(self): return self._get_db_version() - @property def _startup_items(self) -> list: return self.get_item_list('startup', True) - @property def _onchange_items(self) -> list: return self.get_item_list('cycle', 'on-change') - @property def _daily_items(self) -> list: return self.get_item_list('cycle', 'daily') - @property def _weekly_items(self) -> list: return self.get_item_list('cycle', 'weekly') - @property def _monthly_items(self) -> list: return self.get_item_list('cycle', 'monthly') - @property def _yearly_items(self) -> list: return self.get_item_list('cycle', 'yearly') - @property def _static_items(self) -> list: return self.get_item_list('cycle', 'static') - @property def _admin_items(self) -> list: return self.get_item_list('db_addon', 'admin') - @property def _info_items(self) -> list: return self.get_item_list('db_addon', 'info') - @property def _database_items(self) -> list: return self.get_item_list('db_addon', 'database') - @property + def _database_item_path_items(self) -> list: + return self.get_item_list('database_item_path', True) + def _ondemand_items(self) -> list: - return self._daily_items + self._weekly_items + self._monthly_items + self._yearly_items + self._static_items + return self._daily_items() + self._weekly_items() + self._monthly_items() + self._yearly_items() + self._static_items() ############################## - # Public functions + # Public functions / Using item_path ############################## - def gruenlandtemperatursumme(self, item: Item, year: Union[int, str]) -> Union[int, None]: + def gruenlandtemperatursumme(self, item_path: str, year: Union[int, str]) -> Union[int, None]: """ - Query database for gruenlandtemperatursumme for given year or year/month + Query database for gruenlandtemperatursumme for given year or year https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme - :param item: item object or item_id for which the query should be done + Beim Grünland wird die Wärmesumme nach Ernst und Loeper benutzt, um den Vegetationsbeginn und somit den Termin von Düngungsmaßnahmen zu bestimmen. + Dabei erfolgt die Aufsummierung der Tagesmitteltemperaturen über 0 °C, wobei der Januar mit 0.5 und der Februar mit 0.75 gewichtet wird. + Bei einer Wärmesumme von 200 Grad ist eine Düngung angesagt. + + :param item_path: item object or item_id for which the query should be done :param year: year the gruenlandtemperatursumme should be calculated for :return: gruenlandtemperatursumme """ - return self._handle_gruenlandtemperatursumme(item, year) + item = self.items.return_item(item_path) + if item: + return self._handle_gruenlandtemperatursumme(item, year) - def waermesumme(self, item: Item, year, month: Union[int, str] = None) -> Union[int, None]: + def waermesumme(self, item_path: str, year, month: Union[int, str] = None, threshold: int = 0) -> Union[int, None]: """ Query database for waermesumme for given year or year/month + https://de.wikipedia.org/wiki/W%C3%A4rmesumme - :param item: item object or item_id for which the query should be done + :param item_path: item object or item_id for which the query should be done :param year: year the waermesumme should be calculated for :param month: month the waermesumme should be calculated for + :param threshold: threshold for temperature :return: waermesumme """ + item = self.items.return_item(item_path) + if item: + return self._handle_waermesumme(item, year, month, threshold) - return self._handle_waermesumme(item, year, month) - - def kaeltesumme(self, item: Item, year, month: Union[int, str] = None) -> Union[int, None]: + def kaeltesumme(self, item_path: str, year, month: Union[int, str] = None) -> Union[int, None]: """ Query database for kaeltesumme for given year or year/month + https://de.wikipedia.org/wiki/K%C3%A4ltesumme - :param item: item object or item_id for which the query should be done + :param item_path: item object or item_id for which the query should be done :param year: year the kaeltesumme should be calculated for :param month: month the kaeltesumme should be calculated for :return: kaeltesumme """ + item = self.items.return_item(item_path) + if item: + return self._handle_kaeltesumme(item, year, month) - return self._handle_kaeltesumme(item, year, month) - - def tagesmitteltemperatur(self, item: Item, count: int = None) -> list: + def tagesmitteltemperatur(self, item_path: str, timeframe: str = None, count: int = None) -> list: """ Query database for tagesmitteltemperatur + https://www.dwd.de/DE/leistungen/klimadatendeutschland/beschreibung_tagesmonatswerte.html - :param item: item object or item_id for which the query should be done - :param count: start of timeframe defined by number of time increments starting from now to the left (into the past) + :param item_path: item object or item_id for which the query should be done + :param timeframe: timeincrement for determination + :param count: number of time increments starting from now to the left (into the past) :return: tagesmitteltemperatur - :rtype: list of tuples """ - return self._handle_tagesmitteltemperatur(_database_item=item, count=count) + if not timeframe: + timeframe = 'day' - def fetch_log(self, func: str, item: Item, timeframe: str, start: int = None, end: int = 0, count: int = None, group: str = None, group2: str = None, ignore_value=None) -> Union[list, None]: + if not count: + count = 0 + + item = self.items.return_item(item_path) + if item: + return self._handle_tagesmitteltemperatur(database_item=item, db_addon_fct='tagesmitteltemperatur', params={'timeframe': timeframe, 'count': count}) + + def wachstumsgradtage(self, item_path: str, year: Union[int, str], threshold: int) -> Union[int, None]: + """ + Query database for wachstumsgradtage + https://de.wikipedia.org/wiki/Wachstumsgradtag + + :param item_path: item object or item_id for which the query should be done + :param year: year the wachstumsgradtage should be calculated for + :param threshold: Temperature in °C as threshold: Ein Tage mit einer Tagesdurchschnittstemperatur oberhalb des Schellenwertes gilt als Wachstumsgradtag + :return: wachstumsgradtage + """ + + item = self.items.return_item(item_path) + if item: + return self._handle_wachstumsgradtage(item, year, threshold) + + def query_item(self, func: str, item_path: str, timeframe: str, start: int = None, end: int = 0, group: str = None, group2: str = None, ignore_value=None) -> list: + item = self.items.return_item(item_path) + if item is None: + return [] + + return self._query_item(func, item, timeframe, start, end, group, group2, ignore_value) + + def fetch_log(self, func: str, item_path: str, timeframe: str, start: int = None, end: int = 0, count: int = None, group: str = None, group2: str = None, ignore_value=None) -> list: """ Query database, format response and return it :param func: function to be used at query - :param item: item str or item_id for which the query should be done + :param item_path: item str or item_id for which the query should be done :param timeframe: time increment für definition of start, end, count (day, week, month, year) :param start: start of timeframe (oldest) for query given in x time increments (default = None, meaning complete database) :param end: end of timeframe (newest) for query given in x time increments (default = 0, meaning today, end of last week, end of last month, end of last year) @@ -878,12 +977,15 @@ def fetch_log(self, func: str, item: Item, timeframe: str, start: int = None, en :return: formatted query response """ + item = self.items.return_item(item_path) - if isinstance(item, str): - item = self.items.return_item(item) if count: start, end = count_to_start(count) - return self._query_item(func=func, item=item, timeframe=timeframe, start=start, end=end, group=group, group2=group2, ignore_value=ignore_value) + + if item and start and end: + return self._query_item(func=func, item=item, timeframe=timeframe, start=start, end=end, group=group, group2=group2, ignore_value=ignore_value) + else: + return [] def fetch_raw(self, query: str, params: dict = None) -> Union[list, None]: """ @@ -924,346 +1026,717 @@ def suspend(self, state: bool = False) -> bool: # write back value to item, if one exists for item in self.get_item_list('db_addon', 'admin'): item_config = self.get_item_config(item) - if item_config['attribute'] == 'suspend': + if item_config['db_addon_fct'] == 'suspend': item(self.suspended, self.get_shortname()) return self.suspended ############################## - # Support stuff + # Support stuff / Using Item Object ############################## - def _handle_min_max(self, _database_item: Item, _db_addon_fct: str, _ignore_value): + def _handle_min_max(self, database_item: Item, db_addon_fct: str, ignore_value=None) -> Union[list, None]: """ Handle execution of min/max calculation - """ - - _var = _db_addon_fct.split('_') - _result = None - _timeframes = ['heute', 'woche', 'monat', 'jahr'] - # handle all on_change functions of format 'minmax_timeframe_function' like 'minmax_heute_max' - if len(_var) == 3 and _var[1] in _timeframes and _var[2] in ['min', 'max']: + if db_addon_fct in ALL_ONCHANGE_ATTRIBUTES: if self.execute_debug: - self.logger.debug(f"on-change function={_var[0]} with {_var[1]} detected; will be calculated by next change of database item") - - # handle all 'last' functions in format 'minmax_last_window_function' like 'minmax_last_24h_max' - elif len(_var) == 4 and _var[1] == 'last' and _var[3] in ['min', 'max', 'avg']: - _window = _var[2] - _func = _var[3] - _timeframe = convert_timeframe(_window[-1:]) - _timedelta = int(_window[:-1]) + self.logger.debug(f"on-change function with 'min/max' detected; will be calculated by next change of database item") + return - if self.execute_debug: - self.logger.debug(f"_handle_min_max: 'last' function detected. {_window=}, {_func=}") + _var = db_addon_fct.split('_') + group = None + group2 = None - if _timeframe in ['day', 'week', 'month', 'year']: - _result = self._query_item(func=_func, item=_database_item, timeframe=_timeframe, start=_timedelta, end=0, ignore_value=_ignore_value)[0][1] + # handle all 'last' functions in format 'minmax_last_window_function' like 'minmax_last_24h_max' + if len(_var) == 4 and _var[1] == 'last': + func = _var[3] + timeframe = convert_timeframe(_var[2][-1:]) + start = to_int(_var[2][:-1]) + end = 0 + log_text = 'minmax_last' + if timeframe is None or start is None: + return # handle all functions 'min/max/avg' in format 'minmax_timeframe_timedelta_func' like 'minmax_heute_minus2_max' - elif len(_var) == 4 and _var[1] in _timeframes and _var[2].startswith('minus') and _var[3] in ['min', 'max', 'avg']: - _timeframe = convert_timeframe(_var[1]) # day, week, month, year - _timedelta = _var[2][-1] # 1, 2, 3, ... - _func = _var[3] # min, max, avg + elif len(_var) == 4 and _var[2].startswith('minus'): + func = _var[3] # min, max, avg + timeframe = convert_timeframe(_var[1]) # day, week, month, year + start = to_int(_var[2][-1]) # 1, 2, 3, ... + end = start + log_text = 'minmax' + if timeframe is None or start is None: + return - if self.execute_debug: - self.logger.debug(f"_handle_min_max: _db_addon_fct={_func} detected; {_timeframe=}, {_timedelta=}") + # handle all functions 'serie_min/max/avg' in format 'serie_minmax_timeframe_func_count_group' like 'serie_minmax_monat_min_15m' + elif _var[0] == 'serie' and _var[1] == 'minmax': + timeframe = convert_timeframe(_var[2]) + func = _var[3] + start = to_int(_var[4][:-1]) + end = 0 + group = convert_timeframe(_var[4][len(_var[4]) - 1]) + log_text = 'serie_min/max/avg' + if timeframe is None or start is None or group is None: + return + else: + self.logger.info(f"_handle_min_max: No adequate function for {db_addon_fct=} found.") + return - if isinstance(_timedelta, str) and _timedelta.isdigit(): - _timedelta = int(_timedelta) + if func not in ALLOWED_MINMAX_FUNCS: + self.logger.info(f"_handle_min_max: Called {func=} not in allowed functions={ALLOWED_MINMAX_FUNCS}.") + return + + query_params = {'item': database_item, 'ignore_value': ignore_value, 'func': func, 'timeframe': timeframe, 'start': start, 'end': end, 'group': group, 'group2': group2} - if isinstance(_timedelta, int): - _result = self._query_item(func=_func, item=_database_item, timeframe=_timeframe, start=_timedelta, end=_timedelta, ignore_value=_ignore_value)[0][1] + if self.execute_debug: + self.logger.debug(f"_handle_min_max: db_addon_fct={log_text} function detected. {query_params=}") - return _result + return self._query_item(**query_params) - def _handle_zaehlerstand(self, _database_item: Item, _db_addon_fct: str): + def _handle_zaehlerstand(self, database_item: Item, db_addon_fct: str, ignore_value=None) -> Union[list, None]: """ Handle execution of Zaehlerstand calculation - """ + # handle all on_change functions + if db_addon_fct in ALL_ONCHANGE_ATTRIBUTES: + if self.execute_debug: + self.logger.debug(f"on-change function with 'zaehlerstand' detected; will be calculated by next change of database item") + return - _var = _db_addon_fct.split('_') # zaehlerstand_heute_minus1 - _result = None - _func = _var[0] - _timeframe = convert_timeframe(_var[1]) - _timedelta = _var[2][-1] + _var = db_addon_fct.split('_') + group = None + group2 = None + + # handle functions starting with 'zaehlerstand' like 'zaehlerstand_heute_minus1' + if len(_var) == 3 and _var[1] == 'zaehlerstand': + func = 'max' + timeframe = convert_timeframe(_var[1]) + start = to_int(_var[2][-1]) + end = start + log_text = 'zaehlerstand' + if timeframe is None or start is None: + return - if self.execute_debug: - self.logger.debug(f"_handle_zaehlerstand: {_func} function detected. {_timeframe=}, {_timedelta=}") + # handle all functions 'serie_min/max/avg' in format 'serie_minmax_timeframe_func_count_group' like 'serie_zaehlerstand_tag_30d' + elif _var[0] == 'serie' and _var[1] == 'zaehlerstand': + func = 'max' + timeframe = convert_timeframe(_var[2]) + start = to_int(_var[3][:-1]) + end = 0 + group = convert_timeframe(_var[3][len(_var[3]) - 1]) + log_text = 'serie_min/max/avg' + if timeframe is None or start is None or group is None: + return + else: + self.logger.info(f"_handle_zaehlerstand: No adequate function for {db_addon_fct=} found.") + return - if isinstance(_timedelta, str) and _timedelta.isdigit(): - _timedelta = int(_timedelta) + query_params = {'item': database_item, 'ignore_value': ignore_value, 'func': func, 'timeframe': timeframe, 'start': start, 'end': end, 'group': group, 'group2': group2} - if _func == 'zaehlerstand': - _result = self._query_item(func='max', item=_database_item, timeframe=_timeframe, start=_timedelta, end=_timedelta)[0][1] + if self.execute_debug: + self.logger.debug(f"_handle_zaehlerstand: db_addon_fct={log_text} function detected. {query_params=}") - return _result + return self._query_item(**query_params) - def _handle_verbrauch(self, _database_item: Item, _db_addon_fct: str): + def _handle_verbrauch(self, database_item: Item, db_addon_fct: str, ignore_value=None): """ Handle execution of verbrauch calculation - """ - _var = _db_addon_fct.split('_') - _result = None + self.logger.debug(f"_handle_verbrauch called with {database_item=} and {db_addon_fct=}") + + def consumption_calc(c_start, c_end) -> Union[float, None]: + """ + Handle query for Verbrauch + + :param c_start: beginning of timeframe + :param c_end: end of timeframe + """ + + if self.prepare_debug: + self.logger.debug(f"_consumption_calc called with {database_item=}, {timeframe=}, {c_start=}, {c_end=}") + + _result = None + _query_params = {'item': database_item, 'timeframe': timeframe} + + # get value for end and check it; + _query_params.update({'func': 'max', 'start': c_end, 'end': c_end}) + value_end = self._query_item(**_query_params)[0][1] + + if self.prepare_debug: + self.logger.debug(f"_consumption_calc {value_end=}") + + if value_end is None: # if None (Error) return + return + elif value_end == 0: # wenn die Query "None" ergab, was wiederum bedeutet, dass zum Abfragezeitpunkt keine Daten vorhanden sind, ist der value hier gleich 0 → damit der Verbrauch für die Abfrage auch Null + return 0 + + # get value for start and check it; + _query_params.update({'func': 'min', 'start': c_end, 'end': c_end}) + value_start = self._query_item(**_query_params)[0][1] + if self.prepare_debug: + self.logger.debug(f"_consumption_calc {value_start=}") + + if value_start is None: # if None (Error) return + return + + if value_start == 0: # wenn der Wert zum Startzeitpunkt 0 ist, gab es dort keinen Eintrag (also keinen Verbrauch), dann frage den nächsten Eintrag in der DB ab. + self.logger.info(f"No DB Entry found for requested start date. Looking for next DB entry.") + _query_params.update({'func': 'next', 'start': c_start, 'end': c_end}) + value_start = self._query_item(**_query_params)[0][1] + if self.prepare_debug: + self.logger.debug(f"_consumption_calc: next available value is {value_start=}") + + # calculate result + if value_start is not None: + return round(value_end - value_start, 1) # handle all on_change functions of format 'verbrauch_timeframe' like 'verbrauch_heute' - if len(_var) == 2 and _var[1] in ['heute', 'woche', 'monat', 'jahr']: + if db_addon_fct in ALL_ONCHANGE_ATTRIBUTES: if self.execute_debug: - self.logger.debug(f"on_change function={_var[1]} detected; will be calculated by next change of database item") + self.logger.debug(f"on_change function with 'verbrauch' detected; will be calculated by next change of database item") + return + + _var = db_addon_fct.split('_') # handle all functions 'verbrauch' in format 'verbrauch_timeframe_timedelta' like 'verbrauch_heute_minus2' - elif len(_var) == 3 and _var[1] in ['heute', 'woche', 'monat', 'jahr'] and _var[2].startswith('minus'): - _timeframe = convert_timeframe(_var[1]) - _timedelta = _var[2][-1] + if len(_var) == 3 and _var[1] in ['heute', 'woche', 'monat', 'jahr'] and _var[2].startswith('minus'): + timeframe = convert_timeframe(_var[1]) + timedelta = to_int(_var[2][-1]) + if timedelta is None or timeframe is None: + return if self.execute_debug: - self.logger.debug(f"_handle_verbrauch: '{_db_addon_fct}' function detected. {_timeframe=}, {_timedelta=}") - - if isinstance(_timedelta, str) and _timedelta.isdigit(): - _timedelta = int(_timedelta) + self.logger.debug(f"_handle_verbrauch: '{db_addon_fct}' function detected. {timeframe=}, {timedelta=}") - if isinstance(_timedelta, int): - _result = self._consumption_calc(_database_item, _timeframe, start=_timedelta + 1, end=_timedelta) + return consumption_calc(c_start=timedelta + 1, c_end=timedelta) # handle all functions of format 'verbrauch_function_window_timeframe_timedelta' like 'verbrauch_rolling_12m_woche_minus1' elif len(_var) == 5 and _var[1] == 'rolling' and _var[4].startswith('minus'): - _func = _var[1] - _window = _var[2] # 12m - _window_inc = int(_window[:-1]) # 12 - _window_dur = convert_timeframe(_window[-1]) # day, week, month, year - _timeframe = convert_timeframe(_var[3]) # day, week, month, year - _timedelta = _var[4][-1] # 1 + func = _var[1] + window = _var[2] # 12m + window_inc = to_int(window[:-1]) # 12 + window_dur = convert_timeframe(window[-1]) # day, week, month, year + timeframe = convert_timeframe(_var[3]) # day, week, month, year + timedelta = to_int(_var[4][-1]) # 1 + endtime = timedelta + + if window_inc is None or window_dur is None or timeframe is None or timedelta is None: + return if self.execute_debug: - self.logger.debug(f"_handle_verbrauch: '{_func}' function detected. {_window=}, {_timeframe=}, {_timedelta=}") - - if isinstance(_timedelta, str) and _timedelta.isdigit(): - _timedelta = int(_timedelta) - _endtime = _timedelta + self.logger.debug(f"_handle_verbrauch: '{func}' function detected. {window=}, {timeframe=}, {timedelta=}") - if _func == 'rolling' and _window_dur in ['day', 'week', 'month', 'year']: - _starttime = convert_duration(_timeframe, _window_dur) * _window_inc - _result = self._consumption_calc(_database_item, _timeframe, _starttime, _endtime) + if window_dur in ['day', 'week', 'month', 'year']: + starttime = convert_duration(timeframe, window_dur) * window_inc + return consumption_calc(c_start=starttime, c_end=endtime) # handle all functions of format 'verbrauch_timeframe_timedelta' like 'verbrauch_jahreszeitraum_minus1' elif len(_var) == 3 and _var[1] == 'jahreszeitraum' and _var[2].startswith('minus'): - _timeframe = convert_timeframe(_var[1]) # day, week, month, year - _timedelta = _var[2][-1] # 1 oder 2 oder 3 + timeframe = convert_timeframe(_var[1]) # day, week, month, year + timedelta = to_int(_var[2][-1]) # 1 oder 2 oder 3 + if timedelta is None or timeframe is None: + return if self.execute_debug: - self.logger.debug(f"_handle_verbrauch: '{_db_addon_fct}' function detected. {_timeframe=}, {_timedelta=}") + self.logger.debug(f"_handle_verbrauch: '{db_addon_fct}' function detected. {timeframe=}, {timedelta=}") + + today = datetime.date.today() + year = today.year - timedelta + start_date = datetime.date(year, 1, 1) - relativedelta(days=1) # Start ist Tag vor dem 1.1., damit Abfrage den Maximalwert von 31.12. 00:00:00 bis 1.1. 00:00:00 ergibt + end_date = today - relativedelta(years=timedelta) + start = (today - start_date).days + end = (today - end_date).days + + return consumption_calc(c_start=start, c_end=end) + + # handle all functions of format 'serie_verbrauch_timeframe_countgroup' like 'serie_verbrauch_tag_30d' + elif db_addon_fct.startswith('serie_') and len(_var) == 4: + self.logger.debug(f"_handle_verbrauch serie reached") + func = 'diff_max' + timeframe = convert_timeframe(_var[2]) + start = to_int(_var[3][:-1]) + group = convert_timeframe(_var[3][len(_var[3]) - 1]) + group2 = None + if timeframe is None or start is None or group is None: + self.logger.warning(f"For calculating '{db_addon_fct}' not all mandatory parameters given. {timeframe=}, {start=}, {group=}") + return - if isinstance(_timedelta, str) and _timedelta.isdigit(): - _timedelta = int(_timedelta) + query_params = {'func': func, 'item': database_item, 'timeframe': timeframe, 'start': start, 'end': 0, 'group': group, 'group2': group2, 'ignore_value': ignore_value} - if isinstance(_timedelta, int): - _today = datetime.date.today() - _year = _today.year - _timedelta - _start_date = datetime.date(_year, 1, 1) - relativedelta(days=1) # Start ist Tag vor dem 1.1., damit Abfrage den Maximalwert von 31.12. 00:00:00 bis 1.1. 00:00:00 ergibt - _end_date = _today - relativedelta(years=_timedelta) - _start = (_today - _start_date).days - _end = (_today - _end_date).days + if self.execute_debug: + self.logger.debug(f"_handle_verbrauch: 'serie_verbrauch_timeframe_countgroup' function detected. {query_params=}") - _result = self._consumption_calc(_database_item, _timeframe, _start, _end) + return self._query_item(**query_params) - return _result + else: + self.logger.info(f"_handle_verbrauch: No adequate function for {db_addon_fct=} found.") + return - def _handle_serie(self, _db_addon_params: dict): + def _handle_tagesmitteltemperatur(self, database_item: Item, db_addon_fct: str, ignore_value=None, params: dict = None) -> list: """ - Handle execution of serie calculation + Query database for tagesmitteltemperatur + :param database_item: item object or item_id for which the query should be done + :param db_addon_fct + :param ignore_value + :param params: + :return: tagesmitteltemperatur """ - return self._query_item(**_db_addon_params) - def _handle_kaeltesumme(self, _database_item: Item, year: Union[int, str], month: Union[int, str] = None) -> Union[int, None]: + # handle all on_change functions + if db_addon_fct in ALL_ONCHANGE_ATTRIBUTES: + if self.execute_debug: + self.logger.debug(f"on_change function with 'tagesmitteltemperatur' detected; will be calculated by next change of database item") + return [] + + _var = db_addon_fct.split('_') + group = None + group2 = None + + # handle tagesmitteltemperatur + if db_addon_fct == 'tagesmitteltemperatur': + if not params: + return [] + + func = 'max' + timeframe = convert_timeframe(params.get('timeframe')) + log_text = 'tagesmitteltemperatur' + count = to_int(params.get('count')) + if timeframe is None or not count: + return [] + + start, end = count_to_start(count) + + # handle 'tagesmittelwert_timeframe_timedelta' like 'tagesmittelwert_heute_minus1' + elif len(_var) == 3 and _var[2].startswith('minus'): + func = 'max' + timeframe = convert_timeframe(_var[1]) + start = to_int(_var[2][-1]) + end = start + log_text = 'tagesmittelwert_timeframe_timedelta' + if timeframe is None or start is None: + return [] + + # handle 'serie_tagesmittelwert_countgroup' like 'serie_tagesmittelwert_0d' + elif db_addon_fct.startswith('serie_') and len(_var) == 3: + # 'serie_tagesmittelwert_0d': {'func': 'max', 'timeframe': 'year', 'start': 0, 'end': 0, 'group': 'day'}, + func = 'max' + timeframe = 'year' + log_text = 'serie_tagesmittelwert_countgroup' + start = to_int(_var[2][:-1]) + end = 0 + group = convert_timeframe(_var[2][len(_var[2]) - 1]) + if group is None or start is None: + return [] + + # handle 'serie_tagesmittelwert_group2_count_group' like 'serie_tagesmittelwert_stunde_0d' + elif db_addon_fct.startswith('serie_') and len(_var) == 4: + # 'serie_tagesmittelwert_stunde_0d': {'func': 'avg1', 'timeframe': 'day', 'start': 0, 'end': 0, 'group': 'hour', 'group2': 'day'}, + # 'serie_tagesmittelwert_stunde_30d': {'func': 'avg1', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'hour', 'group2': 'day'}, + func = 'avg1' + timeframe = 'day' + log_text = 'serie_tagesmittelwert_group2_countgroup' + start = to_int(_var[3][:-1]) + end = 0 + group = 'hour' + group2 = convert_timeframe(_var[3][len(_var[3]) - 1]) + if group2 is None or start is None: + return [] + + # handle 'serie_tagesmittelwert_group2_start_endgroup' like 'serie_tagesmittelwert_stunde_30_0d' + elif db_addon_fct.startswith('serie_') and len(_var) == 5: + func = 'avg1' + timeframe = 'day' + log_text = 'serie_tagesmittelwert_group2_start_endgroup' + start = to_int(_var[3]) + end = to_int(_var[4][:-1]) + group = 'hour' + group2 = convert_timeframe(_var[4][len(_var[4]) - 1]) + if group2 is None or start is None or end is None: + return [] + + # handle everything else + else: + self.logger.info(f"_handle_tagesmitteltemperatur: No adequate function for {db_addon_fct=} found.") + return [] + + query_params = {'item': database_item, 'ignore_value': ignore_value, 'func': func, 'timeframe': timeframe, 'start': start, 'end': end, 'group': group, 'group2': group2} + + if self.execute_debug: + self.logger.debug(f"_handle_tagesmitteltemperatur: db_addon_fct={log_text} function detected. {query_params=}") + + return self._query_item(**query_params) + + def _handle_kaeltesumme(self, database_item: Item, year: Union[int, str], month: Union[int, str] = None) -> Union[int, None]: """ Query database for kaeltesumme for given year or year/month + https://de.wikipedia.org/wiki/K%C3%A4ltesumme - :param _database_item: item object or item_id for which the query should be done + :param database_item: item object or item_id for which the query should be done :param year: year the kaeltesumme should be calculated for :param month: month the kaeltesumme should be calculated for :return: kaeltesumme """ + self.logger.debug(f"_handle_kaeltesumme called with {database_item=}, {year=}, {month=}") + # check validity of given year if not valid_year(year): - self.logger.error(f"kaeltesumme: Year for item={_database_item.id()} was {year}. This is not a valid year. Query cancelled.") + self.logger.error(f"_handle_kaeltesumme: Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") return + # define year if year == 'current': if datetime.date.today() < datetime.date(int(datetime.date.today().year), 9, 21): year = datetime.date.today().year - 1 else: year = datetime.date.today().year + # define start_date and end_date if month is None: start_date = datetime.date(int(year), 9, 21) end_date = datetime.date(int(year) + 1, 3, 22) - group2 = 'year' elif valid_month(month): start_date = datetime.date(int(year), int(month), 1) end_date = start_date + relativedelta(months=+1) - datetime.timedelta(days=1) - group2 = 'month' else: - self.logger.error(f"kaeltesumme: Month for item={_database_item.id()} was {month}. This is not a valid month. Query cancelled.") + self.logger.error(f"_handle_kaeltesumme: Month for item={database_item.path()} was {month}. This is not a valid month. Query cancelled.") return + # define start / end today = datetime.date.today() if start_date > today: - self.logger.error(f"kaeltesumme: Start time for query of item={_database_item.id()} is in future. Query cancelled.") + self.logger.error(f"_handle_kaeltesumme: Start time for query of item={database_item.path()} is in future. Query cancelled.") return start = (today - start_date).days end = (today - end_date).days if end_date < today else 0 if start < end: - self.logger.error(f"kaeltesumme: End time for query of item={_database_item.id()} is before start time. Query cancelled.") + self.logger.error(f"_handle_kaeltesumme: End time for query of item={database_item.path()} is before start time. Query cancelled.") return - _db_addon_params = STD_REQUEST_DICT.get('kaltesumme_year_month', None) - _db_addon_params.update({'start': start, 'end': end, 'group2': group2, 'item': _database_item}) - - # query db and generate values - _result = self._query_item(**_db_addon_params) - self.logger.debug(f"kaeltesumme: {_result=} for {_database_item.id()=} with {year=} and {month=}") + # get raw data as list + self.logger.debug("_handle_kaeltesumme: Try to get raw data") + raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='raw') + if self.execute_debug: + self.logger.debug(f"_handle_kaeltesumme: raw_value_list={raw_data=}") # calculate value - value = 0 - if _result == [[None, None]]: - return - try: - if month: - value = _result[0][1] - else: - for entry in _result: - entry_value = entry[1] - if entry_value: - value += entry_value - return int(value) - except Exception as e: - self.logger.error(f"Error {e} occurred during calculation of kaeltesumme with {_result=} for {_database_item.id()=} with {year=} and {month=}") + if raw_data and isinstance(raw_data, list): + if raw_data == [[None, None]]: + return - def _handle_waermesumme(self, _database_item: Item, year: Union[int, str], month: Union[int, str] = None) -> Union[int, None]: + ks = 0 + for entry in raw_data: + if entry[1] < 0: + ks -= entry[1] + return int(round(ks, 0)) + + def _handle_waermesumme(self, database_item: Item, year: Union[int, str], month: Union[int, str] = None, threshold: int = 0) -> Union[int, None]: """ Query database for waermesumme for given year or year/month + https://de.wikipedia.org/wiki/W%C3%A4rmesumme - :param _database_item: item object or item_id for which the query should be done - :param year: year the waermesumme should be calculated for + :param database_item: item object or item_id for which the query should be done + :param year: year the waermesumme should be calculated for; "current" for current year :param month: month the waermesumme should be calculated for :return: waermesumme """ + # start: links / älterer Termin end: rechts / jüngerer Termin + + # check validity of given year if not valid_year(year): - self.logger.error(f"waermesumme: Year for item={_database_item.id()} was {year}. This is not a valid year. Query cancelled.") + self.logger.error(f"_handle_waermesumme: Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") return + # define year if year == 'current': year = datetime.date.today().year + # define start_date, end_date if month is None: - start_date = datetime.date(int(year), 3, 20) + start_date = datetime.date(int(year), 1, 1) end_date = datetime.date(int(year), 9, 21) - group2 = 'year' elif valid_month(month): start_date = datetime.date(int(year), int(month), 1) end_date = start_date + relativedelta(months=+1) - datetime.timedelta(days=1) - group2 = 'month' else: - self.logger.error(f"waermesumme: Month for item={_database_item.id()} was {month}. This is not a valid month. Query cancelled.") + self.logger.error(f"_handle_waermesumme: Month for item={database_item.path()} was {month}. This is not a valid month. Query cancelled.") return + # check start_date today = datetime.date.today() if start_date > today: - self.logger.info(f"waermesumme: Start time for query of item={_database_item.id()} is in future. Query cancelled.") + self.logger.info(f"_handle_waermesumme: Start time for query of item={database_item.path()} is in future. Query cancelled.") return + # define start / end start = (today - start_date).days end = (today - end_date).days if end_date < today else 0 + + # check end if start < end: - self.logger.error(f"waermesumme: End time for query of item={_database_item.id()} is before start time. Query cancelled.") + self.logger.error(f"_handle_waermesumme: End time for query of item={database_item.path()} is before start time. Query cancelled.") return - _db_addon_params = STD_REQUEST_DICT.get('waermesumme_year_month', None) - _db_addon_params.update({'start': start, 'end': end, 'group2': group2, 'item': _database_item}) + # get raw data as list + raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='raw') + if self.execute_debug: + self.logger.debug(f"_handle_waermesumme: raw_value_list={raw_data=}") - # query db and generate values - _result = self._query_item(**_db_addon_params)[0][1] - self.logger.debug(f"waermesumme_year_month: {_result=} for {_database_item.id()=} with {year=} and {month=}") + # set threshold to min 0 + threshold = min(0, threshold) # calculate value - if _result == [[None, None]]: - return + if raw_data and isinstance(raw_data, list): + if raw_data == [[None, None]]: + return - if _result is not None: - return int(_result) - else: - return + ws = 0 + for entry in raw_data: + if entry[1] > threshold: + ws += entry[1] + return int(round(ws, 0)) - def _handle_gruenlandtemperatursumme(self, _database_item: Item, year: Union[int, str]) -> Union[int, None]: + def _handle_gruenlandtemperatursumme(self, database_item: Item, year: Union[int, str]) -> Union[int, None]: """ Query database for gruenlandtemperatursumme for given year or year/month + https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme - :param _database_item: item object or item_id for which the query should be done + :param database_item: item object for which the query should be done :param year: year the gruenlandtemperatursumme should be calculated for :return: gruenlandtemperatursumme """ if not valid_year(year): - self.logger.error(f"gruenlandtemperatursumme: Year for item={_database_item.id()} was {year}. This is not a valid year. Query cancelled.") + self.logger.error(f"_handle_gruenlandtemperatursumme: Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") return - current_year = datetime.date.today().year - + # define year if year == 'current': - year = current_year + year = datetime.date.today().year - year = int(year) - year_delta = current_year - year - if year_delta < 0: - self.logger.error(f"gruenlandtemperatursumme: Start time for query of item={_database_item.id()} is in future. Query cancelled.") - return + # define start_date, end_date + start_date = datetime.date(int(year), 1, 1) + end_date = datetime.date(int(year), 9, 21) - _db_addon_params = STD_REQUEST_DICT.get('gts', None) - _db_addon_params.update({'start': year_delta, 'end': year_delta, 'item': _database_item}) + # check start_date + today = datetime.date.today() + if start_date > today: + self.logger.info(f"_handle_gruenlandtemperatursumme: Start time for query of item={database_item.path()} is in future. Query cancelled.") + return - # query db and generate values - _result = self._query_item(**_db_addon_params) + # define start / end + start = (today - start_date).days + end = (today - end_date).days if end_date < today else 0 - # calculate value and return it - if _result == [[None, None]]: + # check end + if start < end: + self.logger.error(f"_handle_gruenlandtemperatursumme: End time for query of item={database_item.path()} is before start time. Query cancelled.") return - try: - gts = 0 - for entry in _result: - dt = datetime.datetime.fromtimestamp(int(entry[0]) / 1000) - if dt.month == 1: - gts += float(entry[1]) * 0.5 - elif dt.month == 2: - gts += float(entry[1]) * 0.75 - else: - gts += entry[1] - return int(round(gts, 0)) - except Exception as e: - self.logger.error(f"Error {e} occurred during calculation of gruenlandtemperatursumme with {_result=} for {_database_item.id()=}") + # get raw data as list + raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='raw') + if self.execute_debug: + self.logger.debug(f"_handle_gruenlandtemperatursumme: raw_value_list={raw_data}") - def _handle_tagesmitteltemperatur(self, _database_item: Item, count: int = None) -> list: + # calculate value + if raw_data and isinstance(raw_data, list): + if raw_data == [[None, None]]: + return + + try: + gts = 0 + for entry in raw_data: + dt = datetime.datetime.fromtimestamp(entry[0] / 1000) + if dt.month == 1: + gts += (entry[1] * 0.5) + elif dt.month == 2: + gts += (entry[1] * 0.75) + else: + gts += entry[1] + return int(round(gts, 0)) + except Exception as e: + self.logger.error(f"Error {e} occurred during calculation of gruenlandtemperatursumme with {raw_data=} for {database_item.path()=}") + + def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], method: int = 0, threshold: int = 10) -> Union[int, None]: """ - Query database for tagesmitteltemperatur + Calculate "wachstumsgradtage" for given year with temperature thershold + https://de.wikipedia.org/wiki/Wachstumsgradtag - :param _database_item: item object or item_id for which the query should be done - :param count: start of timeframe defined by number of time increments starting from now to the left (into the past) - :return: tagesmitteltemperatur + :param database_item: item object or item_id for which the query should be done + :param year: year the wachstumsgradtage should be calculated for + :param threshold: temperature in °C as threshold for evaluation + :return: wachstumsgradtage """ - start, end = count_to_start(count) - _db_addon_params = STD_REQUEST_DICT.get('tagesmittelwert_hour_days', None) - _db_addon_params.update({'item': _database_item, 'start': start, 'end': end}) + if not valid_year(year): + self.logger.error(f"_handle_wachstumsgradtage: Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") + return + + # define year + if year == 'current': + year = datetime.date.today().year + + # define start_date, end_date + start_date = datetime.date(int(year), 1, 1) + end_date = datetime.date(int(year), 9, 21) + + # check start_date + today = datetime.date.today() + if start_date > today: + self.logger.info(f"_handle_wachstumsgradtage: Start time for query of item={database_item.path()} is in future. Query cancelled.") + return + + # define start / end + start = (today - start_date).days + end = (today - end_date).days if end_date < today else 0 + + # check end + if start < end: + self.logger.error(f"_handle_wachstumsgradtage: End time for query of item={database_item.path()} is before start time. Query cancelled.") + return + + # get raw data as list + raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='minmax') + if self.execute_debug: + self.logger.debug(f"_handle_wachstumsgradtage: raw_value_list={raw_data}") + + # calculate value + if raw_data and isinstance(raw_data, list): + if raw_data == [[None, None]]: + return + + # Die Berechnung des einfachen Durchschnitts. + if method == 0: + self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Berechnung des einfachen Durchschnitts'.") + wgt = 0 + for entry in raw_data: + timestamp = entry[0] + min_val = entry[1] + max_val = entry[2] + wgt += ((min_val + max(30, max_val) / 2) - threshold) + return wgt + + # Die modifizierte Berechnung des einfachen Durchschnitts. + elif method == 1: + self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Modifizierte Berechnung des einfachen Durchschnitts'.") + wgt = 0 + for entry in raw_data: + timestamp = entry[0] + min_val = entry[1] + max_val = entry[2] + wgt += ((min(threshold, min_val) + max(30, max_val) / 2) - threshold) + return wgt + else: + self.logger.info(f"Method for 'Wachstumsgradtag' Calculation not defined.'") + + def _prepare_temperature_list(self, database_item: Item, start: int, end: int = 0, ignore_value=None, version: str = 'hour') -> list: + + self.logger.debug(f"_prepare_temperature_list called with {database_item=}, {start=}, {end=}, {ignore_value=}, {version=}") + + def _create_temp_dict() -> dict: + """create dict based on database query result like {'date1': {'hour1': [temp values], 'hour2': [temp values], ...}, 'date2': {'hour1': [temp values], 'hour2': [temp values], ...}, ...}""" + _temp_dict = {} + for _entry in raw_value_list: + dt = datetime.datetime.utcfromtimestamp(_entry[0] / 1000) + date = dt.strftime('%Y-%m-%d') + hour = dt.strftime('%H') + if date not in _temp_dict: + _temp_dict[date] = {} + if hour not in _temp_dict[date]: + _temp_dict[date][hour] = [] + _temp_dict[date][hour].append(_entry[1]) + return _temp_dict + + def _calculate_hourly_average(): + """ calculate hourly average based on list of temperatures and update temp_dict""" + for _date in temp_dict: + for hour in temp_dict[_date]: + hour_raw_value_list = temp_dict[_date][hour] + # hour_value = round(sum(hour_raw_value_list) / len(hour_raw_value_list), 1) # Durchschnittsbildung über alle Werte der Liste + hour_value = hour_raw_value_list[0] # Nehme den ersten Wert der Liste als Stundenwert (kommt am nächsten an die Definition, den Wert exakt zur vollen Stunden zu nehmen) + temp_dict[_date][hour] = [hour_value] + + def _create_list_timestamp_avgtemp() -> list: + """Create list of list with [[timestamp1, value1], [timestamp2, value2], ...] based on temp_dict""" + _temp_list = [] + for _date in temp_dict: + + # wenn mehr als 20 Stundenwerte vorliegen, berechne den Tagesdurchschnitt über alle Werte + if len(temp_dict[_date]) >= 20: + _values = sum(list(temp_dict[_date].values()), []) + _values_avg = round(sum(_values) / len(_values), 1) + + # wenn für 00, 06, 12 und 18 Uhr Werte vorliegen, berechne den Tagesdurchschnitt über diese Werte + elif '00' in temp_dict[_date] and '06' in temp_dict[_date] and '12' in temp_dict[_date] and '18' in temp_dict[_date]: + _values_avg = round((temp_dict[_date]['00'][0] + temp_dict[_date]['06'][0] + temp_dict[_date]['12'][0] + temp_dict[_date]['18'][0]) / 4, 1) + + # sonst berechne den Tagesdurchschnitt über alle Werte + else: + _values = sum(list(temp_dict[_date].values()), []) + _values_avg = round(sum(_values) / len(_values), 1) + + _timestamp = datetime_to_timestamp(datetime.datetime.strptime(_date, '%Y-%m-%d')) + _temp_list.append([_timestamp, _values_avg]) + return _temp_list + + def _create_list_timestamp_minmaxtemp() -> list: + """Create list of list with [[timestamp1, min value1, max_value1], [timestamp2, min value2, max_value2], ...] based on temp_dict""" + _temp_list = [] + for _date in temp_dict: + _timestamp = datetime_to_timestamp(datetime.datetime.strptime(_date, '%Y-%m-%d')) + _day_values = sum(list(temp_dict[_date].values()), []) + _temp_list.append([_timestamp, min(_day_values), max(_day_values)]) + return _temp_list + + if version == 'hour': + raw_value_list = self._query_item(func='avg', item=database_item, timeframe='day', start=start, end=end, group='hour', ignore_value=ignore_value) + self.logger.debug(f"{raw_value_list=}") + + # create nested dict with temps + temp_dict = _create_temp_dict() + + # create list of list like database query response + temp_list = _create_list_timestamp_avgtemp() + self.logger.debug(f"{temp_list=}") + return temp_list + + elif version == 'raw': + raw_value_list = self._query_item(func='raw', item=database_item, timeframe='day', start=start, end=end, ignore_value=ignore_value) + self.logger.debug(f"{raw_value_list=}") + + # create nested dict with temps + temp_dict = _create_temp_dict() + self.logger.debug(f"raw: {temp_dict=}") + + # calculate 'tagesdurchschnitt' and create list of list like database query response + _calculate_hourly_average() + self.logger.debug(f"raw: {temp_dict=}") + + # create list of list like database query response + temp_list = _create_list_timestamp_avgtemp() + self.logger.debug(f"{temp_list=}") + return temp_list + + elif version == 'minmax': + raw_value_list = self._query_item(func='raw', item=database_item, timeframe='day', start=start, end=end, ignore_value=ignore_value) + self.logger.debug(f"{raw_value_list=}") + + # create nested dict with temps + temp_dict = _create_temp_dict() + self.logger.debug(f"raw: {temp_dict=}") + + # create list of list like database query response + temp_list = _create_list_timestamp_minmaxtemp() + self.logger.debug(f"{temp_list=}") + return temp_list - return self._query_item(**_db_addon_params)[0][1] + else: + return [] def _create_due_items(self) -> list: """ @@ -1273,32 +1746,27 @@ def _create_due_items(self) -> list: """ + # täglich zu berechnende Items zur Action Liste hinzufügen _todo_items = set() - _todo_items.update(set(self._daily_items)) + _todo_items.update(set(self._daily_items())) self.current_values[DAY] = {} self.previous_values[DAY] = {} - # wenn jetzt Wochentag = Montag ist, werden auch die wöchentlichen Items berechnet + # wenn Wochentag == Montag, werden auch die wöchentlichen Items berechnet if self.shtime.now().hour == 0 and self.shtime.now().minute == 0 and self.shtime.weekday(self.shtime.today()) == 1: - _todo_items.update(set(self._weekly_items)) - # self.wochenwert_dict = {} - # self.vorwochenendwert_dict = {} + _todo_items.update(set(self._weekly_items())) self.current_values[WEEK] = {} self.previous_values[WEEK] = {} - # wenn jetzt der erste Tage eines Monates ist, werden auch die monatlichen Items berechnet + # wenn der erste Tage eines Monates ist, werden auch die monatlichen Items berechnet if self.shtime.now().hour == 0 and self.shtime.now().minute == 0 and self.shtime.now().day == 1: - _todo_items.update(set(self._monthly_items)) - # self.monatswert_dict = {} - # self.vormonatsendwert_dict = {} + _todo_items.update(set(self._monthly_items())) self.current_values[MONTH] = {} self.previous_values[MONTH] = {} - # wenn jetzt der erste Tage des ersten Monates eines Jahres ist, werden auch die jährlichen Items berechnet + # wenn der erste Tage des ersten Monates eines Jahres ist, werden auch die jährlichen Items berechnet if self.shtime.now().hour == 0 and self.shtime.now().minute == 0 and self.shtime.now().day == 1 and self.shtime.now().month == 1: - _todo_items.update(set(self._yearly_items)) - # self.jahreswert_dict = {} - # self.vorjahresendwert_dict = {} + _todo_items.update(set(self._yearly_items())) self.current_values[YEAR] = {} self.previous_values[YEAR] = {} @@ -1418,7 +1886,7 @@ def _get_oldest_log(self, item: Item) -> int: self.item_cache[item]['oldest_log'] = _oldest_log if self.prepare_debug: - self.logger.debug(f"_get_oldest_log for item {item.id()} = {_oldest_log}") + self.logger.debug(f"_get_oldest_log for item {item.path()} = {_oldest_log}") return _oldest_log @@ -1450,10 +1918,10 @@ def _get_oldest_value(self, item: Item) -> Union[int, float, bool]: validity = True elif i == 10: validity = True - self.logger.error(f"oldest_value for item {item.id()} could not be read; value is set to -999999999") + self.logger.error(f"oldest_value for item {item.path()} could not be read; value is set to -999999999") if self.prepare_debug: - self.logger.debug(f"_get_oldest_value for item {item.id()} = {_oldest_value}") + self.logger.debug(f"_get_oldest_value for item {item.path()} = {_oldest_value}") return _oldest_value @@ -1465,19 +1933,20 @@ def _get_itemid(self, item: Item) -> int: :return: id of the item within the database """ - # self.logger.debug(f"_get_itemid called with item={item.id()}") + # self.logger.debug(f"_get_itemid called with item={item.path()}") _item_id = self.item_cache.get(item, {}).get('id', None) + if _item_id is None: - row = self._read_item_table(item) - if row: - if len(row) > 0: - _item_id = int(row[0]) - if item not in self.item_cache: - self.item_cache[item] = {} - self.item_cache[item]['id'] = _item_id + row = self._read_item_table(item_path=str(item.path())) + if row and len(row) > 0: + _item_id = int(row[0]) + if item not in self.item_cache: + self.item_cache[item] = {} + self.item_cache[item]['id'] = _item_id + return _item_id - def _get_itemid_for_query(self, item: Item) -> Union[int, None]: + def _get_itemid_for_query(self, item: Union[Item, str, int]) -> Union[int, None]: """ Get DB item id for query @@ -1495,89 +1964,7 @@ def _get_itemid_for_query(self, item: Item) -> Union[int, None]: item_id = None return item_id - def _handle_query_result(self, query_result: Union[list, None]) -> list: - """ - Handle query result containing list - - :param query_result: list of query result with [[value, value], [value, value] for regular result, [[None, None]] for errors, [[0,0]] for 'no values for requested timeframe' - - """ - - # if query delivers None, abort - if query_result is None: - # if query delivers None, abort - self.logger.error(f"Error occurred during _query_item. Aborting...") - _result = [[None, None]] - elif len(query_result) == 0: - _result = [[0, 0]] - self.logger.info(f" No values for item in requested timeframe in database found.") - else: - _result = [] - for element in query_result: - timestamp = element[0] - value = element[1] - if timestamp and value is not None: - _result.append([timestamp, round(value, 1)]) - if not _result: - _result = [[None, None]] - - # if self.prepare_debug: - # self.logger.debug(f"_handle_query_result: {_result=}") - - return _result - - def _consumption_calc(self, item, timeframe: str, start: int, end: int) -> Union[float, None]: - """ - Handle query for Verbrauch - - :param item: item, the query should be done for - :param timeframe: timeframe as week, month, year - :param start: beginning of timeframe - :param start: end of timeframe - - """ - - if self.prepare_debug: - self.logger.debug(f"_consumption_calc called with {item=},{timeframe=},{start=},{end=}") - - _result = None - - # get value for end and check it; - value_end = self._query_item(func='max', item=item, timeframe=timeframe, start=end, end=end)[0][1] - if self.prepare_debug: - self.logger.debug(f"_consumption_calc {value_end=}") - - if value_end is None: # if None (Error) return - return - elif value_end == 0: # wenn die Query "None" ergab, was wiederum bedeutet, dass zum Abfragezeitpunkt keine Daten vorhanden sind, ist der value hier gleich 0 → damit der Verbrauch für die Abfrage auch Null - _result = 0 - else: - # get value for start and check it; - # value_start = self._query_item(func='max', item=item, timeframe=timeframe, start=start, end=start)[0][1] - value_start = self._query_item(func='min', item=item, timeframe=timeframe, start=end, end=end)[0][1] - if self.prepare_debug: - self.logger.debug(f"_consumption_calc {value_start=}") - - if value_start is None: # if None (Error) return - return - - # ToDo: Prüfen, unter welchen Bedingungen value_start == 0 bzw. wie man den nächsten Eintrag nutzt. - if value_start == 0: # wenn der Wert zum Startzeitpunkt 0 ist, gab es dort keinen Eintrag (also keinen Verbrauch), dann frage den nächsten Eintrag in der DB ab. - self.logger.info(f"No DB Entry found for requested start date. Looking for next DB entry.") - # value_start = self._handle_query_result(self._query_log_next(item=item, timeframe=timeframe, timedelta=start))[0][1] - value_start = self._handle_query_result(self._query_item(func='next', item=item, timeframe=timeframe, start=start))[0][1] - if self.prepare_debug: - self.logger.debug(f"_consumption_calc: next available value is {value_start=}") - - if value_end is not None and value_start is not None: - _result = round(value_end - value_start, 1) - - if self.prepare_debug: - self.logger.debug(f"_consumption_calc: {_result=} for {item=},{timeframe=},{start=},{end=}") - - return _result - - def _query_item(self, func: str, item, timeframe: str, start: int = None, end: int = 0, group: str = None, group2: str = None, ignore_value=None) -> list: + def _query_item(self, func: str, item: Item, timeframe: str, start: int = None, end: int = 0, group: str = None, group2: str = None, ignore_value=None) -> list: """ Do diverse checks of input, and prepare query of log by getting item_id, start / end in timestamp etc. @@ -1593,29 +1980,62 @@ def _query_item(self, func: str, item, timeframe: str, start: int = None, end: i :return: query response / list for value pairs [[None, None]] for errors, [[0,0]] for """ + def _handle_query_result(query_result) -> list: + """ + Handle query result containing list + """ + + # if query delivers None, abort + if query_result is None: + # if query delivers None, abort + self.logger.error(f"Error occurred during _query_item. Aborting...") + _result = [[None, None]] + elif len(query_result) == 0: + _result = [[0, 0]] + self.logger.info(f" No values for item in requested timeframe in database found.") + else: + _result = [] + for element in query_result: + timestamp = element[0] + value = element[1] + if timestamp and value is not None: + _result.append([timestamp, round(value, 1)]) + if not _result: + _result = [[None, None]] + + return _result + if self.prepare_debug: - self.logger.debug(f"_query_item called with {func=}, item={item.id()}, {timeframe=}, {start=}, {end=}, {group=}, {group2=}, {ignore_value=}") + self.logger.debug(f"_query_item called with {func=}, item={item.path()}, {timeframe=}, {start=}, {end=}, {group=}, {group2=}, {ignore_value=}") - # SET DEFAULT RESULT + # set default result result = [[None, None]] - # CHECK CORRECTNESS OF TIMEFRAME - if timeframe not in ["year", "month", "week", "day"]: - self.logger.error(f"_query_item: Requested {timeframe=} for item={item.id()} not defined; Need to be year, month, week, day'. Query cancelled.") + # check correctness of timeframe + if timeframe not in ALLOWED_QUERY_TIMEFRAMES: + self.logger.error(f"_query_item: Requested {timeframe=} for item={item.path()} not defined; Need to be 'year' or 'month' or 'week' or 'day' or 'hour''. Query cancelled.") + return result + + # check start / end for being int + if isinstance(start, str) and start.isdigit(): + start = int(start) + if isinstance(end, str) and end.isdigit(): + end = int(end) + if not isinstance(start, int) and not isinstance(end, int): return result - # CHECK CORRECTNESS OF START / END + # check correctness of start / end if start < end: - self.logger.warning(f"_query_item: Requested {start=} for item={item.id()} is not valid since {start=} < {end=}. Query cancelled.") + self.logger.warning(f"_query_item: Requested {start=} for item={item.path()} is not valid since {start=} < {end=}. Query cancelled.") return result - # DEFINE ITEM_ID - item_id = self._get_itemid_for_query(item) + # define item_id + item_id = self._get_itemid(item) if not item_id: - self.logger.error(f"_query_item: ItemId for item={item.id()} not found. Query cancelled.") + self.logger.error(f"_query_item: ItemId for item={item.path()} not found. Query cancelled.") return result - # DEFINE START AND END OF QUERY AS TIMESTAMP IN MICROSECONDS + # define start and end of query as timestamp in microseconds ts_start, ts_end = get_start_end_as_timestamp(timeframe, start, end) oldest_log = int(self._get_oldest_log(item)) @@ -1625,24 +2045,24 @@ def _query_item(self, func: str, item, timeframe: str, start: int = None, end: i if self.prepare_debug: self.logger.debug(f"_query_item: Requested {timeframe=} with {start=} and {end=} resulted in start being timestamp={ts_start} / {timestamp_to_timestring(ts_start)} and end being timestamp={ts_end} / {timestamp_to_timestring(ts_end)}") - # CHECK IF VALUES FOR END TIME AND START TIME ARE IN DATABASE + # check if values for end time and start time are in database if ts_end < oldest_log: # (Abfrage abbrechen, wenn Endzeitpunkt in UNIX-timestamp der Abfrage kleiner (und damit jünger) ist, als der UNIX-timestamp des ältesten Eintrages) - self.logger.info(f"_query_item: Requested end time timestamp={ts_end} / {timestamp_to_timestring(ts_end)} of query for Item='{item.id()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Query cancelled.") + self.logger.info(f"_query_item: Requested end time timestamp={ts_end} / {timestamp_to_timestring(ts_end)} of query for Item='{item.path()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Query cancelled.") return result if ts_start < oldest_log: if not self.use_oldest_entry: - self.logger.info(f"_query_item: Requested start time timestamp={ts_start} / {timestamp_to_timestring(ts_start)} of query for Item='{item.id()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Query cancelled.") + self.logger.info(f"_query_item: Requested start time timestamp={ts_start} / {timestamp_to_timestring(ts_start)} of query for Item='{item.path()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Query cancelled.") return result else: - self.logger.info(f"_query_item: Requested start time timestamp={ts_start} / {timestamp_to_timestring(ts_start)} of query for Item='{item.id()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Oldest available entry will be used.") + self.logger.info(f"_query_item: Requested start time timestamp={ts_start} / {timestamp_to_timestring(ts_start)} of query for Item='{item.path()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Oldest available entry will be used.") ts_start = oldest_log - log = self._query_log_timestamp(func=func, item_id=item_id, ts_start=ts_start, ts_end=ts_end, group=group, group2=group2, ignore_value=ignore_value) - result = self._handle_query_result(log) + query_params = {'func': func, 'item_id': item_id, 'ts_start': ts_start, 'ts_end': ts_end, 'group': group, 'group2': group2, 'ignore_value': ignore_value} + result = _handle_query_result(self._query_log_timestamp(**query_params)) if self.prepare_debug: - self.logger.debug(f"_query_item: value for item={item.id()} with {timeframe=}, {func=}: {result}") + self.logger.debug(f"_query_item: value for item={item.path()} with {timeframe=}, {func=}: {result}") return result @@ -1661,7 +2081,7 @@ def _init_cache_dicts(self) -> None: MONTH: {}, YEAR: {} } - + self.previous_values = { DAY: {}, WEEK: {}, @@ -1706,7 +2126,7 @@ def _work_item_queue_thread_shutdown(self): self.work_item_queue_thread = None ############################## - # DB Query Preparation + # Database Query Preparation ############################## def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: int, group: str = None, group2: str = None, ignore_value=None) -> Union[list, None]: @@ -1725,11 +2145,11 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i """ - # DO DEBUG LOG + # do debug log if self.prepare_debug: self.logger.debug(f"_query_log_timestamp: Called with {func=}, {item_id=}, {ts_start=}, {ts_end=}, {group=}, {group2=}, {ignore_value=}") - # DEFINE GENERIC QUERY PARTS + # define query parts _select = { 'avg': 'time, ROUND(AVG(val_num * duration) / AVG(duration), 1) as value ', 'avg1': 'time, ROUND(AVG(value), 1) as value FROM (SELECT time, ROUND(AVG(val_num), 1) as value ', @@ -1743,7 +2163,8 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i 'sum_avg': 'time, ROUND(SUM(value), 1) as value FROM (SELECT time, ROUND(AVG(val_num * duration) / AVG(duration), 1) as value ', 'sum_min_neg': 'time, ROUND(SUM(value), 1) as value FROM (SELECT time, IF(min(val_num) < 0, ROUND(MIN(val_num), 1), 0) as value ', 'diff_max': 'time, value1 - LAG(value1) OVER (ORDER BY time) AS value FROM (SELECT time, ROUND(MAX(val_num), 1) as value1 ', - 'next': 'time, val_num as value ' + 'next': 'time, val_num as value ', + 'raw': 'time, val_num as value ' } _table_alias = { @@ -1760,6 +2181,7 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i 'sum_min_neg': ') AS table1 ', 'diff_max': ') AS table1 ', 'next': '', + 'raw': '', } _order = "time DESC LIMIT 1 " if func == "next" else "time ASC " @@ -1768,29 +2190,25 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i _db_table = 'log ' - # DEFINE mySQL QUERY PARTS _group_by_sql = { - "year": "GROUP BY YEAR(FROM_UNIXTIME(time/1000)) ", - "month": "GROUP BY YEAR(FROM_UNIXTIME(time/1000)), MONTH(FROM_UNIXTIME(time/1000)) ", - "week": "GROUP BY YEARWEEK(FROM_UNIXTIME(time/1000), 5) ", - "day": "GROUP BY DATE(FROM_UNIXTIME(time/1000)) ", - "hour": "GROUP BY DATE(FROM_UNIXTIME(time/1000)), HOUR(FROM_UNIXTIME(time/1000)) ", + "year": "GROUP BY YEAR(FROM_UNIXTIME(time/1000)) ", + "month": "GROUP BY FROM_UNIXTIME((time/1000),'%Y%m') ", + "week": "GROUP BY YEARWEEK(FROM_UNIXTIME(time/1000), 5) ", + "day": "GROUP BY DATE(FROM_UNIXTIME(time/1000)) ", + "hour": "GROUP BY FROM_UNIXTIME((time/1000),'%Y%m%d%H') ", None: '' } - # DEFINE SQLITE QUERY PARTS _group_by_sqlite = { - "year": "GROUP BY strftime('%Y', date((time/1000),'unixepoch')) ", + "year": "GROUP BY strftime('%Y', date((time/1000),'unixepoch')) ", "month": "GROUP BY strftime('%Y%m', date((time/1000),'unixepoch')) ", - "week": "GROUP BY strftime('%Y%W', date((time/1000),'unixepoch')) ", - "day": "GROUP BY date((time/1000),'unixepoch') ", - "hour": "GROUP BY date((time/1000),'unixepoch'), strftime('%H', date((time/1000),'unixepoch')) ", + "week": "GROUP BY strftime('%Y%W', date((time/1000),'unixepoch')) ", + "day": "GROUP BY date((time/1000),'unixepoch') ", + "hour": "GROUP BY strftime('%Y%m%d%H', datetime((time/1000),'unixepoch')) ", None: '' } - ###################################### - - # SELECT QUERY PARTS DEPENDING IN DB DRIVER + # select query parts depending in db driver if self.db_driver.lower() == 'pymysql': _group_by = _group_by_sql elif self.db_driver.lower() == 'sqlite3': @@ -1799,71 +2217,58 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i self.logger.error('DB Driver unknown') return - # CHECK CORRECTNESS OF FUNC + # check correctness of func if func not in _select: self.logger.error(f"_query_log_timestamp: Requested {func=} for {item_id=} not defined. Query cancelled.") return - # CHECK CORRECTNESS OF GROUP AND GROUP2 + # check correctness of group and group2 if group not in _group_by: self.logger.error(f"_query_log_timestamp: Requested {group=} for item={item_id=} not defined. Query cancelled.") return if group2 not in _group_by: - self.logger.error(f"_query_log_timestamp: Requested {group=} for item={item_id=} not defined. Query cancelled.") + self.logger.error(f"_query_log_timestamp: Requested {group2=} for item={item_id=} not defined. Query cancelled.") return - # HANDLE IGNORE VALUES + # handle ignore values if func in ['min', 'max', 'max1', 'sum_max', 'sum_avg', 'sum_min_neg', 'diff_max']: # extend _where statement for excluding boolean values == 0 for defined functions _where = f'{_where}AND val_bool = 1 ' if ignore_value: # if value to be ignored are defined, extend _where statement _where = f'{_where}AND val_num != {ignore_value} ' - # SET PARAMS - params = { - 'item_id': item_id, - 'ts_start': ts_start - } - + # set params + params = {'item_id': item_id, 'ts_start': ts_start} if func != "next": - params['ts_end'] = ts_end + params.update({'ts_end': ts_end}) - # ASSEMBLE QUERY + # assemble query query = f"SELECT {_select[func]}FROM {_db_table}WHERE {_where}{_group_by[group]}ORDER BY {_order}{_table_alias[func]}{_group_by[group2]}".strip() if self.db_driver.lower() == 'sqlite3': query = query.replace('IF', 'IIF') - # DO DEBUG LOG + # do debug log if self.prepare_debug: self.logger.debug(f"_query_log_timestamp: {query=}, {params=}") - # REQUEST DATABASE AND RETURN RESULT + # request database and return result return self._fetchall(query, params) - def _read_log_all(self, item): + def _read_log_all(self, item_id: int): """ Read the oldest log record for given item - :param item: Item to read the record for - :type item: item - - :return: Log record for Item + :param item_id: item_id to read the record for + :return: Log record for item_id """ if self.prepare_debug: - self.logger.debug(f"_read_log_all: Called for item={item}") - - # DEFINE ITEM_ID - create item_id from item or string input of item_id and break, if not given - item_id = self._get_itemid_for_query(item) - if not item_id: - self.logger.error(f"_read_log_all: ItemId for item={item.id()} not found. Query cancelled.") - return + self.logger.debug(f"_read_log_all: Called for {item_id=}") - if item_id: - query = "SELECT * FROM log WHERE (item_id = :item_id) AND (time = None OR 1 = 1)" - params = {'item_id': item_id} - result = self._fetchall(query, params) - return result + query = "SELECT * FROM log WHERE (item_id = :item_id) AND (time = None OR 1 = 1)" + params = {'item_id': item_id} + result = self._fetchall(query, params) + return result def _read_log_oldest(self, item_id: int, cur=None) -> int: """ @@ -1897,12 +2302,12 @@ def _read_log_timestamp(self, item_id: int, timestamp: int, cur=None) -> Union[l query = "SELECT * FROM log WHERE item_id = :item_id AND time = :timestamp;" return self._fetchall(query, params, cur=cur) - def _read_item_table(self, item): + def _read_item_table(self, item_id: int = None, item_path: str = None): """ Read item table - :param item: name or Item_id of the item within the database - :type item: item + :param item_id: unique ID for item within database + :param item_path: item_path for Item within the database :return: Data for the selected item :rtype: tuple @@ -1911,14 +2316,15 @@ def _read_item_table(self, item): columns_entries = ('id', 'name', 'time', 'val_str', 'val_num', 'val_bool', 'changed') columns = ", ".join(columns_entries) - if isinstance(item, Item): - query = f"SELECT {columns} FROM item WHERE name = '{str(item.id())}'" - return self._fetchone(query) + if item_id is None and item_path is None: + return - elif isinstance(item, str) and item.isdigit(): - item = int(item) - query = f"SELECT {columns} FROM item WHERE id = {item}" - return self._fetchone(query) + if item_id: + query = f"SELECT {columns} FROM item WHERE id = {item_id}" + else: + query = f"SELECT {columns} FROM item WHERE name = '{item_path}'" + + return self._fetchone(query) def _get_db_version(self) -> str: """ @@ -1945,29 +2351,28 @@ def _get_db_net_read_timeout(self) -> str: return self._fetchone(query) ############################## - # Database specific stuff + # Database Queries ############################## - def _execute(self, query: str, params: dict = None, cur=None): + def _execute(self, query: str, params: dict = None, cur=None) -> list: if params is None: params = {} return self._query(self._db.execute, query, params, cur) - def _fetchone(self, query: str, params: dict = None, cur=None): + def _fetchone(self, query: str, params: dict = None, cur=None) -> list: if params is None: params = {} return self._query(self._db.fetchone, query, params, cur) - def _fetchall(self, query: str, params: dict = None, cur=None): + def _fetchall(self, query: str, params: dict = None, cur=None) -> list: if params is None: params = {} - tuples = self._query(self._db.fetchall, query, params, cur) - return None if tuples is None else list(tuples) + return self._query(self._db.fetchall, query, params, cur) - def _query(self, fetch, query: str, params: dict = None, cur=None): + def _query(self, fetch, query: str, params: dict = None, cur=None) -> list: if params is None: params = {} @@ -2001,12 +2406,13 @@ def _query(self, fetch, query: str, params: dict = None, cur=None): ############################## -# Helper functions +# Helper functions ############################## def params_to_dict(string: str) -> Union[dict, None]: - """ Parse a string with named arguments and comma separation to dict; (e.g. string = 'year=2022, month=12') + """ + Parse a string with named arguments and comma separation to dict; (e.g. string = 'year=2022, month=12') """ try: @@ -2082,13 +2488,14 @@ def convert_timeframe(timeframe: str) -> str: 'jahr': 'year', 'vorjahreszeitraum': 'day', 'jahreszeitraum': 'day', + 'h': 'hour', 'd': 'day', 'w': 'week', 'm': 'month', 'y': 'year' } - return convertion.get(timeframe, None) + return convertion.get(timeframe) def convert_duration(timeframe: str, window_dur: str) -> int: @@ -2273,602 +2680,50 @@ def datetime_to_timestamp(dt: datetime) -> int: return int(dt.replace(tzinfo=datetime.timezone.utc).timestamp()) -def check_substring_in_str(lookfor: Union[str, list], target: str) -> bool: - for entry in lookfor: - if isinstance(entry, str): - if entry in target: - return True - elif isinstance(entry, list): - result = True - for element in entry: - result = result and element in target # einmal False setzt alles auf False - if result: - return True - return False - - -def onchange_attribute(db_addon_fct) -> bool: - """ - Return True if attribute indicates Item to be calculated on-change - - ONCHANGE_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr', - 'minmax_heute_min', 'minmax_heute_max', - 'minmax_woche_min', 'minmax_woche_max', - 'minmax_monat_min', 'minmax_monat_max', - 'minmax_jahr_min', 'minmax_jahr_max'] - - """ - return True if not any(substring in db_addon_fct for substring in ['minus', 'serie', 'last']) else False - - -def daily_attribute(db_addon_fct) -> bool: - """ - Return True if attribute indicates Item to be calculated daily" - """ - return True if check_substring_in_str(['heute_minus', 'last_', 'jahreszeitraum', ['serie', 'tag'], ['serie', 'stunde']], db_addon_fct) else False - - -def weekly_attribute(db_addon_fct) -> bool: - """ - Return True if attribute indicates Item to be calculated weekly" - """ - return True if check_substring_in_str(['woche_minus', ['serie', 'woche']], db_addon_fct) else False - - -def monthly_attribute(db_addon_fct) -> bool: - """ - Return True if attribute indicates Item to be calculated daily" - """ - return True if check_substring_in_str(['monat_minus', ['serie', 'monat']], db_addon_fct) else False - +def to_int(arg) -> Union[int, None]: + try: + return int(arg) + except (ValueError, TypeError): + return None -def yearly_attribute(db_addon_fct) -> bool: - """ - Return True if attribute indicates Item to be calculated yearly" - """ - return True if check_substring_in_str(['jahr_minus', ['serie', 'jahr']], db_addon_fct) else False - - -STD_REQUEST_DICT = { - 'serie_minmax_monat_min_15m': {'func': 'min', 'timeframe': 'month', 'start': 15, 'end': 0, 'group': 'month'}, - 'serie_minmax_monat_max_15m': {'func': 'max', 'timeframe': 'month', 'start': 15, 'end': 0, 'group': 'month'}, - 'serie_minmax_monat_avg_15m': {'func': 'avg', 'timeframe': 'month', 'start': 15, 'end': 0, 'group': 'month'}, - 'serie_minmax_woche_min_30w': {'func': 'min', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, - 'serie_minmax_woche_max_30w': {'func': 'max', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, - 'serie_minmax_woche_avg_30w': {'func': 'avg', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, - 'serie_minmax_tag_min_30d': {'func': 'min', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, - 'serie_minmax_tag_max_30d': {'func': 'max', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, - 'serie_minmax_tag_avg_30d': {'func': 'avg', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, - 'serie_verbrauch_tag_30d': {'func': 'diff_max', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, - 'serie_verbrauch_woche_30w': {'func': 'diff_max', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, - 'serie_verbrauch_monat_18m': {'func': 'diff_max', 'timeframe': 'month', 'start': 18, 'end': 0, 'group': 'month'}, - 'serie_zaehlerstand_tag_30d': {'func': 'max', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, - 'serie_zaehlerstand_woche_30w': {'func': 'max', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, - 'serie_zaehlerstand_monat_18m': {'func': 'max', 'timeframe': 'month', 'start': 18, 'end': 0, 'group': 'month'}, - 'serie_waermesumme_monat_24m': {'func': 'sum_max', 'timeframe': 'month', 'start': 24, 'end': 0, 'group': 'day', 'group2': 'month'}, - 'serie_kaeltesumme_monat_24m': {'func': 'sum_max', 'timeframe': 'month', 'start': 24, 'end': 0, 'group': 'day', 'group2': 'month'}, - 'serie_tagesmittelwert': {'func': 'max', 'timeframe': 'year', 'start': 0, 'end': 0, 'group': 'day'}, - 'serie_tagesmittelwert_stunde_0d': {'func': 'avg1', 'timeframe': 'day', 'start': 0, 'end': 0, 'group': 'hour', 'group2': 'day'}, - 'serie_tagesmittelwert_tag_stunde_30d': {'func': 'avg1', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'hour', 'group2': 'day'}, - 'waermesumme_year_month': {'func': 'sum_max', 'timeframe': 'day', 'start': None, 'end': None, 'group': 'day', 'group2': None}, - 'kaltesumme_year_month': {'func': 'sum_min_neg', 'timeframe': 'day', 'start': None, 'end': None, 'group': 'day', 'group2': None}, - 'gts': {'func': 'max', 'timeframe': 'year', 'start': None, 'end': None, 'group': 'day'}, - } -############################## -# Backup -############################## -# -# def _delta_value(self, item, time_str_1, time_str_2): -# """ Computes a difference of values on 2 points in time for an item -# -# :param item: Item, for which query should be done -# :param time_str_1: time sting as per database-Plugin for newer point in time (e.g.: 200i) -# :param time_str_2: Zeitstring gemäß database-Plugin for older point in time(e.g.: 400i) -# """ -# -# time_since_oldest_log = self._time_since_oldest_log(item) -# end = int(time_str_1[0:len(time_str_1) - 1]) -# -# if time_since_oldest_log > end: -# # self.logger.debug(f'_delta_value: fetch DB with {item.id()}.db(max, {time_str_1}, {time_str_1})') -# value_1 = self._db_plugin._single('max', time_str_1, time_str_1, item.id()) -# -# # self.logger.debug(f'_delta_value: fetch DB with {item.id()}.db(max, {time_str_2}, {time_str_2})') -# value_2 = self._db_plugin._single('max', time_str_2, time_str_2, item.id()) -# -# if value_1 is not None: -# if value_2 is None: -# self.logger.info(f'No entries for Item {item.id()} in DB found for requested enddate {time_str_1}; try to use oldest entry instead') -# value_2 = self._get_oldest_value(item) -# if value_2 is not None: -# value = round(value_1 - value_2, 2) -# # self.logger.debug(f'_delta_value for item={item.id()} with time_str_1={time_str_1} and time_str_2={time_str_2} is {value}') -# return value -# else: -# self.logger.info(f'_delta_value for item={item.id()} using time_str_1={time_str_1} is older as oldest_entry. Therefore no DB request initiated.') -# -# def _single_value(self, item, time_str_1, func='max'): -# """ Gets value at given point im time from database -# -# :param item: item, for which query should be done -# :param time_str_1: time sting as per database-Plugin for point in time (e.g.: 200i) -# :param func: function of database plugin -# """ -# -# # value = None -# # value = item.db(func, time_str_1, time_str_1) -# value = self._db_plugin._single(func, time_str_1, time_str_1, item.id()) -# if value is None: -# self.logger.info(f'No entries for Item {item.id()} in DB found for requested end {time_str_1}; try to use oldest entry instead') -# value = int(self._get_oldest_value(item)) -# # self.logger.debug(f'_single_value for item={item.id()} with time_str_1={time_str_1} is {value}') -# return value -# -# def _connect_to_db(self, host=None, user=None, password=None, db=None): -# """ Connect to DB via pymysql -# """ -# -# if not host: -# host = self.connection_data[0].split(':', 1)[1] -# if not user: -# user = self.connection_data[1].split(':', 1)[1] -# if not password: -# password = self.connection_data[2].split(':', 1)[1] -# if not db: -# db = self.connection_data[3].split(':', 1)[1] -# port = self.connection_data[4].split(':', 1)[1] -# -# try: -# connection = pymysql.connect(host=host, user=user, password=password, db=db, charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor) -# except Exception as e: -# self.logger.error(f"Connection to Database failed with error {e}!.") -# return -# else: -# return connection -# -# -# def _get_itemid_via_db_plugin(self, item): -# """ Get item_id of item out of dict or request it from db via database plugin and put it into dict -# """ -# -# # self.logger.debug(f"_get_itemid called for item={item}") -# -# _item_id = self.itemid_dict.get(item, None) -# if _item_id is None: -# _item_id = self._db_plugin.id(item) -# self.itemid_dict[item] = _item_id -# -# return _item_id -# -# def _get_time_strs(self, key, x): -# """ Create timestrings for database query depending in key with -# -# :param key: key for getting the time strings -# :param x: time difference as increment -# :return: tuple of timestrings (timestr closer to now, timestr more in the past) -# -# """ -# -# self.logger.debug(f"_get_time_strs called with key={key}, x={x}") -# -# if key == 'heute': -# _time_str_1 = self._time_str_heute_minus_x(x - 1) -# _time_str_2 = self._time_str_heute_minus_x(x) -# elif key == 'woche': -# _time_str_1 = self._time_str_woche_minus_x(x - 1) -# _time_str_2 = self._time_str_woche_minus_x(x) -# elif key == 'monat': -# _time_str_1 = self._time_str_monat_minus_x(x - 1) -# _time_str_2 = self._time_str_monat_minus_x(x) -# elif key == 'jahr': -# _time_str_1 = self._time_str_jahr_minus_x(x - 1) -# _time_str_2 = self._time_str_jahr_minus_x(x) -# elif key == 'vorjahreszeitraum': -# _time_str_1 = self._time_str_heute_minus_jahre_x(x + 1) -# _time_str_2 = self._time_str_jahr_minus_x(x+1) -# else: -# _time_str_1 = None -# _time_str_2 = None -# -# # self.logger.debug(f"_time_str_1={_time_str_1}, _time_str_2={_time_str_2}") -# return _time_str_1, _time_str_2 -# -# def _time_str_heute_minus_x(self, x=0): -# """ Creates an str for db request in min from time since beginning of today""" -# return f"{self.shtime.time_since(self.shtime.today(-x), 'im')}i" -# -# def _time_str_woche_minus_x(self, x=0): -# """ Creates an str for db request in min from time since beginning of week""" -# return f"{self.shtime.time_since(self.shtime.beginning_of_week(self.shtime.calendar_week(), None, -x), 'im')}i" -# -# def _time_str_monat_minus_x(self, x=0): -# """ Creates an str for db request in min for time since beginning of month""" -# return f"{self.shtime.time_since(self.shtime.beginning_of_month(None, None, -x), 'im')}i" -# -# def _time_str_jahr_minus_x(self, x=0): -# """ Creates an str for db request in min for time since beginning of year""" -# return f"{self.shtime.time_since(self.shtime.beginning_of_year(None, -x), 'im')}i" -# -# def _time_str_heute_minus_jahre_x(self, x=0): -# """ Creates an str for db request in min for time since now x years ago""" -# return f"{self.shtime.time_since(self.shtime.now() + relativedelta(years=-x), 'im')}i" -# -# def _time_since_oldest_log(self, item): -# """ Ermittlung der Zeit in ganzen Minuten zwischen "now" und dem ältesten Eintrag eines Items in der DB -# -# :param item: Item, for which query should be done -# :return: time in minutes from oldest entry to now -# """ -# -# _timestamp = self._get_oldest_log(item) -# _oldest_log_dt = datetime.datetime.fromtimestamp(int(_timestamp) / 1000, -# datetime.timezone.utc).astimezone().strftime( -# '%Y-%m-%d %H:%M:%S %Z%z') -# return self.shtime.time_since(_oldest_log_dt, resulttype='im') -# -# @staticmethod -# def _get_dbtimestamp_from_date(date): -# """ Compute a timestamp for database entry from given date -# -# :param date: datetime object / string of format 'yyyy-mm' -# """ -# -# d = None -# if isinstance(date, datetime.date): -# d = date -# elif isinstance(date, str): -# date = date.split('-') -# if len(date) == 2: -# year = int(date[0]) -# month = int(date[1]) -# if (1980 <= year <= datetime.date.today().year) and (1 <= month <= 12): -# d = datetime.date(year, month, 1) -# -# if d: -# return int(time.mktime(d.timetuple()) * 1000) -# -# def fetch_min_monthly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_min_monthly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# # query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '-', LPAD(MONTH(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MIN(val_num) FROM log WHERE item_id = {item} GROUP BY Date ORDER BY Date ASC" -# query = f"SELECT time, MIN(val_num) FROM log WHERE item_id = {item} GROUP BY YEAR(FROM_UNIXTIME(time/1000)), MONTH(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# else: -# # query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '-', LPAD(MONTH(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MIN(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(now(), INTERVAL {count} MONTH) GROUP BY Date ORDER BY Date ASC" -# query = f"SELECT time, MIN(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(DATE_FORMAT(NOW() ,'%Y-%m-01'), INTERVAL {count} MONTH) GROUP BY YEAR(FROM_UNIXTIME(time/1000)), MONTH(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['MIN(val_num)']]) -# -# _logger.warning(f'mysql.fetch_min_monthly_count value_list: {value_list}') -# return value_list -# -# def fetch_max_monthly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_max_monthly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# # query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '-', LPAD(MONTH(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MAX(val_num) FROM log WHERE item_id = {item} GROUP BY Date ORDER BY Date ASC" -# query = f"SELECT time, MAX(val_num) FROM log WHERE item_id = {item} GROUP BY YEAR(FROM_UNIXTIME(time/1000)), MONTH(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# else: -# # query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '-', LPAD(MONTH(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MAX(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(now(), INTERVAL {count} MONTH) GROUP BY Date ORDER BY Date ASC" -# query = f"SELECT time, MAX(val_num), DATE(FROM_UNIXTIME(time/1000)) as DATE FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(DATE_FORMAT(NOW() ,'%Y-%m-01'), INTERVAL {count} MONTH) GROUP BY YEAR(FROM_UNIXTIME(time/1000)), MONTH(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# _logger.warning(f'mysql.fetch_max_monthly_count result: {result}') -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['MAX(val_num)']]) -# -# _logger.warning(f'mysql.fetch_max_monthly_count value_list: {value_list}') -# return value_list -# -# def fetch_avg_monthly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_avg_monthly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# query = f"SELECT time, ROUND(AVG(val_num * duration) / AVG(duration),2) as AVG FROM log WHERE item_id = {item} GROUP BY YEAR(FROM_UNIXTIME(time/1000)), MONTH(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# else: -# query = f"SELECT time, ROUND(AVG(val_num * duration) / AVG(duration),2) as AVG FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(DATE_FORMAT(NOW() ,'%Y-%m-01'), INTERVAL {count} MONTH) GROUP BY YEAR(FROM_UNIXTIME(time/1000)), MONTH(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['AVG']]) -# -# _logger.warning(f'mysql.fetch_avg_monthly_count value_list: {value_list}') -# return value_list -# -# def fetch_min_max_monthly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_min_max_monthly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '-', LPAD(MONTH(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MAX(val_num), MIN(val_num) FROM log WHERE item_id = {item} GROUP BY Date ORDER BY Date DESC" -# else: -# query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '-', LPAD(MONTH(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MAX(val_num), MIN(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(now(), INTERVAL {count} MONTH) GROUP BY Date ORDER BY Date DESC" -# -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# _logger.warning(f'mysql result: {result}') -# return result -# -# def fetch_min_max_monthly_year(sh, item, year=None): -# _logger.warning(f"Die Userfunction 'fetch_min_max_monthly_year' wurde aufgerufen mit item {item} and year {year}") -# -# if type(item) is str: -# item = get_item_id(item) -# if year is None: -# year = datetime.now().year -# -# query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '-', LPAD(MONTH(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MAX(val_num), MIN(val_num) FROM log WHERE item_id = {item} AND YEAR(FROM_UNIXTIME(time/1000)) = {year} GROUP BY Date ORDER BY Date DESC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# _logger.warning(f'mysql result: {result}') -# return result -# -# def fetch_min_weekly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_min_weekly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# count = 51 -# query = f"SELECT time, MIN(val_num), DATE(FROM_UNIXTIME(time/1000)) as DATE FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(DATE_ADD(CURDATE(), INTERVAL - WEEKDAY(CURDATE()) DAY), INTERVAL {count} WEEK) GROUP BY YEAR(FROM_UNIXTIME(time/1000)), WEEK(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['MIN(val_num)']]) -# -# _logger.warning(f'mysql.fetch_min_weekly_count value_list: {value_list}') -# return value_list -# -# def fetch_max_weekly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_max_weekly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# count = 51 -# query = f"SELECT time, MAX(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(DATE_ADD(CURDATE(), INTERVAL - WEEKDAY(CURDATE()) DAY), INTERVAL {count} WEEK) GROUP BY YEAR(FROM_UNIXTIME(time/1000)), WEEK(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['MAX(val_num)']]) -# -# _logger.warning(f'mysql.fetch_max_weekly_count value_list: {value_list}') -# return value_list -# -# def fetch_avg_weekly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_avg_weekly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# count = 51 -# query = f"SELECT time, ROUND(AVG(val_num * duration) / AVG(duration),2) as AVG FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(DATE_ADD(CURDATE(), INTERVAL - WEEKDAY(CURDATE()) DAY), INTERVAL {count} WEEK) GROUP BY YEAR(FROM_UNIXTIME(time/1000)), WEEK(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['AVG']]) -# -# _logger.warning(f'mysql.fetch_avg_weekly_count value_list: {value_list}') -# return value_list -# -# def fetch_min_max_weekly_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_min_max_weekly_count' wurde aufgerufen mit item {item} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# count = 51 -# query = f"SELECT time, MAX(val_num), MIN(val_num), DATE(FROM_UNIXTIME(time/1000)) as DATE FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(DATE_ADD(CURDATE(), INTERVAL - WEEKDAY(CURDATE()) DAY), INTERVAL {count} WEEK) GROUP BY YEAR(FROM_UNIXTIME(time/1000)), WEEK(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# _logger.warning(f'mysql result: {result}') -# return result -# -# def fetch_min_max_weekly_year(sh, item, year=None): -# _logger.warning(f"Die Userfunction 'fetch_min_max_weekly_year' wurde aufgerufen mit item {item} and year {year}") -# -# if type(item) is str: -# item = get_item_id(item) -# if year is None: -# year = datetime.now().year -# -# query = f"SELECT CONCAT(YEAR(FROM_UNIXTIME(time/1000)), '/', LPAD(WEEK(FROM_UNIXTIME(time/1000)), 2, '0')) AS Date, MAX(val_num), MIN(val_num) FROM log WHERE item_id = {item} AND YEAR(FROM_UNIXTIME(time/1000)) = {year} GROUP BY Date ORDER BY Date DESC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# _logger.warning(f'mysql result: {result}') -# return result -# -# def fetch_min_daily_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_min_daily_count' wurde aufgerufen mit item {item} as type {type(item)} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# count = 30 -# -# query = f"SELECT time, MIN(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(now(), INTERVAL {count} DAY) GROUP BY DATE(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['MIN(val_num)']]) -# -# _logger.warning(f'mysql.fetch_min_daily_count value_list: {value_list}') -# return value_list -# -# def fetch_max_daily_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_max_daily_count' wurde aufgerufen mit item {item} as type {type(item)} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# count = 30 -# -# query = f"SELECT time, MAX(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(now(), INTERVAL {count} DAY) GROUP BY DATE(FROM_UNIXTIME(time/1000)) ORDER BY time ASC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# -# -# value_list = [] -# for element in result: -# value_list.append([element['time'], element['MAX(val_num)']]) -# -# _logger.warning(f'mysql.fetch_max_daily_count value_list: {value_list}') -# return value_list -# -# def fetch_min_max_daily_count(sh, item, count=None): -# _logger.warning(f"Die Userfunction 'fetch_min_max_daily_count' wurde aufgerufen mit item {item} as type {type(item)} and count {count}") -# -# if type(item) is str: -# item = get_item_id(item) -# if count is None: -# count = 30 -# -# query = f"SELECT DATE(FROM_UNIXTIME(time/1000)) AS Date, MAX(val_num), MIN(val_num) FROM log WHERE item_id = {item} AND DATE(FROM_UNIXTIME(time/1000)) > DATE_SUB(now(), INTERVAL {count} DAY) GROUP BY Date ORDER BY Date DESC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# _logger.warning(f'mysql result: {result}') -# return result -# -# def fetch_min_max_daily_year(sh, item, year=None): -# _logger.warning(f"Die Userfunction 'fetch_min_max_daily_year' wurde aufgerufen mit item {item} and year {year}") -# -# if type(item) is str: -# item = get_item_id(item) -# if year is None: -# year = datetime.now().year -# -# query = f"SELECT DATE(FROM_UNIXTIME(time/1000)) AS Date, MAX(val_num), MIN(val_num) FROM log WHERE item_id = {item} AND YEAR(FROM_UNIXTIME(time/1000)) = {year} GROUP BY Date ORDER BY Date DESC" -# result = [] -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# finally: -# connection.close() -# _logger.warning(f'mysql result: {result}') -# return result -# -# def _fetch_query(self, query): -# -# self.logger.debug(f"'_fetch_query' has been called with query={query}") -# connection = self._connect_to_db() -# if connection: -# try: -# connection = connect_db(sh) -# with connection.cursor() as cursor: -# cursor.execute(query) -# result = cursor.fetchall() -# except Exception as e: -# self.logger.error(f"_fetch_query failed with error={e}") -# else: -# self.logger.debug(f'_fetch_query result={result}') -# return result -# finally: -# connection.close() +ALLOWED_QUERY_TIMEFRAMES = ['year', 'month', 'week', 'day', 'hour'] +ALLOWED_MINMAX_FUNCS = ['min', 'max', 'avg'] +ALL_ONCHANGE_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr', 'minmax_heute_min', 'minmax_heute_max', 'minmax_woche_min', 'minmax_woche_max', 'minmax_monat_min', 'minmax_monat_max', 'minmax_jahr_min', 'minmax_jahr_max', 'tagesmitteltemperatur_heute'] +ALL_DAILY_ATTRIBUTES = ['verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3', 'zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d', 'serie_verbrauch_tag_30d', 'serie_zaehlerstand_tag_30d', 'serie_tagesmittelwert_stunde_0d', 'serie_tagesmittelwert_tag_stunde_30d', 'kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'tagesmitteltemperatur', 'wachstumsgradtage'] +ALL_WEEKLY_ATTRIBUTES = ['verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_rolling_12m_woche_minus1', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_verbrauch_woche_30w', 'serie_zaehlerstand_woche_30w'] +ALL_MONTHLY_ATTRIBUTES = ['verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_rolling_12m_monat_minus1', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_verbrauch_monat_18m', 'serie_zaehlerstand_monat_18m', 'serie_waermesumme_monat_24m', 'serie_kaeltesumme_monat_24m'] +ALL_YEARLY_ATTRIBUTES = ['verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_rolling_12m_jahr_minus1', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] +ALL_NEED_PARAMS_ATTRIBUTES = ['kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'tagesmitteltemperatur', 'wachstumsgradtage', 'db_request'] +ALL_VERBRAUCH_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr', 'verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_woche_minus1', 'verbrauch_rolling_12m_monat_minus1', 'verbrauch_rolling_12m_jahr_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3'] +ALL_ZAEHLERSTAND_ATTRIBUTES = ['zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3'] +ALL_HISTORIE_ATTRIBUTES = ['minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_min', 'minmax_heute_max', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_woche_min', 'minmax_woche_max', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'minmax_monat_min', 'minmax_monat_max', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'minmax_jahr_min', 'minmax_jahr_max', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] +ALL_TAGESMITTEL_ATTRIBUTES = ['tagesmitteltemperatur_heute', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3'] +ALL_SERIE_ATTRIBUTES = ['serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d', 'serie_verbrauch_tag_30d', 'serie_verbrauch_woche_30w', 'serie_verbrauch_monat_18m', 'serie_zaehlerstand_tag_30d', 'serie_zaehlerstand_woche_30w', 'serie_zaehlerstand_monat_18m', 'serie_waermesumme_monat_24m', 'serie_kaeltesumme_monat_24m', 'serie_tagesmittelwert_stunde_0d', 'serie_tagesmittelwert_tag_stunde_30d'] +ALL_GEN_ATTRIBUTES = ['general_oldest_value', 'general_oldest_log'] +ALL_COMPLEX_ATTRIBUTES = ['kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'tagesmitteltemperatur', 'wachstumsgradtage', 'db_request'] + + +""" + 'serie_minmax_monat_min_15m': {'func': 'min', 'timeframe': 'month', 'start': 15, 'end': 0, 'group': 'month'}, + 'serie_minmax_monat_max_15m': {'func': 'max', 'timeframe': 'month', 'start': 15, 'end': 0, 'group': 'month'}, + 'serie_minmax_monat_avg_15m': {'func': 'avg', 'timeframe': 'month', 'start': 15, 'end': 0, 'group': 'month'}, + 'serie_minmax_woche_min_30w': {'func': 'min', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, + 'serie_minmax_woche_max_30w': {'func': 'max', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, + 'serie_minmax_woche_avg_30w': {'func': 'avg', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, + 'serie_minmax_tag_min_30d': {'func': 'min', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, + 'serie_minmax_tag_max_30d': {'func': 'max', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, + 'serie_minmax_tag_avg_30d': {'func': 'avg', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, + 'serie_verbrauch_tag_30d': {'func': 'diff_max', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, + 'serie_verbrauch_woche_30w': {'func': 'diff_max', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, + 'serie_verbrauch_monat_18m': {'func': 'diff_max', 'timeframe': 'month', 'start': 18, 'end': 0, 'group': 'month'}, + 'serie_zaehlerstand_tag_30d': {'func': 'max', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'day'}, + 'serie_zaehlerstand_woche_30w': {'func': 'max', 'timeframe': 'week', 'start': 30, 'end': 0, 'group': 'week'}, + 'serie_zaehlerstand_monat_18m': {'func': 'max', 'timeframe': 'month', 'start': 18, 'end': 0, 'group': 'month'}, + 'serie_waermesumme_monat_24m': {'func': 'sum_max', 'timeframe': 'month', 'start': 24, 'end': 0, 'group': 'day', 'group2': 'month'}, + 'serie_kaeltesumme_monat_24m': {'func': 'sum_min_neg', 'timeframe': 'month', 'start': 24, 'end': 0, 'group': 'day', 'group2': 'month'}, + 'serie_tagesmittelwert_0d': {'func': 'max', 'timeframe': 'year', 'start': 0, 'end': 0, 'group': 'day'}, + 'serie_tagesmittelwert_stunde_0d': {'func': 'avg1', 'timeframe': 'day', 'start': 0, 'end': 0, 'group': 'hour', 'group2': 'day'}, + 'serie_tagesmittelwert_stunde_30d': {'func': 'avg1', 'timeframe': 'day', 'start': 30, 'end': 0, 'group': 'hour', 'group2': 'day'}, + 'gts': {'func': 'max', 'timeframe': 'year', 'start': None, 'end': None, 'group': 'day'}, +""" diff --git a/db_addon/item_attributes_master.py b/db_addon/item_attributes_master.py new file mode 100644 index 000000000..9d010ce70 --- /dev/null +++ b/db_addon/item_attributes_master.py @@ -0,0 +1,194 @@ +# !/usr/bin/env python +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Copyright 2023 Michael Wenzel +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# AVM for SmartHomeNG. https://github.com/smarthomeNG// +# +# This plugin is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this plugin. If not, see . +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +ITEM_ATTRIBUTS = { + 'DB_ADDON_FCTS': { + 'verbrauch_heute': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages)'}, + 'verbrauch_woche': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch in der aktuellen Woche'}, + 'verbrauch_monat': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch im aktuellen Monat'}, + 'verbrauch_jahr': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch im aktuellen Jahr'}, + 'verbrauch_heute_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages danach)'}, + 'verbrauch_heute_minus2': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch vorgestern (heute -2 Tage)'}, + 'verbrauch_heute_minus3': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -3 Tage'}, + 'verbrauch_heute_minus4': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -4 Tage'}, + 'verbrauch_heute_minus5': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -5 Tage'}, + 'verbrauch_heute_minus6': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -6 Tage'}, + 'verbrauch_heute_minus7': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -7 Tage'}, + 'verbrauch_woche_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch Vorwoche (aktuelle Woche -1)'}, + 'verbrauch_woche_minus2': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -2 Wochen'}, + 'verbrauch_woche_minus3': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -3 Wochen'}, + 'verbrauch_woche_minus4': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -4 Wochen'}, + 'verbrauch_monat_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch Vormonat (aktueller Monat -1)'}, + 'verbrauch_monat_minus2': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -2 Monate'}, + 'verbrauch_monat_minus3': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -3 Monate'}, + 'verbrauch_monat_minus4': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -4 Monate'}, + 'verbrauch_monat_minus12': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -12 Monate'}, + 'verbrauch_jahr_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'verbrauch_jahr_minus2': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch aktuelles Jahr -2 Jahre'}, + 'verbrauch_rolling_12m_heute_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages'}, + 'verbrauch_rolling_12m_woche_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende der letzten Woche'}, + 'verbrauch_rolling_12m_monat_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Monats'}, + 'verbrauch_rolling_12m_jahr_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Jahres'}, + 'verbrauch_jahreszeitraum_minus1': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag des Vorjahres'}, + 'verbrauch_jahreszeitraum_minus2': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 2 Jahren'}, + 'verbrauch_jahreszeitraum_minus3': {'cat': 'verbrauch', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 3 Jahren'}, + 'zaehlerstand_heute_minus1': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag)'}, + 'zaehlerstand_heute_minus2': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Tages (heute -2 Tag)'}, + 'zaehlerstand_heute_minus3': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorvorletzten Tages (heute -3 Tag)'}, + 'zaehlerstand_woche_minus1': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der vorvorletzten Woche (aktuelle Woche -1 Woche)'}, + 'zaehlerstand_woche_minus2': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der vorletzten Woche (aktuelle Woche -2 Wochen)'}, + 'zaehlerstand_woche_minus3': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der aktuellen Woche -3 Wochen'}, + 'zaehlerstand_monat_minus1': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Monates (aktueller Monat -1 Monat)'}, + 'zaehlerstand_monat_minus2': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Monates (aktueller Monat -2 Monate)'}, + 'zaehlerstand_monat_minus3': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des aktuellen Monats -3 Monate'}, + 'zaehlerstand_jahr_minus1': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Jahres (aktuelles Jahr -1 Jahr)'}, + 'zaehlerstand_jahr_minus2': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Jahres (aktuelles Jahr -2 Jahre)'}, + 'zaehlerstand_jahr_minus3': {'cat': 'zaehler', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des aktuellen Jahres -3 Jahre'}, + 'minmax_last_24h_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'minimaler Wert der letzten 24h'}, + 'minmax_last_24h_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'maximaler Wert der letzten 24h'}, + 'minmax_last_24h_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'durchschnittlicher Wert der letzten 24h'}, + 'minmax_last_7d_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'minimaler Wert der letzten 7 Tage'}, + 'minmax_last_7d_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'maximaler Wert der letzten 7 Tage'}, + 'minmax_last_7d_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'durchschnittlicher Wert der letzten 7 Tage'}, + 'minmax_heute_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Tagesbeginn'}, + 'minmax_heute_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Tagesbeginn'}, + 'minmax_heute_minus1_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert gestern (heute -1 Tag)'}, + 'minmax_heute_minus1_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert gestern (heute -1 Tag)'}, + 'minmax_heute_minus1_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert gestern (heute -1 Tag)'}, + 'minmax_heute_minus2_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert vorgestern (heute -2 Tage)'}, + 'minmax_heute_minus2_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert vorgestern (heute -2 Tage)'}, + 'minmax_heute_minus2_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert vorgestern (heute -2 Tage)'}, + 'minmax_heute_minus3_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert heute vor 3 Tagen'}, + 'minmax_heute_minus3_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert heute vor 3 Tagen'}, + 'minmax_heute_minus3_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert heute vor 3 Tagen'}, + 'minmax_woche_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Wochenbeginn'}, + 'minmax_woche_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Wochenbeginn'}, + 'minmax_woche_minus1_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Minimalwert Vorwoche (aktuelle Woche -1)'}, + 'minmax_woche_minus1_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Maximalwert Vorwoche (aktuelle Woche -1)'}, + 'minmax_woche_minus1_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Durchschnittswert Vorwoche (aktuelle Woche -1)'}, + 'minmax_woche_minus2_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Minimalwert aktuelle Woche -2 Wochen'}, + 'minmax_woche_minus2_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Maximalwert aktuelle Woche -2 Wochen'}, + 'minmax_woche_minus2_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Durchschnittswert aktuelle Woche -2 Wochen'}, + 'minmax_monat_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Monatsbeginn'}, + 'minmax_monat_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Monatsbeginn'}, + 'minmax_monat_minus1_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Minimalwert Vormonat (aktueller Monat -1)'}, + 'minmax_monat_minus1_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Maximalwert Vormonat (aktueller Monat -1)'}, + 'minmax_monat_minus1_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Durchschnittswert Vormonat (aktueller Monat -1)'}, + 'minmax_monat_minus2_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Minimalwert aktueller Monat -2 Monate'}, + 'minmax_monat_minus2_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Maximalwert aktueller Monat -2 Monate'}, + 'minmax_monat_minus2_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Durchschnittswert aktueller Monat -2 Monate'}, + 'minmax_jahr_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Jahresbeginn'}, + 'minmax_jahr_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Jahresbeginn'}, + 'minmax_jahr_minus1_min': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Minimalwert Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'minmax_jahr_minus1_max': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Maximalwert Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'minmax_jahr_minus1_avg': {'cat': 'wertehistorie', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Durchschnittswert Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'tagesmitteltemperatur_heute': {'cat': 'tagesmittel', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Tagesmitteltemperatur heute'}, + 'tagesmitteltemperatur_heute_minus1': {'cat': 'tagesmittel', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des letzten Tages (heute -1 Tag)'}, + 'tagesmitteltemperatur_heute_minus2': {'cat': 'tagesmittel', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorletzten Tages (heute -2 Tag)'}, + 'tagesmitteltemperatur_heute_minus3': {'cat': 'tagesmittel', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorvorletzten Tages (heute -3 Tag)'}, + 'serie_minmax_monat_min_15m': {'cat': 'serie', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Minimalwert der letzten 15 Monate (gleitend)'}, + 'serie_minmax_monat_max_15m': {'cat': 'serie', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Maximalwert der letzten 15 Monate (gleitend)'}, + 'serie_minmax_monat_avg_15m': {'cat': 'serie', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Mittelwert der letzten 15 Monate (gleitend)'}, + 'serie_minmax_woche_min_30w': {'cat': 'serie', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Minimalwert der letzten 30 Wochen (gleitend)'}, + 'serie_minmax_woche_max_30w': {'cat': 'serie', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Maximalwert der letzten 30 Wochen (gleitend)'}, + 'serie_minmax_woche_avg_30w': {'cat': 'serie', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Mittelwert der letzten 30 Wochen (gleitend)'}, + 'serie_minmax_tag_min_30d': {'cat': 'serie', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Minimalwert der letzten 30 Tage (gleitend)'}, + 'serie_minmax_tag_max_30d': {'cat': 'serie', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Maximalwert der letzten 30 Tage (gleitend)'}, + 'serie_minmax_tag_avg_30d': {'cat': 'serie', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Mittelwert der letzten 30 Tage (gleitend)'}, + 'serie_verbrauch_tag_30d': {'cat': 'serie', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Verbrauch pro Tag der letzten 30 Tage'}, + 'serie_verbrauch_woche_30w': {'cat': 'serie', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch pro Woche der letzten 30 Wochen'}, + 'serie_verbrauch_monat_18m': {'cat': 'serie', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch pro Monat der letzten 18 Monate'}, + 'serie_zaehlerstand_tag_30d': {'cat': 'serie', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Zählerstand am Tagesende der letzten 30 Tage'}, + 'serie_zaehlerstand_woche_30w': {'cat': 'serie', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand am Wochenende der letzten 30 Wochen'}, + 'serie_zaehlerstand_monat_18m': {'cat': 'serie', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand am Monatsende der letzten 18 Monate'}, + 'serie_waermesumme_monat_24m': {'cat': 'serie', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatliche Wärmesumme der letzten 24 Monate'}, + 'serie_kaeltesumme_monat_24m': {'cat': 'serie', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatliche Kältesumme der letzten 24 Monate'}, + 'serie_tagesmittelwert_stunde_0d': {'cat': 'serie', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert für den aktuellen Tag'}, + 'serie_tagesmittelwert_tag_stunde_30d': {'cat': 'serie', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert pro Tag der letzten 30 Tage (bspw. zur Berechnung der Tagesmitteltemperatur basierend auf den Mittelwert der Temperatur pro Stunde'}, + 'general_oldest_value': {'cat': 'gen', 'item_type': 'num ', 'calc': False, 'params': False, 'description': 'Ausgabe des ältesten Wertes des entsprechenden "Parent-Items" mit database Attribut'}, + 'general_oldest_log': {'cat': 'gen', 'item_type': 'list', 'calc': False, 'params': False, 'description': 'Ausgabe des Timestamp des ältesten Eintrages des entsprechenden "Parent-Items" mit database Attribut'}, + 'kaeltesumme': {'cat': 'complex', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Kältesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional)'}, + 'waermesumme': {'cat': 'complex', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional)'}, + 'gruenlandtempsumme': {'cat': 'complex', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=mandatory)'}, + 'tagesmitteltemperatur': {'cat': 'complex', 'item_type': 'list', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Tagesmitteltemperatur auf Basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (timeframe=day, count=integer)'}, + 'wachstumsgradtage': {'cat': 'complex', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Wachstumsgradtage auf Basis der stündlichen Durchschnittswerte eines Tages für das laufende Jahr mit an Angabe des Temperaturschwellenwertes (threshold=Schwellentemperatur)'}, + 'db_request': {'cat': 'complex', 'item_type': 'list', 'calc': 'group', 'params': True, 'description': 'Abfrage der DB: db_addon_params: (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional)'}, + }, + 'DB_ADDON_INFO': { + 'db_version': {'cat': 'info', 'item_type': 'str', 'calc': False, 'params': False, 'description': 'Version der verbundenen Datenbank'}, + }, + 'DB_ADDON_ADMIN': { + 'suspend': {'cat': 'admin', 'item_type': 'bool', 'calc': False, 'params': False, 'description': 'Unterbricht die Aktivitäten des Plugin'}, + 'recalc_all': {'cat': 'admin', 'item_type': 'bool', 'calc': False, 'params': False, 'description': 'Startet einen Neuberechnungslauf aller on-demand Items'}, + 'clean_cache_values': {'cat': 'admin', 'item_type': 'bool', 'calc': False, 'params': False, 'description': 'Löscht Plugin-Cache und damit alle im Plugin zwischengespeicherten Werte'}, + }, +} + + +def get_attrs(sub_dict: dict = {}) -> list: + attributes = [] + for entry in ITEM_ATTRIBUTS: + for db_addon_fct in ITEM_ATTRIBUTS[entry]: + if sub_dict.items() <= ITEM_ATTRIBUTS[entry][db_addon_fct].items(): + attributes.append(db_addon_fct) + return attributes + + +def export_db_addon_data(): + ATTRS = {} + ATTRS['ALL_ONCHANGE_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'onchange'}) + ATTRS['ALL_DAILY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'daily'}) + ATTRS['ALL_WEEKLY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'weekly'}) + ATTRS['ALL_MONTHLY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'monthly'}) + ATTRS['ALL_YEARLY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'yearly'}) + ATTRS['ALL_NEED_PARAMS_ATTRIBUTES'] = get_attrs(sub_dict={'params': True}) + ATTRS['ALL_VERBRAUCH_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'verbrauch'}) + ATTRS['ALL_ZAEHLERSTAND_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'zaehler'}) + ATTRS['ALL_HISTORIE_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'wertehistorie'}) + ATTRS['ALL_TAGESMITTEL_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'tagesmittel'}) + ATTRS['ALL_SERIE_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'serie'}) + ATTRS['ALL_GEN_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'gen'}) + ATTRS['ALL_COMPLEX_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'complex'}) + + for attr, alist in ATTRS.items(): + print(f'{attr} = {alist!r}') + + +def export_for_plugin_yaml(): + for entry in ITEM_ATTRIBUTS: + print(f'{entry}:') + print('valid_list:') + for func in ITEM_ATTRIBUTS[entry]: + print(f" - '{func}'") + + for title in ['description', 'item_type', 'calc']: + print(f'valid_list_{entry}:') + for func in ITEM_ATTRIBUTS[entry]: + print(f" - '{ITEM_ATTRIBUTS[entry][func][title]}'") + print() + + +if __name__ == '__main__': + export_db_addon_data() + print() + print('--------------------------------------------------------------') + print() + export_for_plugin_yaml() diff --git a/db_addon/plugin.yaml b/db_addon/plugin.yaml index 7aeb65439..176ffbdc0 100644 --- a/db_addon/plugin.yaml +++ b/db_addon/plugin.yaml @@ -11,7 +11,7 @@ plugin: # keywords: iot xyz # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1848494-support-thread-databaseaddon-plugin - version: 1.0.0 # Plugin version (must match the version specified in __init__.py) + version: 1.1.0 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.9.3.5 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: 3.8 # minimum Python version to use for this plugin @@ -60,435 +60,449 @@ item_attributes: de: 'Auswertefunktion des DB-Addon Plugins' en: 'Evaluation Function of DB-Addon Plugins' valid_list: - # Verbrauch - - 'verbrauch_heute' #num onchange Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages) - - 'verbrauch_woche' #num onchange Verbrauch in der aktuellen Woche - - 'verbrauch_monat' #num onchange Verbrauch im aktuellen Monat - - 'verbrauch_jahr' #num onchange Verbrauch im aktuellen Jahr - - 'verbrauch_heute_minus1' #num daily Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages danach) - - 'verbrauch_heute_minus2' #num daily Verbrauch vorgestern (heute -2 Tage) - - 'verbrauch_heute_minus3' #num daily Verbrauch heute -3 Tage - - 'verbrauch_heute_minus4' #num daily Verbrauch heute -4 Tage - - 'verbrauch_heute_minus5' #num daily Verbrauch heute -5 Tage - - 'verbrauch_heute_minus6' #num daily Verbrauch heute -6 Tage - - 'verbrauch_heute_minus7' #num daily Verbrauch heute -7 Tage - - 'verbrauch_woche_minus1' #num weekly Verbrauch Vorwoche (aktuelle Woche -1) - - 'verbrauch_woche_minus2' #num weekly Verbrauch aktuelle Woche -2 Wochen - - 'verbrauch_woche_minus3' #num weekly Verbrauch aktuelle Woche -3 Wochen - - 'verbrauch_woche_minus4' #num weekly Verbrauch aktuelle Woche -4 Wochen - - 'verbrauch_monat_minus1' #num monthly Verbrauch Vormonat (aktueller Monat -1) - - 'verbrauch_monat_minus2' #num monthly Verbrauch aktueller Monat -2 Monate - - 'verbrauch_monat_minus3' #num monthly Verbrauch aktueller Monat -3 Monate - - 'verbrauch_monat_minus4' #num monthly Verbrauch aktueller Monat -4 Monate - - 'verbrauch_monat_minus12' #num monthly Verbrauch aktueller Monat -12 Monate - - 'verbrauch_jahr_minus1' #num yearly Verbrauch Vorjahr (aktuelles Jahr -1 Jahr) - - 'verbrauch_jahr_minus2' #num yearly Verbrauch aktuelles Jahr -2 Jahre - - 'verbrauch_rolling_12m_heute_minus1' #num daily Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages - - 'verbrauch_rolling_12m_woche_minus1' #num weekly Verbrauch der letzten 12 Monate ausgehend im Ende der letzten Woche - - 'verbrauch_rolling_12m_monat_minus1' #num monthly Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Monats - - 'verbrauch_rolling_12m_jahr_minus1' #num yearly Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Jahres - - 'verbrauch_jahreszeitraum_minus1' #num daily Verbrauch seit dem 1.1. bis zum heutigen Tag des Vorjahres - - 'verbrauch_jahreszeitraum_minus2' #num daily Verbrauch seit dem 1.1. bis zum heutigen Tag vor 2 Jahren - - 'verbrauch_jahreszeitraum_minus3' #num daily Verbrauch seit dem 1.1. bis zum heutigen Tag vor 3 Jahren - # Zaehlerstand - - 'zaehlerstand_heute_minus1' #num daily Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag) - - 'zaehlerstand_woche_minus1' #num weekly Zählerstand / Wert am Ende der letzten Woche (aktuelle Woche -1 Woche) - - 'zaehlerstand_woche_minus2' #num weekly Zählerstand / Wert am Ende der vorletzten Woche (aktuelle Woche -2 Woche) - - 'zaehlerstand_woche_minus3' #num weekly Zählerstand / Wert am Ende der aktuellen Woche -3 Woche - - 'zaehlerstand_monat_minus1' #num monthly Zählerstand / Wert am Ende des letzten Monates (aktueller Monat -1 Monat) - - 'zaehlerstand_monat_minus2' #num monthly Zählerstand / Wert am Ende des vorletzten Monates (aktueller Monat -2 Monate) - - 'zaehlerstand_monat_minus3' #num monthly Zählerstand / Wert am Ende des aktuellen Monats -3 Monate - - 'zaehlerstand_jahr_minus1' #num yearly Zählerstand / Wert am Ende des letzten Jahres (aktuelles Jahr -1 Jahr) - - 'zaehlerstand_jahr_minus2' #num yearly Zählerstand / Wert am Ende des vorletzten Jahres (aktuelles Jahr -2 Jahre) - - 'zaehlerstand_jahr_minus3' #num yearly Zählerstand / Wert am Ende des aktuellen Jahres -3 Jahre - # Wertehistorie min/max - - 'minmax_last_24h_min' #num daily minimaler Wert der letzten 24h - - 'minmax_last_24h_max' #num daily maximaler Wert der letzten 24h - - 'minmax_last_24h_avg' #num daily durchschnittlicher Wert der letzten 24h - - 'minmax_last_7d_min' #num daily minimaler Wert der letzten 7 Tage - - 'minmax_last_7d_max' #num daily maximaler Wert der letzten 7 Tage - - 'minmax_last_7d_avg' #num daily durchschnittlicher Wert der letzten 7 Tage - - 'minmax_heute_min' #num onchange Minimalwert seit Tagesbeginn - - 'minmax_heute_max' #num onchange Maximalwert seit Tagesbeginn - - 'minmax_heute_minus1_min' #num daily Minimalwert gestern (heute -1 Tag) - - 'minmax_heute_minus1_max' #num daily Maximalwert gestern (heute -1 Tag) - - 'minmax_heute_minus1_avg' #num daily Durchschnittswert gestern (heute -1 Tag) - - 'minmax_heute_minus2_min' #num daily Minimalwert vorgestern (heute -2 Tage) - - 'minmax_heute_minus2_max' #num daily Maximalwert vorgestern (heute -2 Tage) - - 'minmax_heute_minus2_avg' #num daily Durchschnittswert vorgestern (heute -2 Tage) - - 'minmax_heute_minus3_min' #num daily Minimalwert heute vor 3 Tagen - - 'minmax_heute_minus3_max' #num daily Maximalwert heute vor 3 Tagen - - 'minmax_heute_minus3_avg' #num daily Durchschnittswert heute vor 3 Tagen - - 'minmax_woche_min' #num onchange Minimalwert seit Wochenbeginn - - 'minmax_woche_max' #num onchange Maximalwert seit Wochenbeginn - - 'minmax_woche_minus1_min' #num weekly Minimalwert Vorwoche (aktuelle Woche -1) - - 'minmax_woche_minus1_max' #num weekly Maximalwert Vorwoche (aktuelle Woche -1) - - 'minmax_woche_minus1_avg' #num weekly Durchschnittswert Vorwoche (aktuelle Woche -1) - - 'minmax_woche_minus2_min' #num weekly Minimalwert aktuelle Woche -2 Wochen - - 'minmax_woche_minus2_max' #num weekly Maximalwert aktuelle Woche -2 Wochen - - 'minmax_woche_minus2_avg' #num weekly Durchschnittswert aktuelle Woche -2 Wochen - - 'minmax_monat_min' #num onchange Minimalwert seit Monatsbeginn - - 'minmax_monat_max' #num onchange Maximalwert seit Monatsbeginn - - 'minmax_monat_minus1_min' #num monthly Minimalwert Vormonat (aktueller Monat -1) - - 'minmax_monat_minus1_max' #num monthly Maximalwert Vormonat (aktueller Monat -1) - - 'minmax_monat_minus1_avg' #num monthly Durchschnittswert Vormonat (aktueller Monat -1) - - 'minmax_monat_minus2_min' #num monthly Minimalwert aktueller Monat -2 Monate - - 'minmax_monat_minus2_max' #num monthly Maximalwert aktueller Monat -2 Monate - - 'minmax_monat_minus2_avg' #num monthly Durchschnittswert aktueller Monat -2 Monate - - 'minmax_jahr_min' #num onchange Minimalwert seit Jahresbeginn - - 'minmax_jahr_max' #num onchange Maximalwert seit Jahresbeginn - - 'minmax_jahr_minus1_min' #num yearly Minimalwert Vorjahr (aktuelles Jahr -1 Jahr) - - 'minmax_jahr_minus1_max' #num yearly Maximalwert Vorjahr (aktuelles Jahr -1 Jahr) - - 'minmax_jahr_minus1_avg' #num yearly Durchschnittswert Vorjahr (aktuelles Jahr -1 Jahr) - # Serie - - 'serie_minmax_monat_min_15m' #list monthly monatlicher Minimalwert der letzten 15 Monate (gleitend) - - 'serie_minmax_monat_max_15m' #list monthly monatlicher Maximalwert der letzten 15 Monate (gleitend) - - 'serie_minmax_monat_avg_15m' #list monthly monatlicher Mittelwert der letzten 15 Monate (gleitend) - - 'serie_minmax_woche_min_30w' #list weekly wöchentlicher Minimalwert der letzten 30 Wochen (gleitend) - - 'serie_minmax_woche_max_30w' #list weekly wöchentlicher Maximalwert der letzten 30 Wochen (gleitend) - - 'serie_minmax_woche_avg_30w' #list weekly wöchentlicher Mittelwert der letzten 30 Wochen (gleitend) - - 'serie_minmax_tag_min_30d' #list daily täglicher Minimalwert der letzten 30 Tage (gleitend) - - 'serie_minmax_tag_max_30d' #list daily täglicher Maximalwert der letzten 30 Tage (gleitend) - - 'serie_minmax_tag_avg_30d' #list daily täglicher Mittelwert der letzten 30 Tage (gleitend) - - 'serie_verbrauch_tag_30d' #list daily Verbrauch pro Tag der letzten 30 Tage - - 'serie_verbrauch_woche_30w' #list weekly Verbrauch pro Woche der letzten 30 Wochen - - 'serie_verbrauch_monat_18m' #list monthly Verbrauch pro Monat der letzten 18 Monate - - 'serie_zaehlerstand_tag_30d' #list daily Zählerstand am Tagesende der letzten 30 Tage - - 'serie_zaehlerstand_woche_30w' #list weekly Zählerstand am Wochenende der letzten 30 Wochen - - 'serie_zaehlerstand_monat_18m' #list monthly Zählerstand am Monatsende der letzten 18 Monate - - 'serie_waermesumme_monat_24m' #list monthly monatliche Wärmesumme der letzten 24 Monate - - 'serie_kaeltesumme_monat_24m' #list monthly monatliche Kältesumme der letzten 24 Monate - - 'serie_tagesmittelwert_stunde_0d' #list daily Stundenmittelwert für den aktuellen Tag - - 'serie_tagesmittelwert_tag_stunde_30d' #list daily Stundenmittelwert pro Tag der letzten 30 Tage (bspw. zur Berechnung der Tagesmitteltemperatur basierend auf den Mittelwert der Temperatur pro Stunde - # Allgemein - - 'general_oldest_value' #num ------ Ausgabe des ältesten Wertes des entsprechenden "Parent-Items" mit database Attribut - - 'general_oldest_log' #list ------ Ausgabe des Timestamp des ältesten Eintrages des entsprechenden "Parent-Items" mit database Attribut - # Komplex Hinweis: db_addon_params needed - - 'kaeltesumme' #num daily Berechnet die Kältesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional) - - 'waermesumme' #num daily Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional) - - 'gruenlandtempsumme' #num daily Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=mandatory) - - 'tagesmitteltemperatur' #list daily Berechnet die Tagesmitteltemperatur auf basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (days=optional) - - 'db_request' #list 'group' Abfrage der DB: db_addon_params: (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional): - valid_list_description: # Beschreibung -> notwendiger Item-Type - # Verbrauch - - 'Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages) -> num \n - Berechnungszyklus = onchange' - - 'Verbrauch in der aktuellen Woche -> num \n - Berechnungszyklus = onchange' - - 'Verbrauch im aktuellen Monat -> num \n - Berechnungszyklus = onchange' - - 'Verbrauch im aktuellen Jahr -> num \n - Berechnungszyklus = onchange' - - 'Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages danach) -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch vorgestern (heute -2 Tage) -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch heute -3 Tage -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch heute -4 Tage -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch heute -5 Tage -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch heute -6 Tage -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch heute -7 Tage -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch Vorwoche (aktuelle Woche -1) -> num \n - Berechnungszyklus = wöchentlich' - - 'Verbrauch aktuelle Woche -2 Wochen) -> num \n - Berechnungszyklus = wöchentlich' - - 'Verbrauch aktuelle Woche -3 Wochen) -> num \n - Berechnungszyklus = wöchentlich' - - 'Verbrauch aktuelle Woche -4 Wochen) -> num \n - Berechnungszyklus = wöchentlich' - - 'Verbrauch Vormonat (aktueller Monat -1) -> num \n - Berechnungszyklus = monatlich' - - 'Verbrauch aktueller Monat -2 Monate -> num \n - Berechnungszyklus = monatlich' - - 'Verbrauch aktueller Monat -3 Monate -> num \n - Berechnungszyklus = monatlich' - - 'Verbrauch aktueller Monat -4 Monate -> num \n - Berechnungszyklus = monatlich' - - 'Verbrauch aktueller Monat -12 Monate -> num \n - Berechnungszyklus = monatlich' - - 'Verbrauch Vorjahr (aktuelles Jahr -1 Jahr) -> num \n - Berechnungszyklus = jährlich' - - 'Verbrauch aktuelles Jahr -2 Jahre) -> num \n - Berechnungszyklus = jährlich' - - 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch der letzten 12 Monate ausgehend im Ende der letzten Woche -> num \n - Berechnungszyklus = wöchentlich' - - 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Monats -> num \n - Berechnungszyklus = monatlich' - - 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Jahres -> num \n - Berechnungszyklus = jährlich' - - 'Verbrauch seit dem 1.1. bis zum heutigen Tag des Vorjahres -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 2 Jahren -> num \n - Berechnungszyklus = täglich' - - 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 3 Jahren -> num \n - Berechnungszyklus = täglich' - # Zaehlerstand - - 'Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag) -> num \n - Berechnungszyklus = täglich' - - 'Zählerstand / Wert am Ende der letzten Woche (aktuelle Woche -1 Woche) -> num \n - Berechnungszyklus = wöchentlich' - - 'Zählerstand / Wert am Ende der vorletzten Woche (aktuelle Woche -2 Woche) -> num \n - Berechnungszyklus = wöchentlich' - - 'Zählerstand / Wert am Ende der aktuellen Woche -3 Woche -> num \n - Berechnungszyklus = wöchentlich' - - 'Zählerstand / Wert am Ende des letzten Monates (aktueller Monat -1 Monat)) -> num \n - Berechnungszyklus = monatlich' - - 'Zählerstand / Wert am Ende des vorletzten Monates (aktueller Monat -2 Monate)) -> num \n - Berechnungszyklus = monatlich' - - 'Zählerstand / Wert am Ende des aktuellen Monats -3 Monate) -> num \n - Berechnungszyklus = monatlich' - - 'Zählerstand / Wert am Ende des letzten Jahres (aktuelles Jahr -1 Jahr) -> num \n - Berechnungszyklus = jährlich' - - 'Zählerstand / Wert am Ende des vorletzten Jahres (aktuelles Jahr -2 Jahre) -> num \n - Berechnungszyklus = jährlich' - - 'Zählerstand / Wert am Ende des aktuellen Jahres -3 Jahre -> num \n - Berechnungszyklus = jährlich' - # Wertehistorie min/max - - 'minimaler Wert der letzten 24h -> num \n - Berechnungszyklus = täglich' - - 'maximaler Wert der letzten 24h -> num \n - Berechnungszyklus = täglich' - - 'durchschnittlicher Wert der letzten 24h -> num \n - Berechnungszyklus = täglich' - - 'minimaler Wert der letzten 7 Tage -> num \n - Berechnungszyklus = täglich' - - 'maximaler Wert der letzten 7 Tage -> num \n - Berechnungszyklus = täglich' - - 'durchschnittlicher Wert der letzten 7 Tage -> num \n - Berechnungszyklus = täglich' + - 'verbrauch_heute' + - 'verbrauch_woche' + - 'verbrauch_monat' + - 'verbrauch_jahr' + - 'verbrauch_heute_minus1' + - 'verbrauch_heute_minus2' + - 'verbrauch_heute_minus3' + - 'verbrauch_heute_minus4' + - 'verbrauch_heute_minus5' + - 'verbrauch_heute_minus6' + - 'verbrauch_heute_minus7' + - 'verbrauch_woche_minus1' + - 'verbrauch_woche_minus2' + - 'verbrauch_woche_minus3' + - 'verbrauch_woche_minus4' + - 'verbrauch_monat_minus1' + - 'verbrauch_monat_minus2' + - 'verbrauch_monat_minus3' + - 'verbrauch_monat_minus4' + - 'verbrauch_monat_minus12' + - 'verbrauch_jahr_minus1' + - 'verbrauch_jahr_minus2' + - 'verbrauch_rolling_12m_heute_minus1' + - 'verbrauch_rolling_12m_woche_minus1' + - 'verbrauch_rolling_12m_monat_minus1' + - 'verbrauch_rolling_12m_jahr_minus1' + - 'verbrauch_jahreszeitraum_minus1' + - 'verbrauch_jahreszeitraum_minus2' + - 'verbrauch_jahreszeitraum_minus3' + - 'zaehlerstand_heute_minus1' + - 'zaehlerstand_heute_minus2' + - 'zaehlerstand_heute_minus3' + - 'zaehlerstand_woche_minus1' + - 'zaehlerstand_woche_minus2' + - 'zaehlerstand_woche_minus3' + - 'zaehlerstand_monat_minus1' + - 'zaehlerstand_monat_minus2' + - 'zaehlerstand_monat_minus3' + - 'zaehlerstand_jahr_minus1' + - 'zaehlerstand_jahr_minus2' + - 'zaehlerstand_jahr_minus3' + - 'minmax_last_24h_min' + - 'minmax_last_24h_max' + - 'minmax_last_24h_avg' + - 'minmax_last_7d_min' + - 'minmax_last_7d_max' + - 'minmax_last_7d_avg' + - 'minmax_heute_min' + - 'minmax_heute_max' + - 'minmax_heute_minus1_min' + - 'minmax_heute_minus1_max' + - 'minmax_heute_minus1_avg' + - 'minmax_heute_minus2_min' + - 'minmax_heute_minus2_max' + - 'minmax_heute_minus2_avg' + - 'minmax_heute_minus3_min' + - 'minmax_heute_minus3_max' + - 'minmax_heute_minus3_avg' + - 'minmax_woche_min' + - 'minmax_woche_max' + - 'minmax_woche_minus1_min' + - 'minmax_woche_minus1_max' + - 'minmax_woche_minus1_avg' + - 'minmax_woche_minus2_min' + - 'minmax_woche_minus2_max' + - 'minmax_woche_minus2_avg' + - 'minmax_monat_min' + - 'minmax_monat_max' + - 'minmax_monat_minus1_min' + - 'minmax_monat_minus1_max' + - 'minmax_monat_minus1_avg' + - 'minmax_monat_minus2_min' + - 'minmax_monat_minus2_max' + - 'minmax_monat_minus2_avg' + - 'minmax_jahr_min' + - 'minmax_jahr_max' + - 'minmax_jahr_minus1_min' + - 'minmax_jahr_minus1_max' + - 'minmax_jahr_minus1_avg' + - 'tagesmitteltemperatur_heute' + - 'tagesmitteltemperatur_heute_minus1' + - 'tagesmitteltemperatur_heute_minus2' + - 'tagesmitteltemperatur_heute_minus3' + - 'serie_minmax_monat_min_15m' + - 'serie_minmax_monat_max_15m' + - 'serie_minmax_monat_avg_15m' + - 'serie_minmax_woche_min_30w' + - 'serie_minmax_woche_max_30w' + - 'serie_minmax_woche_avg_30w' + - 'serie_minmax_tag_min_30d' + - 'serie_minmax_tag_max_30d' + - 'serie_minmax_tag_avg_30d' + - 'serie_verbrauch_tag_30d' + - 'serie_verbrauch_woche_30w' + - 'serie_verbrauch_monat_18m' + - 'serie_zaehlerstand_tag_30d' + - 'serie_zaehlerstand_woche_30w' + - 'serie_zaehlerstand_monat_18m' + - 'serie_waermesumme_monat_24m' + - 'serie_kaeltesumme_monat_24m' + - 'serie_tagesmittelwert_stunde_0d' + - 'serie_tagesmittelwert_tag_stunde_30d' + - 'general_oldest_value' + - 'general_oldest_log' + - 'kaeltesumme' + - 'waermesumme' + - 'gruenlandtempsumme' + - 'tagesmitteltemperatur' + - 'wachstumsgradtage' + - 'db_request' + valid_list_description: + - 'Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages)' + - 'Verbrauch in der aktuellen Woche' + - 'Verbrauch im aktuellen Monat' + - 'Verbrauch im aktuellen Jahr' + - 'Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages danach)' + - 'Verbrauch vorgestern (heute -2 Tage)' + - 'Verbrauch heute -3 Tage' + - 'Verbrauch heute -4 Tage' + - 'Verbrauch heute -5 Tage' + - 'Verbrauch heute -6 Tage' + - 'Verbrauch heute -7 Tage' + - 'Verbrauch Vorwoche (aktuelle Woche -1)' + - 'Verbrauch aktuelle Woche -2 Wochen' + - 'Verbrauch aktuelle Woche -3 Wochen' + - 'Verbrauch aktuelle Woche -4 Wochen' + - 'Verbrauch Vormonat (aktueller Monat -1)' + - 'Verbrauch aktueller Monat -2 Monate' + - 'Verbrauch aktueller Monat -3 Monate' + - 'Verbrauch aktueller Monat -4 Monate' + - 'Verbrauch aktueller Monat -12 Monate' + - 'Verbrauch Vorjahr (aktuelles Jahr -1 Jahr)' + - 'Verbrauch aktuelles Jahr -2 Jahre' + - 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages' + - 'Verbrauch der letzten 12 Monate ausgehend im Ende der letzten Woche' + - 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Monats' + - 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Jahres' + - 'Verbrauch seit dem 1.1. bis zum heutigen Tag des Vorjahres' + - 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 2 Jahren' + - 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 3 Jahren' + - 'Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag)' + - 'Zählerstand / Wert am Ende des vorletzten Tages (heute -2 Tag)' + - 'Zählerstand / Wert am Ende des vorvorletzten Tages (heute -3 Tag)' + - 'Zählerstand / Wert am Ende der vorvorletzten Woche (aktuelle Woche -1 Woche)' + - 'Zählerstand / Wert am Ende der vorletzten Woche (aktuelle Woche -2 Wochen)' + - 'Zählerstand / Wert am Ende der aktuellen Woche -3 Wochen' + - 'Zählerstand / Wert am Ende des letzten Monates (aktueller Monat -1 Monat)' + - 'Zählerstand / Wert am Ende des vorletzten Monates (aktueller Monat -2 Monate)' + - 'Zählerstand / Wert am Ende des aktuellen Monats -3 Monate' + - 'Zählerstand / Wert am Ende des letzten Jahres (aktuelles Jahr -1 Jahr)' + - 'Zählerstand / Wert am Ende des vorletzten Jahres (aktuelles Jahr -2 Jahre)' + - 'Zählerstand / Wert am Ende des aktuellen Jahres -3 Jahre' + - 'minimaler Wert der letzten 24h' + - 'maximaler Wert der letzten 24h' + - 'durchschnittlicher Wert der letzten 24h' + - 'minimaler Wert der letzten 7 Tage' + - 'maximaler Wert der letzten 7 Tage' + - 'durchschnittlicher Wert der letzten 7 Tage' - 'Minimalwert seit Tagesbeginn' - 'Maximalwert seit Tagesbeginn' - - 'Minimalwert gestern (heute -1 Tag) -> num \n - Berechnungszyklus = täglich' - - 'Maximalwert gestern (heute -1 Tag) -> num \n - Berechnungszyklus = täglich' - - 'Durchschnittswert gestern (heute -1 Tag) -> num \n - Berechnungszyklus = täglich' - - 'Minimalwert vorgestern (heute -2 Tage) -> num \n - Berechnungszyklus = täglich' - - 'Maximalwert vorgestern (heute -2 Tage) -> num \n - Berechnungszyklus = täglich' - - 'Durchschnittswert vorgestern (heute -2 Tage) -> num \n - Berechnungszyklus = täglich' - - 'Minimalwert heute vor 3 Tagen -> num \n - Berechnungszyklus = täglich' - - 'Maximalwert heute vor 3 Tagen -> num \n - Berechnungszyklus = täglich' - - 'Durchschnittswert heute vor 3 Tagen -> num \n - Berechnungszyklus = täglich' - - 'Minimalwert seit Wochenbeginn -> num \n - Berechnungszyklus = onchange' - - 'Maximalwert seit Wochenbeginn -> num \n - Berechnungszyklus = onchange' - - 'Minimalwert Vorwoche (aktuelle Woche -1) -> num \n - Berechnungszyklus = wöchentlich' - - 'Maximalwert Vorwoche (aktuelle Woche -1) -> num \n - Berechnungszyklus = wöchentlich' - - 'Durchschnittswert Vorwoche (aktuelle Woche -1) -> num \n - Berechnungszyklus = wöchentlich' - - 'Minimalwert aktuelle Woche -2 Wochen -> num \n - Berechnungszyklus = wöchentlich' - - 'Maximalwert aktuelle Woche -2 Wochen -> num \n - Berechnungszyklus = wöchentlich' - - 'Durchschnittswert aktuelle Woche -2 Wochen -> num \n - Berechnungszyklus = wöchentlich' - - 'Minimalwert seit Monatsbeginn -> num \n - Berechnungszyklus = onchange' - - 'Maximalwert seit Monatsbeginn -> num \n - Berechnungszyklus = onchange' - - 'Minimalwert Vormonat (aktueller Monat -1) -> num \n - Berechnungszyklus = monatlich' - - 'Maximalwert Vormonat (aktueller Monat -1) -> num \n - Berechnungszyklus = monatlich' - - 'Durchschnittswert Vormonat (aktueller Monat -1) -> num \n - Berechnungszyklus = monatlich' - - 'Minimalwert aktueller Monat -2 Monate -> num \n - Berechnungszyklus = monatlich' - - 'Maximalwert aktueller Monat -2 Monate -> num \n - Berechnungszyklus = monatlich' - - 'Durchschnittswert aktueller Monat -2 Monate -> num \n - Berechnungszyklus = monatlich' - - 'Minimalwert seit Jahresbeginn -> num \n - Berechnungszyklus = onchange' - - 'Maximalwert seit Jahresbeginn -> num \n - Berechnungszyklus = onchange' - - 'Minimalwert Vorjahr (aktuelles Jahr -1 Jahr) -> num \n - Berechnungszyklus = jährlich' - - 'Maximalwert Vorjahr (aktuelles Jahr -1 Jahr) -> num \n - Berechnungszyklus = jährlich' - - 'Durchschnittswert Vorjahr (aktuelles Jahr -1 Jahr) -> num \n - Berechnungszyklus = jährlich' - # Serie - - 'monatlicher Minimalwert der letzten 15 Monate (gleitend) -> list \n - Berechnungszyklus = monatlich' - - 'monatlicher Maximalwert der letzten 15 Monate (gleitend) -> list \n - Berechnungszyklus = monatlich' - - 'monatlicher Mittelwert der letzten 15 Monate (gleitend) -> list \n - Berechnungszyklus = monatlich' - - 'wöchentlicher Minimalwert der letzten 30 Wochen (gleitend) -> list \n - Berechnungszyklus = wöchentlich' - - 'wöchentlicher Maximalwert der letzten 30 Wochen (gleitend) -> list \n - Berechnungszyklus = wöchentlich' - - 'wöchentlicher Mittelwert der letzten 30 Wochen (gleitend) -> list \n - Berechnungszyklus = wöchentlich' - - 'täglicher Minimalwert der letzten 30 Tage (gleitend) -> list \n - Berechnungszyklus = täglich' - - 'täglicher Maximalwert der letzten 30 Tage (gleitend)) -> list \n - Berechnungszyklus = täglich' - - 'täglicher Mittelwert der letzten 30 Tage (gleitend)) -> list \n - Berechnungszyklus = täglich' - - 'Verbrauch pro Tag der letzten 30 Tage) -> list \n - Berechnungszyklus = täglich' - - 'Verbrauch pro Woche der letzten 30 Wochen) -> list \n - Berechnungszyklus = wöchentlich' - - 'Verbrauch pro Monat der letzten 18 Monate) -> list \n - Berechnungszyklus = monatlich' - - 'Zählerstand am Tagesende der letzten 30 Tage) -> list \n - Berechnungszyklus = täglich' - - 'Zählerstand am Wochenende der letzten 30 Wochen) -> list \n - Berechnungszyklus = wöchentlich' - - 'Zählerstand am Monatsende der letzten 18 Monate) -> list \n - Berechnungszyklus = monatlich' - - 'monatliche Wärmesumme der letzten 24 Monate) -> list \n - Berechnungszyklus = monatlich' - - 'monatliche Kältesumme der letzten 24 Monate) -> list \n - Berechnungszyklus = monatlich' - - 'Stundenmittelwert für den aktuellen Tag) -> list \n - Berechnungszyklus = täglich' - - 'Stundenmittelwert pro Tag der letzten 30 Tage (bspw. zur Berechnung der Tagesmitteltemperatur basierend auf den Mittelwert der Temperatur pro Stunde) -> list \n - Berechnungszyklus = täglich' - # Allgemein - - 'Ausgabe des ältesten Wertes des entsprechenden "Parent-Items" mit database Attribut -> num' - - 'Ausgabe des Timestamp des ältesten Eintrages des entsprechenden "Parent-Items" mit database Attribut -> list' - # Komplex - - 'Berechnet die Kältesumme für einen Zeitraum, db_addon_params sind für die Definition des Zeitraums notwendig (year=optional, month=optional) -> num \n - Berechnungszyklus = täglich' - - 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params sind für die Definition des Zeitraums notwendig (year=optional, month=optional) -> num \n - Berechnungszyklus = täglich' - - 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params sind für die Definition des Zeitraums notwendig (year=optional) siehe https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme -> num \n - Berechnungszyklus = täglich' - - 'Berechnet die Tagesmitteltemperatur auf basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (days=optional) -> num \n - Berechnungszyklus = täglich' - - 'Abfrage der DB mit db_addon_params (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional) -> foo \n - Berechnungszyklus = group' - + - 'Minimalwert gestern (heute -1 Tag)' + - 'Maximalwert gestern (heute -1 Tag)' + - 'Durchschnittswert gestern (heute -1 Tag)' + - 'Minimalwert vorgestern (heute -2 Tage)' + - 'Maximalwert vorgestern (heute -2 Tage)' + - 'Durchschnittswert vorgestern (heute -2 Tage)' + - 'Minimalwert heute vor 3 Tagen' + - 'Maximalwert heute vor 3 Tagen' + - 'Durchschnittswert heute vor 3 Tagen' + - 'Minimalwert seit Wochenbeginn' + - 'Maximalwert seit Wochenbeginn' + - 'Minimalwert Vorwoche (aktuelle Woche -1)' + - 'Maximalwert Vorwoche (aktuelle Woche -1)' + - 'Durchschnittswert Vorwoche (aktuelle Woche -1)' + - 'Minimalwert aktuelle Woche -2 Wochen' + - 'Maximalwert aktuelle Woche -2 Wochen' + - 'Durchschnittswert aktuelle Woche -2 Wochen' + - 'Minimalwert seit Monatsbeginn' + - 'Maximalwert seit Monatsbeginn' + - 'Minimalwert Vormonat (aktueller Monat -1)' + - 'Maximalwert Vormonat (aktueller Monat -1)' + - 'Durchschnittswert Vormonat (aktueller Monat -1)' + - 'Minimalwert aktueller Monat -2 Monate' + - 'Maximalwert aktueller Monat -2 Monate' + - 'Durchschnittswert aktueller Monat -2 Monate' + - 'Minimalwert seit Jahresbeginn' + - 'Maximalwert seit Jahresbeginn' + - 'Minimalwert Vorjahr (aktuelles Jahr -1 Jahr)' + - 'Maximalwert Vorjahr (aktuelles Jahr -1 Jahr)' + - 'Durchschnittswert Vorjahr (aktuelles Jahr -1 Jahr)' + - 'Tagesmitteltemperatur heute' + - 'Tagesmitteltemperatur des letzten Tages (heute -1 Tag)' + - 'Tagesmitteltemperatur des vorletzten Tages (heute -2 Tag)' + - 'Tagesmitteltemperatur des vorvorletzten Tages (heute -3 Tag)' + - 'monatlicher Minimalwert der letzten 15 Monate (gleitend)' + - 'monatlicher Maximalwert der letzten 15 Monate (gleitend)' + - 'monatlicher Mittelwert der letzten 15 Monate (gleitend)' + - 'wöchentlicher Minimalwert der letzten 30 Wochen (gleitend)' + - 'wöchentlicher Maximalwert der letzten 30 Wochen (gleitend)' + - 'wöchentlicher Mittelwert der letzten 30 Wochen (gleitend)' + - 'täglicher Minimalwert der letzten 30 Tage (gleitend)' + - 'täglicher Maximalwert der letzten 30 Tage (gleitend)' + - 'täglicher Mittelwert der letzten 30 Tage (gleitend)' + - 'Verbrauch pro Tag der letzten 30 Tage' + - 'Verbrauch pro Woche der letzten 30 Wochen' + - 'Verbrauch pro Monat der letzten 18 Monate' + - 'Zählerstand am Tagesende der letzten 30 Tage' + - 'Zählerstand am Wochenende der letzten 30 Wochen' + - 'Zählerstand am Monatsende der letzten 18 Monate' + - 'monatliche Wärmesumme der letzten 24 Monate' + - 'monatliche Kältesumme der letzten 24 Monate' + - 'Stundenmittelwert für den aktuellen Tag' + - 'Stundenmittelwert pro Tag der letzten 30 Tage (bspw. zur Berechnung der Tagesmitteltemperatur basierend auf den Mittelwert der Temperatur pro Stunde' + - 'Ausgabe des ältesten Wertes des entsprechenden "Parent-Items" mit database Attribut' + - 'Ausgabe des Timestamp des ältesten Eintrages des entsprechenden "Parent-Items" mit database Attribut' + - 'Berechnet die Kältesumme für einen Zeitraum, db_addon_params: (year=mandatory: int, month=optional: str)' + - 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=mandatory: int, month=optional: str, threshold=optional: int)' + - 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=mandatory)' + - 'Berechnet die Tagesmitteltemperatur auf Basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (timeframe=day, count=integer)' + - 'Berechnet die Wachstumsgradtage auf Basis der stündlichen Durchschnittswerte eines Tages für das laufende Jahr mit an Angabe des Temperaturschwellenwertes (threshold=Schwellentemperatur: int)' + - 'Abfrage der DB: db_addon_params: (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional)' valid_list_item_type: - # Verbrauch - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - # Zaehlerstand - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - # Wertehistorie min/max - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - - num - # Serie - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - - list - # Allgemein - - num - - list - # Komplex - - num - - num - - num - - list - - list + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'num' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'list' + - 'num ' + - 'list' + - 'num' + - 'num' + - 'num' + - 'list' + - 'num' + - 'list' + valid_list_calculation: + - 'onchange' + - 'onchange' + - 'onchange' + - 'onchange' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'weekly' + - 'weekly' + - 'weekly' + - 'weekly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'yearly' + - 'yearly' + - 'daily' + - 'weekly' + - 'monthly' + - 'yearly' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'weekly' + - 'weekly' + - 'weekly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'yearly' + - 'yearly' + - 'yearly' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'onchange' + - 'onchange' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'onchange' + - 'onchange' + - 'weekly' + - 'weekly' + - 'weekly' + - 'weekly' + - 'weekly' + - 'weekly' + - 'onchange' + - 'onchange' + - 'monthly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'onchange' + - 'onchange' + - 'yearly' + - 'yearly' + - 'yearly' + - 'onchange' + - 'daily' + - 'daily' + - 'daily' + - 'monthly' + - 'monthly' + - 'monthly' + - 'weekly' + - 'weekly' + - 'weekly' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'weekly' + - 'monthly' + - 'daily' + - 'weekly' + - 'monthly' + - 'monthly' + - 'monthly' + - 'daily' + - 'daily' + - 'False' + - 'False' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'daily' + - 'group' db_addon_info: type: str @@ -496,11 +510,11 @@ item_attributes: de: 'Info-Funktion des DB-Addon Plugins' en: 'Info-Function of DB-Addon Plugins' valid_list: - - 'db_version' #str ------ Version der verbundenen Datenbank + - 'db_version' valid_list_description: - - 'Version der verbundenen Datenbank -> str' + - 'Version der verbundenen Datenbank' valid_list_item_type: - - str + - 'str' db_addon_admin: type: str @@ -508,17 +522,17 @@ item_attributes: de: 'Admin-Funktion des DB-Addon Plugins' en: 'Admin-Function of DB-Addon Plugins' valid_list: - - 'suspend' #bool ------ unterbricht die Aktivitäten des Plugin - - 'recalc_all' #bool ------ Startet einen Neuberechnungslauf aller on-demand items - - 'clean_cache_values' #bool ------ Löscht Plugin-Cache und damit alle im Plugin zwischengespeicherten Werte + - 'suspend' + - 'recalc_all' + - 'clean_cache_values' valid_list_description: - 'unterbricht die Aktivitäten des Plugin -> bool' - 'Startet einen Neuberechnungslauf aller on-demand items -> bool' - 'Löscht Plugin-Cache und damit alle im Plugin zwischengespeicherten Werte -> bool' valid_list_item_type: - - bool - - bool - - bool + - 'bool' + - 'bool' + - 'bool' db_addon_params: type: str @@ -538,6 +552,12 @@ item_attributes: de: 'Wert der bei Abfrage bzw. Auswertung der Datenbank für diese Item ignoriert werden soll' en: 'Value which will be ignored at database query' + db_addon_database_item: + type: str + description: + de: 'Optional: Pfad des zu verwendenden Items mit Database Attribut' + en: 'Optional: Path of item with database attribut to be used' + item_structs: verbrauch_1: name: Struct für Verbrauchsauswertung bei Zählern mit stetig ansteigendem Zählerstand (Teil 1) diff --git a/db_addon/user_doc.rst b/db_addon/user_doc.rst index 65ec9bfc4..06faf71bc 100644 --- a/db_addon/user_doc.rst +++ b/db_addon/user_doc.rst @@ -21,10 +21,13 @@ Diese Auswertungen werden zyklisch zum Tageswechsel, Wochenwechsel, Monatswechse der Funktion erzeugt. Um die Zugriffe auf die Datenbank zu minimieren, werden diverse Daten zwischengespeichert. -Die Items mit einem DatabaseAddon-Attribut müssen im gleichen Pfad sein, wie das Item, für das das Database Attribut -konfiguriert ist. -Bedeutet: Die Items mit dem DatabaseAddon-Attribute müssen Kinder oder Kindeskinder oder Kindeskinderkinder des Items -sein, für das das Database Attribut konfiguriert ist +Sind Items mit einem DatabaseAddon-Attribut im gleichen Pfad, wie das Item, für das das Database Attribut +konfiguriert ist, wird dieses Item automatisch ermittelt. Bedeutet: Sind die Items mit dem DatabaseAddon-Attribute Kinder +oder Kindeskinder oder Kindeskinderkinder des Items, für das das Database Attribut konfiguriert ist, wird dieses automatisch +ermittelt. + +Alternativ kann mit dem Attribute "db_addon_database_item" auch der absolute Pfad des Items angegeben werden, für das +das Database Attribut konfiguriert ist. Bsp: @@ -46,6 +49,12 @@ Bsp: type: num db_addon_fct: heute_minus1_max + + tagesmitteltemperatur_gestern: + type: num + db_addon_fct: heute_minus1_avg + db_addon_database_item: 'temperatur' + | Anforderungen @@ -182,3 +191,52 @@ db_addon Maintenance Das Webinterface zeigt detaillierte Informationen über die im Plugin verfügbaren Daten an. Dies dient der Maintenance bzw. Fehlersuche. Dieser Tab ist nur bei Log-Level "Debug" verfügbar. + + +Erläuterungen zu Temperatursummen +================================= + + +Grünlandtemperatursumme +----------------------- + +Beim Grünland wird die Wärmesumme nach Ernst und Loeper benutzt, um den Vegetationsbeginn und somit den Termin von Düngungsmaßnahmen zu bestimmen. +Dabei erfolgt die Aufsummierung der Tagesmitteltemperaturen über 0 °C, wobei der Januar mit 0.5 und der Februar mit 0.75 gewichtet wird. +Bei einer Wärmesumme von 200 Grad ist eine Düngung angesagt. + +siehe: https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme + + +Wachstumsgradtag +---------------- +Der Begriff Wachstumsgradtage (WGT) ist ein Überbegriff für verschiedene Größen. +Gemeinsam ist ihnen, daß zur Berechnung eine Lufttemperatur von einem Schwellenwert subtrahiert wird. +Je nach Fragestellung und Pflanzenart werden der Schwellenwert unterschiedlich gewählt und die Temperatur unterschiedlich bestimmt. +Verfügbar sind die Berechnung über "einfachen Durchschnitt der Tagestemperaturen" und "modifizierten Durchschnitt der Tagestemperaturen". + +siehe https://de.wikipedia.org/wiki/Wachstumsgradtag + + +Wärmesumme +---------- + +Die Wärmesumme soll eine Aussage über den Sommer und die Pflanzenreife liefern. Es gibt keine eindeutige Definition dier Größe "Wärmesumme". +Berechnet wird die Wärmesumme als Summe aller Tagesmitteltemperaturen über einem Schwellenwert ab dem 1.1. des Jahres. + +siehe https://de.wikipedia.org/wiki/W%C3%A4rmesumme + + +Kältesumme +---------- + +Die Kältesumme soll eine Aussage über die Härte des Winters liefern. +Berechnet wird die Kältesumme als Summe aller negtiven Tagesmitteltemperaturenab dem 21.9. des Jahres bis 31.3. des Folgejahres. + +siehe https://de.wikipedia.org/wiki/K%C3%A4ltesumme + + + +Tagesmitteltemperatur +--------------------- + +Die Tagesmitteltemperatur wird auf Basis der stündlichen Durchschnittswerte eines Tages (aller in der DB enthaltenen Datensätze) für die angegebene Anzahl von Tagen (days=optional) berechnet. diff --git a/db_addon/webif/__init__.py b/db_addon/webif/__init__.py index 0397f8ab9..bd5c6c41f 100644 --- a/db_addon/webif/__init__.py +++ b/db_addon/webif/__init__.py @@ -104,7 +104,7 @@ def get_data_html(self, dataSet=None): data['plugin_suspended'] = self.plugin.suspended data['maintenance'] = True if self.plugin.log_level == 10 else False - data['queue_length'] = self.plugin.queue_backlog + data['queue_length'] = self.plugin.queue_backlog() data['active_queue_item'] = self.plugin.active_queue_item try: diff --git a/db_addon/webif/templates/index.html b/db_addon/webif/templates/index.html index 37688339b..2661bd915 100644 --- a/db_addon/webif/templates/index.html +++ b/db_addon/webif/templates/index.html @@ -267,7 +267,6 @@ {% endif %} {% set tab3title = "" ~ plugin_shortname ~ " API/Doku" %} - {% if item_count > 0 %} {% set start_tab = 1 %} @@ -278,218 +277,214 @@ {% block bodytab1 %} - -
- - - {% for item in items %} - - - - - - - - - - - - {% endfor %} - -
{{ item._path }}{{ item._type }}{{ p.get_item_config(item._path)['attribute'] }}{{ _(p.get_item_config(item)['cycle']|string) }}{% if p.get_item_config(item)['startup'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}.{{ item._value | float }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
-
+ + + {% for item in items %} + + + + + + + + + + + + {% endfor %} + +
{{ item._path }}{{ item._type }}{{ p.get_item_config(item._path)['attribute'] }}{{ _(p.get_item_config(item)['cycle']|string) }}{% if p.get_item_config(item)['startup'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}{{ item._value | float }}{{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }}{{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }}
{% endblock bodytab1 %} {% block bodytab2 %} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ _('00_items') }}{{ len(p.get_item_path_list('database_addon', True)) }}{{ p.get_item_path_list('database_addon', True) }}
{{ _('02_admin_items') }}{{ len(p.get_item_path_list('database_addon', 'admin')) }}{{ p.get_item_path_list('database_addon', 'admin') }}
{{ _('10_daily_items') }}{{ len(p._daily_items) }}{{ p._daily_items }}
{{ _('11_weekly_items') }}{{ len(p._weekly_items) }}{{ p._weekly_items }}
{{ _('12_monthly_items') }}{{ len(p._monthly_items) }}{{ p._monthly_items }}
{{ _('13_yearly_items') }}{{ len(p._yearly_items) }}{{ p._yearly_items }}
{{ _('14_onchange_items') }}{{ len(p._onchange_items) }}{{ p._onchange_items }}
{{ _('15_startup_items') }}{{ len(p._startup_items) }}{{ p._startup_items }}
{{ _('17_database_items') }}{{ len(p._database_items) }}{{ p._database_items }}
{{ _('16_static_items') }}{{ len(p._static_items) }}{{ p._static_items }}
{{ _('32_item_cache') }}{{ len(p.item_cache) }}{{ p.item_cache }}
{{ _('20_tageswert_dict') }}{{ len(p.current_values['day']) }}{{ p.current_values['day'] }}
{{ _('21_wochenwert_dict') }}{{ len(p.current_values['week']) }}{{ p.current_values['week'] }}
{{ _('22_monatswert_dict') }}{{ len(p.current_values['month']) }}{{ p.current_values['month'] }}
{{ _('23_jahreswert_dict') }}{{ len(p.current_values['year']) }}{{ p.current_values['year'] }}
{{ _('24_vortagsendwert_dict') }}{{ len(p.previous_values['day']) }}{{ p.previous_values['day'] }}
{{ _('25_vorwochenendwert_dict') }}{{ len(p.previous_values['week']) }}{{ p.previous_values['week'] }}
{{ _('26_vormonatsendwert_dict') }}{{ len(p.previous_values['month']) }}{{ p.previous_values['month'] }}
{{ _('27_vorjahresendwert_dict') }}{{ len(p.previous_values['year']) }}{{ p.previous_values['year'] }}
{{ _('get_item_list') }}{{ len(p.get_item_list('database_addon', True)) }}{{ p.get_item_list('database_addon', True) }}
{{ _('_plg_item_dict') }}{{ len(p._plg_item_dict) }}{{ p._plg_item_dict }}
{{ _('work_item_queue_thread') }}{{ len(p._plg_item_dict) }}{{ p.work_item_queue_thread.is_alive() }}
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _('00_items') }}{{ len(p.get_item_path_list('database_addon', True)) }}{{ p.get_item_path_list('database_addon', True) }}
{{ _('02_admin_items') }}{{ len(p.get_item_path_list('database_addon', 'admin')) }}{{ p.get_item_path_list('database_addon', 'admin') }}
{{ _('10_daily_items') }}{{ len(p._daily_items()) }}{{ p._daily_items() }}
{{ _('11_weekly_items') }}{{ len(p._weekly_items()) }}{{ p._weekly_items() }}
{{ _('12_monthly_items') }}{{ len(p._monthly_items()) }}{{ p._monthly_items() }}
{{ _('13_yearly_items') }}{{ len(p._yearly_items()) }}{{ p._yearly_items() }}
{{ _('14_onchange_items') }}{{ len(p._onchange_items()) }}{{ p._onchange_items() }}
{{ _('15_startup_items') }}{{ len(p._startup_items()) }}{{ p._startup_items() }}
{{ _('17_database_items') }}{{ len(p._database_items()) }}{{ p._database_items() }}
{{ _('16_static_items') }}{{ len(p._static_items()) }}{{ p._static_items() }}
{{ _('32_item_cache') }}{{ len(p.item_cache) }}{{ p.item_cache }}
{{ _('20_tageswert_dict') }}{{ len(p.current_values['day']) }}{{ p.current_values['day'] }}
{{ _('21_wochenwert_dict') }}{{ len(p.current_values['week']) }}{{ p.current_values['week'] }}
{{ _('22_monatswert_dict') }}{{ len(p.current_values['month']) }}{{ p.current_values['month'] }}
{{ _('23_jahreswert_dict') }}{{ len(p.current_values['year']) }}{{ p.current_values['year'] }}
{{ _('24_vortagsendwert_dict') }}{{ len(p.previous_values['day']) }}{{ p.previous_values['day'] }}
{{ _('25_vorwochenendwert_dict') }}{{ len(p.previous_values['week']) }}{{ p.previous_values['week'] }}
{{ _('26_vormonatsendwert_dict') }}{{ len(p.previous_values['month']) }}{{ p.previous_values['month'] }}
{{ _('27_vorjahresendwert_dict') }}{{ len(p.previous_values['year']) }}{{ p.previous_values['year'] }}
{{ _('get_item_list') }}{{ len(p.get_item_list('database_addon', True)) }}{{ p.get_item_list('database_addon', True) }}
{{ _('_plg_item_dict') }}{{ len(p._plg_item_dict) }}{{ p._plg_item_dict }}
{{ _('work_item_queue_thread') }}{{ len(p._plg_item_dict) }}{% if p.work_item_queue_thread != None %}{{ p.work_item_queue_thread.is_alive() }}{% endif %}
{% endblock bodytab2 %} {% block bodytab3 %} -
-

{{_('Item Attribute')}}

- {% for function, itemdefinitions_dict in p.metadata.itemdefinitions.items() %} -
-
- {{ function }}      {{('Beschreibung:')}} {{ itemdefinitions_dict['description'][language] }}       {{ ('Ergebnisdatentyp:')}} {{ itemdefinitions_dict['type'] }} -
- {% if 'valid_list' in itemdefinitions_dict %} -
- - +

{{_('Item Attribute')}}

+{% for function, itemdefinitions_dict in p.metadata.itemdefinitions.items() %} +
+
+ {{ function }}      {{('Beschreibung:')}} {{ itemdefinitions_dict['description'][language] }}       {{ ('Ergebnisdatentyp:')}} {{ itemdefinitions_dict['type'] }} +
+ {% if 'valid_list' in itemdefinitions_dict %} +
+
+ + + + + + + + + + {% for entry in itemdefinitions_dict['valid_list'] %} - - - + + + + - - - {% for entry in itemdefinitions_dict['valid_list'] %} - - - - - - {% endfor %} - -
{{_('Item Attributwert')}}{{_('Item_Type')}}{{_('Berechnung')}}{{_('Beschreibung')}}
{{_('Item Attributwert')}}{{_('Item_Type')}}{{_('Beschreibung')}}{{ entry }}{% if 'valid_list_item_type' in itemdefinitions_dict %} {{ itemdefinitions_dict.valid_list_item_type[loop.index0] }} {% else %} {{_('-')}} {% endif %}{% if 'valid_list_calculation' in itemdefinitions_dict %} {{ _(itemdefinitions_dict.valid_list_calculation[loop.index0] | string) }} {% else %} {{_('-')}} {% endif %}{% if 'valid_list_description' in itemdefinitions_dict %} {{ itemdefinitions_dict.valid_list_description[loop.index0] | string }} {% else %} {{_('-')}} {% endif %}
{{ entry }}{% if 'valid_list_item_type' in itemdefinitions_dict %} {{ itemdefinitions_dict.valid_list_item_type[loop.index0] }} {% else %} {{_('-')}} {% endif %}{% if 'valid_list_description' in itemdefinitions_dict %} {{ itemdefinitions_dict.valid_list_description[loop.index0] |string }} {% else %} {{_('-')}} {% endif %}
-
- {% endif %} -
- {% endfor %} + {% endfor %} + + +
+ {% endif %} +
+{% endfor %} -
-

{{_('Plugin Funktionen')}}

- {% for function, plugin_functions_dict in p.metadata.plugin_functions.items() %} -
-
- {{ function }}      {{('Beschreibung:')}} {{ plugin_functions_dict['description'][language] }}       {{ ('Ergebnisdatentyp:')}} {{ plugin_functions_dict['type'] }} -
+
+

{{_('Plugin Funktionen')}}

+{% for function, plugin_functions_dict in p.metadata.plugin_functions.items() %} +
+
+ {{ function }}      {{('Beschreibung:')}} {{ plugin_functions_dict['description'][language] }}       {{ ('Ergebnisdatentyp:')}} {{ plugin_functions_dict['type'] }} +
- {% if 'parameters' in plugin_functions_dict %} -
-
- - + {% if 'parameters' in plugin_functions_dict %} +
+
+
+ + + + + + + + + + {% for entry in plugin_functions_dict['parameters'] %} - - - - + + + + - - - {% for entry in plugin_functions_dict['parameters'] %} - - - - - - - {% endfor %} - -
{{_('Parameter')}}{{_('Beschreibung')}}{{_('Type')}}{{_('zulässige Werte')}}
{{_('Parameter')}}{{_('Beschreibung')}}{{_('Type')}}{{_('zulässige Werte')}}{{ entry }}{{ plugin_functions_dict['parameters'][entry]['description'][language] }}{{ plugin_functions_dict['parameters'][entry]['type'] }}{{ plugin_functions_dict['parameters'][entry]['valid_list'] }}
{{ entry }}{{ plugin_functions_dict['parameters'][entry]['description'][language] }}{{ plugin_functions_dict['parameters'][entry]['type'] }}{{ plugin_functions_dict['parameters'][entry]['valid_list'] }}
-
- {% endif %} -
- {% endfor %} + {% endfor %} + + +
+ {% endif %} +
+{% endfor %}
{% endblock bodytab3 %} From dbbf775977aabbde9e6a27ade92ad58c51d513b2 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 29 Mar 2023 17:16:20 +0200 Subject: [PATCH 159/178] DB_ADDON Plugin - fix type hints --- db_addon/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db_addon/__init__.py b/db_addon/__init__.py index 629331a4c..a79b5000e 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -2334,7 +2334,7 @@ def _get_db_version(self) -> str: query = 'SELECT sqlite_version()' if self.db_driver.lower() == 'sqlite3' else 'SELECT VERSION()' return self._fetchone(query)[0] - def _get_db_connect_timeout(self) -> str: + def _get_db_connect_timeout(self) -> list: """ Query database timeout """ @@ -2342,7 +2342,7 @@ def _get_db_connect_timeout(self) -> str: query = "SHOW GLOBAL VARIABLES LIKE 'connect_timeout'" return self._fetchone(query) - def _get_db_net_read_timeout(self) -> str: + def _get_db_net_read_timeout(self) -> list: """ Query database timeout net_read_timeout """ @@ -2372,7 +2372,7 @@ def _fetchall(self, query: str, params: dict = None, cur=None) -> list: return self._query(self._db.fetchall, query, params, cur) - def _query(self, fetch, query: str, params: dict = None, cur=None) -> list: + def _query(self, fetch, query: str, params: dict = None, cur=None) -> Union[None, list]: if params is None: params = {} From 43280349681f72ae1c1950bc0006e0ba97fd358c Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 29 Mar 2023 18:06:47 +0200 Subject: [PATCH 160/178] DB_ADDON Plugin - round result wachstumsgradtage --- db_addon/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db_addon/__init__.py b/db_addon/__init__.py index a79b5000e..187485190 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -1619,7 +1619,7 @@ def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], min_val = entry[1] max_val = entry[2] wgt += ((min_val + max(30, max_val) / 2) - threshold) - return wgt + return int(round(wgt, 0)) # Die modifizierte Berechnung des einfachen Durchschnitts. elif method == 1: @@ -1630,7 +1630,7 @@ def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], min_val = entry[1] max_val = entry[2] wgt += ((min(threshold, min_val) + max(30, max_val) / 2) - threshold) - return wgt + return int(round(wgt, 0)) else: self.logger.info(f"Method for 'Wachstumsgradtag' Calculation not defined.'") From dcbff1e48d756b4b93d3ecf569fecbcb7bd92f64 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Wed, 29 Mar 2023 20:57:00 +0200 Subject: [PATCH 161/178] viessmann: fix KW response handling --- viessmann/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/viessmann/__init__.py b/viessmann/__init__.py index c260bf1c1..11bd79325 100755 --- a/viessmann/__init__.py +++ b/viessmann/__init__.py @@ -1414,8 +1414,9 @@ def _parse_response(self, response, commandname='', read_response=True): self.logger.debug(f'Response decoded to: commandcode: {commandcode}, responsedatacode: {responsedatacode}, valuebytecount: {valuebytecount}, responsetypecode: {responsetypecode}') self.logger.debug(f'Rawdatabytes formatted: {self._bytes2hexstring(rawdatabytes)} and unformatted: {rawdatabytes}') - # Process response for items if read response and not error - if responsedatacode == 1 and responsetypecode != 3: + # Process response for items if response and not error + # added: only in P300 or if read_response is set, do not try if KW replies with 0x00 (OK) + if responsedatacode == 1 and responsetypecode != 3 and (self._protocol == 'P300' or read_response): # parse response if command config is available commandname = self._commandname_by_commandcode(commandcode) From 229d61c0d89251f4aaa14a47d6b26176ff43b348 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 29 Mar 2023 21:57:15 +0200 Subject: [PATCH 162/178] DB_ADDON Plugin - bugfix handling of defaults at parsing wachstumgsgradtage --- db_addon/__init__.py | 69 ++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/db_addon/__init__.py b/db_addon/__init__.py index 187485190..a9f32f6c1 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -309,10 +309,12 @@ def check_db_addon_fct(check_item) -> bool: elif db_addon_fct == 'wachstumsgradtage': DEFAULT_THRESHOLD = 10 db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) - if db_addon_params is None or 'threshold' not in db_addon_params: + if db_addon_params is None or 'year' not in db_addon_params: + self.logger.info(f"No 'year' for evaluation via 'db_addon_params' of item {item.path()} for function {db_addon_fct} given. Default with 'current year' will be used.") + db_addon_params = {'year': 'current'} + if 'threshold' not in db_addon_params: self.logger.info(f"No 'threshold' for evaluation via 'db_addon_params' of item {item.path()} for function {db_addon_fct} given. Default with {DEFAULT_THRESHOLD} will be used.") - db_addon_params = {'threshold': DEFAULT_THRESHOLD} if db_addon_params is None else db_addon_params - + db_addon_params.update({'threshold': DEFAULT_THRESHOLD}) if not isinstance(db_addon_params['threshold'], int): threshold = to_int(db_addon_params['threshold']) db_addon_params['threshold'] = DEFAULT_THRESHOLD if threshold is None else threshold @@ -1428,6 +1430,7 @@ def _handle_kaeltesumme(self, database_item: Item, year: Union[int, str], month: if raw_data == [[None, None]]: return + # akkumulieren alle negativen Werte ks = 0 for entry in raw_data: if entry[1] < 0: @@ -1495,9 +1498,10 @@ def _handle_waermesumme(self, database_item: Item, year: Union[int, str], month: if raw_data == [[None, None]]: return + # akkumulieren alle Werte, größer/gleich Schwellenwert ws = 0 for entry in raw_data: - if entry[1] > threshold: + if entry[1] >= threshold: ws += entry[1] return int(round(ws, 0)) @@ -1548,21 +1552,22 @@ def _handle_gruenlandtemperatursumme(self, database_item: Item, year: Union[int, if raw_data == [[None, None]]: return + # akkumulieren alle Werte, größer/gleich Schwellenwert, im Januar gewichtet mit 50%, im Februar mit 75% try: gts = 0 for entry in raw_data: - dt = datetime.datetime.fromtimestamp(entry[0] / 1000) + timestamp, value = entry + dt = datetime.datetime.fromtimestamp(timestamp / 1000) if dt.month == 1: - gts += (entry[1] * 0.5) + value = value * 0.5 elif dt.month == 2: - gts += (entry[1] * 0.75) - else: - gts += entry[1] + value = value * 0.75 + gts += value return int(round(gts, 0)) except Exception as e: self.logger.error(f"Error {e} occurred during calculation of gruenlandtemperatursumme with {raw_data=} for {database_item.path()=}") - def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], method: int = 0, threshold: int = 10) -> Union[int, None]: + def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], method: int = 0, threshold: int = 10): """ Calculate "wachstumsgradtage" for given year with temperature thershold https://de.wikipedia.org/wiki/Wachstumsgradtag @@ -1610,29 +1615,37 @@ def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], if raw_data == [[None, None]]: return - # Die Berechnung des einfachen Durchschnitts. - if method == 0: + # Die Berechnung des einfachen Durchschnitts // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert + wgte = 0 + wgte_list = [] + if method == 0 or method == 10: self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Berechnung des einfachen Durchschnitts'.") - wgt = 0 for entry in raw_data: - timestamp = entry[0] - min_val = entry[1] - max_val = entry[2] - wgt += ((min_val + max(30, max_val) / 2) - threshold) - return int(round(wgt, 0)) - - # Die modifizierte Berechnung des einfachen Durchschnitts. - elif method == 1: + timestamp, min_val, max_val = entry + wgt = (((min_val + min(30, max_val)) / 2) - threshold) + if wgt > 0: + wgte += wgt + wgte_list.append([timestamp, int(round(wgte, 0))]) + if method == 0: + return int(round(wgte, 0)) + else: + return wgte_list + + # Die modifizierte Berechnung des einfachen Durchschnitts. // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur mit mind Schwellentemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert + elif method == 1 or method == 11: self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Modifizierte Berechnung des einfachen Durchschnitts'.") - wgt = 0 for entry in raw_data: - timestamp = entry[0] - min_val = entry[1] - max_val = entry[2] - wgt += ((min(threshold, min_val) + max(30, max_val) / 2) - threshold) - return int(round(wgt, 0)) + timestamp, min_val, max_val = entry + wgt = (((max(threshold, min_val) + min(30.0, max_val)) / 2) - threshold) + if wgt > 0: + wgte += wgt + wgte_list.append([timestamp, int(round(wgte, 0))]) + if method == 1: + return int(round(wgte, 0)) + else: + return wgte_list else: - self.logger.info(f"Method for 'Wachstumsgradtag' Calculation not defined.'") + self.logger.info(f"Method for 'Wachstumsgradtag' calculation not defined.'") def _prepare_temperature_list(self, database_item: Item, start: int, end: int = 0, ignore_value=None, version: str = 'hour') -> list: From 84fd286e54bee440bea6cea7fb86ed936d65e673 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Wed, 29 Mar 2023 21:58:42 +0200 Subject: [PATCH 163/178] DB_ADDON Plugin - Update Doku --- db_addon/plugin.yaml | 2 +- db_addon/user_doc.rst | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/db_addon/plugin.yaml b/db_addon/plugin.yaml index 176ffbdc0..85d8d3c5f 100644 --- a/db_addon/plugin.yaml +++ b/db_addon/plugin.yaml @@ -279,7 +279,7 @@ item_attributes: - 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=mandatory: int, month=optional: str, threshold=optional: int)' - 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=mandatory)' - 'Berechnet die Tagesmitteltemperatur auf Basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (timeframe=day, count=integer)' - - 'Berechnet die Wachstumsgradtage auf Basis der stündlichen Durchschnittswerte eines Tages für das laufende Jahr mit an Angabe des Temperaturschwellenwertes (threshold=Schwellentemperatur: int)' + - 'Berechnet die Wachstumsgradtage auf Basis der stündlichen Durchschnittswerte eines Tages für das laufende Jahr mit an Angabe des Temperaturschwellenwertes (year=Jahr: int, method=0/1: int, threshold=Schwellentemperatur: int)' - 'Abfrage der DB: db_addon_params: (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional)' valid_list_item_type: - 'num' diff --git a/db_addon/user_doc.rst b/db_addon/user_doc.rst index 06faf71bc..563bd533b 100644 --- a/db_addon/user_doc.rst +++ b/db_addon/user_doc.rst @@ -206,6 +206,13 @@ Bei einer Wärmesumme von 200 Grad ist eine Düngung angesagt. siehe: https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme +Folgende Parameter sind möglich / notwendig: + +.. code-block:: yaml + db_addon_params: "year=current" + +- year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') + Wachstumsgradtag ---------------- @@ -216,27 +223,54 @@ Verfügbar sind die Berechnung über "einfachen Durchschnitt der Tagestemperatur siehe https://de.wikipedia.org/wiki/Wachstumsgradtag +Folgende Parameter sind möglich / notwendig: + +.. code-block:: yaml + db_addon_params: "year=current, method=1, threshold=10" + +- year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') +- method: 0-Berechnung über "einfachen Durchschnitt der Tagestemperaturen", 1-Berechnung über "modifizierten Durchschnitt (default: 0) +der Tagestemperaturen" // 10, 11 Ausgabe aus Zeitserie +- threshold: Schwellentemperatur in °C (int) (default: 10) + Wärmesumme ---------- -Die Wärmesumme soll eine Aussage über den Sommer und die Pflanzenreife liefern. Es gibt keine eindeutige Definition dier Größe "Wärmesumme". +Die Wärmesumme soll eine Aussage über den Sommer und die Pflanzenreife liefern. Es gibt keine eindeutige Definition der Größe "Wärmesumme". Berechnet wird die Wärmesumme als Summe aller Tagesmitteltemperaturen über einem Schwellenwert ab dem 1.1. des Jahres. siehe https://de.wikipedia.org/wiki/W%C3%A4rmesumme +Folgende Parameter sind möglich / notwendig: + +.. code-block:: yaml + db_addon_params: "year=current, month=1, threshold=10" + +- year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') +- month: Monat (int) des Jahres, für das die Berechnung ausgeführt werden soll (optional) (default: None) +- threshold: Schwellentemperatur in °C (int) (default: 10) + Kältesumme ---------- Die Kältesumme soll eine Aussage über die Härte des Winters liefern. -Berechnet wird die Kältesumme als Summe aller negtiven Tagesmitteltemperaturenab dem 21.9. des Jahres bis 31.3. des Folgejahres. +Berechnet wird die Kältesumme als Summe aller negativen Tagesmitteltemperaturen ab dem 21.9. des Jahres bis 31.3. des Folgejahres. siehe https://de.wikipedia.org/wiki/K%C3%A4ltesumme +Folgende Parameter sind möglich / notwendig: + +.. code-block:: yaml + db_addon_params: "year=current, month=1" + +- year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') +- month: Monat (int) des Jahres, für das die Berechnung ausgeführt werden soll (optional) (default: None) Tagesmitteltemperatur --------------------- -Die Tagesmitteltemperatur wird auf Basis der stündlichen Durchschnittswerte eines Tages (aller in der DB enthaltenen Datensätze) für die angegebene Anzahl von Tagen (days=optional) berechnet. +Die Tagesmitteltemperatur wird auf Basis der stündlichen Durchschnittswerte eines Tages (aller in der DB enthaltenen Datensätze) +für die angegebene Anzahl von Tagen (days=optional) berechnet. From b271beedf892586b2339af820d9ed78a5d5d6aba Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 30 Mar 2023 11:19:35 +0200 Subject: [PATCH 164/178] Workflows: Update to unittests.yml --- .github/workflows/unittests.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index b200e52a4..2b2406663 100755 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -31,23 +31,23 @@ jobs: - name: Workflow Information run: | - echo github.event_name ${{ github.event_name }} - echo github.workflow ${{ github.workflow }} - echo github.action_repository ${{ github.action_repository }} - echo github.actor ${{ github.actor }} - echo github.ref_name ${{ github.ref_name }} - echo github.ref ${{ github.ref }} - echo github.base_ref '${{ github.base_ref }}' - echo github.head_ref '${{ github.head_ref }}' + echo github.event_name '${{ github.event_name }}' + echo github.workflow '${{ github.workflow }}' + echo github.action_repository '${{ github.action_repository }}' + echo github.actor '${{ github.actor }}' + echo github.ref_name '${{ github.ref_name }}' + echo github.ref '${{ github.ref }}' + echo github.base_ref '${{ github.base_ref }}' + echo github.head_ref '${{ github.head_ref }}' echo github.pull_request.base.ref '${{ github.pull_request.base.ref }}' echo steps.extract_branch.outputs.branch '${{ steps.extract_branch.outputs.branch }}' - - name: Checkout core from branch '${{steps.extract_branch.outputs.branch}}' (for push) + - name: Checkout core from branch '${{ steps.extract_branch.outputs.branch }}' (for push) if: github.event_name != 'pull_request' uses: actions/checkout@v3 with: repository: smarthomeNG/smarthome - ref: ${{steps.extract_branch.outputs.branch}} + ref: ${{ steps.extract_branch.outputs.branch }} - name: Checkout core from branch 'develop' (for pull request) if: github.event_name == 'pull_request' From 699d28cf6dca2dc14ecf28cf416f84749381d22e Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 30 Mar 2023 11:20:09 +0200 Subject: [PATCH 165/178] avm: Changed requirement from lxml==4.9.2 to lxml>=4.9.2 --- avm/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avm/requirements.txt b/avm/requirements.txt index ad2e259bc..3437857be 100644 --- a/avm/requirements.txt +++ b/avm/requirements.txt @@ -1,2 +1,2 @@ requests -lxml==4.9.2 \ No newline at end of file +lxml>=4.9.2 From 30f4db4966ec4479adee1a9e943111243794b390 Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 30 Mar 2023 11:22:12 +0200 Subject: [PATCH 166/178] onewire: Added _parasitic_power_wait to webinterface and added unit to values in headtable --- onewire/__init__.py | 3 ++- onewire/locale.yaml | 4 ++-- onewire/webif/templates/index.html | 12 ++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/onewire/__init__.py b/onewire/__init__.py index fe72c6124..59266bf29 100755 --- a/onewire/__init__.py +++ b/onewire/__init__.py @@ -405,13 +405,14 @@ def _sensor_cycle(self): continue try: value = self.owbase.read('/uncached' + path).decode() + self.stopevent.wait(self._parasitic_power_wait) value = float(value) if key.startswith('T') and value == 85: self.logger.error(f"reading {addr} gives error value 85.") continue except Exception as e: # time.sleep(self._parasitic_power_wait) - self.stopevent.wait(self._parasitic_power_wait) + #self.stopevent.wait(self._parasitic_power_wait) self._sensors[addr][key]['readerrors'] = self._sensors[addr][key].get('readerrors', 0) + 1 if self._sensors[addr][key]['readerrors'] % self.warn_after == 0: self.logger.warning(f"_sensor_cycle: {self._sensors[addr][key]['readerrors']}. problem reading {addr}-{key}, error: {e}") diff --git a/onewire/locale.yaml b/onewire/locale.yaml index 411b03160..17645c346 100755 --- a/onewire/locale.yaml +++ b/onewire/locale.yaml @@ -20,7 +20,7 @@ plugin_translations: 'Gerät(e)' : { 'de': '=', 'en': 'Device(s)' } # Alternative format for translations of longer texts: - 'Hier kommt der Inhalt des Webinterfaces hin.': + 'Wartezeit für parasitäre Spannung': de: '=' - en: 'Here goes the content of the web interface.' + en: 'Parasitic power wait time' diff --git a/onewire/webif/templates/index.html b/onewire/webif/templates/index.html index 78031eadb..4e84fcb60 100755 --- a/onewire/webif/templates/index.html +++ b/onewire/webif/templates/index.html @@ -251,7 +251,7 @@ {{ p.host }} {{ _('IO Wartezeit') }} - {{ p._io_wait }} + {{ p._io_wait }} {{ _('Sek.') }} @@ -259,19 +259,15 @@ {{ p.port }} {{ _('iButton Wartezeit') }} - {{ p._button_wait }} + {{ p._button_wait }} {{ _('Sek.') }} {{ _('Cycle') }} {{ p._cycle }} {{ _('Sek.') }} - - - + {{ _('Wartezeit für parasitäre Spannung') }} + {{ p._parasitic_power_wait }} {{ _('Sek.') }} From ea12c00d138d8b6fb35c304eae45bb5272acd1c6 Mon Sep 17 00:00:00 2001 From: msinn Date: Thu, 30 Mar 2023 12:55:57 +0200 Subject: [PATCH 167/178] snmp: Changed requirement from puresnmp >=1.7.2 to puresnmp >=1.7.2,<2.0.0 because v2 implements breaking changes --- snmp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snmp/requirements.txt b/snmp/requirements.txt index ce710bede..ea8985ff0 100755 --- a/snmp/requirements.txt +++ b/snmp/requirements.txt @@ -1 +1 @@ -puresnmp >=1.7.2 \ No newline at end of file +puresnmp >=1.7.2,<2.0.0 From 1ace588655db7c9bffb528f2c1de27ac5814cc26 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Thu, 30 Mar 2023 19:00:49 +0200 Subject: [PATCH 168/178] DB_ADDON Plugin - Bugfix in _handle_gruenlandtempsumme --- db_addon/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/db_addon/__init__.py b/db_addon/__init__.py index a9f32f6c1..910d532f4 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -1552,20 +1552,18 @@ def _handle_gruenlandtemperatursumme(self, database_item: Item, year: Union[int, if raw_data == [[None, None]]: return - # akkumulieren alle Werte, größer/gleich Schwellenwert, im Januar gewichtet mit 50%, im Februar mit 75% - try: - gts = 0 - for entry in raw_data: - timestamp, value = entry + # akkumulieren alle positiven Tagesmitteltemperaturen, im Januar gewichtet mit 50%, im Februar mit 75% + gts = 0 + for entry in raw_data: + timestamp, value = entry + if value > 0: dt = datetime.datetime.fromtimestamp(timestamp / 1000) if dt.month == 1: value = value * 0.5 elif dt.month == 2: value = value * 0.75 gts += value - return int(round(gts, 0)) - except Exception as e: - self.logger.error(f"Error {e} occurred during calculation of gruenlandtemperatursumme with {raw_data=} for {database_item.path()=}") + return int(round(gts, 0)) def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], method: int = 0, threshold: int = 10): """ From 71dccf51a09e599003ead768245a64350c74c986 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 30 Mar 2023 20:59:58 +0200 Subject: [PATCH 169/178] Version 1.0.3 --- alexarc4shng/README.md | 92 ++- alexarc4shng/__init__.py | 671 +++++++++++------- alexarc4shng/assets/Alexa_lists.jpg | Bin 0 -> 40947 bytes alexarc4shng/cmd/Announce_Trumpet.cmd | 3 + alexarc4shng/cmd/Announcement.cmd | 3 + alexarc4shng/cmd/Klingel.cmd | 3 + alexarc4shng/cmd/MultiText2Speech.cmd | 3 + alexarc4shng/cmd/SSML.cmd | 6 +- alexarc4shng/cmd/StartRoutine.cmd | 3 + alexarc4shng/cmd/StartTuneInStation.cmd | 6 +- alexarc4shng/cmd/TuneInNew.cmd | 3 + alexarc4shng/locale.yaml | 83 +-- alexarc4shng/plugin.yaml | 26 +- alexarc4shng/requirements.txt | 1 + alexarc4shng/user_doc.rst | 2 +- alexarc4shng/webif/static/img/plugin_logo.png | Bin 14507 -> 164796 bytes alexarc4shng/webif/static/js/handler.js | 380 +++++++++- alexarc4shng/webif/templates/index.html | 329 +++++++-- 18 files changed, 1232 insertions(+), 382 deletions(-) create mode 100755 alexarc4shng/assets/Alexa_lists.jpg create mode 100755 alexarc4shng/cmd/Announce_Trumpet.cmd create mode 100755 alexarc4shng/cmd/Announcement.cmd create mode 100755 alexarc4shng/cmd/Klingel.cmd create mode 100755 alexarc4shng/cmd/MultiText2Speech.cmd create mode 100755 alexarc4shng/cmd/StartRoutine.cmd create mode 100755 alexarc4shng/cmd/TuneInNew.cmd diff --git a/alexarc4shng/README.md b/alexarc4shng/README.md index f162d96f2..a1e0eaedc 100755 --- a/alexarc4shng/README.md +++ b/alexarc4shng/README.md @@ -1,6 +1,6 @@ # AlexaRc4shNG -#### Version 1.0.2 +#### Version 1.0.3 The plugin gives the possibilty to control an Alexa-Echo-Device remote by smartHomeNG. So its possible to switch on an TuneIn-Radio Channel, send some messages via Text2Speech when an event happens on the knx-bus or on the Visu. On the Web-Interface you can define your own commandlets (functions). The follwing functions are available on the Web-Interface : @@ -38,7 +38,7 @@ Special thanks to Jonofe from the [Edomi-Forum](https://knx-user-forum.de/forum/ - Pause (pauses the actual media) - Text2Speech (sends a Text to the echo, echo will speak it) - StartTuneInStation (starts a TuneInRadiostation with the guideID you send) -- SSML (Speak to Text with[Speech Synthesis Markup Language](https://developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html)) +- SSML (Speak to Text with [Speech Synthesis Markup Language](https://developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html)) - VolumeAdj (adjusts the volume during playing some media not working from webinterface test functions) - VolumeSet (sets the volume to value from 0-100 percent) @@ -47,7 +47,7 @@ Special thanks to Jonofe from the [Edomi-Forum](https://knx-user-forum.de/forum/ ```yaml = Value to send as alpha = Value to send as numeric -#item.path/# = item-path of the value that should be inserted into text or ssml +"#item.path/#" = item-path of the value that should be inserted into text or ssml = SerialNo. of the device where the command should go to = device family = deviceType @@ -57,6 +57,14 @@ Special thanks to Jonofe from the [Edomi-Forum](https://knx-user-forum.de/forum/ ## ChangeLog +#### 2021.02.10 Version 1.0.3 + +- added MFA for Auto-Login +- added new Parameter (mfa_secret) in the etc/plugin.yaml +- added Step by Step Setup in Web-IF for MFA +- added public function to get the ToDo-List +- added public function to get the Shopping-List + #### 2020.03.20 Version 1.0.2 - changed public function "send_cmd_by_curl" to "send_cmd" @@ -160,7 +168,7 @@ Item2EnableAlexaRC->Item controlled by UZSU or something else which enables the alexa_credentials-> user:pwd (base64 encoded)
item_2_enable_alexa_rc -> Item to allow smarthomeNG to send Commands to Echo's
login_update_cycle->seconds to wait for automatic Login in to refresh the cookie - +mfa_secret-> The MFA-Secret you got from Amazon-Website (fill it out with the Web-Interface) ```yaml @@ -171,6 +179,7 @@ AlexaRc4shNG: item_2_enable_alexa_rc: Item_to_enable_Alexaremote alexa_credentials: : login_update_cycle: 432000 + mfa_secret: ``` @@ -203,12 +212,15 @@ alexa_cmd_01: True:EchoDotKueche:StartTuneInStation:s96141 Value = <20.0 - send command when value of the item becomes less then 20.0 EchodotKueche = Devicename where the Command should be send to Text2Speech = Name of the Commandlet -Value_to_Send = Die Temperatur in der Kueche ist niedriger als 20 Grad Die Temperatur ist jetzt #test.testzimmer.temperature.actual/# Grad #test.testzimmer.temperature.actual/# = item-path of the value that should be inserted +Value_to_Send = Die Temperatur in der Kueche ist niedriger als 20 Grad Die Temperatur ist jetzt #test.testzimmer.temperature.actual/# Grad ``` +```yaml +#test.testzimmer.temperature.actual/# = item-path of the value that should be inserted +``` example:
` -alexa_cmd_01: <20.0:EchoDotKueche:Text2Speech:Die Temperatur in der Kueche ist niedriger als 20 Grad Die Temperatur ist jetzt \#test.testzimmer.temperature.actual/\# Grad +alexa_cmd_01: <20.0:EchoDotKueche:Text2Speech:Die Temperatur in der Kueche ist niedriger als 20 Grad Die Temperatur ist jetzt #test.testzimmer.temperature.actual/# Grad ` You can find the paths of the items on the backend-WebInterface - section items. @@ -266,12 +278,13 @@ Example for settings in an item.conf file : alexa_cmd_01 = '"True:EchoDotKueche:StartTuneInStation:s96141" alexa_cmd_02 ="True:EchoDotKueche:Text2Speech:Hallo das Licht im Buero ist eingeschalten" alexa_cmd_03 = "False:EchoDotKueche:Text2Speech:Hallo das Licht im Buero ist aus" - alexa_cmd_04 = "False:EchoDotKueche:Pause: " + alexa_cmd_04 = "False:EchoDotKueche:Pause:" visu_acl = rw knx_dpt = 1 knx_listen = 1/1/105 knx_send = 1/1/105 - enforce_updates = truey_attr: setting + enforce_updates = true + ``` ### logic.yaml @@ -282,7 +295,7 @@ Right now no logics are implemented. But you can trigger the functions by your o The plugin provides the following publich functions. You can use it for example in logics. -### send_cmd(dvName, cmdName, mValue) +### send_cmd(dvName:str, cmdName:str, mValue:str) example how to use in logics: @@ -293,7 +306,7 @@ sh.AlexaRc4shNG.send_cmd('Kueche','Text2Speech','Der Sensor der Hebenlage signal ``` Sends a command to the device. "dvName" is the name of the device, "cmdName" is the name of the CommandLet, mValue is the value you would send. You can find all this informations on the Web-Interface. -You can also user the [placeholders](#placeholders) +You can also use the [placeholders](#placeholders) - the result will be the HTTP-Status of the request as string (str) @@ -305,11 +318,68 @@ This function returns the Device-Name of the last Echo Device which got a voice myLastDevice = sh.AlexaRc4shNG.get_last_alexa() ``` + +### get_list(type:str) + +This function returns the ToDo or the Shopping list - depending on "type" as dict
+ +valid types are : +```yaml + 'SHOPPING_LIST' + 'TO_DO' +``` + + +```yaml +sh.AlexaRc4shNG.get_list(type:str) +``` +## Example logic to fill Items with List-Infos + +
+
+from datetime import datetime
+# get the Todo-List
+myList=sh.AlexaRc4shNG.get_list('TO_DO')
+for entry in myList:
+  if entry['completed'] == True:
+    entry['icon'] = 'control_clear'
+  else:
+    entry['icon'] = 'control_home'
+  entry['date'] = datetime.fromtimestamp((entry['updatedDateTime']/1000)).strftime("%d.%m.%Y, %H:%M:%S")
+# Write list to Item - type should be list
+sh.Alexa_Lists.list.todo(myList)
+# get the shopping-List
+myList=sh.AlexaRc4shNG.get_list('SHOPPING_LIST')
+for entry in myList:
+  if entry['completed'] == True:
+    entry['icon'] = 'control_clear'
+  else:
+    entry['icon'] = 'jquery_shop'
+  entry['date'] = datetime.fromtimestamp((entry['updatedDateTime']/1000)).strftime("%d.%m.%Y, %H:%M:%S")
+# Write list to Item - type should be list
+sh.Alexa_Lists.list.shopping(myList)
+
+
+ +## Example to show lists in smartVisu with status.activelist +
+
+status.activelist('','Alexa_Lists.list.todo','value','date','value','info')
+
+status.activelist('','Alexa_Lists.list.shopping','value','date','value','info')
+
+
+ +### Ergebnis : +![PlaceHolder](./assets/Alexa_lists.jpg "jpg") + + + # Web-Interface
The Webinterface is reachable on you smarthomeNG server here :
-yourserver:8383/alexarc4shng/ +http://yourserver:8383/plugins/alexarc4shng/ ## Cookie-Handling diff --git a/alexarc4shng/__init__.py b/alexarc4shng/__init__.py index 57af04716..4d686018f 100755 --- a/alexarc4shng/__init__.py +++ b/alexarc4shng/__init__.py @@ -3,7 +3,7 @@ ######################################################################### # Copyright 2020 AndreK andre.kohler01@googlemail.com ######################################################################### -# This file is part of SmartHomeNG. +# This file is part of SmartHomeNG. # # Sample plugin for new plugins to run with SmartHomeNG version 1.5.2 and # upwards. @@ -41,12 +41,16 @@ import time import base64 import requests +from urllib.parse import urlencode - - +ImportPyOTPError = False +try: + import pyotp +except Exception as err: + ImportPyOTPError = True class shngObjects(object): def __init__(self): @@ -113,7 +117,7 @@ def __init__(self, id): ############################################################################## class AlexaRc4shNG(SmartPlugin): - PLUGIN_VERSION = '1.0.2' + PLUGIN_VERSION = '1.0.3' ALLOW_MULTIINSTANCE = False """ Main class of the Plugin. Does all plugin specific stuff and provides @@ -127,7 +131,7 @@ def __init__(self, sh, *args, **kwargs): self.items = Items.get_instance() self.shngObjects = shngObjects() self.shtime = Shtime.get_instance() - + # Init values self.header = '' self.cookie = {} @@ -136,19 +140,27 @@ def __init__(self, sh, *args, **kwargs): self.login_state = False self.last_update_time = '' self.next_update_time = '' + self.ImportPyOTPError = False # get parameters self.cookiefile = self.get_parameter_value('cookiefile') self.host = self.get_parameter_value('host') self.AlexaEnableItem = self.get_parameter_value('item_2_enable_alexa_rc') - self.credentials = self.get_parameter_value('alexa_credentials').encode('utf-8') - self.credentials = base64.decodebytes(self.credentials).decode('utf-8') + self.credentials = self.get_parameter_value('alexa_credentials') + if (self.credentials != 'None'): + self.credentials = self.get_parameter_value('alexa_credentials').encode('utf-8') + self.credentials = base64.decodebytes(self.credentials).decode('utf-8') self.LoginUpdateCycle = self.get_parameter_value('login_update_cycle') + self.mfa_Secret = self.get_parameter_value('mfa_secret') self.update_file=self.sh.get_basedir()+"/plugins/alexarc4shng/lastlogin.txt" self.rotating_log = [] + # Check if MFA is possible + if (ImportPyOTPError == True): + self.logger.warning("Plugin '{}': problem during import of pyotp, you will not be able to use MFA-Authentication".format(self.get_fullname())) + self.ImportPyOTPError = True if not self.init_webinterface(): self._init_complete = False - + return def run(self): @@ -158,12 +170,11 @@ def run(self): self.logger.info("Plugin '{}': start method called".format(self.get_fullname())) # get additional parameters from files self.csrf = self.parse_cookie_file(self.cookiefile) - + # Check login-state - if logged off and credentials are availabel login in if os.path.isfile(self.cookiefile): self.login_state=self.check_login_state() self.check_refresh_login() - if (self.login_state == False and self.credentials != ''): try: os.remove(self.update_file) @@ -171,19 +182,26 @@ def run(self): pass self.check_refresh_login() self.login_state=self.check_login_state() - - # Collect all devices + + # Collect all devices if (self.login_state): self.Echos = self.get_devices_by_request() else: self.Echos = None # enable scheduler if Login should be updated automatically - + if self.credentials != '': self.scheduler_add('check_login', self.check_refresh_login,cycle=300) #self.scheduler.add('plugins.alexarc4shng.check_login', self.check_refresh_login,cycle=300,from_smartplugin=True) + + if self.ImportPyOTPError: + logline = str(self.shtime.now())[0:19] + ' no pyOTP installed you can not use MFA' + else: + logline = str(self.shtime.now())[0:19] + ' pyOTP installed you can use MFA' + self._insert_protocoll_entry(logline) + self.alive = True - + # if you want to create child threads, do not make them daemon = True! # They will not shutdown properly. (It's a python bug) @@ -198,41 +216,41 @@ def stop(self): def parse_item(self, item): itemFound=False i=1 - + myValue = 'alexa_cmd_{}'.format( '%0.2d' %(i)) while myValue in item.conf: - + self.logger.debug("Plugin '{}': parse item: {} Command {}".format(self.get_fullname(), item,myValue)) - + CmdItem_ID = item._name try: myCommand = item.conf[myValue].split(":") - - + + if not self.shngObjects.exists(CmdItem_ID): self.shngObjects.put(CmdItem_ID) - + actDevice = self.shngObjects.get(CmdItem_ID) actDevice.Commands.append(Cmd(myValue)) - + actCommand = len(actDevice.Commands)-1 - + actDevice.Commands[actCommand].command = item.conf[myValue] myCommand = actDevice.Commands[actCommand].command.split(":") self.logger.info("Plugin '{}': parse item: {}".format(self.get_fullname(), item.conf[myValue])) - + actDevice.Commands[actCommand].ItemValue = myCommand[0] actDevice.Commands[actCommand].EndPoint = myCommand[1] actDevice.Commands[actCommand].Action = myCommand[2] actDevice.Commands[actCommand].Value = myCommand[3] itemFound=True - + except Exception as err: print("Error:" ,err) i += 1 myValue = 'alexa_cmd_{}'.format( '%0.2d' %(i)) - + # todo # if interesting item for sending values: # return update_item @@ -240,18 +258,18 @@ def parse_item(self, item): return self.update_item else: return None - + def parse_logic(self, logic): pass def update_item(self, item, caller=None, source=None, dest=None): - + # Item was not changed but double triggered the Upate_Item-Function if (self.AlexaEnableItem != ""): - AlexaEnabledItem = self.items.return_item(self.AlexaEnableItem) + AlexaEnabledItem = self.items.return_item(self.AlexaEnableItem) if AlexaEnabledItem() != True: return - + if item._type == "str": newValue=str(item()) oldValue=str(item.prev_value()) @@ -261,24 +279,24 @@ def update_item(self, item, caller=None, source=None, dest=None): else: newValue=str(item()) oldValue=str(item.prev_value()) - + # Nur bei Wertänderung, sonst nix wie raus hier if(oldValue == newValue): - return + return # End Test + - - + CmdItem_ID = item._name - - + + if self.shngObjects.exists(CmdItem_ID): self.logger.debug("Plugin '{}': update_item ws called with item '{}' from caller '{}', source '{}' and dest '{}'".format(self.get_fullname(), item, caller, source, dest)) - + actDevice = self.shngObjects.get(CmdItem_ID) - + for myCommand in actDevice.Commands: - + newValue2Set = myCommand.Value myItemBuffer = myCommand.ItemValue # Spezialfall auf bigger / smaller @@ -287,7 +305,7 @@ def update_item(self, item, caller=None, source=None, dest=None): myCompValue = myCommand.ItemValue.replace("<="," ") myCompValue = myCompValue.replace(".",",") myCompValue = float(myCompValue) - myCommand.ItemValue = actValue + myCommand.ItemValue = actValue if newValue > myCompValue: return elif myCommand.ItemValue.find(">=") >=0: @@ -295,7 +313,7 @@ def update_item(self, item, caller=None, source=None, dest=None): myCompValue = myCommand.ItemValue.replace(">="," ") myCompValue = myCompValue.replace(".",",") myCompValue = float(myCompValue) - myCommand.ItemValue = actValue + myCommand.ItemValue = actValue if newValue < myCompValue: return elif myCommand.ItemValue.find("=") >=0 : @@ -303,7 +321,7 @@ def update_item(self, item, caller=None, source=None, dest=None): myCompValue = myCommand.ItemValue.replace("="," ") myCompValue = myCompValue.replace(".",",") myCompValue = float(myCompValue) - myCommand.ItemValue = actValue + myCommand.ItemValue = actValue if newValue != myCompValue: return elif myCommand.ItemValue.find("<") >=0: @@ -324,7 +342,7 @@ def update_item(self, item, caller=None, source=None, dest=None): return else: actValue = str(item()) - + if ("volume" in myCommand.Action.lower()): httpStatus, myPlayerInfo = self.receive_info_by_request(myCommand.EndPoint,"LoadPlayerInfo","") # Store Player-Infos to Device @@ -341,7 +359,7 @@ def update_item(self, item, caller=None, source=None, dest=None): actVolume = int(item()) except: actVolume = 50 - + if ("volumeadj" in myCommand.Action.lower()): myDelta = int(myCommand.Value) if actVolume+myDelta < 0: @@ -350,38 +368,38 @@ def update_item(self, item, caller=None, source=None, dest=None): newValue2Set = 100 else: newValue2Set =actVolume+myDelta - + # neuen Wert speichern in item if ("volume" in myCommand.Action.lower()): item._value = newValue2Set - + if (actValue == str(myCommand.ItemValue) and myCommand): myCommand.ItemValue = myItemBuffer self.send_cmd(myCommand.EndPoint,myCommand.Action,newValue2Set) - - + + # find Value for Key in Json-structure - + def search(self,p, strsearch): - if type(p) is dict: + if type(p) is dict: if strsearch in p: tokenvalue = p[strsearch] if not tokenvalue is None: return tokenvalue else: for i in p: - tokenvalue = self.search(p[i], strsearch) + tokenvalue = self.search(p[i], strsearch) if not tokenvalue is None: return tokenvalue - + # handle Protocoll Entries def _insert_protocoll_entry(self, entry): if len(self.rotating_log) > 400: del self.rotating_log[400:] self.rotating_log.insert (0,entry) - + # Check if update of login is needed def check_refresh_login(self): my_file= self.update_file @@ -392,7 +410,7 @@ def check_refresh_login(self): fp.close() except: last_update_time = 0 - + mytime = time.time() if (last_update_time + self.LoginUpdateCycle < mytime): self.log_off() @@ -405,10 +423,10 @@ def check_refresh_login(self): else: self.last_update_time = datetime.fromtimestamp(last_update_time).strftime('%Y-%m-%d %H:%M:%S') self.next_update_time = datetime.fromtimestamp(last_update_time+self.LoginUpdateCycle).strftime('%Y-%m-%d %H:%M:%S') - - - - + + + + def replace_mutated_vowel(self,mValue): search = ["ä" , "ö" , "ü" , "ß" , "Ä" , "Ö", "Ü", "&" , "é", "á", "ó", "ß"] replace = ["ae", "oe", "ue", "ss", "Ae", "Oe","Ue", "und", "e", "a", "o", "ss"] @@ -421,16 +439,16 @@ def replace_mutated_vowel(self,mValue): counter +=1 except: pass - + return myNewValue - + ############################################## # Amazon API - Calls ############################################## - - + + def check_login_state(self): try: myHeader={ @@ -447,12 +465,12 @@ def check_login_state(self): myHeader = response.headers myDict=json.loads(myContent) mySession.close() - + self.logger.info('Status of check_login_state: %d' % response.status_code) - + logline = str(self.shtime.now())[0:19] +' Status of check_login_state: %d' % response.status_code self._insert_protocoll_entry(logline) - + myAuth =myDict['authentication']['authenticated'] if (myAuth == True): self.logger.info('Login-State checked - Result: Logged ON' ) @@ -461,17 +479,53 @@ def check_login_state(self): return True else: self.logger.info('Login-State checked - Result: Logged OFF' ) - logline = str(self.shtime.now())[0:19] +' Login-State checked - Result: Logged OFF' + logline = str(self.shtime.now())[0:19] +' Login-State checked - Result: Logged OFF' self._insert_protocoll_entry(logline) return False - - - + + + except Exception as err: self.logger.error('Login-State checked - Result: Logged OFF - try to login again') return False - - + + + def get_list(self, type=""): + if (self.login_state == False): + return [] + myReturnList = [] + myHeader = { "Host": "alexa.amazon.de", + "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", + "Connection": "keep-alive", + "Content-Type": "application/json; charset=UTF-8", + "Accept-Language": "en-US,en;q=0.5", + "Referer": "https://alexa.amazon.de/spa/index.html", + "Origin":"https://alexa.amazon.de", + "DNT": "1" + } + mySession = requests.Session() + mySession.cookies.update(self.cookie) + response= mySession.get('https://'+self.host + '/api/namedLists?_=1',headers=myHeader,allow_redirects=True) + myContent= response.content.decode() + self.logger.warning('Lists loaded - content : {}'.format(myContent)) + self._insert_protocoll_entry('Lists loaded - content : {}'.format(myContent)) + + myLists = json.loads(myContent) + for mylistItem in myLists['lists']: + actList = mylistItem['itemId'] + encoded_args = urlencode({'listIds': actList}) + myListUrl = 'https://'+self.host + '/api/namedLists/{0}/items?startTime=&endTime=&completed=&{1}&_=2'.format(actList,encoded_args) + myListResponse = mySession.get(myListUrl,headers=myHeader,allow_redirects=True) + myListResponse= myListResponse.content.decode() + myListResponse = json.loads(myListResponse) + self.logger.warning('List-Entry loaded : {}'.format(myListResponse)) + self._insert_protocoll_entry('List-Entry loaded : {}'.format(myListResponse)) + for ListEntry in myListResponse['list']: + if mylistItem['type'] == type: + myReturnList.append({'value': ListEntry['value'].capitalize(), 'completed' : ListEntry['completed'], 'version': ListEntry['version'], 'createdDateTime': ListEntry['createdDateTime'], 'updatedDateTime': ListEntry['updatedDateTime']}) + + return myReturnList + def receive_info_by_request(self,dvName,cmdName,mValue): actEcho = self.Echos.get(dvName) myUrl='https://'+self.host @@ -484,9 +538,9 @@ def receive_info_by_request(self,dvName,cmdName,mValue): actEcho.family, actEcho.deviceType, actEcho.deviceOwnerCustomerId) - + myDescription,myUrl,myDict = self.load_command_let(cmdName,None) - + myHeader = { "Host": "alexa.amazon.de", "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", "Connection": "keep-alive", @@ -495,7 +549,7 @@ def receive_info_by_request(self,dvName,cmdName,mValue): "Referer": "https://alexa.amazon.de/spa/index.html", "Origin":"https://alexa.amazon.de", "DNT": "1" - } + } mySession = requests.Session() mySession.cookies.update(self.cookie) response= mySession.get(myUrl,headers=myHeader,allow_redirects=True) @@ -504,13 +558,13 @@ def receive_info_by_request(self,dvName,cmdName,mValue): myContent= response.content.decode() myHeader = response.headers myDict=json.loads(myContent) - mySession.close() - + mySession.close() + return myResult,myDict + - - + def get_last_alexa(self): myHeader = { "Host": "alexa.amazon.de", "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", @@ -520,7 +574,7 @@ def get_last_alexa(self): "Referer": "https://alexa.amazon.de/spa/index.html", "Origin":"https://alexa.amazon.de", "DNT": "1" - } + } mySession = requests.Session() mySession.cookies.update(self.cookie) response= mySession.get('https://'+self.host+'/api/activities?startTime=&size=10&offset=0', @@ -529,11 +583,11 @@ def get_last_alexa(self): myContent= response.content.decode() myHeader = response.headers myDict=json.loads(myContent) - mySession.close() + mySession.close() myDevice = myDict["activities"][0]["sourceDeviceIds"][0]["serialNumber"] myLastDevice = self.Echos.get_Device_by_Serial(myDevice) return myLastDevice - + def send_cmd(self,dvName, cmdName,mValue,path=None): # Parse the value field for dynamic content if (str(mValue).find("#") >= 0 and str(mValue).find("/#") >0): @@ -541,7 +595,7 @@ def send_cmd(self,dvName, cmdName,mValue,path=None): LastPos = str(mValue).find("/#",FirstPos) myItemName = str(mValue)[FirstPos+1:LastPos] myItem=self.items.return_item(myItemName) - + if myItem._type == "num": myValue = str(myItem()) myValue = myValue.replace(".", ",") @@ -552,8 +606,8 @@ def send_cmd(self,dvName, cmdName,mValue,path=None): mValue = mValue[0:FirstPos]+myValue+mValue[LastPos:LastPos-2]+mValue[LastPos+2:len(mValue)] mValue = self.replace_mutated_vowel(mValue) - - + + buffer = BytesIO() actEcho = None try: @@ -566,13 +620,13 @@ def send_cmd(self,dvName, cmdName,mValue,path=None): self.logger.warning('found no Echo with Name : {}'.format(dvName)) self._insert_protocoll_entry('found no Echo with Name : {}'.format(dvName)) return - + myUrl='https://'+self.host - + myDescriptions = '' myDict = {} - - + + myDescription,myUrl,myDict = self.load_command_let(cmdName,path) # complete the URL myUrl='https://'+self.host+myUrl @@ -584,31 +638,40 @@ def send_cmd(self,dvName, cmdName,mValue,path=None): actEcho.family, actEcho.deviceType, actEcho.deviceOwnerCustomerId) - + # replace the placeholders in Payload myHeaders=self.create_request_header() - + postfields = self.parse_json(myDict, mValue, actEcho.serialNumber, actEcho.family, actEcho.deviceType, actEcho.deviceOwnerCustomerId) - - + + try: + logline = str(self.shtime.now())[0:19] + ' sending command to "{}" payload {}'.format(dvName, json.dumps(postfields)) + self._insert_protocoll_entry(logline) + except Exception as err: + pass + myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(myUrl,myHeaders,self.cookie,postfields) - + myResult = myStatus + logline = str(self.shtime.now())[0:19] + ' Result of sending Command : {}'.format(myResult) + self._insert_protocoll_entry(logline) + if myResult == 200: self.logger.info('Status of send_cmd: %d' % myResult) + else: self.logger.warning("itemStatus of send_cmd: {}: {}".format(myResult, myContent)) - - return myResult - + + return myResult + def get_devices_by_request(self): try: myHeader={ @@ -626,15 +689,15 @@ def get_devices_by_request(self): myDict=json.loads(myContent) mySession.close() myDevices = EchoDevices() - + self.logger.info('Status of get_devices_by_request: %d' % response.status_code) - - - + + + except Exception as err: self.logger.error('Error while getting Devices: %s' %err) return None - + for device in myDict['devices']: deviceFamily=device['deviceFamily'] #if deviceFamily == 'WHA' or deviceFamily == 'VOX' or deviceFamily == 'FIRE_TV' or deviceFamily == 'TABLET': @@ -642,7 +705,7 @@ def get_devices_by_request(self): try: actName = device['accountName'] myDevices.put(Echo(actName)) - + actDevice = myDevices.get(actName) actDevice.serialNumber=device['serialNumber'] actDevice.deviceType=device['deviceType'] @@ -652,12 +715,23 @@ def get_devices_by_request(self): except Exception as err: self.logger.debug('Error while getting Devices: %s' %err) myDevices = None - + return myDevices - - - - + + + + def read_cookie_file(self,cookiefile): + CookieFile = "" + try: + with open (cookiefile, 'r') as fp: + for line in fp: + CookieFile += line + fp.close() + except Exception as err: + self.logger.debug('Cookiefile could not be opened %s' % cookiefile) + + return CookieFile + def parse_cookie_file(self,cookiefile): self.cookie = {} csrf = 'N/A' @@ -666,25 +740,25 @@ def parse_cookie_file(self,cookiefile): for line in fp: if line.find('amazon.de')<0: continue - + lineFields = line.strip().split('\t') if len(lineFields) >= 7: # add Line to self.cookie if lineFields[2] == '/': self.cookie[lineFields[5]]=lineFields[6] - - + + if lineFields[5] == 'csrf': csrf = lineFields[6] fp.close() except Exception as err: self.logger.debug('Cookiefile could not be opened %s' % cookiefile) - + return csrf - - + + def parse_url(self,myDummy,mValue,serialNumber,familiy,deviceType,deviceOwnerCustomerId): - + myDummy = myDummy.strip() myDummy=myDummy.replace(' ','') # for String @@ -693,34 +767,34 @@ def parse_url(self,myDummy,mValue,serialNumber,familiy,deviceType,deviceOwnerCus except Exception as err: print("no String") # for Numbers - try: + try: myDummy=myDummy.replace('""',mValue) except Exception as err: print("no Integer") - + # Inject the Device informations myDummy=myDummy.replace('',serialNumber) myDummy=myDummy.replace('',familiy) myDummy=myDummy.replace('',deviceType) myDummy=myDummy.replace('',deviceOwnerCustomerId) - + return myDummy - + def parse_json(self,myDict,mValue,serialNumber,familiy,deviceType,deviceOwnerCustomerId): myDummy = json.dumps(myDict, sort_keys=True) - + count = 0 for char in myDummy: if char == '{': count = count + 1 - - + + if count > 1: # Find First Pos for inner Object FirstPos = myDummy.find("{",1) - + # Find last Pos for inner Object LastPos = 0 pos1 = 1 @@ -730,14 +804,14 @@ def parse_json(self,myDict,mValue,serialNumber,familiy,deviceType,deviceOwnerCus correctPos = LastPos LastPos = pos1 LastPos = correctPos - - + + innerJson = myDummy[FirstPos+1:LastPos] innerJson = innerJson.replace('"','\\"') - + myDummy = myDummy[0:FirstPos]+'"{'+innerJson+'}"'+myDummy[LastPos+1:myDummy.__len__()] - - + + myDummy = myDummy.strip() myDummy=myDummy.replace(' ','') # for String @@ -746,20 +820,20 @@ def parse_json(self,myDict,mValue,serialNumber,familiy,deviceType,deviceOwnerCus except Exception as err: print("no String") # for Numbers - try: + try: myDummy=myDummy.replace('""',str(mValue)) except Exception as err: print("no Integer") - + # Inject the Device informations myDummy=myDummy.replace('',serialNumber) myDummy=myDummy.replace('',familiy) myDummy=myDummy.replace('',deviceType) myDummy=myDummy.replace('',deviceOwnerCustomerId) - + return myDummy - - + + def create_request_header(self): myheaders= {"Host": "alexa.amazon.de", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0", @@ -774,16 +848,16 @@ def create_request_header(self): "Cache-Control": "no-cache" } return myheaders - + def load_command_let(self,cmdName,path=None): myDescription = '' myUrl = '' myJson = '' retJson = {} - + if path==None: path=self.sh.get_basedir()+"/plugins/alexarc4shng/cmd/" - + try: file=open(path+cmdName+'.cmd','r') for line in file: @@ -804,12 +878,12 @@ def load_command_let(self,cmdName,path=None): except: self.logger.error("Error while loading Commandlet : {}".format(cmdName)) return myDescription,myUrl,retJson + - - + def load_cmd_list(self): retValue=[] - + files = os.listdir(self.sh.get_basedir()+'/plugins/alexarc4shng/cmd/') for line in files: try: @@ -819,7 +893,7 @@ def load_cmd_list(self): retValue.append(newCmd) except: pass - + return json.dumps(retValue) def check_json(self,payload): @@ -828,7 +902,7 @@ def check_json(self,payload): return 'Json OK' except Exception as err: return 'Json - Not OK - '+ err.args[0] - + def delete_cmd_let(self,name): result = "" try: @@ -838,7 +912,7 @@ def delete_cmd_let(self,name): except Exception as err: result = "Status:failure\n" result += "value1:Error - "+err.args[1]+"\n" - + ################## # prepare Response ################## @@ -849,15 +923,15 @@ def delete_cmd_let(self,name): myFields=line.split(":") newEntry[myFields[0]] = myFields[1] - myResponse.append(newEntry) + myResponse.append(newEntry) ################## return json.dumps(myResponse,sort_keys=True) - + def test_cmd_let(self,selectedDevice,txtValue,txtDescription,txt_payload,txtApiUrl): result = "" if (txtApiUrl[0:1] != "/"): txtApiUrl = "/"+txtApiUrl - + JsonResult = self.check_json(txt_payload) if (JsonResult != 'Json OK'): result = "Status:failure\n" @@ -871,7 +945,7 @@ def test_cmd_let(self,selectedDevice,txtValue,txtDescription,txt_payload,txtApiU except Exception as err: result = "Status:failure\n" result += "value1:"+err.args[0]+"\n" - + ################## # prepare Response ################## @@ -882,7 +956,7 @@ def test_cmd_let(self,selectedDevice,txtValue,txtDescription,txt_payload,txtApiU myFields=line.split(":") newEntry[myFields[0]] = myFields[1] - myResponse.append(newEntry) + myResponse.append(newEntry) ################## return json.dumps(myResponse,sort_keys=True) @@ -892,7 +966,7 @@ def load_cmd_2_webIf(self,txtCmdName): result = "Status|OK\n" result += "Description|"+myDescription+"\n" result += "myUrl|"+myUrl+"\n" - result += "payload|"+str(myDict)+"\n" + result += "payload|"+json.dumps(myDict)+"\n" except Exception as err: result = "Status|failure\n" result += "value1|"+err.args[0]+"\n" @@ -906,25 +980,25 @@ def load_cmd_2_webIf(self,txtCmdName): myFields=line.split("|") newEntry[myFields[0]] = myFields[1] - myResponse.append(newEntry) + myResponse.append(newEntry) ################## return json.dumps(myResponse,sort_keys=True) - - + + def save_cmd_let(self,name,description,payload,ApiURL,path=None): if path==None: path=self.sh.get_basedir()+"/plugins/alexarc4shng/cmd/" - + result = "" mydummy = ApiURL[0:1] if (ApiURL[0:1] != "/"): ApiURL = "/"+ApiURL - + JsonResult = self.check_json(payload) if (JsonResult != 'Json OK'): result = "Status:failure\n" result += "value1:"+JsonResult+"\n" - + else: try: myDict = json.loads(payload) @@ -936,13 +1010,13 @@ def save_cmd_let(self,name,description,payload,ApiURL,path=None): file.write("description|"+description+"\r\n") file.write("json|"+myDump+"\r\n") file.close - + result = "Status:OK\n" result += "value1:"+JsonResult + "\n" result += "value2:Saved Commandlet\n" except Exception as err: print (err) - + ################## # prepare Response ################## @@ -953,10 +1027,10 @@ def save_cmd_let(self,name,description,payload,ApiURL,path=None): myFields=line.split(":") newEntry[myFields[0]] = myFields[1] - myResponse.append(newEntry) + myResponse.append(newEntry) ################## return json.dumps(myResponse,sort_keys=True) - + def send_get_request(self,url="", myHeader="",Cookie=""): mySession = requests.Session() mySession.cookies.update(Cookie) @@ -964,7 +1038,7 @@ def send_get_request(self,url="", myHeader="",Cookie=""): headers=myHeader, allow_redirects=True) return response.status_code, response.headers, response.cookies, response.content.decode(),response.url - + def send_post_request(self,url="", myHeader="",Cookie="",postdata=""): mySession = requests.Session() mySession.cookies.update(Cookie) @@ -974,18 +1048,18 @@ def send_post_request(self,url="", myHeader="",Cookie="",postdata=""): allow_redirects=True) mySession.close() return response.status_code, response.headers, mySession.cookies, response.content.decode() - + def parse_response_cookie_2_txt(self, cookie, CollectingTxtCookie): for c in cookie: if c.domain != '': CollectingTxtCookie += c.domain+"\t"+str(c.domain_specified)+"\t"+ c.path+"\t"+ str(c.secure)+"\t"+ str(c.expires)+"\t"+ c.name+"\t"+ c.value+"\r\n" return CollectingTxtCookie - + def parse_response_cookie(self, cookie, CollectingCookie): for c in cookie: - CollectingCookie[c.name] = c.value + CollectingCookie[c.name] = c.value return CollectingCookie - + def collect_postdata(self,content): content = str(content.replace('hidden', '\r\nhidden')) postdata = {} @@ -995,12 +1069,12 @@ def collect_postdata(self,content): data = re.findall(r'hidden.*name="([^"]+).*value="([^"]+).*/',myLine) if len(data) >0: postdata[data[0][0]]= data[0][1] - - + + postdata['showPasswordChecked'] = 'false' return postdata - - + + def auto_login_by_request(self): if self.credentials == '': return False @@ -1024,13 +1098,13 @@ def auto_login_by_request(self): "Connection" : "keep-alive", "Accept-Encoding" : "gzip, deflate, br" } - myStatus,myRespHeader, myRespCookie, myContent,myLocation = self.send_get_request('https://'+self.host,myHeaders) + myStatus,myRespHeader, myRespCookie, myContent,myLocation = self.send_get_request('https://'+self.host+'/spa/index.html',myHeaders) myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) PostData = self.collect_postdata(myContent) - + actSessionID = myRespCookie['session-id'] - + self.logger.info('Status of Auto-Login First Step: %d' % myStatus) myResults.append('HTTP : ' + str(myStatus)+'- Step 1 - get Session-ID') #################################################### @@ -1049,17 +1123,17 @@ def auto_login_by_request(self): } newUrl = "https://www.amazon.de"+"/ap/signin/"+actSessionID postfields = urllib3.request.urlencode(PostData) - + myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(newUrl,myHeaders,myCollectionCookie,PostData) myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) PostData = self.collect_postdata(myContent) - + #actSessionID = myRespCookie['session-id'] - + self.logger.info('Status of Auto-Login Second Step: %d' % myStatus) myResults.append('HTTP : ' + str(myStatus)+'- Step 2 - login blank to get referer') - + #################################################### # Start Step 3 - login with form #################################################### @@ -1080,23 +1154,34 @@ def auto_login_by_request(self): PostData['email'] =user PostData['password'] = pwd - + + # If MFA Secret is Set - try with MFA + if (self.mfa_Secret and self.ImportPyOTPError == False): + self.logger.info("Plugin '{}': Try to login via MFA".format(self.get_fullname())) + self.mfa_Secret = self.mfa_Secret.replace(" ","") + totp = pyotp.TOTP(self.mfa_Secret) + mfaCode = totp.now() + PostData['password'] += mfaCode + myResults.append('MFA : ' + 'use MFA/OTP - Login OTP : {}'.format(mfaCode)) + + postfields = urllib3.request.urlencode(PostData) myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(newUrl,myHeaders,myCollectionCookie,PostData) myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) PostData = self.collect_postdata(myContent) - + self.logger.info('Status of Auto-Login third Step: %d' % myStatus) + myResults.append('HTTP : ' + str(myStatus)+'- Step 3 - login with credentials') file=open("/tmp/alexa_step2.html","w") file.write(myContent) file.close - + ################################################################# ## done - third Step - logged in now go an get the goal (csrf) ################################################################# - + myHeaders ={ "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", "Accept-Language" : "de,en-US;q=0.7,en;q=0.3", @@ -1108,14 +1193,14 @@ def auto_login_by_request(self): } Url = 'https://'+self.host+'/templates/oobe/d-device-pick.handlebars' #Url = 'https://'+self.host+'/api/language' - + myStatus,myRespHeader, myRespCookie, myContent,myLocation = self.send_get_request(Url,myHeaders,myCollectionCookie) myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) myResults.append('HTTP : ' + str(myStatus)+'- Step 4 - get csrf') self.logger.info('Status of Auto-Login fourth Step: %d' % myStatus) - + #################################################### # check the csrf #################################################### @@ -1133,16 +1218,16 @@ def auto_login_by_request(self): #################################################### try: with open (self.cookiefile, 'w') as myFile: - - + + myFile.write("# AlexaRc4shNG HTTP Cookie File"+"\r\n") myFile.write("# https://www.smarthomeng.de/user/"+"\r\n") myFile.write("# This file was generated by alexarc4shng@smarthomeNG! Edit at your own risk."+"\r\n") - myFile.write("\r\n") + myFile.write("# ---------------------------------------------------------------------------\r\n") for line in myCollectionTxtCookie.splitlines(): myFile.write(line+"\r\n") - myFile.close() - + myFile.close() + myResults.append('cookieFile- Step 6 - creation done') self.cookie = myCollectionCookie self.login_state= self.check_login_state() @@ -1150,31 +1235,34 @@ def auto_login_by_request(self): file=open(self.update_file,"w") file.write(str(mytime)+"\r\n") file.close() - + myResults.append('login state : %s' % self.login_state) except: myResults.append('cookieFile- Step 6 - error while writing new cookie-File') - + for entry in myResults: - logline = str(self.shtime.now())[0:19] + ' ' + entry + logline = str(self.shtime.now())[0:19] + ' ' + entry self._insert_protocoll_entry(logline) + if (self.mfa_Secret != "" and self.ImportPyOTPError == False): + myResults.append('use OTP-Code : '+mfaCode) + return myResults - - - - - + + + + + def log_off(self): myUrl='https://'+self.host+"/logout" myHeaders={"DNT" :"1", "Connection":"keep-alive", "User-Agent":"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0" } - + myStatus,myRespHeader, myRespCookie, myContent,myLocation = self.send_get_request(myUrl,myHeaders, self.cookie) - + self.logger.info('Status of log_off: {}'.format(myStatus)) - + if myStatus == 200: logline = str(self.shtime.now())[0:19] +' successfully logged off' self._insert_protocoll_entry(logline) @@ -1182,13 +1270,13 @@ def log_off(self): else: logline = str(self.shtime.now())[0:19] +' Error while logging off' return "HTTP - " + str(myStatus)+" Error while logging off" + - - + ############################################## # Web-Interface ############################################## - + def init_webinterface(self): """" Initialize the web interface for this plugin @@ -1226,8 +1314,8 @@ def init_webinterface(self): return True - - + + # ------------------------------------------ # Webinterface of the plugin @@ -1258,11 +1346,11 @@ def render_template(self, tmpl_name, **kwargs): """ Render a template and add vars needed gobally (for navigation, etc.) - + :param tmpl_name: Name of the template file to be rendered :param **kwargs: keyworded arguments to use while rendering - - :return: contents of the template after beeing rendered + + :return: contents of the template after beeing rendered """ tmpl = self.tplenv.get_template(tmpl_name) @@ -1271,7 +1359,7 @@ def render_template(self, tmpl_name, **kwargs): **kwargs) def set_cookie_pic(self,CookieOK=False): - dstFile = self.plugin.sh.get_basedir()+'/plugins/alexarc4shng/webif/static/img/plugin_logo_old.png' + dstFile = self.plugin.sh.get_basedir()+'/plugins/alexarc4shng/webif/static/img/plugin_logo.png' srcGood = self.plugin.sh.get_basedir()+'/plugins/alexarc4shng/webif/static/img/alexa_cookie_good.png' srcBad = self.plugin.sh.get_basedir()+'/plugins/alexarc4shng/webif/static/img/alexa_cookie_bad.png' if os.path.isfile(dstFile): @@ -1281,7 +1369,7 @@ def set_cookie_pic(self,CookieOK=False): os.popen('cp '+srcGood + ' ' + dstFile) else: if os.path.isfile(srcBad): - os.popen('cp '+srcBad + ' ' + dstFile) + os.popen('cp '+srcBad + ' ' + dstFile) @cherrypy.expose def index(self, reload=None): @@ -1292,34 +1380,104 @@ def index(self, reload=None): :return: contents of the template after beeing rendered """ - - if (self.plugin.login_state != 'N/A'): + + if (self.plugin.login_state == True): self.set_cookie_pic(True) else: self.set_cookie_pic(False) - + log_file = '' for line in self.plugin.rotating_log: log_file += str(line)+'\n' - + myDevices = self.get_device_list() alexa_device_count = len(myDevices) - - login_info = self.plugin.last_update_time + '('+ self.plugin.next_update_time + ')' - return self.render_template('index.html',device_list=myDevices,csrf_cookie=self.plugin.csrf,alexa_device_count=alexa_device_count,time_auto_login=login_info, log_file=log_file) - - + + login_info = self.plugin.last_update_time + ' ('+ self.plugin.next_update_time + ')' + + if (self.plugin.cookiefile != ""): + cookie_txt = self.plugin.read_cookie_file(self.plugin.cookiefile) + else: + cookie_txt = "" + return self.render_template('index.html', + device_list=myDevices, + csrf_cookie=self.plugin.csrf, + alexa_device_count=alexa_device_count, + time_auto_login=login_info, + log_file=log_file, + cookie_txt=cookie_txt, + pyOTP = self.plugin.ImportPyOTPError) + + @cherrypy.expose + def handle_mfa_html(self, data = None ): + txt_Result = {} + myCommand = json.loads(data) + myOrder = myCommand["Key"] + if myOrder == "Step1": + myUser = myCommand["data"]["User"] + myPwd = myCommand["data"]["Pwd"] + myResult = self.store_credentials_html('', myPwd, myUser, False, '', False) + + txt_Result["Status"] = "OK" + txt_Result["Step"] = myOrder + txt_Result["data"] = { "Result" : myResult } + + + elif myOrder =="Step3": + myMFA = myCommand["data"]["MFA"] + myMFA = myMFA.replace(" ","") + if (len(myMFA) != 52): + txt_Result["Status"] = "ERROR" + txt_Result["Step"] = myOrder + txt_Result["data"] = { "Message" : "MFA - code has not correct length (should be 52)
Try again" } + else: + try: + totp = pyotp.TOTP(myMFA) + mfaCode = totp.now() + txt_Result["Status"] = "OK" + txt_Result["Step"] = myOrder + txt_Result["data"] = { "OTPCode" : mfaCode } + except err as Exception: + txt_Result["Status"] = "ERROR" + txt_Result["Step"] = myOrder + txt_Result["data"] = { "Message" : "OTP could not calculated something seems to be wrong with the MFA
Try again" } + + elif myOrder =="Step5": + myMFA = myCommand["data"]["MFA"] + myMFA = myMFA.replace(" ","") + myUser = myCommand["data"]["User"] + myPwd = myCommand["data"]["Pwd"] + myResult = self.store_credentials_html('', myPwd, myUser, True, myMFA, False) + if ('stored new config to filesystem' in myResult): + txt_Result["Status"] = "OK" + txt_Result["Step"] = myOrder + txt_Result["data"] = { "Result" : myResult } + else: + txt_Result["Status"] = "ERROR" + txt_Result["Step"] = myOrder + txt_Result["data"] = { "Message" : 'could not store Credentials + MFA to /etc/plugin.yaml' } + + elif myOrder =="Step6": + if (myCommand["data"]["command"] == 'login'): + myResult=self.plugin.auto_login_by_request() + txt_Result["Status"] = "OK" + txt_Result["Step"] = myOrder + txt_Result["data"] = { "Result" :{ "LoginState" : self.plugin.login_state} } + + + return json.dumps(txt_Result) + @cherrypy.expose def log_off_html(self,txt_Result=None): txt_Result=self.plugin.log_off() return json.dumps(txt_Result) - + @cherrypy.expose def log_in_html(self,txt_Result=None): txt_Result=self.plugin.auto_login_by_request() return json.dumps(txt_Result) - - + + @cherrypy.expose def handle_buttons_html(self,txtValue=None, selectedDevice=None,txtButton=None,txt_payload=None,txtCmdName=None,txtApiUrl=None,txtDescription=None): if txtButton=="BtnSave": @@ -1335,21 +1493,21 @@ def handle_buttons_html(self,txtValue=None, selectedDevice=None,txtButton=None,t result = self.plugin.delete_cmd_let(txtCmdName) else: pass - + #return self.render_template("index.html",txtresult=result) return result - - + + @cherrypy.expose def build_cmd_list_html(self,reload=None): myCommands = self.plugin.load_cmd_list() return myCommands - - + + def get_device_list(self): if (self.plugin.login_state == True): self.plugin.Echos = self.plugin.get_devices_by_request() - + Device_items = [] try: myDevices = self.plugin.Echos.devices @@ -1362,78 +1520,99 @@ def get_device_list(self): newEntry['deviceType'] = Echo2Add.deviceType newEntry['deviceOwnerCustomerId'] = Echo2Add.deviceOwnerCustomerId Device_items.append(newEntry) - + except Exception as err: self.logger.debug("No devices found: {}".format(err)) - + return Device_items @cherrypy.expose - def store_credentials_html(self, encoded='', pwd = '', user= '', store_2_config=None): + def store_credentials_html(self, encoded='', pwd = '', user= '', store_2_config=None, mfa='',login=False): txt_Result = [] myCredentials = user+':'+pwd byte_credentials = base64.b64encode(myCredentials.encode('utf-8')) encoded = byte_credentials.decode("utf-8") - txt_Result.append("encoded:"+encoded) + txt_Result.append("encoded:"+encoded) txt_Result.append("Encoding done") conf_file=self.plugin.sh.get_basedir()+'/etc/plugin.yaml' - if (store_2_config == 'true'): + if (store_2_config == True): new_conf = "" with open (conf_file, 'r') as myFile: for line in myFile: if line.find('alexa_credentials') > 0: line = ' alexa_credentials: '+encoded+ "\r\n" - new_conf += line - myFile.close() + if line.find('mfa_secret') > 0 : + line = ' mfa_secret: '+mfa+ "\r\n" + new_conf += line + myFile.close() txt_Result.append("replaced credentials in temporary file") with open (conf_file, 'w') as myFile: for line in new_conf.splitlines(): myFile.write(line+'\r\n') myFile.close() txt_Result.append("stored new config to filesystem") + self.plugin.credentials = myCredentials + if login == True: + if (mfa != '' and self.plugin.ImportPyOTPError == False): + # Try to login asap with MFA + self.plugin.mfa_Secret = mfa + else: + self.plugin.mfa_Secret = "" + + txt_Result_Login=self.plugin.auto_login_by_request() + for entry in txt_Result_Login: + txt_Result.append(entry) + return json.dumps(txt_Result) - + @cherrypy.expose - def storecookie_html(self, save=None, cookie_txt=None, txt_Result=None, txtUser=None, txtPwd=None, txtEncoded=None, store_2_config=None): - myLines = cookie_txt.splitlines() + def storecookie_html(self, cookie_txt=None,): + txt_Result={} + myLines = bytes(cookie_txt, "utf-8").decode("unicode_escape").replace('"','').splitlines() # # Problem - different Handling of Cookies by Browser file=open("/tmp/cookie.txt","w") for line in myLines: - file.write(line+"\r\n") + if (line != ""): + file.write(line+"\r\n") file.close() value1 = self.plugin.parse_cookie_file("/tmp/cookie.txt") self.plugin.login_state = self.plugin.check_login_state() - + if (self.plugin.login_state == True): self.set_cookie_pic(True) else: self.set_cookie_pic(False) - - + + if (self.plugin.login_state == False) : # Cookies not found give back an error + + ''' tmpl = self.tplenv.get_template('index.html') return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), plugin_info=self.plugin.get_info(), p=self.plugin, txt_Result=' Cookies are not saved missing csrf', cookie_txt=cookie_txt, csrf_cookie=value1) - + ''' + txt_Result["data"] = { "Result" : False } + return json.dumps(txt_Result) + # Store the Cookie-file for permanent use file=open(self.plugin.cookiefile,"w") for line in myLines: file.write(line+"\r\n") file.close() - + self.plugin.csrf = value1 - - + + myDevices = self.get_device_list() alexa_device_count = len(myDevices) - - + + ''' tmpl = self.tplenv.get_template('index.html') return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), plugin_info=self.plugin.get_info(), p=self.plugin, @@ -1442,7 +1621,11 @@ def storecookie_html(self, save=None, cookie_txt=None, txt_Result=None, txtUser= csrf_cookie=value1, device_list=myDevices, alexa_device_count=alexa_device_count) + ''' + txt_Result["data"] = { "Result" : True } + return json.dumps(txt_Result) + + - - + diff --git a/alexarc4shng/assets/Alexa_lists.jpg b/alexarc4shng/assets/Alexa_lists.jpg new file mode 100755 index 0000000000000000000000000000000000000000..968d248ad764f438b20fb0cef1a85a6fde3d7f33 GIT binary patch literal 40947 zcmd?R1z256mN0s75AGf;xVr@i?(XjH8Z2mnyGw!wcX#LD65KTecMI}Pa{J!yd0*f3 z?f=a`GvB;ov-hfHCA(^^TD1$F=bzUBC{kjQVgN8uq64D?0G^)!=ps(0hAyTiB<_|j z79`@5vI@_e;FXdhBKitS@?w%Qq9ASn0Q{qkv8^*C698aq=i;O!E=;1KsYL>_3jhZo z0nh=o0CGcPX9pn#1sTAfrt4V%ND}}sP5(pJpR)aP0+NZTvoQbwMgk(gHg<4w0l@|! zn8V%0;Rl=of-#IO3{60ADF~)>0#y(M&;P(1{RSWYfX#ox;2;oy;iRG@0%{uu2qrQA z2iWKzU}Fm>TM!Q$h=<(7)()f}T;n&`hEMJO@5K$m}E?S?eqN1U1pzrA7k-O(hHFvV4`COuV z{M<#<^1qUYQ`0b4wzIZ^W*<-*+tuSN&wER5uHyDDN`?Lx^#5cs4wDrCvRYB)U#XoR z1OSiZUYF5+$X6T$0C)*r|4O10(Dmt1o&ed%v)l84$9XPUgWG-8cvQ)n^Q`sDa7qcr z*{`JW@c|Z&{IOd?htWLpqzis#js~N3VGAXM&Xofic>VF(%XdZj{(r6kqG$py4K+F! z(|=nLi{dqBjpkbdBYrsvX#p;^yw`s&2awK_29n z2_p3~0C+M^;w3D!EjeIKeNk4IPOzUg0?@_GGe)g5m`{mOH&!MAnV5f`Z~$FJCsN9D zSsoX8n{$PN=EHf^&TAp7f;!RqS?+{)O)W?l+fuXMX70x~VOjk4=wtWX(i=;XJjb%% z#u|Wock;dl|6DTw9~bat^Q~~_-QB@Bmj;nTEzL+EG-4_M2O2TZ7#cDCPs9L(vR#3~ z%=ux3&ZtFbbfszKm47Y>kmUUZ@$V$Wzk`1c{j}kKPX5oP{G~em=M~E2{%!o5p54R! z#kp&{z0^170(T^?Va1|@Fx}08XltZRNKcT<`c)!y&Hi+;Ml7D)kUrzqf3NQHb+`2? z+jDJvy~(v=)qHtu)~3i=*RUO*QhlPcDawgcEEYxtIKNCY`XGmc{WW?-CUe+%~cRB ze&?&#xltT0ESwX3xw71@H7;MOW-ovM+%G=q{pV$Hr}-mh=SFux++9o1A@f<8bh02q z7%Lkq09dSS41j@@GXDQm5X>J*sK`VEVjK(V!w^vEvMDrEx*<(%W|xjova0+HKiQtM z^`8pu0a4?P>nXFG|0+vf~=e<$jW_oY5EX3WAzKc7a^VaaR5M)6pjBcxPFib zUKn&uq`Y;0hjJ+%{9_96;%gST4t4%Y44k?b!bu3^-+kU(gl1DEtI743xF_gOV-LIZ zt;X@S9o!pt&({A!16b!SSk=VMKrtWc@$BiW5d9#N&Zx1Y$&<%zno&XtyX*f8u})}2Jb#dHCp@f8uCLJ_S8kSZT1)ws ztv)tqC;R^Us60cKz2`6FL5{A=AHc!E6$k(ZI)kb}@(X6*uZ+MSa$ukT0RP7T|HWQL z5VX=~K>>h+L4j8HkWio~3nJ_H(ymHBM5+Ir*=;nc6+IAEldyWl~bm|<~c zw|e~67bZ@KNMKBdj+gn;&u~j|iJ_m{c5GJK$LYeYDMe}1FMKVYEoRrQ<{D)Vcs&yh zW1U(`nI`i=hA>I3`p&%tZu`O6@W{G$xCNcovvM@_889A}Oz@KHjl=s5)NV3tJp973 z-hiGUao8x-zoY->W%KJ7&Z{5YS~RUEkYC%n&aThB=xCZx68FxAW{xl>54-)`4|?MK z^;`7L=W)iTO=Aw?B`$r}8a6`4sU72i=H(~ddIi5PF$MyNF2X^)PWDPV)cNi{< zbN0}0Q0*DQX%Pe(-{3cQQBuWeMv1gG(^d32e(ewUbkUUg_sXNc_qKncn>QBOc7D=~ zd8i?60S+|a>+7!^y+12R<7O@hqdh$*tC!K-NK1QY9+#0kz`R}|oS8G7(k^WWQkRZ2 z^D>>22iRCv9sPR+)7^WUJRut6jJmJdA^8f(rfAN4rp(W6n&`(L)=&(`6L~taZUVy| zgFMlXsd`5~F}`wZL7<--CL%oFIwk>jNB&!{F(Ul-!LGhRyZa*eE5aCd1|#V}TD8gM zw$@wgaa+fua{ju3`o`-y0+Y+{*3tH!M<+iJ?V&$ex%3=bH$4B4{O`#oX=_uT70#qI zKeTS0KTQB<%nb*0+3GDTpSFzty%GJle#3Pi8mAn~MmD^SQq1WdJVzX-9_=vJp?Ugh zVz|CF2dzzi6ipbnLPVHs^@xp;Kc|*Z)<>;1Z5eDwc&=?wvS0B{YpPaIdD<1x+e+3^ z+OFt6UsX$O@~^&?hmms`ubn$H_vo(&^+ZB^zWilB20YAheMlH#zR^a!jBW(xl4dG@ za%xM)RFW*+RsGxRxuvQWMcJS!vuOqy9UZ!;Is5H2@5jW3&4UZ(oi1vf?7mB7vmMRq z&JH7+`g76f>d|;AR_Azj(j{}*^q9Xo>4&_NeQA6hVX4;tHlt;B|J zXC1dS_&};q{0rv0OnkyDu{kPz{Z-pR=CiiU``g^{1_bQo^{p-0_*)l(yg=yr3xjBU zUPjGmq{drCNyK?Y{b1-8d`h}KvM7#!Yb>yg7xq5`Hj8P(ay}OuN%oKl(xkq@bz6wO zO-xKa(TKZ#bUTxFlQM zar116VOSk=`p|gN)MOyOIQsnZi8nJk`iTOGu2~1?<7Ahw9ltxSz?}OBx)^)rQ|oAo z#k(_C%z2X@hLAPex?~`~+pug|Lf@ICakzD9SB+xh+*))zL2_4Y-r`|tyZo6&O@1f> zGLH^%)Qc${*o%6lL&1JaDP6rkTw{17()3p&A;WI6|9LL*hd=+LR=T$J-bs%dBaC>1 z!+qmj*_sI>JWpSZ{01ie>gfGPzTu}1JRi+X0Sf#0fy{MByMy%gSUXD!el$a$*^vot zEV)1`Y03dw!>y-oI(^)7QR^*^K0cHM4m>X*QPLz$$1YZx;}R1A$@ zW*8c+XXg(pys}`#4832a6d%yMaM2f|_LwcEow9T|2 zAbQ7I^;a#{Z+w2Ne8j_5d{&G%>Pmji&2id~P6r3y+p4hmYO-ei9POZ0&$}W8B#+Fm zujY2N$&>UkpL%FB8*A1e8*91<=i?W!#MtJO|Gnlt)ZK#w*w;K8nJr^ui#v$E*Z3rf zS_XbdSK=~DXwgm8uE!i~pcN!~NIhdVuTEvwbrY!Pb{x4i>6`oaN=kYZV6Sf=Am@^B zh}j_R7ACMfpp;{fEv%2Zp39oOSkeQzOKZ=5efj!U5MuEM!t&KmR*|4kxEE-DNd7lv zrD@H9=I5XxY53UR`;Q~@uUTS{1dII6S;PLYP4mUe+Xpw~m$M6qhusy&ZemzO6pM#x zq6gC(%Dkqow6@k#i$3s5b#(PZ1DVCW0fcG<$~`vBE7%1)Qfjr2qE4!p#S>+;mfCjt zR!2;(H@m}ojcLQYqrtXm1%K_GFy22{UYXx3UZ3`96k(`FHGu{<@`^1Y-Gak|JvXEA^SMtHfqX&VK@ zwKNf5u^wo>8tZyZToqtEjcl0@%MvBW02M1%(vD{3@W^QI* z>$#R4^LyFrX8>Y!W9Rp_ik@j}6_xx~N(r@(P@+;wp#`%vEcH|QmQq=1g0j^5vbBOD z)3zWVy7>9C+LE=XeBfVlm4c}$=`yYGz9vh=sT_TFu5)uY=}w5=_U69ruP%~Kw@N$0MKnH*4Mbz{m%3@+wh|p%e=H+k5+|=R7lL9O3#gj2w$G(y;3jkpg!KO)Omy+nIfbH^g<+n_3!20?YYaI`i6 z9j+a!zkDcvTd~KS+~@5k1lgoCY&tRc*T%>{%twkReSE)ozK-u7(q7q|9hr?SN)~i~ zFG~%?VD(u23J&}hnHzFjHreBIZbi2!8d*}KOu=@x#y4r)_bxUs+`K+{V7-SX{Vxrg zKPKC6&S^UpqwjoKRxEn(>pB09`&*=7zXB$K11n{L+S!F|;ya49*>j{T!?2T=l?~?5 z+`%0MbgIi^*ORe4$U3k*Hky}8|GfgA%$Ot0T245j=~-CV z)wkB%?5hXMm>gNH#1W^Gt!rrAx|^_e%(vF!1t{sRl__b<2SV{`dsmgl4*%ip{Ocn9 zSWtpq&Y9Y`e?t~U7JcEuJ#ne}S1NC#u1oTZ5l@yIx2;!xpp=0c)s;)_ccc&;UN*m~8tindsXOJW!PX2K@`ncmryy_c5bfWsh&S*w$qJ!V|8pJ)G)#>rNNQc~otz1=Pd;kj>5^ zXpS23A4uBfcJxp}wQ%{?N)$Cp&> zMHn6uU4*>j6F&~CDqk824m~m6;FHlm@X9aLY9Dr=RiLOQKGC#0WBsafsvYMnyG6CY zhg{(Bz^}!Vz?!zC-nHnQ8%IgVOdfL70=b9=(`NC|nk3C;VvIi8OP6@ZJ@I79G-&9X z>>UrZlRKgwy>HFM*IA=uSgj5+C3yJAT`%i4fQhw^U&d;B>($4%SeFOy*kZ17p&lU{ z-#LU7uo^{&U9NX}B{-NJ!R2l~!r!8v0ZEq!d6aXrAAM|Oiz4}QE*B%0#^9R1EVRau zMMKgoTsiQ2Gf?wv1>LB>F`PBeWV06GI8i0P5Dv=LcxZT)he*lywMi_G5dM-HJ;)(y zDj1dua*H? zUcIPu5DOWjp|+l1s|O`H02L>he%AAtD@dS#yDQZ@`3q3Pw_BfnU;-2}IwhMqC57waWOWo8w z!Qs@zX+isHN?TTEdSpk}7~9i!Ioy_&I?gU0Kh%WKa@04HOSyY9C{u9;o{Lx9;(FPH zjxKsHF^K)dxjbY%mHk}!wK4P9`lf&8bT1_0!W)1xL^xYe=!R!7m3}pnQ(0OswcW%CBIM7fxd8c<*h` zNlQ9XY3cnf;H$9TY}0I2NA$Q6LyGfhgW%U?GSbCy<0-d7#3e4u*}(yx;AO7A4~w(I*1) z*2ljV3VT<6JjyX^o9Z z_2`7|o$%0OeukeYniy~kcK@`;+15G=eFe@orQ0KZ1@^}ya0SjrEYAJC-l1k^e5){q zgWHEqQs`*cIsF4CHxCc@)q^*Zl(U>fgfadm7EvUZr<3^yI2zsUJ3+b96J>*H@8Cbc zs^#_|{ih8-%6b>O&s+1wnvmA(62v#}K<@Mn$en_NL&1Z?LPEem{rDII2963Kg#h_e z!(^BLFL)T@qO zB};v-9CnM(=$VY&3Nw~i?+nO=^G!D6Pm%BA$FOfP3SSse+v|7Zt7dGJ+)J&VJd&X0 zQK)6ya@y{M4pn%g?2It3@>_2VtNMeBAS(}*vgh)B^7MU0o(QE!;xGLS;J;}|dQWq0 z;A_8xVW7k_O_EF3P5ZX8^rRU(h7IN`e4iWEVg z4UB57Av?g~>m9DV8-sOX=iydTPxb&`l<(QYFLcSh8QYpg{H@6~B=No{P?3@&x_6-^ zmM+l+veaBa-&;Mdk3!)tIl?5M9ng#=Eqiqb=DqW>I<{b?6=bq6s942JWVV+SO4Ymz z^kMgX(qMUEFjy6msl6^IOdp=b6hUIDiA)tpYt%h3bQy=NvZn54Lvz60vWm1w1^t5d3I|b335mXK7bP`O&Lnf6 zL7dM19TVE@DWNA$s0RVey&30)T=&Rs>!{Xx93#vQJkViQVT$aipWuM4Llfu|=`Ab3-F>CDXLR)NKKEG0qtN;TiA*^9;BtSw>9@Tx3@osSsGD+M3=Y{D;X~>Sz-)p36TXk zh3#U_T^4j3rgc0*Zc=cA)(-|RYyD|g+2e}wlv<9Umt5cp1%ZSXW+sc8MGc>6(@>LR z?>y}_MFd^8iGpoX(GslSqFPDE9jY)*!3VQ#f9J*1Pg;>QHT2>Z%#tUfFGIKSVf_p9 zh!bh>4Iqvk#JTbSbAmS%S0eEeIVGlDL9{eLA9Ah^-zd}>!$QvfXMI}4>rvDR-sH8}L+(^NIwQRG2Myq0YeH`TQ9 zanLA_i~EW{MUu3*lm%O?`VPXvqY`^;UgKotG@`^Ht*+9zww68s3wa26cKIWD915R! z2~uUSx`aZ!0 zjEivLla$RkH+{@7^&`j&Glv9$Tlp4YtNnR_MFY5)@O{q7iEfPoAedRz_2h>z)})*G zF*@lD`of}MAjRpUnkc1>MzIG)ZBX#p5P4b z220$PgH1(UNmGG)9Bh(PPv3hU4s#UI;(JMq^PlDEDNw!ad`;S%M&=8>a z%c4U@7(S#~8T)G6Fr~ki4=b*nr#W!9NHolC2rg(UPPgQGH(M}`*;cGBk+JneI)Hl+ zV6yd<_>-a(P?kiHrTAO9?@18j>69Snk&2SZTM~?(KHqaRMel8~c2FB!+y%|cIdRvh;@WY|VKni+Xx;An1W*R~>AqHgs`W|=A*mRs$H1iF6> z+Tg|U_xcpirnYQl7uy_T}PEbyoRkP6$L z>qCf05oQyEWle00lakAA@5#5>+m5#Am7)`;k33_dWY(KGloihQHJFL?BGWXn`N*KR z`^yXbPAc!KXFxj)Xk8$S@*~WK28V%!f`NtqaW(-Qgg}KvBV}fdL*IbHc*!E9_>RoL z&=Ex`wrY}uUDzq$Qx`O*k#jzAE1n?ciIWCTTP&IhAH*8`94ie~lT4mL&X4_x+w{&0n6)cfuu2x{9C5+dO zKB!KokXZ4q@;qk0!Tt~)i8A!%+|SJ~-eI_qyny1oeYn6VYZy|<#CoFDk%`ZzS+^p( zxUz;_uio!Ctai!ftlJrA$F(YHZ25|Nnwb4?%fh}N4Z}F5C7id~`9(sACUxHA;P~Ur zwBUpOSr`Yiu^nWEu+>vwLCw6}I6-XZ344+?uRf2Nk;@b9M;-xnqC#?}Rs2Bpfdi%D z1&sKGoearFF!O2*c_OYlyLyZpqgP?#9-C!d_jRLe9O;_F_{OdaX&pS~`{k~4W-U&+ zD9p-Nsm0;Ni}3i5ySeCsxj9M7)Z|n}xB`rqxC^EmsF72LaH?&Z6RC@Mw^aySy%2$Q z#FV<)W!TV1L%Mh?=WSBcGf;_N-TjF^vROqB*2p7X=frW2mWS)zwXo~YJp(Fg-AcUG zVr;d9>MWsW^6){R@HmR#;m17YhC$8WQU5^wUkj>2rD$*5DMm}kTF^>{fg>ifYyh2Arhur3+DKfl2U?wnXL=p(KBwZ!?)K!$8B{q7EkB;+P>61jo)a0 z+w(i1CtiP@d!;0fEs-pikyMEl`+o6a#haJL(6N3{5w_nPD(wuF1MuiI1n<`+}QhC=gN)y{6bey-_y)h>7VNcMa6SSLMo+D_E-9!$@HqU6!s*ujmNi(hV7hp?TnIoBE>_w#4f~2RE;!j&pQqvPW9;JY6U@-`s`EF4-Mv3sz-V;I`(oz@Yk zT8!p9Mh@<5Hg06m%Q%D7rvF{- z_x!n!4VoW8XPD@)Qn~ENM`dUO>k%$O#r$lb?gnxN>%psBeA4tPvDB!j(_gZ9Tb_2s zcs&j=syK-whT^!t7b_KHL(5=+G+$xCWBok%F+ogjt5MK;I}MjKhQd3S)Qz!ODVrDQQ_k5>y7I5xU@Zg@TQwl(<)zFK~d zmtmS`KO8*Pm>5j+>SuTjlL&cKXFR>Tw^`4CMB<9&oGTG)*uq+-*wX-JcT_KSi4Mp! zKCBN?+WF*T$nMZxq0@a@jWUk>ITMVC6|py2xqGFv9tO3VA*7DzNt|smx~tiC@Y*Ev zM|5YW$8Us>NRjR9}AKCC=_f#j?hdfi2Kn+P*7(U`8vOYK1adLSa9zNw13-lX)`O*5jW_5uKD6gnsg0l!h1*A zB*e)35OYQtJ-o>K41ii8Tuydzcha3)0lJI3Q~WKf?$EO17k3mDNv>g-A&wFG&gP#g6OrcRLo%Ru6*NbHu88mnFAZIz@I| zK8}<*w|T@$;~*yz-K?iq-|htDvbL1suJBIot8;_XO}RB&hvUa+1ENqG=} zPZ$6irIjA(#6O_B=@Z1)#?@k2T?b_!o1W^dgA)rYgGrXyLpEC}|ZSt+<#qZcY{kz?X*kK3h_K?awusw>Js?ymlFs1O$(>mN7BSYgg?5MaSl1F3jM+0!0WIThqs^I~!jk4^(_M=v`%p>3 zW_mt88hgJLY|=WcM=|E&5f*;cw^Yq2B{&SDOV1wiMRBbmx;Cgttyzz@U_D8lmP>QK zn{g8Dv(f1KZF8+dX*ID}I)o^*R_X>;iPE|<%+-bd5LaPJei*}-*YavGmbfhf8iGye z$$Kg9!td((TbXP0nd8b{`-tMZl`xn8t*!-B3X}W9*_TAb zs;#P^N~6Q<3fAu`*i)4uK=;ETg0xvn)LuUu*33sBPqJsZoYk-b#bO$^ie4FsLcH;7;3UDCO;}AdMr5E~#%$JF~E~ zC3Yi1rO5-Fm8*846uz`4$9|KRIGPNu%{of>l@9)3XCuIUzp!WH)b5PANuJ02$s&rX zT4u5Zmh*)b=Ip6^OBfeV=_O|om)yw7>9I-h>Yyks415KZY0r+#w*X#o)!H*y-3r%` z&%PDL$P|Z02}B(y@Ygur_?!9JMU9K_k4VukW}K~zZVNpQL#9?p&^#S-H^P+(o?vqj zNc&mPaldPAj$9Vtn4jtO_pfgsp0ffK{gOagMR5!Y48_?9Q_p>n&u0120N41^Op}AZ zwdTJjBmWFg)VOU{PrKqvn4@zO-}}H6pz#q6za819s$$f|CP^dbNTcg`4V9W0Z?eYa z{0#|7pVMVE6KnE})oqD1n=!GPmux==G;*f$f*7Tzhqu~M-5fH9cjAXISX*Q6DITdl zf^3A5Z%5!veaNU=rSY)rCv>{<<{pb=fw+1~1}JwepBH@zO{{yI#Aq6_h=r=c@m)Q$ zf@U6J)I7RTYfVqm} z!2hn@u{Piw!n1LA^;@8XH`mH{ajD7Hz2?{1`E>Q^d=nVPkZ5|IaNogan0Z6sFZlAg@1-BC$kn5 zlO{-d*U!^3CrDYmmrkv1bAhtt)f*@58pYhzGK3Q%M2$B`-KdVoi$v(s_HCdTGi_Y+ zO<#*l&;6UjRjWY)?67eHBm(kXDsQXXFAn2C2fuV1yBoW+7WXHg!@0UNF33*_j6>>T zdVui_tQMd}m7)4jvu^S0H+k2u)sd?hV+^V`4#OwP4&~o9R$sQRB3};U zizA*&OP`LhW4i$#zQ`C8DE7`7hoLr;9gh+guVMa-TYisB6l!{rzrUhJv^~fA9rYgr zod2a*AuOz0UkxqtT1^{8$->$ErrtZUnnm#K+>J|6sG`0yEQy)FzTUzCrZw|M)+PzH zrhnE9Ep*>d70_0#Kuch2=Je|AWq(H-${jz-t4GyhAU&;REv%URJ(cV66HzfLWm3{x zL^(qg{rI7Xp=m0Nwv>JwXl&^^9k^Cv$lgA>*t!fV(g50l#Z$B5+S)h}C)jcb{v>8rroN0z6Wk5}Il^gfqHa1TwL(I(<9-4>D8 zX$m`}frVI{C|wCkW_Rru0`{)=l@XXVZ^yOjMZHq*sY9%BB#q;k!%LW__M+W_ce51t zHo>g;Yzd2c1Fws=&Xu%RNvUGTE^TfcXlA1~(yD{7P@~3S3G7iN-@d2H)C(K$s)}&a zc+=y5t~&Hxp+)5h)|esMGAB$t37R{rs^wOIu%Kr>FHO8?Q0~n$fY{~8*ec4{Q%AQ^ zW<1e*#jj?G1NmwAF>9gH?^;YYzD?n0@WICwB|R?I%wAFs11%?@!&~w8P#kBWz!Rbs z5n1srXT68K!I3sli$M@t4(4IF!u?1Hf1$t<8HEV{OS@!eU*{4GniVfue2Hui%qPr& zj}MbFOoIwhPE2jHS3=}5KmzlXm)K@5XbCKc<|~}oxdpfzacJI}d3bM2ev3OWI3sTh z5BtvCnrF&e^ zwT6D-M=8Pz#*~;G+BeAPqo290rbk%zt^!C`fxeo0S~mL~8)H3bSPH;gGgDrpJe#1_ z&YmGRLcv{^pHYHq9JnR3&^|+*!3s%+m;)`p5DHU*h5MLVU5imKsd0oR{+>|F=JNDG zLD~{M>T-ft&VHWRCbeTcphlF<95F)@e!FQK=dtcaATn*x-AVJsu{7xlD9~u43w0-- zWBA~?wjKIj*jI0I?G}WgSV01X8=OwM6QFQIA%ZNlxULt1Y0)M&D3}^um-kDFQi^+G`6QDh(`)*}94~I(Hr75= z#~|cxY$JES6_E1}tM&3MbTfCec?$L7$7k2#=bJXT-LA5tG&k=v`l2MhO!7XzuO^r3 zu$>rE1O}z2`*xze&!iUVYn*Uw_)w($z_MQKqruaw3JIkz87+FmP!uNAjF`J^_kz$$ zzBL~e?@L5_g?rULnYZC&Ve5~^1|}A~C7qWz$$07tOKc{US$Lw&A@Lmc2ZNeFal<|4 zU{j;~24s|-JrNz?a5rpz5`geJ=@KF=2eL7GXPDG!d4NvDLwB{%Aea_^!)dtiM7Tp= z_YKsq(kew!(>zQv3&9G}!;M`nO_oaidd(0=ZNy8M2adSgM)j7Hj#-z)dFGycvnck) zBZH$w{+n8Oz8r28E`PEWOY`H-B;m`kag!Pm$sMO-UT%YfoYF1s2xN#@F$yvFO)Oul zVRBK+B-YxMG_J23efWkS>X?i7rfOC^O5Fmt_;>Bl^~>}(L@gWD!s+8K&)JLNd}7tR z6(5Jj*=tHQ>8eh(gU(lLM!4Ly#|H-bV1cH|#hKNdVUzSldGLBYAz!T6`I4o?t?LrI z>IQnaiCSsW>xndIku|NQ34F*Ap=`3>+i`6rAzsR}<Qi>bpvv1I zs9byY^YWK#4dW`q3l8+qB{(Eph>|YmTc!d}upm}-UlHP%%^uUqJ%Hy$6OK36>Jku| z~r6h7(ggM8=INkeg80}UrIdiE%_=F=EvsAJ*`-APFE6=d`?1_Nr~7! zt&DGkRtbfcQHhM2?4`+?=&LXHEcT8cN@t5o4>eEo94qEKc(es;u{ZzrD8Bpr=(~em z47cMcktGZR=x#I=SLzpGg>-ZzqWLa0*}`6!e)p<-=VRjt11dB7##o(~hw^$B+UCfy zyXdBync*3yx=+~?Hg=@mz)-^Z?dRPV3kx7Hgp;z3p2e*Pc`eQ(bx@>6Yf~tM{=(D( zN8@W8YiwOthG?q}wYoC%0Iw_xYU(!gG^JZ%^qH7K4{u}4`}9p#^UTVr&Mtkl^%lg9g_E|Oqe&riE}uZ%ZN zZ#g|0mkTplp8;&*=nJM6D?#|7yv(}xFmD@k1B=H+yyAln$DRQ%b|rKVqm(KgiqGA2 z1XRXS$ZB1!#ZiJ6Z#1kz_ph1G_4>Jm4xUcuD|^=0FzFuUjy_gQEXwezjuwS)W^PsE zOS||>?sP|o{MeFv38=>K7Dh`tOr74}2tAQIGQI8v!;K{rnzy`XdkXjX6oSAlM0gW> zLU`!g-?MzrtJ+R0!BIxCSQ}_Hg;XNJ@yYx>@wI{y&eAsB@2G#E{_h2=N9a!U&ScjD0HF5nOwq(Jtc&sAgrhn8RXg)>1A)GTx^J9{= zq&WTp(UiJz>n-7;S%_L~vVsTEWUt3}n~@t!x2CzxW)@zyI3Pe%PODaLDnq>c$a!E? zo2!tkk0K~qHpJklM(M~eY_Bs)X=6z*(!&v}!+q36A|5zkDI2oKK6jnPRUXg9Gbsug z-HWqf#7RYUTzA;cP?m31C&9dM>tYaAp#DIuL&(o^`#qm+97m?}wGKC4j&4%dv^Gil zT`SpjQVZ@aa(MV1?dCEsoo!D5V==;~XFyr4-@BiNLfp9Noz&D?MLe(aO~IySh=kq< zhvmx->5*0TQ*VqMEW;@5nk$Q~4(&j%E9=dV?@PDHN#R}_`(_9^we95qDiJir&}tH( zXxq-*%Uwpb@P>`{&qk4<7Is?iYF5hx;K!yk`X|{FR}Rlxqc|PQ>9@kds7c!RD8CXM zy5#FsC>GGn_~ljzSslq@6JH)^cVG7KRjHNmVW|b+zK2VDJZP}K{(79Qozt*TT7wfV z3zb){kXod6VT1iuFXCwu=;o77zWqpX93>jqxYqu9{66fGZg@FpE^SsVm+b>DVa!LE+7N_hC6 zuI8W1K{G(lbHytz5M*lE;qb$j^TX_h8HNZ4ImZScHx$E>he&J*d_!qr{gkp1W5n>S zv5k1fEp-5X>$udn!zp|QQDK#HGrI=6Z}C7iRfY^BQkzzcSybbN&iTerG zyt_gBP)9jHoBkD`e_XA3R9;I_t8vzmmnma+HmOZxn1C41Z1lQFW+R0Z#}tLdcZ7>y zqjLOkH1T78wbXz`9N+8xY4gI+&B&vh^(f)6`hZ?UzL3bY5QR8yuBtU(!hN$R+v76P zmF&@Ei@|pLJ9n!a?~eTe&=*cR6wv>o0tW{JhXLJx`{QdcRMInMMIpme6cU5qjz0Ns zFeywr{QhUxf~5J>Qc4N`r?*>m+?etAGU}t z%Lmao`Ev$Bcf~tdJhodOweWjC*vh^}3VE59pa9ecdYvIfDu~C{4y31u0;W`nVO``e zwrd4^mPm9-c;jRiEQ6vwF0zjn>)HjbEz*z0Pv9+G;42emusf;|2 z?y!5u#g+$*Yd!T11IEmh=1dJL0d)M)c;|@5*gN?rfjT31Y8v)HjUU%LCUb}B+&J3} z7i;Ls|K7{3^i4JuF{F9zJ~`p3Wp9-)+%q(s%~E z;b0A0$u#ob`j3-vKNF{~95IBqoHO6h6X@`|{AqvIaZr=?l;|4r7!!l4tJ->8MGF^C zR0Q?>@m((t91%8{e$k2w6+pqNPJGGn@5oI@;C{cR+Of{eKX_ zbdW*cqM&{9sw*LlbKAa|`%W>0oh*OaS0b&JMC9FzegXe>IW%i5;t}&?85@T2Wp<6P z#GFb>^IUcp5B(cGUQ|tLw4*-KoS`|{bz2FKE$U6>8KBf;BtKpBXM+|>?`r2>_d}(% zRgv^gS<$jLN7;g+{=YXH*-!A{HPW#g-rIZ3#zAue9V4#-qn*w0)Mr3s%Gf<~uy$HX zk8r}Qx%OjFY!S8{>DMry>T}Y#m)#Av1o8+7@ONnLjp)REt$y^k-lg^CSBZ?vdU5hY zijZe}4LeinXmQq*wjInn`C(YAG>Avd;lX#=xk#|IbY{%3-+J)|*P$u%J;wBV@9BQ8 z5dhc@I@%M5Znw zgOQYcKu_GdxASo`O?%Ibym%h|6}u>UwU=8H#2Q)6*mU?HnOe0n7wE#=E9awXxphu8~3@7=V7=G>C$N>a_!jSMi-X1yVYfGyb-3xqQn`6vHMC6 zr#AZDTo@4_j}9v>b(~}FRCVAC<=g|aB&(ftk3Vql2YlY50aaz76PP36dGu z1qafI4r;{V<;o8M*I(;dNw3Re`bxdA*<|fLM@Sohc1P?(mQhX11+Rw=nf^k>-E-GKs6 zN`4|5_6P|wJ#Gt{K9!59stXxN3X&(v7W%y&1WFG~_V3Lw4ubD+g2vFx#)kiVA(+1U0KpRwvd=93|6anKEk;QE!=8s z1VvNDq8-7?MS9bN*gH#Js&}C#$?O@YOXU>Zdsc<9n4UY{g?{fdseKK$Zo>Jx;e_b7 z@;#<3qlie#za8b3qb9~(?)6xnxu(YjedB30h8AxiXRIS%X4@jn)e{baJyYLpRV2pP zgkziPX&yENxew9>ZHuT@j~^Am6Oph)`mIj?mq4|wqU<8e{}1aGYQIOU|Kr6DXrYw+ zgyHN~%~vyiD$<09aau{$z!q-=B|JUDA^bw~N}J{$mH5T}(e}Y7dmrhO66teV5|pi~ zGySTbw0(@T-LK$9bRG*@;fXh7>4(P?`+Se%4Lc?~S0VK6ZM*160 z4P7ownWH@$oQQY&l6BP>QE&O=adcInyNBs{YfS3lw!ZM~iL2Qxy+43W5EMGH(i4h0n21iGa~L||v!4pO&667q z?^;vnC(`5H^&IGb7c=ByfYVjY4tq zrS831B!mCn%X%9&CjZu`12q(?Fo!$kY=5ABcScqK&sX`D{!Z|R)o1Peis43M^;J;H z|967`fY9&RMGcQTmsg$Wp*(Y=uqrs{m@Lr9IgP7RNvj}8Jzflb$_9Cw71&W;zUzlt zIIT}r*R-7Vd8SibCAusG0-OGh&8lybHyh-FH|FD?G4hS`(xnt62evYyyY3le`O+Iezb{X41^ z`+%|!t(RX25PF20?=qyKzAj|JGjVukKyTbxE}jaea5Xud>&<%!4fOiPz8PFJ7|t#B zhJN*^bO5P9w;y4hq~&oM4sL+>1^!DaD)0vF_4|Bt{mGBNmkMS-c-oGL!{J5K(-F~R zT`8OYJ>jManLiUTwwKdDqcBBzh}T?C*ofarJ;z|f7&zX*#qTE1{QzIh)_FuJ){)`w z^8a-A6<~2J%fgGp;_e#U9fCWHJHg%EHNfH?T!Xs?cXtgA!3l(5AqkcQ4gVg!=bd|B z&in7Z@BiQTcJ`a@nXaj+sjlv>uI{d$KOCG2pR|oKQ)J!o4iSyUaCJ`Fv||vRKO63s z$G7i(!29iG0J=bB=geQUBht+_hEZSVIS)Y*>=}1%pL-vQ)mL<&3BR!!GeZ+oq2d;} zNYPzi+eRHasPkQ6Exy8~LWU1*m_byAG;qK_bC!gB;0n zP}gD>CM2TPct{keS;(-efTr1GKg3KHlW(t$X<8ESM{2AuFm@d`BEIr9^u(V!n?l-N zOZKPK1_cihGeOTy9|0B)Pkf2TS7rE+T5GYN_ss{!?>n2bd{4@MNNgwX{pBfhf8y2c z(O*&gKe-4~!@QjMo-uxX-|zhSJHg`Gn}xT(GZ-L3?wo>q&9d7u&eJ|M+|Z4m$^^Fc zY^|MExxM~X@ge6*vRcoIJx5g&v6H-|4P&okYP8Lhf2Dfx^d=g?m!4DV^))_KC|s%b zjNS5a1V4qnATk{Z=c#$@dc&CqwFjJ2?(QJ}116W_q4p}2? zY7F|=6-7?y5wPggl@iyn)rhRsIv7jL zt0z(#aYz^%p9ma9>75(>3W}P>X9{~%*LdS!vGdN8+t5ZucMtHdYaPN4Aw%ymopEoL z+#@Nb`-HeqcbkYQP56Y!B6-jz$09>I0l&T*`9g>-gy8%Y%Sg?=rM!9OhGnb%>b}so zE3*Tb{#uiZje|hgG?=5L+(2pMTimu~Ojgp14`pPULJxW4h2`o8=J@os2j|BhE_*-E zomJLAzvA6^@0T#xzph(IY|(5#Muuk1;_$9ttF=f7PEhfqMk$M~vQ%>^cU3ooz~>}= zPox$)SX%oRxJ*?sjV0OX2myhQfWP&#kap5%6@NPvIDS%*#uz@~PiDaUsv>PNTYkX& zSw%Y7q1JXEa%#^&F6@ku);dhP$o)>mZFy6iu+4mlNW)OA!Uwroko<2u*)_#PE!|%=IKmph`d4F&>!uZ z=gYPDKkwjw9y1}BnnpBG^V7dSldIi!euDR`^H-b4FP4x0A7^SRX1NRVvp7CpN)U4W zvYgrFS=5V=67yk&Vs8Ttl_$e0qF<*1RnEpB-Sjg^O|H2-6G%m~p@daVY@9mAUU7Mb zu_L2T%&KuK_N%x;{IqM{JDC<0Ifr=w_az|5ilaOf*Qh%^G!OrBgvw zGYT!^XBxIElbn9J$@?tdOE*?{rr5}xi+^VhRP_z^HDOh0XKwcDitHM{*3Zr%pXH2l=h+-?WLAHevHl|Krhc~BGo znpTl=BxLo!0>h{yBKj+se}*sr3Qo{p=LDAAUuPHqi5df7_hgRw2lOXGQ70lg1=F%G z{D5FF>JT{qEP5vU4`6<#IVn+obnA!2ivchYsed;L{lh$l22m#>{Se8|WQ2$VLO=j) zF^2zC5+Hb>DAKH?(gJwt)&AkDe!7_mpazRW7)6l`fc;N_API8eRAP{QIsWsX7;G|# z5*ue)?+09x`_CW$zfJisu;>T*{>P;M%A28o^ihAES$}-pem;1-Ckc|7wPXHu~{Ur=_G9qlEQ~3e$k03)J??Q}U3OoG~ACRcQ zPzX6_SELg^LIuH)Nv@vc`xh{Y;uIKwAHn=Y(qD=b5^M+*Y!nD)0pid{E$kl?4+21{ z?_^GAavehKQToJx%x#Dd18OjUIs^)kj6?wz`^P{SkSzPD#Nq$rt|Cri8iZqA8N92DO#}bDna}k{TyZe8{ z41lDX1jtKW{qg3X;$SEM0Fp0auwcNy`uh{+|K`j861D%$H2kSV{N(YQ2@@jRP_R%x z?Mn2^7C(?NXAXck#B&wI)rLvK0LJ{;xeItVk^Sc}=bs!^VcfoeNiSkUdfNY{C@_`a z$upQ|aRJS1;PSQ)vKfskGqDF8f}}lxsU!-!f#izq7*mV>ivl+quh?h(PiJaq-=Eaj zOimRD2|=dyC}~eLrmq|( zpFT36h$a+E3cQhQTg0eTV3-X43ZodW52g=hB>lWxQ^zPl-m1rb*QEUA9I{5mEY=RU<>4GyO zMeGl($PQ07djnq+!dtX?D3)6h*k)#XLKgr_z@U#Dn+_-T7OJ~p2od}#*5c&-)%VZ8 z)QNqEiS?5yq04VEJ`8Mecnkg{ga2InNnd16;A7gbL&qsjRMo>at z3+2?YJM@-X;!;W)-^PySTkch&9PbLt?um{=edV<)r+^k}UjEOc-7GK&T$np#o_4yT zeJw`=W1oCsl=m(Q7i8<{FBQ8KsR^=;FzW%Zsz^C-XzEDCjM-S@ z)1dvhTfgvSm=1-KaRD%`47NC)x+xf*wC!zFN#D`BGzZc*G5^}S09_{$uY2!OMUsci zZ&7oEjbgsV>!jxTqiyk4Et^}*@$1|QvM8-M=-47s-O)N+CG?vRfSFp-3 zlLUbi+O6LfvPl2Et$tP0BrqGRgHy0nVg5Ml4ItGYj|}v11rPhM{)LjB&}rJ(10Th+ zz#$X!spBg%o9kM}X`&HYakMbS3BK&cddkTtkMOvpB4{b#r0Fh{E5A|JIR=AeITSoF z0u}Eb#JB1;spu|%@^X+OPG|zl7DMo}UD?H`WuFpecY8Q?4!_>VA&#Q(loVTx=hNGc zw<06MKFK%v2^E?XVzmpR?E+cWL~bO&nc;|4&|AV-xRfO+jp)I2)%IKx40d?UWs&HdZY+&da~SS8c>5WCQriac|RK@$iy>1TC;V6$h=LsFBb;ja2=` zzT~Ss7>RpjUgB0jUvF-M9L+=p07?_-uR;v>xMScyp$pm2$I%K@-{j+$0kgcvzW?&-%rchE#QRMbLciDa@7t5+3ti+J|@o zGb&Pa$7dELA7P~4aedydBprS!H!<<>?wFgBjpRgfIEA|;#%k&4P!a4jdP(^KBQLv@ z_MJtH^5&MLUU)~MvUon0>a6xLhm1v>3V_zA< zxa;LmITeN+4lK;v&65w`f?k7YfY*^4!ZFtY&9MzsPl1SDd1$Ni+|g3ISZrBC0#wK_ ztC9|%FrR2#^?0*2&~+-Qy3>C#HvfDZQVWz^<3g{^PNambtH$abYrB;{HS-XWL!LWg zsi1w{$6OG&Ua-cxR?*8U>#k-B@*aAVR_7fr5fx3bs0n|%LuiYYez)=nnB372rlTdsR_R)6BS4XE1}&AH4`X;_dB8x zO%rWw#P`=i2XVI=kHFpt*uHxh@7A z1y?**T63L=W?J(XZ6EMW<<9yLm-tsU6Tij2C?~bM$utsmy)yLPMPD2GP~9^KM0uVL z6&D@P4o?i3r8jOrZMGS$xt)m{D$w*?{p--hHe<{ZY>A>TdGZ4)vl|U7lM0gh?$dY> zTH8mJW|Q1dn?=nmHu*jVk_B$*6e9k{0}rmw)Y%jPs@e-GkdS@}ZbX8%TJ9dK#RO?7=cs*KdL~JW1+|j&sx<_@+>K@K75K# zAgl(3^d*o#{(U}eR4Aq_j1a&4VAD*pwaIDCiN++$k_e`E^t;ssir3db?@#eU+~eMq z3Bz{+06NjV5l&SmkAzQXWxAs|k11J1pwd`SM-j`c ztK}`VnPS{KhLta}!?V=}N(P!K9Kl_Z6k!bPj{v?ITH+q)&0&J9tgTA8-R+83a-`@< z*R&4HXc{b~DplrLY0u;nQ^Pzcd=pQ;dL#p7uv_2WSWb*Qu+SzPk#D<1$j^U17x_T) zRW+A`0G1@hbOS|IYJ(f~Lw$}=ly3_Pu!+#X{SsHoaflfsSgl?H;8En|ZkC+$2ynkn zFtYC)*@yPa7L<#3hK*c8kEZ1UGS#lEDpaae;YX*l!oaRlyi`nC2NE36sdnqpa@51G zIemmUA4ou^j80Hz+W$6(q$dkaUQUdn9TxgD?KIGQF+fTmA*$xy+{h5#>&5VdRNSFI|O*h29g; zNd7IYBz9P*w`M;#CwJTu{t4s530sMNiy-?QP}98_=}}m#AHe*GkJhg-GYkHJnt$Du z*wqEG&rL;2@$+F6YJ3?h6kPz<^a)Z(2g>OWg z{qn7@bhBuPV^1|;@>9q=U4N9X;>_;A5~Rs+b)gW19ICsV8!HqCwX)5uODr+~{IS$b2PM@_AdqZWiOZZ4 z>Pm&4+9BStGJHXZaCkQ}sR%bIoiq4NJ%EmpDJux#9r|xdGBiX>hWk4u*`%-mq9eP2 z2X@bYRg!;lJqJ;et>YmMPkPgp5I~xu3=hnX5+}|a=X%5PNT0uSjN-T4#`E+b#c$Am zzG|`zJq^lOq=j~RVJiNS`{Sw)MxzXSihF>Xp%iSHVG%l_6>_dPcbtB4&z|{8UHo|6 z>-$GQPY~ZsD!g2-0Lq9z(3NX^e~X_R@#*vxmO0<_cW$7ibiaD`B@T7Rt(ht+fGk`k z+*#lW6AeE;y_tW!&PU62tF;$Cqit0aixXqDG6bS>vt|j6<`>8GtDL7<3Ay^bnR)N9 zXFC?c2hfNpMoor~u-Io|_e^Rb?s%SKKLQ3y8f7=J_hpX@Gwh8_zG#>;4^-rvkRVz| zTY@V;h`DGA5JYuXYXy5~=8R`^h;O|JQC&=IMzbp#s@Hz&%2jSE0)qKmuyxIgj6G*Z z)#MQxOI1QBz7^t>3vWx=eQY^Qwv0y_(gqWqYklk`75?P< z{Py1mgGc+FT;X$-y_G;F16~m*6T0jRq{8KO=6VW3eM=Qf;cgzp*$#EdS z{r>V5?0Kcx+0Hxd_a17Bgdb*7VTK~}iQr7L63mhI?Pr2DEoO~8W1kM&0v!2-`2$Xp z3f>)v@*Jwbp7$+W`!^b(qW7IK6f5dUbG7SwrZmn27&WFF(g*}sFJ!z?U#KD5tSppE zFqY6jWty%L^3-n-hQM;W^W$W~4*JewRq6>NB_7Q<yi9h=JYkU5c#n#qbvoDKOA}W_96(h3z#NR~f zq~A&MublTn5?ZWX+e+_)-}7c%eZJZjNr)kI*hWD8lz=7Ukv(l@IB2YxRVmlLB(}hc zIu=In3LiZ{3=6nGlROEj zB$JCBwhtio5KsDOoz+7LhkZVD{iPBQD>*ftqXon2Ywi<(m{~~3xnR!>;XJHiXfU!f zXVhBEeDA`mm%{W2C@bko1FUcMvuHVMC9sBHVh}l@Dp(+S%B2;q16AIBM#?4h3iHvF zdxx;sLd9)fi1UG9g!ssuooWLs+HIFYJlV5-&ghKVr>I_>9O8frA*ksxueT5*iQc~6 zzgiGfb24rL&1dw9_69^{t!P$Z7^X#gvSRT`(n(6!EJCrhDJqW9PQk9p^EJV^J4Z7` zp(>d~p@TtkszSZ2RC2P^%TTPKrCo(ZcuuT+JQ}ko)_0?sFIBBQ*>{n5PywA|rnv_a zLWEY8SUMiV=M-DOxKj>P_O78y<+s<(!m&3|b)|1%+V=ZauTE3S-yJZ~p*(zGY3WIx z8p6qa`kr08me^PUPdUkjGD}r6iDQ-=>$OlvnY8Xym&H^ct1AapZsNluz`6IOT{3x= zc{sx?hd?Kv#-8;7MVDLSC&B@KteS-%kQChY{V?!lP; zTl6_!nau7xlwk(!*WN`91N5vc^(I+&C#r0wU4)_TJrpw2=+AvOUfVQOwN_+K-{cdy zn@n_8*iK8`aL2@zy>EPBuZS!|Q*FoQOY~Y`Qd=@Ivb_&!_E_8^Yq2l|cHvs_cD5E~ zdN%1uNYH0a*0iQoRcD%KSx7+A3nh7ASDyS*_f`>0d?u5m4ATsHiU7M~G)luJ(yDGc z&tmCCRK&0*=lI7CKdER6GE5Q>7b#_FB!-6i=8+!V47Z?~kJBi+$^@ZcI?2};gG*M$a1u9+L?=MN(_3E5FhL6O95L(hi#B`M&g^P!5RMK`p@ zAQ~v*H-EYOtVRd4+Y~&DnGLlzSWmP%`p&l$BuUvUFPkbT-c>jZzJK6Mb}0!W?R8(S z=qE%S#$1vk5ms*80x@FvgJQW}Qxgrb(`r&-7|Q1zcuc!kS5zrY=4^I}8Kg#R#9O-) z#4K9vGpJE2ZWgxuX3D z_#jG@>{e!&5SYs=MEtRAKyECM2iTUye_0|cOb|uhi4iKJ>bhcD3%DQRq66WYULwuFSEXNAFgF$5}m5eXOQXsXeSI5cB&JxN> z1I*7f%Nd|jCXr;13W48G%wyJ1tfU0B4jHZs@ys%x0Hi1cu@M}?CDiBbt@MP#3OAD$ zIk9P|HFPJ;I;(OMFXlp1WKP07nVp!BBr3i>2O@MYTbVrq8oJ7i&%RyTJR}f-wo%_3 zPxz@Dbw22$q}@rrIADtQ7)cc5=z0GJ%@c@K-~x($JLJix7@Fa;aag_GwZap-a$Z}m zvvPjxOtj5&GH!I0*EiH9la!EHuS^rU7Kvn z@WW_Aag<0OzJ$E-q7gTyUcFspX_k_1l}&Qxb6O85MB0_QL!F~w$s&wgWf$LoFF-Q_ z0tsTe8`R4M02t9nOcfD>hPDkvieH_M;!p4`DFowW)<(nLM;V|v&-xXX?KbLC_@G%0 z^DV{i^ppv$!RtQJ(4$UEv|exM9^iPjM;2<28MO!3$v@t_ZZ@*{GBX-6dO>K2`m+n& z05KZ((the@1Y9SR9Q9dtlC+p%f5#-QR?`b^>J@bdl&?^RHQa44&22Z&zxE)pvZg+? zuGJ!*HpV9^`3$ux9^<5uCmfrEMN1lnZt1Q|TWd>FV0M0U<(m}87BI(^IHjuO({7@3 zBeRfBr?;50=Z_jJD`~CfGnkKy;;zI2+;*Cr&NOi$_$T3jMwR6xtDC6|7n^Uri zn`zUBd=j!lYJvCG@R0a6B*t3ffI-sF22xOSqgO5(5*K9&=OBh@)tkGhV0NO3+^t{K zxita^1}Z!xC{G9kikLOJ%x*Vs=&vz%bI2{`X~5adw_G(?9OFY1roqW!lboM3FHU7! zXytUZDt28QVq-C^q3?XXJ8rF{yh-O*&$+~|PE`_Inw^&3S|zpwCZpjZznD* zma|OERegG!D=#sQaj>VgWeOe(T72__T_i{3ORx};|S;50@Gn6wWMl7si;-mYhUA4|JhpA&50?@j)&HOjM zBX&%{&~zFfIH$>o2(Pu35oWR{l^X)7#J!o{Ssww`hr;@*Z=%GwdQh&${9ptf&LjY5 zQq$9&ssiZYAaX$|soRcv6kK4c^=H9LX@B|a%)7imREE`#YWSyc2n5MWnAc+aQlaG; zM54s9xwx!*9HBEYUdG9hmD7ES6C3%hfwW)J`fg`OOL8hR%y-I_65#J_>(yO%;w5Si zl!cBlXXT-T$Gpk2FZD`n_}@VL>}b*(AKvhE2e4A&1hbkzW_nfY<#Kz|IALauguBEX zs}Co?RXT^J)s&<3fL@1H3a`4P8iHQs+*7T*iC~zR6<#5(Y7 z6%QF)%oM;%z%;OXjcd}d;IkIR!zkaAISs!Q|aq%&1juA#F~auJeASPVK0au7fv z18Ib+gv(1%3myR*KdbSK7{A!M;eOxj9twczO35*BriLMIx_eGx5}edfsG9iuu9l)0 z-$(ywx5v=_pwFX?&`23+uv#Y!>Sw!7 zl{q^k)-zO)N9xvlVrFcwD*qK#CS3EQwqj{#!D5g!&36K-0xF#lkqPp1_ZkQPvv^=x z4xD(%OYvIh=MvRJJMFJVv|xj37`iQV;Q^^=)mo)WQmAQUrrDIJ1Jq^U9?#D( z>(G*wJnhT2ejJKVVeX0-&F926MT}#iPp-BZ_a>%>2{h}=jy&U!&#;Y&7T0aHN^BVw zhs=spDejcM%D+i2@cc&f`4MpWzTsdyN%#xlnon*<^slGv7f0uvpQN49S?+lm z{qH+bq+1n+U&>ymPX)lgIWBqS)2ALELw1;-jb~eNruChIH+pJ)LJC;243YHJGwEk#$+~Ys3_Dn?fDd?VLG+k}r_G86X6& zuly|?&!|l%-kv=x)NgQ)d8BSjih|86s2rEFWGAT@WhNw;k)Em+PoWXA&?YsD$qc7u zA?1^l04-h^)K?Up;ttL%#WD?s)Je;KUqtr%tbQt8QJpc-lD@(mygN4(Jj}uJsrLG2 zTlUdSwQjH}eT_SiTL`IE7f%{T3uv_>{S|RlLwgFm4sg)EuIg3t@U`o=TOq1>j_;r3 z_lilqaVM-V+M+I$l{iy=xEjRn)MJAhi>h^DUuc*g^P$^CGnG$uv21YhK_B2_ygyUIbi!-KlSmdf$-G-9M5|zYBGgM`jc8MRX9}tzu zE1RK%9%&_|UWwEb^AW(29Y#TX=i&)m;CxLXT*@MIRlBQBd#0*KwQH?lsDY~)Mt};P zO0!X#BD*L)Pyi)Y>7T#wXAyY>+(p**g_Mjv(Egk`Z0O>Qg%a)5iF&<;Auz)Jf_8Jy z2TF8FSwsr}^&O?R!Jv>YJTM~60xDj`W?l5Le$sCeB~??LJeDv9)59NPg5i;+iM3i8 zkhU~?-syjll~B?GV?0rS`uaAo^OeIoY`>&B9d^RG<^`rwYgmqNB} zDJ@Xw`sB>IAvxU1Q^?wLBHwArW2eI%TX~RxK@~%T7Mf!A6kl3uU6vVr?5cD3Cp7#n z|9nt(3(J%7oqRymN^Le5>d@1VUC&3&Zat@MWQx@;97c-qBCS^Tts}W2{J*)_{9QSW z$q9mLxpn*&r>_1wi00d~5_5aFQLEul*t6v-Ro*mv|8eSPdPQ66g~ws?drntCp=J4I zA`Yn}7OfWrg$rA`CYe=qVe?ch8rGjB-aXfARvNr?{2J3C5bj`$E^thW8Pm-afYiOx zTRf$Y73S!&*3vr{+!X^W1z@r9tn+%RMZ4F`M-^7u6YZp(-L3r%8?yo@AuO^UHR{>K z0cADTU9~aMXp9yz)Bc5$`&xc5bU&#Qj;({LU8S_1QHl0Pi;9Iex8z^&p)6=DHpw|~ z7C7s-^L-MN;DVK~jmhWw{-ex8T(}CT6pg$d!YP{fYo07hCT16aQ$g~BpRkW2-6jw# z<5lA!;L9(|5#^UdaIAuolR)o$0>dF)v9whiffqTlNr$0eN`Ja(%{8UgBu13Y<5DOs z#DQ%AXHWcT2yW7L*Y2ew99#q4eok8_z0*`)Ke=R1K8VNCUgk*t<1LpXeUlw{CHb0q zeSyV+hkBlhxEQ`` zHnMHBlSWePQ3Q!y%;RMwivxA-JP!8)DX(5GhWP9YtJ5=UrN zyKKYA@^34hcu5rnZriZoW6eB?AqzPreyg|a$VCUF8WU)Y_%My$)!izMV;_~hvtm?T z)sVn(WLzPr@Yd2wBPATE@LyP3rYxOOn3H{0{82;I+j*06jjv8So5>ITWwW7s)nVqpwd9Th&)dlQ8yK^nsyNHGD|fd?5%!;lb?z5P46{byU0(pb^{ zpKEHSqmp#3RzB?`;gBBwm5RpTAgV~TlvM^Kj={A8|5sJDl5@~aQP|uIsg4wkDJ(8Q zR_eh_Yox5n-%j!>!5?{&TMP0k-*BT*+?Ywp6?B~l*Fwfl*dpp@_6#b~LgnPdGKlVA zZA<4F7N!6zu1een%xPebfW;&u4U z+->5a;})_%7eBE5N9q0Ps@-qn8X(a;$F?1~@Y~^E@1Rv*UFY>2t-xqR{o3Sp8i|Ba~sNg)*HR~mov@6q7j1(h=z+&jH-1|NW%bTwY^6pR~N5Q9aputTn zNL6$;U_jtgWxKK1>W9!MZZMiX_Mkyy-AdT;0%xkNHxF)B;#!wh~dHP zgFa0hTr8MmBDK8F9vbQTh-Fm%EWoN_9-!81x!U=qQVtm3k;(lJ)&2d1ho=6%Ujw_@mKHPG&T>bQA=kc z(SB*x8Ism>>pUWbD#D3Y#xQ&jSY6a-4;J3EgaQ~;FldH*MRj765&&4jbzhduEj_QJ zi%9a}M6Fop=LvWhdTO#cL9u+Kd*Fm0X$p41&UW;aET|vrEr|AzVe=4MWJ$7<(u#Wa zMT{7>BD8~mW(%uM3dU{ps_L7Xa$lA736n(nY=qjLrf(y^8)^F*7xM+O7QSX~|o9>{*M7;Gt$+upk?D0njvePiOy2* zdP|RxcbD1`o86d>_7ZzxY25lV->(3FiaD9yOAB%Kep`9K<2^P*?3#2Nosa~*`pC`_)#Cx+Q!{X_az~GXZZBnhu zs9m8eD<^GyYTr`Itz+2OwJC7~ZBr5QV(wXdPiy#&B#c=&6JDB5FfKGyr@c1;7%V

BCRF{`W#hi;!=X8(cASjw?-Aydx9oYge8^=voP0y#LQ|2IS1-HkQb`eY;^w{x&usH_gLw3 z{Eq;WN5JfRtJg1iRohN>)W5K`%-*X4jUyl{fHZtvJ~j(U)cWg7G%`{%@~$2Egv%dS zf?>o;6MBS{#Fr?w@D?!U8oq@Q+xbT94Us|=k2i^R_ zf16vAe_v<$ciI1sk*;DdeU>ZjCdeLQbVYnbSrv)32C9khg-eDWwKj^7b**MgA#C;M zIP^z((-nTE+)83cSN)hpok=EI=}g6LPL;=}N>ALEsMIku3|q4|t!jL*CoX}FV2N^T zojFX3bgdWD^WKH{hPDT6;B{efvqDW~4?8yC?KvC{^HMv-Tg^1Po>TZa)KA2`jW(Vc z+A#@!ulivULkpVu_O8O?9H~W~twS$<^e`>YtU{Lfz_-r0s9wW|V55Ptu4bITF5{~? z8j;=eB@|&KiFMq6bE8EU=famFb8;T{$SIXGRll(1hKzCQoL*sJB8Hnm>9n~VglX>V z_7?wBS^#9s^6#uB>W~?n1-C`AI53@euv-d+Rbw%QC%vnlo^!ZBIZ3VTip-ebFXMd%cIQE}7Z zdY-he7h*&#;V?22l`@eNMu0GrT{ShUrFh_JdEhdXs^TZp(aZ(>VZ`QvSyeTeBO{>g z%+=Hdm|_V)0gwq1T8B$-?=|hGT-JDew|4;?iyYgLteq+H$Cft(d+Jvng9*I(j*LgP zzAZ5oYY{XnyKVMeAuOZO1@by!dMLKfu4&5DsR{LZJndGRnJhoYjf((8@A`kO%L>P8 z2g)q(VT~Sc-_^TJ`2Gc-nYyfGrCWSI+kojo zOkBGF7y|Z!9$$r8)li4Fa2Uxy&=teABd%{Zl|a_;Hb`8daC@EEbpQIq1<(K{QstS} z*G`T{MRY?<1uw;{r?vB*LlwKk1Pz~c+Q3H)FDptQip@#rg=!4L0&EbP{~b+6v6Vre ztogB{AD*BNe5~Eqwk6fl~>(U^``SwLJ*Md8G3%(GjqYB4o$Q~XXPI0_=kbp~;Ic=`hQgnyu%F9 zAcGR#ezg8Bl00)NSMEKBicD9m{|H#3(lX|O(RIzOPv0JiaW?;mgRVr|Qo#EvWACsD zgi}qLZzvRxyAsfk>L1sB5wkIGc3jhj0n&n6!xXLfJ5hZE)O%LnlhOw5Pd-CUEq)$x zN{;d>=d`BB+1Bv&z33;1W+|(wrum|fsCABe`-Fom_Q@Qo(RnE)dLm}4esV!H-l9x> zeL*m1j2?U~d|0DMrJ&-`S~li4-Dfsx^hTGYcJ};&_c1uCs(Nick%5Wb{C1r%rzJ(8 zX(Lz68j0-0h~jXRz@jBey<9dAOcw=PlY%$l|EwY5M+R**jeB6|Q7xh6alk~QM7F>B zc$%HK_7qXAo}zXx_gsPAhJtGLPj+hrcWURR@xtY5`KlG&P--1gHzuhnv8fn6Q(Q~+ zE%a6}Qz;WtlvC}JLq&{ujiIfsOD3#w{T#%h9xg&b;j%!n&1E$Z6DqZ~1(!S2Vzyk7 z(xaqqvb6Py`LmZ&Hn#RHY9nUT>cqUcRJJk+$Kc=8P$_Ux!oE$iu_M|4gtrfF%!h^i|s;c~5pF<(8kTQm*?;`$wUcqD+} z%i+^(o0IFDMNh4At>b6ia+jzaP9^r61Q8t*>8Yawu&ol^@V6RI zfsN&l4ZO*((`@$`jEJQ%9g?iJj51lYvz7qFK%KkA;bqccEu1BAf!*C;y~Y82rP*f@ otKSjLX61AJ|Geaq{eHXoFWN5t;p32g3)62}$WIcD{", "content": [{"display": {"title": "smartHomeNG", "body": "Hallo"}, "speak": {"type": "text", "value": ""}, "locale": "de-DE"}], "expireAfter": "PT10S", "target": {"customerId": "", "devices": [{"deviceSerialNumber": "", "deviceTypeId": ""}]}}}}, "status": "ENABLED"} diff --git a/alexarc4shng/cmd/Announcement.cmd b/alexarc4shng/cmd/Announcement.cmd new file mode 100755 index 000000000..04647a686 --- /dev/null +++ b/alexarc4shng/cmd/Announcement.cmd @@ -0,0 +1,3 @@ +apiurl|/api/behaviors/preview +description|Use SSML to speak-Example: +json|{"behaviorId": "PREVIEW", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": {"operationPayload": {"customerId": "", "content": [{"display": {"title": "smartHomeNG", "body": ""}, "speak": {"type": "text", "value": ""}, "locale": "de-DE"}], "expireAfter": "PT5S", "target": {"customerId": ""}}, "type": "AlexaAnnouncement", "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"}}, "status": "ENABLED"} diff --git a/alexarc4shng/cmd/Klingel.cmd b/alexarc4shng/cmd/Klingel.cmd new file mode 100755 index 000000000..26f360a4a --- /dev/null +++ b/alexarc4shng/cmd/Klingel.cmd @@ -0,0 +1,3 @@ +apiurl|/api/behaviors/preview +description|play doorbell routine +json|{"behaviorId": "PREVIEW", "status": "ENABLED", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "sequenceId": "amzn1.alexa.sequence.8d9b40ab-91a7-46c1-8d42-1cd53408874f", "startNode": {"@type": "com.amazon.alexa.behaviors.model.SerialNode", "name": null, "nodesToExecute": [{"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.DeviceControls.Volume", "skillId": "amzn1.ask.1p.alexadevicecontrols", "operationPayload": {"customerId": "", "deviceType": "", "deviceSerialNumber": "", "value": 80, "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}, {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.Sound", "skillId": "amzn1.ask.1p.sound", "operationPayload": {"customerId": "", "deviceType": "", "deviceSerialNumber": "", "soundStringId": "amzn_sfx_doorbell_chime_02", "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}, {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.DeviceControls.Volume", "skillId": "amzn1.ask.1p.alexadevicecontrols", "operationPayload": {"customerId": "", "deviceType": "", "deviceSerialNumber": "", "value": 20, "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}]}}} diff --git a/alexarc4shng/cmd/MultiText2Speech.cmd b/alexarc4shng/cmd/MultiText2Speech.cmd new file mode 100755 index 000000000..bd0661f3f --- /dev/null +++ b/alexarc4shng/cmd/MultiText2Speech.cmd @@ -0,0 +1,3 @@ +apiurl|/api/behaviors/preview +description|Text to speach +json|{"behaviorId": "PREVIEW", "status": "ENABLED", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "sequenceId": "amzn1.alexa.sequence.8d9b40ab-91a7-46c1-8d42-1cd53408874f", "startNode": {"@type": "com.amazon.alexa.behaviors.model.SerialNode", "name": null, "nodesToExecute": [{"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "type": "Alexa.Speak", "operationPayload": {"textToSpeak": "", "locale": "de-DE", "customerId": "", "deviceSerialNumber": "", "deviceType": ""}}, {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "type": "Alexa.Speak", "operationPayload": {"textToSpeak": "", "locale": "de-DE", "customerId": "A145HAI7HRJWX3", "deviceSerialNumber": "G091830895260311", "deviceType": "A1Z88NGR2BK6A2"}}]}}} diff --git a/alexarc4shng/cmd/SSML.cmd b/alexarc4shng/cmd/SSML.cmd index 778c0263a..47ed3a987 100755 --- a/alexarc4shng/cmd/SSML.cmd +++ b/alexarc4shng/cmd/SSML.cmd @@ -1,3 +1,3 @@ -apiurl|/api/behaviors/preview -description|Use SSML to speak-Example: -json|{"behaviorId": "PREVIEW", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": {"operationPayload": {"customerId": "", "content": [{"display": {"title": "smartHomeNG", "body": ""}, "speak": {"type": "ssml", "value": ""}, "locale": "de-DE"}], "expireAfter": "PT5S", "target": {"customerId": "", "devices": [{"deviceSerialNumber": "", "deviceTypeId": ""}]}}, "type": "AlexaAnnouncement", "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"}}, "status": "ENABLED"} +apiurl|/api/behaviors/preview +description|Use SSML to speak-Example: +json|{"behaviorId": "PREVIEW", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": {"operationPayload": {"customerId": "", "content": [{"display": {"title": "smartHomeNG", "body": ""}, "speak": {"type": "ssml", "value": ""}, "locale": "de-DE"}], "expireAfter": "PT10S", "target": {"customerId": "", "devices": [{"deviceSerialNumber": "", "deviceTypeId": ""}]}}, "type": "AlexaAnnouncement", "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"}}, "status": "ENABLED"} diff --git a/alexarc4shng/cmd/StartRoutine.cmd b/alexarc4shng/cmd/StartRoutine.cmd new file mode 100755 index 000000000..b91311b2c --- /dev/null +++ b/alexarc4shng/cmd/StartRoutine.cmd @@ -0,0 +1,3 @@ +apiurl|/api/behaviors/preview +description|Start a routine +json|{"behaviorId": "PREVIEW", "status": "ENABLED", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "sequenceId": "amzn1.alexa.sequence.8d9b40ab-91a7-46c1-8d42-1cd53408874f", "startNode": {"@type": "com.amazon.alexa.behaviors.model.SerialNode", "name": null, "nodesToExecute": [{"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.DeviceControls.Volume", "skillId": "amzn1.ask.1p.alexadevicecontrols", "operationPayload": {"customerId": "", "deviceType": "", "deviceSerialNumber": "", "value": 20, "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}, {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.Sound", "skillId": "amzn1.ask.1p.sound", "operationPayload": {"customerId": "", "deviceType": "", "deviceSerialNumber": "", "soundStringId": "amzn_sfx_doorbell_chime_02", "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}, {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.Sound", "skillId": "amzn1.ask.1p.sound", "operationPayload": {"customerId": "A145HAI7HRJWX3", "deviceType": "A1Z88NGR2BK6A2", "deviceSerialNumber": "G091830895260311", "soundStringId": "amzn_sfx_doorbell_chime_02", "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}, {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.DeviceControls.Volume", "skillId": "amzn1.ask.1p.alexadevicecontrols", "operationPayload": {"customerId": "", "deviceType": "", "deviceSerialNumber": "", "value": 20, "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}, {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "nodeState": null, "name": null, "type": "Alexa.DeviceControls.Volume", "skillId": "amzn1.ask.1p.alexadevicecontrols", "operationPayload": {"customerId": "", "deviceType": "", "deviceSerialNumber": "", "value": 20, "locale": "de-DE"}, "presentationDataList": null, "clientData": null, "context": null, "tag": null}]}}} diff --git a/alexarc4shng/cmd/StartTuneInStation.cmd b/alexarc4shng/cmd/StartTuneInStation.cmd index 1f2966ab8..0937d3740 100755 --- a/alexarc4shng/cmd/StartTuneInStation.cmd +++ b/alexarc4shng/cmd/StartTuneInStation.cmd @@ -1,3 +1,3 @@ -apiurl|/api/tunein/queue-and-play?deviceSerialNumber=&deviceType=&guideId=&contentType=station&callSign=&mediaOwnerCustomerId= -description|Startet einen TuneIn Radio-Kanel -json|{} +apiurl|/api/tunein/queue-and-play?deviceSerialNumber=&deviceType=&guideId=&contentType=station&callSign=&mediaOwnerCustomerId= +description|Startet einen TuneIn Radio-Kanal +json|{} diff --git a/alexarc4shng/cmd/TuneInNew.cmd b/alexarc4shng/cmd/TuneInNew.cmd new file mode 100755 index 000000000..dc87e9937 --- /dev/null +++ b/alexarc4shng/cmd/TuneInNew.cmd @@ -0,0 +1,3 @@ +apiurl|/api/entertainment/v1/player/queue?deviceSerialNumber=&deviceType= +description|NEW API for TuneIn +json|{"contentToken": "music:V3lKdGRYTnBZeTkwZFc1bFNXNHZjM1JoZEdsdmJrbGtJaXdpY3prMk1UUXhJbDE4ZXlKd2NtVjJhVzkxYzFCaFoyVkpaQ0k2SWxSMWJtVkpibDlUUlVGU1EwZ2lmUT09"} diff --git a/alexarc4shng/locale.yaml b/alexarc4shng/locale.yaml index 9bff4b0db..7b96d043b 100755 --- a/alexarc4shng/locale.yaml +++ b/alexarc4shng/locale.yaml @@ -1,48 +1,39 @@ plugin_translations: # Translations for the plugin specially for the web interface - 'allowed IP': {'de': 'erlaubte IP', 'en': '=', 'fr': 'Adresses IP aprouvées'} - 'last Session': {'de': 'letzte Sitzung', 'en': '=', 'fr': 'Dernière session'} - 'Stream-Modifiers': {'de': 'Stream-Modikatoren', 'en': '=', 'fr': '='} - 'last Session duration': {'de': 'letzte Sitzungs- dauer', 'en': '=', 'fr': '='} - 'Sessions total': {'de': 'Sitzungen gesamt', 'en': '=', 'fr': '='} - 'Settings': {'de': 'Einstellungen', 'en': '=', 'fr': '='} - 'Credentials:': {'de': 'Zugangsdaten:', 'en': '=', 'fr': '='} - 'delete Protocol': {'de': 'Protokoll löschen:', 'en': '=', 'fr': '='} - 'Real-URL': {'de': 'tatsächliche URL', 'en': '=', 'fr': '='} - 'Commit Changes': {'de': 'Änderungen speichern', 'en': '=', 'fr': '='} - 'Store to Config': {'de': 'in Konfiguration speichern', 'en': '=', 'fr': '='} - 'Settings / Cam-Info': {'de': 'Einstellungen / Kamera-Infos', 'en': '=', 'fr': '='} - 'Communication-Log': {'de': 'Kommunikations-Log', 'en': '=', 'fr': '='} - 'active Camera Threads': {'de': 'aktive Kamera-Threads', 'en': '=', 'fr': '='} - 'SSL Certificate Info': {'de': 'SSL Zertifikas Info', 'en': '=', 'fr': '='} - 'Proxy-Credentials': {'de': 'Proxy-Zugangsdaten', 'en': '=', 'fr': '='} - 'Proxy-Authorization': {'de': 'Proxy-Authorisierungs-Typ', 'en': '=', 'fr': '='} - 'Video-Buffer-Size :': {'de': 'Video-Puffer-Grösse', 'en': '=', 'fr': '='} - 'Authorization :': {'de': 'Authorisierungs-Typ', 'en': '=', 'fr': '='} - 'Encode': {'de': 'enkodieren', 'en': '=', 'fr': 'Encoder'} - 'encoded Cred.:': {'de': 'enkodierte Zugangsdaten', 'en': '=', 'fr': 'Cred. encodés'} - 'Result :': {'de': 'Ergebnis', 'en': '=', 'fr': 'Résultat'} - 'Value': {'de': 'Wert', 'en': '=', 'fr': 'Valeur'} - 'Property': {'de': 'Eigenschaft', 'en': '=', 'fr': 'Prioriété'} - 'Threads existing ...': {'de': 'existierende Threads', 'en': '=', 'fr': '='} - 'Auto Update ( 2 sec.)': {'de': 'Auto Update ( 2 Sek.)', 'en': '=', 'fr': '='} - 'last/next Auto-Login' : {'de': 'letztes/nächstes Auto-Login', 'en': '=', 'fr': '='} - 'selected Device' : {'de': 'gwähltes Gerät', 'en': '=', 'fr': '='} - 'No. of Alexa-Devices': {'de': 'Anzahl Alexa-Geräte', 'en': '=', 'fr': '='} - 'LogOff': {'de': 'Ausloggen', 'en': '=', 'fr': 'Déconnection'} - 'LogIn': {'de': 'Einloggen', 'en': '=', 'fr': 'Connection'} - 'Store Cookie': {'de': 'Cookie speichern', 'en': '=', 'fr': 'Sauvegarder Cookie'} - 'Paste the Cookie-File here': {'de': 'Cookie File hier einfügen', 'en': '=', 'fr': 'Coller le fichier cookie ici'} - 'existing Commands': {'de': 'existierende Kommandos', 'en': '=', 'fr': '='} - 'Command-Name': {'de': 'Kommando-Name', 'en': '=', 'fr': '='} - - -# '': {'de': 'Proxy-Authorisierungs-Typ', 'en': '=', 'fr': ''} - - - - - - - - + 'allowed IP' : {'de': 'erlaubte IP', 'en': '=', 'fr': 'Adresses IP aprouvées'} + 'last Session' : {'de': 'letzte Sitzung', 'en': '=', 'fr': 'Dernière session'} + 'Stream-Modifiers' : {'de': 'Stream-Modikatoren', 'en': '=', 'fr': 'Modificateurs de flux'} + 'last Session duration' : {'de': 'letzte Sitzungs- dauer', 'en': '=', 'fr': 'durèe de la dernière session'} + 'Sessions total' : {'de': 'Sitzungen gesamt', 'en': '=', 'fr': 'Nombre de sessions'} + 'Settings' : {'de': 'Einstellungen', 'en': '=', 'fr': 'Réglages'} + 'Credentials:' : {'de': 'Zugangsdaten:', 'en': '=', 'fr': "Données d'accès"} + 'delete Protocol' : {'de': 'Protokoll löschen:', 'en': '=', 'fr': 'Supprimer journal'} + 'Real-URL' : {'de' : 'tatsächliche URL', 'en': '=', 'fr': 'URL réelle'} + 'Commit Changes' : {'de': 'Änderungen speichern', 'en': '=', 'fr': 'Sauvegarder modifications'} + 'Store to Config' : {'de': 'in Konfiguration speichern', 'en': '=', 'fr': 'Sauvegarder la config'} + 'Settings / Cam-Info' : {'de': 'Einstellungen / Kamera-Infos', 'en': '=', 'fr': 'Règlages / Infos caméra'} + 'Communication-Log' : {'de': 'Kommunikations-Log', 'en': '=', 'fr': 'Journal de communication'} + 'active Camera Threads' : {'de': 'aktive Kamera-Threads', 'en': '=', 'fr': 'Threads de caméra actifs'} + 'SSL Certificate Info' : {'de': 'SSL Zertifikas Info', 'en': '=', 'fr': 'Infos sur le certificat SSL'} + 'Proxy-Credentials' : {'de': 'Proxy-Zugangsdaten', 'en': '=', 'fr': "Données d'accès du proxy"} + 'Proxy-Authorization' : {'de': 'Proxy-Authorisierungs-Typ', 'en': '=', 'fr': "Type d'autentification du proxy"} + 'Video-Buffer-Size:' : {'de': 'Video-Puffer-Grösse', 'en': '=', 'fr': 'Taille de la mémoire tampon vidéo'} + 'Authorization :' : {'de': 'Authorisierungs-Typ', 'en': '=', 'fr': "Type d'autentification"} + 'Encode, save and login' : {'de': 'enkodieren,speichern und einloggen', 'en': '=', 'fr': 'Encoder, sauvegarder et connexion'} + 'encoded Cred.:' : {'de': 'enkodierte Zugangsdaten', 'en': '=', 'fr': 'Cred. encodés'} + 'Result :' : {'de': 'Ergebnis', 'en': '=', 'fr': 'Résultat'} + 'Value' : {'de': 'Wert', 'en': '=', 'fr': 'Valeur'} + 'Property' : {'de': 'Eigenschaft', 'en': '=', 'fr': 'Prioriété'} + 'Threads existing ...' : {'de': 'existierende Threads', 'en': '=', 'fr': 'Threads existants'} + 'Auto Update ( 2 sec.)' : {'de': 'Auto Update ( 2 Sek.)', 'en': '=', 'fr': 'Màj automatique ( 2 sec. )'} + 'last/next Auto-Login' : {'de': 'letztes/nächstes Auto-Login', 'en': '=', 'fr': 'Dernière / prochaine connexion'} + 'selected Device' : {'de': 'gewähltes Gerät', 'en': '=', 'fr': 'Appareil choisi'} + 'No. of Alexa-Devices' : {'de': 'Anzahl Alexa-Geräte', 'en': '=', 'fr': "Nombre d'appareils Alexa"} + 'LogOff' : {'de': 'Ausloggen', 'en': '=', 'fr': 'Déconnection'} + 'LogIn' : {'de': 'Einloggen', 'en': '=', 'fr': 'Connection'} + 'Store Cookie' : {'de': 'Cookie speichern', 'en': '=', 'fr': 'Sauvegarder Cookie'} + 'Paste the Cookie-File here': {'de': 'Cookie File hier einfügen', 'en': '=', 'fr': 'Coller le fichier cookie ici'} + 'existing Commands' : {'de': 'existierende Kommandos', 'en': '=', 'fr': 'Commandes existantes'} + 'Command-Name' : {'de': 'Kommando-Name', 'en': '=', 'fr': 'Nom de la commande'} + 'Step' : {'de': 'Schritt', 'en': '=', 'fr': 'Étape'} + 'Reload Page' : {'de': 'Seite neu laden', 'en': '=', 'fr': 'Recharger page'} diff --git a/alexarc4shng/plugin.yaml b/alexarc4shng/plugin.yaml index 2cab5a9a1..447b7ac04 100755 --- a/alexarc4shng/plugin.yaml +++ b/alexarc4shng/plugin.yaml @@ -8,7 +8,7 @@ plugin: maintainer: AndreK tester: henfri, juergen, psilo #documentation: https://www.smarthomeng.de/user/plugins/alexarc4shng/user_doc.html # url of documentation - version: 1.0.2 # Plugin version + version: 1.0.3 # Plugin version sh_minversion: 1.5.2 # minimum shNG version to use this plugin multi_instance: False # plugin supports multi instance classname: AlexaRc4shNG # class containing the plugin @@ -53,6 +53,13 @@ parameters: de: 'Sekunden bis zum automatischen refreshen des Cookie-files' en: 'seconds till the next automatic login to get a new cookie' + mfa_secret: + type: str + default: '' + description: + de: 'Das OTP MFA Secret welches auf der Amazon-Login-Seite angegben wird' + en: 'The OTP MFA Secret from the Amazon Login-Page' + logic_parameters: NONE # No logic parameters for this plugin item_structs: NONE # no item structure needed item_attributes: NONE # no item attributes needed @@ -92,6 +99,23 @@ plugin_functions: de: "Wert, der gesendet werden soll, numerische Werte ohne Hochkomma als Zahl" en: "Value to send, numeric Values without Quotes as Num" + get_list: + type: str + description: + de: "Liefert die Alexa-Shopping-Liste als dict-zurück" + en: "gives back a dict with the Alexa-Shopping-List" + parameters: + type: + type: str + description: + de: "Art der Liste 'SHOPPING_LIST' oder 'TO_DO'" + en: "type of the List 'SHOPPING_LIST' or 'TO_DO'" + valid_list: + - 'SHOPPING_LIST' + - 'TO_DO' + + + get_last_alexa: type: str description: diff --git a/alexarc4shng/requirements.txt b/alexarc4shng/requirements.txt index f2293605c..503fd8f90 100755 --- a/alexarc4shng/requirements.txt +++ b/alexarc4shng/requirements.txt @@ -1 +1,2 @@ requests +pyotp >= 2.6.0 diff --git a/alexarc4shng/user_doc.rst b/alexarc4shng/user_doc.rst index c44c62694..73f3f6089 100755 --- a/alexarc4shng/user_doc.rst +++ b/alexarc4shng/user_doc.rst @@ -43,7 +43,7 @@ Aufruf des Webinterfaces Das Plugin kann aus dem backend aufgerufen werden. Dazu auf der Seite Plugins in der entsprechenden Zeile das Icon in der Spalte **Web Interface** anklicken. -Außerdem kann das Webinterface direkt über ``http://smarthome.local:8383/alexarc4shng`` aufgerufen werden. +Außerdem kann das Webinterface direkt über ``http://smarthome.local:8383/plugins/alexarc4shng`` aufgerufen werden. Beispiele diff --git a/alexarc4shng/webif/static/img/plugin_logo.png b/alexarc4shng/webif/static/img/plugin_logo.png index 43cf3f7d41637690adbedf9dceaf539dff69339b..75a6edfa78e50d5a2c97db29e9e0bba03956de40 100755 GIT binary patch literal 164796 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 z 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 BW3vDN literal 14507 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",myValue); var myTest = JSON.stringify(JSON.parse(myPayload),null,2) - myCodeMirrorConf.setValue(myTest); - myCodeMirrorConf.focus; - myCodeMirrorConf.setCursor(myCodeMirrorConf.lineCount(),0); + myCodeMirrorPayload.setValue(myTest); + myCodeMirrorPayload.focus; + myCodeMirrorPayload.setCursor(myCodeMirrorPayload.lineCount(),0); document.getElementById("txtresult").value = "JSON-Structure is OK"; document.getElementById("resultOK").style.visibility="visible"; document.getElementById("resultNOK").style.visibility="hidden"; @@ -158,7 +467,7 @@ function BtnTest(result) } document.getElementById("txtButton").value ="BtnTest"; - myPayload = myCodeMirrorConf.getValue(); + myPayload = myCodeMirrorPayload.getValue(); TestCMD ( @@ -177,8 +486,6 @@ function BtnTest(result) function BtnDelete(result) { - buildCmdSequence(); - return var filetodelete = document.getElementById("txtCmdName").value; if (filetodelete == "") { alert ("No Command selected to delete, first select one"); @@ -420,7 +727,6 @@ function build_cmd_list(result) function reloadCmds() { - $("#refresh-element").addClass("fa-spin"); $("#reload-element").addClass("fa-spin"); $("#cardOverlay").show(); $.getJSON("build_cmd_list_html", function(result) @@ -497,9 +803,9 @@ function ShowCommand(response,txtCmdName) myjson = objResponse[0][x].split("'").join("\""); myjson = myjson.split("\\").join(""); var myTest = JSON.stringify(JSON.parse(myjson),null,2) - myCodeMirrorConf.setValue(myTest); - myCodeMirrorConf.focus; - myCodeMirrorConf.setCursor(myCodeMirrorConf.lineCount(),0); + myCodeMirrorPayload.setValue(myTest); + myCodeMirrorPayload.focus; + myCodeMirrorPayload.setCursor(myCodeMirrorPayload.lineCount(),0); } } document.getElementById("txtresult").value = myResult; @@ -517,3 +823,47 @@ function ShowCommand(response,txtCmdName) } +//************************************************************************ +//copyToClipboard - copies the finalized Widget to the Clipboard +//************************************************************************ +const copyToClipboard = str => { + const el = document.createElement('textarea'); // Create a + + -

-
+ - + - - - + +
-
+
- - - - Result : - - - -
@@ -143,6 +140,188 @@ {% endblock bodytab1 %} {% block bodytab2 %} +
+
+ +
+ +
+ + + +{% if pyOTP == true %} + +{% endif %} + + + +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ _('Schritt') }} 1 : + {{ _('Credentials:') }} + + + + + + + +
+
+
+ + +{% endblock bodytab2 %} + + + +{% block bodytab3 %}
@@ -187,12 +366,9 @@ }); +{% endblock bodytab3 %} - - -{% endblock bodytab2 %} - -{% block bodytab3 %} +{% block bodytab4 %} +
+
+
+

Miele Control Center (Dryer)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Gerätetyp:{{ basic.print('', 'MieleDevices.Dryer.ident.device_type.value_localized', 'text') }}
Typenbezeichnung:{{ basic.print('', 'MieleDevices.Dryer.ident.deviceIdentLabel.techType','text') }}
Materialnummer:{{ basic.print('', 'MieleDevices.Dryer.ident.deviceIdentLabel.matNumber', 'text') }}
fabNummer:{{ basic.print('Serial', 'MieleDevices.Dryer.ident.deviceIdentLabel.fabNumber', '') }}
Gerätestatus:{{ basic.print('', 'MieleDevices.Dryer.state.status.value_localized','') }}
Programm:{{ basic.print('', 'MieleDevices.Dryer.state.programType.value_localized', '') }}
Trockenstufe:{{ basic.print('', 'MieleDevices.Dryer.state.dryingStep.value_localized', '') }}
Programm Phase:{{ basic.print('', 'MieleDevices.Dryer.state.programPhase.value_localized', 'text') }}
Energieverbrauch Forecast:{{ basic.print('', 'MieleDevices.Dryer.state.ecoFeedback.energyForecast', 'text') }} + {{ basic.print('', 'MieleDevices.Dryer.state.ecoFeedback.currentEnergyConsumption.unit', 'text') }} +
Gerätetür:{{basic.symbol('','MieleDevices.Dryer.state.signalDoor',['offen','geschlossen'],['fts_door_unlocked','fts_door_locked'],['1','0'],'',['red','green'],'','','','')}}
verbleibende Zeit:{{ basic.print('', 'MieleDevices.Dryer.visu.times.remainingTime', 'text') }}
verstrichene Zeit:{{ basic.print('', 'MieleDevices.Dryer.visu.times.elapsedTime', 'text') }}
Start-Zeit:{{ basic.input('', 'MieleDevices.Dryer.visu.times.startTime', 'time') }}
End-Zeit:{{ basic.input('', 'MieleDevices.Dryer.visu.times.stopTime', 'time') }}
+ + + + + +
+ {{ status.collapse('Control_1', 'MieleDevices.Dryer.visu.allowed_actions.start', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.start','midi',[0,1],'audio_play','Start',['icon0','icon1'],'','','','')}} +
+
+ {{ status.collapse('Control_2', 'MieleDevices.Dryer.visu.allowed_actions.stop', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.stop','midi',[0,1],'audio_stop','Stop',['icon0','icon1'],'','','','')}} +
+
+ {{ status.collapse('Control_3', 'MieleDevices.Dryer.visu.allowed_actions.pause', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.pause','midi',[0,1],'audio_pause','Pause',['icon0','icon1'],'','','','')}} +
+
+ {{ status.collapse('Control_4', 'MieleDevices.Dryer.visu.allowed_actions.start_supercooling', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.start_supercooling','midi',[0,1],'weather_snow','Cooling',['red','red'],'','','','')}} +
+
+ {{ status.collapse('Control_5', 'MieleDevices.Dryer.visu.allowed_actions.stop_supercooling', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.stop_supercooling','midi',[0,1],'weather_snow','Cooling',['green','green'],'','','','')}} +
+
+ {{ status.collapse('Control_6', 'MieleDevices.Dryer.visu.allowed_actions.start_superfreezing', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.start_superfreezing','midi',[0,1],'weather_frost','Freezing',['red','red'],'','','','')}} +
+
+ {{ status.collapse('Control_7', 'MieleDevices.Dryer.visu.allowed_actions.stop_superfreezing', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.stop_superfreezing','midi',[0,1],'weather_frost','Freezing',['green','green'],'','','','')}} +
+
+ {{ status.collapse('Control_8', 'MieleDevices.Dryer.visu.allowed_actions.powerOn', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.powerOn','midi',[0,1],'info_ack','Power On',['red','red'],'','','')}} +
+
+ {{ status.collapse('Control_9', 'MieleDevices.Dryer.visu.allowed_actions.powerOff', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Dryer.visu.action_buttons.powerOff','midi',[0,1],'info_error','Power Off',['green','green'],'','','')}} +
+
+ {{ status.collapse('Dryer_device_failure', 'MieleDevices.Dryer.state.signalFailure') }} +
+ {{ basic.symbol('','','','info_warning','','','red','','','') }} + Fehler am Gerät +
+ {{ status.collapse('Dryer_device_info', 'MieleDevices.Dryer.state.signalInfo') }} +
+ {{ basic.symbol('','','','info_attention','','','orange','','','') }} + Info am Gerät +
+ +
+
+
+ +
+
+
+

Miele Control Center (Freezer)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Gerätetyp:{{ basic.print('', 'MieleDevices.Freezer.ident.device_type.value_localized', 'text') }}
Typenbezeichnung:{{ basic.print('', 'MieleDevices.Freezer.ident.deviceIdentLabel.techType','text') }}
Materialnummer:{{ basic.print('', 'MieleDevices.Freezer.ident.deviceIdentLabel.matNumber', 'text') }}
fabNummer:{{ basic.print('Serial', 'MieleDevices.Freezer.ident.deviceIdentLabel.fabNumber', '') }}
Gerätestatus:{{ basic.print('', 'MieleDevices.Freezer.state.status.value_localized','') }}
Ist-Temperatur Zone 1:{{ basic.print('', 'MieleDevices.Freezer.visu.values.temperatur_zone_1.temperature', '°') }}
Soll-Temperatur Zone 1: {{ basic.select('','MieleDevices.Freezer.visu.values.temperatur_zone_1.target_temperature','','','','','','horizontal', + 'MieleDevices.Freezer.values.temperatur_zone_1.range_index','MieleDevices.Freezer.values.temperatur_zone_1.range_description') }}
Gerätetür:{{basic.symbol('','MieleDevices.Freezer.state.signalDoor',['offen','geschlossen'],['fts_door_unlocked','fts_door_locked'],['1','0'],'',['red','green'],'','','','')}}
+ + + + +
+ {{ status.collapse('Control_Freezer_1', 'MieleDevices.Freezer.visu.allowed_actions.start', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.start','midi',[0,1],'audio_play','Start',['icon0','icon1'],'','','','')}} +
+
+ {{ status.collapse('Control_Freezer_2', 'MieleDevices.Freezer.visu.allowed_actions.stop', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.stop','midi',[0,1],'audio_stop','Stop',['icon0','icon1'],'','','','')}} +
+
+ {{ status.collapse('Control_Freezer_3', 'MieleDevices.Freezer.visu.allowed_actions.pause', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.pause','midi',[0,1],'audio_pause','Pause',['icon0','icon1'],'','','','')}} +
+
+ {{ status.collapse('Control_Freezer_4', 'MieleDevices.Freezer.visu.allowed_actions.start_supercooling', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.start_supercooling','midi',[0,1],'weather_snow','Start Super Cooling',['red','red'],'','','','')}} +
+
+ {{ status.collapse('Control_Freezer_5', 'MieleDevices.Freezer.visu.allowed_actions.stop_supercooling', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.stop_supercooling','midi',[0,1],'weather_snow','Stop Super Cooling',['green','green'],'','','','')}} +
+
+ {{ status.collapse('Control_Freezer_6', 'MieleDevices.Freezer.visu.allowed_actions.start_superfreezing', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.start_superfreezing','midi',[0,1],'weather_frost','Start Super Freezing',['red','red'],'','','','')}} +
+
+ {{ status.collapse('Control_Freezer_7', 'MieleDevices.Freezer.visu.allowed_actions.stop_superfreezing', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.stop_superfreezing','midi',[0,1],'weather_frost','Stop Super Freezing',['green','green'],'','','','')}} +
+
+ {{ status.collapse('Control_Freezer_8', 'MieleDevices.Freezer.visu.allowed_actions.powerOn', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.powerOn','midi',[0,1],'info_ack','Power On',['red','red'],'','','')}} +
+
+ {{ status.collapse('Control_Freezer_9', 'MieleDevices.Freezer.visu.allowed_actions.powerOff', 0) }} +
+ {{basic.stateswitch('','MieleDevices.Freezer.visu.action_buttons.powerOff','midi',[0,1],'info_error','Power Off',['green','green'],'','','')}} +
+
+ + {{ status.collapse('Freezer_device_failure', 'MieleDevices.Freezer.state.signalFailure') }} +
+ {{ basic.symbol('','','','info_warning','','','red','','','') }} + Fehler am Gerät +
+ {{ status.collapse('Freezer_device_info', 'MieleDevices.Freezer.state.signalInfo') }} +
+ {{ basic.symbol('','','','info_attention','','','orange','','','') }} + Info am Gerät +
+ + +
+
+
+ + + + + +{% endblock %} + + + + diff --git a/mieleathome/plugin.yaml b/mieleathome/plugin.yaml new file mode 100755 index 000000000..22c9077ea --- /dev/null +++ b/mieleathome/plugin.yaml @@ -0,0 +1,465 @@ +# Metadata for the plugin +plugin: + # Global plugin attributes + type: gateway # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Miele@Home-Anbindung' + en: 'Connect Miele@Home' + maintainer: sipple, AndreK01 +# tester: # Who tests this plugin? + state: develop # change to ready when done with development + keywords: iot Miele Home +# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1512798-miele-home-mit-mqtt + + version: 1.0.0 # Plugin version (must match the version specified in __init__.py) + sh_minversion: 1.5 # minimum shNG version to use this plugin +# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) +# py_minversion: 3.6 # minimum Python version to use for this plugin +# py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) + multi_instance: False # plugin supports multi instance + restartable: true + classname: mieleathome # class containing the plugin + +parameters: + # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) + miele_cycle: + type: int + default: 300 + description: + de: 'Zeitlicher Abstand zwischen zwei Verbindungen zur API' + en: 'Time between two connects to the API' + miele_client_id: + type: str + default: '' + description: + de: 'Client ID der Miele API' + en: 'Client ID for the Miele-API' + miele_client_secret: + type: str + default: '' + description: + de: 'Client Secret der Miele API' + en: 'Client secret for the Miele-API' + miele_client_country: + type: str + default: 'de-DE' + description: + de: 'Länderkennung der Miele-API' + en: 'Counry to use for the Miele-API' + miele_user: + type: str + default: '' + description: + de: 'Zugangsdaten User' + en: 'Credentials User' + miele_pwd: + type: str + default: '' + description: + de: 'Zugangsdaten Passwort' + en: 'Credentials password' + +item_attributes: + # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) + miele_deviceid: + type: str + mandatory: False + description: + de: 'Die DeviceId der Miele-cloud' + en: 'deviceId of the Miele-cloud' + miele_command: + type: str + mandatory: False + description: + de: 'Eine interne Funktion des Miele@Home-Plugins' + en: 'internal function of the Miele@Home-Plugin' + miele_visu_function: + type: str + mandatory: False + description: + de: 'Eine interne Funktion des Miele@Home-Plugins' + en: 'internal function of the Miele@Home-Plugin' + miele_parse_item: + type: bool + mandatory: False + description: + de: 'Eine interne Funktion des Miele@Home-Plugins' + en: 'internal function of the Miele@Home-Plugin' +item_structs: + # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) + child: + name: Vorlage Struktur Miele Geräte + ident: + device_type: + value_raw: + type: num + cache: 'on' + value_localized: + type: str + cache: 'on' + deviceName: + type: str + cache: 'on' + deviceIdentLabel: + fabNumber: + type: str + cache: 'on' + fabIndex: + type: str + cache: 'on' + techType: + type: str + cache: 'on' + matNumber: + type: str + cache: 'on' + xkmIdentLabel: + techType: + type: str + cache: 'on' + releaseVersion: + type: str + cache: 'on' + state: + ProgramID: + value_localized: + type: str + cache: 'on' + status: + value_localized: + type: str + cache: 'on' + enforce_updates: yes + value_raw: + type: num + cache: 'on' + enforce_updates: yes + programType: + value_localized: + type: str + cache: 'on' + programPhase: + value_localized: + type: str + cache: 'on' + remainingTime: + type: list + cache: 'on' + enforce_updates: yes + startTime: + type: list + cache: 'on' + enforce_updates: yes + targetTemperature: + type: list + cache: 'on' + miele_parse_item: true + enforce_updates: yes + temperature: + type: list + cache: 'on' + miele_parse_item: true + signalInfo: + type: bool + cache: 'on' + enforce_updates: yes + signalFailure: + type: bool + cache: 'on' + enforce_updates: yes + signalDoor: + type: bool + cache: 'on' + enforce_updates: yes + dryingStep: + value_localized: + type: str + cache: 'on' + enforce_updates: yes + value_raw: + type: num + cache: 'on' + enforce_updates: yes + elapsedTime: + type: list + cache: 'on' + enforce_updates: yes + ecoFeedback: + currentWaterConsumption: + unit: + type: str + cache: 'on' + value: + type: num + cache: 'on' + currentEnergyConsumption: + unit: + type: str + cache: 'on' + value: + type: num + cache: 'on' + waterForecast: + type: num + cache: 'on' + energyForecast: + type: num + cache: 'on' + batteryLevel: + type: num + cache: 'on' + + + actions: + processAction: + type: list + cache: 'on' + light: + type: list + cache: 'on' + ventilationStep: + type: list + cache: 'on' + programId: + type: list + cache: 'on' + targetTemperature: + type: list + cache: 'on' + miele_parse_item: true + enforce_updates: yes + startTime: + type: list + cache: 'on' + deviceName: + type: bool + cache: 'on' + powerOn: + type: bool + cache: 'on' + powerOff: + type: bool + cache: 'on' + modes: + type: list + cache: 'on' + + + visu: + times: + scheduled_startTime: + visu_acl: rw + type: str + eval_trigger: ....state.startTime + eval: str("%0.2d"%sh.....state.startTime()[0])+":"+str("%0.2d"%sh.....state.startTime()[1]) + miele_parse_item: true + startTime: + visu_acl: rw + type: str + initial_value: "00:00:00" + miele_parse_item: true + stopTime: + visu_acl: rw + type: str + initial_value: "00:00:00" + miele_parse_item: true + elapsedTime: + type: str + eval_trigger: ....state.elapsedTime + eval: str("%0.2d"%sh.....state.elapsedTime()[0])+":"+str("%0.2d"%sh.....state.elapsedTime()[1]) if len(sh.....state.elapsedTime()) >0 else '00:00' + remainingTime: + type: str + eval_trigger: ....state.remainingTime + eval: str("%0.2d"%sh.....state.remainingTime()[0])+":"+str("%0.2d"%sh.....state.remainingTime()[1]) if len(sh.....state.remainingTime()) >0 else '00:00' + values: + temperatur_zone_1: + target_temperature: + type: num + visu_acl: rw + cache: 'on' + miele_command: '{"targetTemperature": [ { "zone": 1, "value": %1 } ]}' + temperature: + type: num + visu_acl: rw + cache: 'on' + unit: + type: str + visu_acl: rw + cache: 'on' + temperatur_zone_2: + target_temperature: + type: num + visu_acl: rw + cache: 'on' + miele_command: '{"targetTemperature": [ { "zone": 2, "value": %1 } ]}' + temperature: + type: num + visu_acl: rw + cache: 'on' + unit: + type: str + visu_acl: rw + cache: 'on' + temperatur_zone_3: + target_temperature: + type: num + visu_acl: rw + cache: 'on' + miele_command: '{"targetTemperature": [ { "zone": 3, "value": %1 } ]}' + temperature: + type: num + visu_acl: rw + cache: 'on' + unit: + type: str + visu_acl: rw + cache: 'on' + + allowed_actions: + start: + type: bool + visu_acl: rw + + stop: + type: bool + visu_acl: rw + + pause: + type: bool + visu_acl: rw + + start_superfreezing: + type: bool + visu_acl: rw + + stop_superfreezing: + type: bool + visu_acl: rw + + start_supercooling: + type: bool + visu_acl: rw + + stop_supercooling: + type: bool + visu_acl: rw + + deviceName: + type: bool + visu_acl: rw + + powerOn: + type: bool + visu_acl: rw + eval_trigger: ....actions.powerOn + eval: sh.....actions.powerOn() + powerOff: + type: bool + visu_acl: rw + eval_trigger: ....actions.powerOff + eval: sh.....actions.powerOff() + temp_Zone1: + type: bool + visu_acl: rw + eval_trigger: ....visu.values.temperatur_zone_1.target_temperature + eval: 1 if sh.....visu.values.temperatur_zone_1.target_temperature() != 0 else 0 + temp_Zone2: + type: bool + visu_acl: rw + eval_trigger: ....visu.values.temperatur_zone_2.target_temperature + eval: 1 if sh.....visu.values.temperatur_zone_2.target_temperature() != 0 else 0 + temp_Zone3: + type: bool + visu_acl: rw + eval_trigger: ....visu.values.temperatur_zone_3.target_temperature + eval: 1 if sh.....visu.values.temperatur_zone_3.target_temperature() != 0 else 0 + + + + action_buttons: + start: + type: bool + visu_acl: rw + autotimer: 1 = False + miele_command: '{"processAction": 1}' + enforce_updates: yes + stop: + type: bool + visu_acl: rw + autotimer: 1 = False + miele_command: '{"processAction": 2}' + enforce_updates: yes + pause: + type: bool + visu_acl: rw + autotimer: 1 = False + miele_command: '{"processAction": 3}' + enforce_updates: yes + start_superfreezing: + type: bool + visu_acl: rw + autotimer: 1 = False + miele_command: '{"processAction": 4}' + enforce_updates: yes + stop_superfreezing: + type: bool + visu_acl: rw + autotimer: 1 = False + miele_command: '{"processAction": 5}' + enforce_updates: yes + start_supercooling: + type: bool + visu_acl: rw + autotimer: 1 = False + miele_command: '{"processAction": 6}' + enforce_updates: yes + stop_supercooling: + type: bool + visu_acl: rw + autotimer: 1 = False + miele_command: '{"processAction": 7}' + enforce_updates: yes + powerOn: + type: bool + visu_acl: rw + miele_command: '{"powerOn":true}' + enforce_updates: yes + powerOff: + type: bool + visu_acl: rw + miele_command: '{"powerOff":true}' + enforce_updates: yes + + values: + temperatur_zone_1: + range_index: + type: list + visu_acl: rw + range_description: + type: list + visu_acl: rw + temperatur_zone_2: + range_index: + type: list + visu_acl: rw + range_description: + type: list + visu_acl: rw + temperatur_zone_3: + range_index: + type: list + visu_acl: rw + range_description: + type: list + visu_acl: rw + +#item_attribute_prefixes: + # Definition of item attributes that only have a common prefix (enter 'item_attribute_prefixes: NONE' or ommit this section, if section should be empty) + # NOTE: This section should only be used, if really nessesary (e.g. for the stateengine plugin) + +plugin_functions: + # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) + +logic_parameters: + # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) diff --git a/mieleathome/user_doc.rst b/mieleathome/user_doc.rst new file mode 100755 index 000000000..fcb29e588 --- /dev/null +++ b/mieleathome/user_doc.rst @@ -0,0 +1,6 @@ +Sample Plugin <- hier den Namen des Plugins einsetzen +===================================================== + +Anforderungen +------------- +Wird nachgereicht diff --git a/mieleathome/webif/__init__.py b/mieleathome/webif/__init__.py new file mode 100755 index 000000000..acedea7ee --- /dev/null +++ b/mieleathome/webif/__init__.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2020- +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and +# upwards. +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + +import datetime +import time +import os +import json + +from lib.item import Items +from lib.model.smartplugin import SmartPluginWebIf + + +# ------------------------------------------ +# Webinterface of the plugin +# ------------------------------------------ + +import cherrypy +import csv +from jinja2 import Environment, FileSystemLoader + + +class WebInterface(SmartPluginWebIf): + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + self.logger = plugin.logger + self.webif_dir = webif_dir + self.plugin = plugin + self.items = Items.get_instance() + + self.tplenv = self.init_template_environment() + + + @cherrypy.expose + def index(self, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + tmpl = self.tplenv.get_template('index.html') + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) + return tmpl.render(p=self.plugin, + items=sorted(self.items.return_items(), key=lambda k: str.lower(k['_path'])), + item_count=len (self.plugin.miele_items)) + + @cherrypy.expose + def get_data_html(self, dataSet=None): + """ + Return data to update the webpage + + For the standard update mechanism of the web interface, the dataSet to return the data for is None + + :param dataSet: Dataset for which the data should be returned (standard: None) + :return: dict with the data needed to update the web page. + """ + if dataSet is None: + # get the new data + data = {} + data['Device']=self.plugin.last_event_device + data['Action']=self.plugin.last_event_action + data['last_Event']=self.plugin.last_event_time + data['last_Ping']=self.plugin.last_ping_time + + + # data['item'] = {} + # for i in self.plugin.items: + # data['item'][i]['value'] = self.plugin.getitemvalue(i) + # + # return it as json the the web page + try: + return json.dumps(data) + except Exception as e: + self.logger.error("get_data_html exception: {}".format(e)) + + diff --git a/mieleathome/webif/static/img/plugin_logo.svg b/mieleathome/webif/static/img/plugin_logo.svg new file mode 100644 index 000000000..d823c6150 --- /dev/null +++ b/mieleathome/webif/static/img/plugin_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mieleathome/webif/static/img/readme.txt b/mieleathome/webif/static/img/readme.txt new file mode 100755 index 000000000..1a7c55eef --- /dev/null +++ b/mieleathome/webif/static/img/readme.txt @@ -0,0 +1,6 @@ +This directory is for storing images that are used by the web interface. + +If you want to have your own logo on the top of the web interface, store it here and name it plugin_logo.. + +Extension can be png, svg or jpg + diff --git a/mieleathome/webif/templates/index.html b/mieleathome/webif/templates/index.html new file mode 100755 index 000000000..8e5a42f4d --- /dev/null +++ b/mieleathome/webif/templates/index.html @@ -0,0 +1,286 @@ +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} + + +{% set update_interval = 5000 %} + + +{% block pluginscripts %} + +{% endblock pluginscripts %} + + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
client_id{{ p.client_id }}client_secret{{ p.client_secret }}
access_token{{ p.AccessToken }}refresh_token{{ p.RefreshToken }}
valid from{{ p.ValidFrom }}valid through{{ p.ValidThrough }}{{ p.ValidFor }} {{ _('d') }}
Country{{ p.country }}Cycle{{ p._cycle }} {{ _('s') }}
+{% endblock headtable %} + + + +{% block buttons %} +{% if 1==2 %} +
+ +
+{% endif %} +{% endblock %} + + +{% set tabcount = 3 %} + + + +{% if item_count==0 %} + {% set start_tab = 1 %} +{% endif %} + + + +{% set tab1title = "" ~ p.get_shortname() ~ " Items (" ~ item_count ~ ")" %} +{% block bodytab1 %} +
+ +
+
+ + + + + + + + + + + + {% for item in p.miele_items %} + + + + + + {% endfor %} + +
{{ _('Item') }}{{ _('Type') }}{{ _('Value') }}
{{ item._path }}{{ item._type }}{{ item._value }}
+ +
+
+ +
+{% endblock bodytab1 %} + + + +{% set tab2title = "" ~ p.get_shortname() ~ " Geräte " %} +{% block bodytab2 %} +
+ +
+
+ +
+ + + + + + + + + + + {% for item in p.miele_devices_raw %} + + + + + + + {% endfor %} + +
{{ _('Device ID') }}{{ _('linked Item') }}{{ _('Device Type') }}{{ _('Model') }}
{{ item.DeviceID }}{{ p.miele_devices_by_deviceID[item.DeviceID]}}{{ item.DeviceTyp }}{{ item.DeviceModel }}
+
+
+
+ +
+{% endblock bodytab2 %} + + + +{% set tab3title = "Event-Informations " %} +{% block bodytab3 %} +
+
+
+ + + + + + + + + + + + + + + +
+ Last Event Update :

{{ p.last_event_time }}

+
+ Last Ping :

{{ p.last_ping_time }}

+
Device-Event Action-Event
+ + + + +
+
+
+
+ +" %} + + It has to be defined before (and outside) the block bodytab4 +--> +{% block bodytab4 %} +{% endblock bodytab4 %} From 06c8fa99e837b97327d12e2989f986601d0eeefa Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Thu, 30 Mar 2023 21:22:56 +0200 Subject: [PATCH 171/178] DB_ADDON Plugin - Add Docu - add further calculation method for wachstumsgradtage - code cleanup --- db_addon/__init__.py | 289 ++++++++++++++++++++++++++++-------------- db_addon/user_doc.rst | 5 +- 2 files changed, 197 insertions(+), 97 deletions(-) diff --git a/db_addon/__init__.py b/db_addon/__init__.py index 910d532f4..e0fa02843 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -880,7 +880,6 @@ def gruenlandtemperatursumme(self, item_path: str, year: Union[int, str]) -> Uni :param item_path: item object or item_id for which the query should be done :param year: year the gruenlandtemperatursumme should be calculated for - :return: gruenlandtemperatursumme """ @@ -897,9 +896,9 @@ def waermesumme(self, item_path: str, year, month: Union[int, str] = None, thres :param year: year the waermesumme should be calculated for :param month: month the waermesumme should be calculated for :param threshold: threshold for temperature - :return: waermesumme """ + item = self.items.return_item(item_path) if item: return self._handle_waermesumme(item, year, month, threshold) @@ -912,9 +911,9 @@ def kaeltesumme(self, item_path: str, year, month: Union[int, str] = None) -> Un :param item_path: item object or item_id for which the query should be done :param year: year the kaeltesumme should be calculated for :param month: month the kaeltesumme should be calculated for - :return: kaeltesumme """ + item = self.items.return_item(item_path) if item: return self._handle_kaeltesumme(item, year, month) @@ -927,7 +926,6 @@ def tagesmitteltemperatur(self, item_path: str, timeframe: str = None, count: in :param item_path: item object or item_id for which the query should be done :param timeframe: timeincrement for determination :param count: number of time increments starting from now to the left (into the past) - :return: tagesmitteltemperatur """ @@ -956,6 +954,21 @@ def wachstumsgradtage(self, item_path: str, year: Union[int, str], threshold: in if item: return self._handle_wachstumsgradtage(item, year, threshold) + def temperaturserie(self, item_path: str, year: Union[int, str], method: str) -> Union[list, None]: + """ + Query database for wachstumsgradtage + https://de.wikipedia.org/wiki/Wachstumsgradtag + + :param item_path: item object or item_id for which the query should be done + :param year: year the wachstumsgradtage should be calculated for + :param method: Calculation method + :return: wachstumsgradtage + """ + + item = self.items.return_item(item_path) + if item: + return self._handle_temperaturserie(item, year, method) + def query_item(self, func: str, item_path: str, timeframe: str, start: int = None, end: int = 0, group: str = None, group2: str = None, ignore_value=None) -> list: item = self.items.return_item(item_path) if item is None: @@ -1349,16 +1362,15 @@ def _handle_tagesmitteltemperatur(self, database_item: Item, db_addon_fct: str, # handle 'serie_tagesmittelwert_group2_start_endgroup' like 'serie_tagesmittelwert_stunde_30_0d' elif db_addon_fct.startswith('serie_') and len(_var) == 5: - func = 'avg1' timeframe = 'day' - log_text = 'serie_tagesmittelwert_group2_start_endgroup' + method = 'raw' start = to_int(_var[3]) end = to_int(_var[4][:-1]) - group = 'hour' - group2 = convert_timeframe(_var[4][len(_var[4]) - 1]) - if group2 is None or start is None or end is None: + if start is None or end is None: return [] + return self._prepare_temperature_list(database_item=database_item, timeframe=timeframe, start=start, end=end, method=method) + # handle everything else else: self.logger.info(f"_handle_tagesmitteltemperatur: No adequate function for {db_addon_fct=} found.") @@ -1421,15 +1433,14 @@ def _handle_kaeltesumme(self, database_item: Item, year: Union[int, str], month: # get raw data as list self.logger.debug("_handle_kaeltesumme: Try to get raw data") - raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='raw') + raw_data = self._prepare_temperature_list(database_item=database_item, timeframe='day', start=start, end=end, method='raw') if self.execute_debug: self.logger.debug(f"_handle_kaeltesumme: raw_value_list={raw_data=}") # calculate value - if raw_data and isinstance(raw_data, list): - if raw_data == [[None, None]]: - return - + if raw_data is None: + return + elif isinstance(raw_data, list): # akkumulieren alle negativen Werte ks = 0 for entry in raw_data: @@ -1486,18 +1497,17 @@ def _handle_waermesumme(self, database_item: Item, year: Union[int, str], month: return # get raw data as list - raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='raw') + raw_data = self._prepare_temperature_list(database_item=database_item, timeframe='day', start=start, end=end, method='raw') if self.execute_debug: self.logger.debug(f"_handle_waermesumme: raw_value_list={raw_data=}") # set threshold to min 0 - threshold = min(0, threshold) + threshold = max(0, threshold) # calculate value - if raw_data and isinstance(raw_data, list): - if raw_data == [[None, None]]: - return - + if raw_data is None: + return + elif isinstance(raw_data, list): # akkumulieren alle Werte, größer/gleich Schwellenwert ws = 0 for entry in raw_data: @@ -1543,15 +1553,14 @@ def _handle_gruenlandtemperatursumme(self, database_item: Item, year: Union[int, return # get raw data as list - raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='raw') + raw_data = self._prepare_temperature_list(database_item=database_item, timeframe='day', start=start, end=end, method='raw') if self.execute_debug: self.logger.debug(f"_handle_gruenlandtemperatursumme: raw_value_list={raw_data}") # calculate value - if raw_data and isinstance(raw_data, list): - if raw_data == [[None, None]]: - return - + if raw_data is None: + return + elif isinstance(raw_data, list): # akkumulieren alle positiven Tagesmitteltemperaturen, im Januar gewichtet mit 50%, im Februar mit 75% gts = 0 for entry in raw_data: @@ -1572,6 +1581,7 @@ def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], :param database_item: item object or item_id for which the query should be done :param year: year the wachstumsgradtage should be calculated for + :param method: calculation method to be used :param threshold: temperature in °C as threshold for evaluation :return: wachstumsgradtage """ @@ -1604,55 +1614,129 @@ def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str], return # get raw data as list - raw_data = self._prepare_temperature_list(database_item=database_item, start=start, end=end, version='minmax') + raw_data = self._prepare_temperature_list(database_item=database_item, timeframe='day', start=start, end=end, method='minmax') if self.execute_debug: self.logger.debug(f"_handle_wachstumsgradtage: raw_value_list={raw_data}") # calculate value - if raw_data and isinstance(raw_data, list): - if raw_data == [[None, None]]: - return + if raw_data is None: + return - # Die Berechnung des einfachen Durchschnitts // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert - wgte = 0 - wgte_list = [] - if method == 0 or method == 10: - self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Berechnung des einfachen Durchschnitts'.") - for entry in raw_data: - timestamp, min_val, max_val = entry - wgt = (((min_val + min(30, max_val)) / 2) - threshold) - if wgt > 0: - wgte += wgt - wgte_list.append([timestamp, int(round(wgte, 0))]) - if method == 0: - return int(round(wgte, 0)) - else: - return wgte_list + elif isinstance(raw_data, list): + # Die Berechnung des einfachen Durchschnitts // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert + wgte = 0 + wgte_list = [] + if method == 0 or method == 10: + self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Berechnung des einfachen Durchschnitts'.") + for entry in raw_data: + timestamp, min_val, max_val = entry + wgt = (((min_val + min(30, max_val)) / 2) - threshold) + if wgt > 0: + wgte += wgt + wgte_list.append([timestamp, int(round(wgte, 0))]) + if method == 0: + return int(round(wgte, 0)) + else: + return wgte_list + + # Die modifizierte Berechnung des einfachen Durchschnitts. // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur mit mind Schwellentemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert + elif method == 1 or method == 11: + self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Modifizierte Berechnung des einfachen Durchschnitts'.") + for entry in raw_data: + timestamp, min_val, max_val = entry + wgt = (((max(threshold, min_val) + min(30.0, max_val)) / 2) - threshold) + if wgt > 0: + wgte += wgt + wgte_list.append([timestamp, int(round(wgte, 0))]) + if method == 1: + return int(round(wgte, 0)) + else: + return wgte_list + + # Zähle Tage, bei denen die Tagesmitteltemperatur oberhalb des Schwellenwertes lag + elif method == 2 or method == 12: + self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Anzahl der Tage, bei denen die Tagesmitteltemperatur oberhalb des Schwellenwertes lag'.") + for entry in raw_data: + timestamp, min_val, max_val = entry + wgt = (((min_val + min(30, max_val)) / 2) - threshold) + if wgt > 0: + wgte += 1 + wgte_list.append([timestamp, wgte]) + if method == 0: + return wgte + else: + return wgte_list - # Die modifizierte Berechnung des einfachen Durchschnitts. // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur mit mind Schwellentemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert - elif method == 1 or method == 11: - self.logger.info(f"Caluclate 'Wachstumsgradtag' according to 'Modifizierte Berechnung des einfachen Durchschnitts'.") - for entry in raw_data: - timestamp, min_val, max_val = entry - wgt = (((max(threshold, min_val) + min(30.0, max_val)) / 2) - threshold) - if wgt > 0: - wgte += wgt - wgte_list.append([timestamp, int(round(wgte, 0))]) - if method == 1: - return int(round(wgte, 0)) else: - return wgte_list - else: - self.logger.info(f"Method for 'Wachstumsgradtag' calculation not defined.'") + self.logger.info(f"Method for 'Wachstumsgradtag' calculation not defined.'") + + def _handle_temperaturserie(self, database_item: Item, year: Union[int, str], method: str = 'raw'): + """ + provide list of lists having timestamp and temperature(s) per day + + :param database_item: item object or item_id for which the query should be done + :param year: year the wachstumsgradtage should be calculated for + :param method: calculation method to be used + :return: list of temperatures + """ + + if not valid_year(year): + self.logger.error(f"_handle_temepraturserie: Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") + return + + # define year + if year == 'current': + year = datetime.date.today().year + + # define start_date, end_date + start_date = datetime.date(int(year), 1, 1) + end_date = datetime.date(int(year), 12, 31) + + # check start_date + today = datetime.date.today() + if start_date > today: + self.logger.info(f"_handle_temepraturserie: Start time for query of item={database_item.path()} is in future. Query cancelled.") + return + + # define start / end + start = (today - start_date).days + end = (today - end_date).days if end_date < today else 0 + + # check end + if start < end: + self.logger.error(f"_handle_temepraturserie: End time for query of item={database_item.path()} is before start time. Query cancelled.") + return + + # check method + if method not in ['hour', 'raw', 'minmax']: + self.logger.error(f"_handle_temepraturserie: Calculation method {method!r} unknown. Need to be 'hour', 'raw' or 'minmax'. Query cancelled.") + return - def _prepare_temperature_list(self, database_item: Item, start: int, end: int = 0, ignore_value=None, version: str = 'hour') -> list: + # get raw data as list + temp_list = self._prepare_temperature_list(database_item=database_item, timeframe='day', start=start, end=end, method=method) + if self.execute_debug: + self.logger.debug(f"_handle_temepraturserie: {temp_list=}") - self.logger.debug(f"_prepare_temperature_list called with {database_item=}, {start=}, {end=}, {ignore_value=}, {version=}") + return temp_list + + def _prepare_temperature_list(self, database_item: Item, timeframe: str, start: int, end: int = 0, ignore_value=None, method: str = 'hour') -> Union[list, None]: + """ + returns list of lists having timestamp and temperature(s) per day + + :param database_item: item object or item_id for which the query should be done + :param timeframe: tiemfram for query + :param start: increments for timeframe from now to start + :param end: increments for timeframe from now to end + :param ignore_value: value to be ignored during query + :param method: Calculation method + :return: list of temperatures + """ def _create_temp_dict() -> dict: """create dict based on database query result like {'date1': {'hour1': [temp values], 'hour2': [temp values], ...}, 'date2': {'hour1': [temp values], 'hour2': [temp values], ...}, ...}""" + _temp_dict = {} - for _entry in raw_value_list: + for _entry in raw_data: dt = datetime.datetime.utcfromtimestamp(_entry[0] / 1000) date = dt.strftime('%Y-%m-%d') hour = dt.strftime('%H') @@ -1665,6 +1749,7 @@ def _create_temp_dict() -> dict: def _calculate_hourly_average(): """ calculate hourly average based on list of temperatures and update temp_dict""" + for _date in temp_dict: for hour in temp_dict[_date]: hour_raw_value_list = temp_dict[_date][hour] @@ -1674,6 +1759,7 @@ def _calculate_hourly_average(): def _create_list_timestamp_avgtemp() -> list: """Create list of list with [[timestamp1, value1], [timestamp2, value2], ...] based on temp_dict""" + _temp_list = [] for _date in temp_dict: @@ -1697,6 +1783,7 @@ def _create_list_timestamp_avgtemp() -> list: def _create_list_timestamp_minmaxtemp() -> list: """Create list of list with [[timestamp1, min value1, max_value1], [timestamp2, min value2, max_value2], ...] based on temp_dict""" + _temp_list = [] for _date in temp_dict: _timestamp = datetime_to_timestamp(datetime.datetime.strptime(_date, '%Y-%m-%d')) @@ -1704,50 +1791,62 @@ def _create_list_timestamp_minmaxtemp() -> list: _temp_list.append([_timestamp, min(_day_values), max(_day_values)]) return _temp_list - if version == 'hour': - raw_value_list = self._query_item(func='avg', item=database_item, timeframe='day', start=start, end=end, group='hour', ignore_value=ignore_value) - self.logger.debug(f"{raw_value_list=}") + # temp_list = [[timestamp1, avg-value1], [timestamp2, avg-value2], [timestamp3, avg-value3], ...] Tagesmitteltemperatur pro Stunde wird in der Datenbank per avg ermittelt + if method == 'hour': + raw_data = self._query_item(func='avg', item=database_item, timeframe=timeframe, start=start, end=end, group='hour', ignore_value=ignore_value) + self.logger.debug(f"{raw_data=}") - # create nested dict with temps - temp_dict = _create_temp_dict() + if raw_data and isinstance(raw_data, list): + if raw_data == [[None, None]]: + return - # create list of list like database query response - temp_list = _create_list_timestamp_avgtemp() - self.logger.debug(f"{temp_list=}") - return temp_list + # create nested dict with temps + temp_dict = _create_temp_dict() - elif version == 'raw': - raw_value_list = self._query_item(func='raw', item=database_item, timeframe='day', start=start, end=end, ignore_value=ignore_value) - self.logger.debug(f"{raw_value_list=}") + # create list of list like database query response + temp_list = _create_list_timestamp_avgtemp() + self.logger.debug(f"{temp_list=}") + return temp_list - # create nested dict with temps - temp_dict = _create_temp_dict() - self.logger.debug(f"raw: {temp_dict=}") + # temp_list = [[timestamp1, avg-value1], [timestamp2, avg-value2], [timestamp3, avg-value3], ...] Tagesmitteltemperatur pro Stunde wird hier im Plugin ermittelt ermittelt + elif method == 'raw': + raw_data = self._query_item(func='raw', item=database_item, timeframe=timeframe, start=start, end=end, ignore_value=ignore_value) + self.logger.debug(f"{raw_data=}") - # calculate 'tagesdurchschnitt' and create list of list like database query response - _calculate_hourly_average() - self.logger.debug(f"raw: {temp_dict=}") + if raw_data and isinstance(raw_data, list): + if raw_data == [[None, None]]: + return - # create list of list like database query response - temp_list = _create_list_timestamp_avgtemp() - self.logger.debug(f"{temp_list=}") - return temp_list + # create nested dict with temps + temp_dict = _create_temp_dict() + self.logger.debug(f"raw: {temp_dict=}") - elif version == 'minmax': - raw_value_list = self._query_item(func='raw', item=database_item, timeframe='day', start=start, end=end, ignore_value=ignore_value) - self.logger.debug(f"{raw_value_list=}") + # calculate 'tagesdurchschnitt' and create list of list like database query response + _calculate_hourly_average() + self.logger.debug(f"raw: {temp_dict=}") - # create nested dict with temps - temp_dict = _create_temp_dict() - self.logger.debug(f"raw: {temp_dict=}") + # create list of list like database query response + temp_list = _create_list_timestamp_avgtemp() + self.logger.debug(f"{temp_list=}") + return temp_list - # create list of list like database query response - temp_list = _create_list_timestamp_minmaxtemp() - self.logger.debug(f"{temp_list=}") - return temp_list + # temp_list = [[timestamp1, min-value1, max-value1], [timestamp2, min-value2, max-value2], [timestamp3, min-value3, max-value3], ...] + elif method == 'minmax': + raw_data = self._query_item(func='raw', item=database_item, timeframe=timeframe, start=start, end=end, ignore_value=ignore_value) + self.logger.debug(f"{raw_data=}") - else: - return [] + if raw_data and isinstance(raw_data, list): + if raw_data == [[None, None]]: + return + + # create nested dict with temps + temp_dict = _create_temp_dict() + self.logger.debug(f"raw: {temp_dict=}") + + # create list of list like database query response + temp_list = _create_list_timestamp_minmaxtemp() + self.logger.debug(f"{temp_list=}") + return temp_list def _create_due_items(self) -> list: """ diff --git a/db_addon/user_doc.rst b/db_addon/user_doc.rst index 563bd533b..74c774c71 100644 --- a/db_addon/user_doc.rst +++ b/db_addon/user_doc.rst @@ -219,7 +219,8 @@ Wachstumsgradtag Der Begriff Wachstumsgradtage (WGT) ist ein Überbegriff für verschiedene Größen. Gemeinsam ist ihnen, daß zur Berechnung eine Lufttemperatur von einem Schwellenwert subtrahiert wird. Je nach Fragestellung und Pflanzenart werden der Schwellenwert unterschiedlich gewählt und die Temperatur unterschiedlich bestimmt. -Verfügbar sind die Berechnung über "einfachen Durchschnitt der Tagestemperaturen" und "modifizierten Durchschnitt der Tagestemperaturen". +Verfügbar sind die Berechnung über 0) "einfachen Durchschnitt der Tagestemperaturen", 1) "modifizierten Durchschnitt der Tagestemperaturen" +und 2) Anzahl der Tage, deren Mitteltempertatur oberhalb der Schwellentemperatur lag. siehe https://de.wikipedia.org/wiki/Wachstumsgradtag @@ -230,7 +231,7 @@ Folgende Parameter sind möglich / notwendig: - year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') - method: 0-Berechnung über "einfachen Durchschnitt der Tagestemperaturen", 1-Berechnung über "modifizierten Durchschnitt (default: 0) -der Tagestemperaturen" // 10, 11 Ausgabe aus Zeitserie +der Tagestemperaturen" 2-Anzahl der Tage, mit Mitteltempertatur oberhalb Schwellentemperatur// 10, 11 Ausgabe aus Zeitserie - threshold: Schwellentemperatur in °C (int) (default: 10) From 1d83aebda58843714648df58d2bd3b3b9d499e09 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Thu, 30 Mar 2023 21:23:15 +0200 Subject: [PATCH 172/178] DB_ADDON Plugin - Add Docu - add further calculation method for wachstumsgradtage - code cleanup --- db_addon/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db_addon/__init__.py b/db_addon/__init__.py index e0fa02843..a3fa2bf73 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -1724,7 +1724,7 @@ def _prepare_temperature_list(self, database_item: Item, timeframe: str, start: returns list of lists having timestamp and temperature(s) per day :param database_item: item object or item_id for which the query should be done - :param timeframe: tiemfram for query + :param timeframe: timeframe for query :param start: increments for timeframe from now to start :param end: increments for timeframe from now to end :param ignore_value: value to be ignored during query From 517e07761e5c783fa41f48c1f20b3797d07b40a3 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 30 Mar 2023 21:27:15 +0200 Subject: [PATCH 173/178] added Login via Bosch-ID --- indego4shng/README.md | 6 +- indego4shng/__init__.py | 500 +++++++++++++++++++++++- indego4shng/plugin.yaml | 2 +- indego4shng/webif/static/img/garden.svg | 6 +- 4 files changed, 485 insertions(+), 29 deletions(-) mode change 100755 => 120000 indego4shng/webif/static/img/garden.svg diff --git a/indego4shng/README.md b/indego4shng/README.md index 3421940c5..005e693c4 100755 --- a/indego4shng/README.md +++ b/indego4shng/README.md @@ -109,7 +109,8 @@ Bei den "Kleinen" gibt es folgende Einschränkungen: folgende Einträge werden in der "./etc/plugin.yaml" benötigt. -* `plugin_name: indego4shng`: fix "indego4shng" +* `plugin_name: Indego4shNG`: fix "Indego4shNG" +* `class_path: plugins.indego4shng`: fix "plugins.indego4shng" * `path_2_weather_pics: XXXXXXX`: ist der Pfad zu den Bilder des Wetter-Widgets. (default ="/smartvisu/lib/weather/pics/") * `img_pfad: XXXXXXX`: ist der Pfad unter dem die Gartenkarte gespeichert wird. @@ -128,7 +129,8 @@ Beispiel: ```yaml Indego4shNG: - plugin_name: indego4shng + plugin_name: Indego4shNG + class_path: plugins.indego4shng path_2_weather_pics: /smartvisu/lib/weather/pics/ img_pfad: /tmp/garden.svg indego_credentials: diff --git a/indego4shng/__init__.py b/indego4shng/__init__.py index ec7513545..9888b1ebf 100755 --- a/indego4shng/__init__.py +++ b/indego4shng/__init__.py @@ -64,7 +64,7 @@ class Indego4shNG(SmartPlugin): Main class of the Indego Plugin. Does all plugin specific stuff and provides the update functions for the items """ - PLUGIN_VERSION = '3.0.2' + PLUGIN_VERSION = '4.0.0' def __init__(self, sh, *args, **kwargs): """ @@ -141,6 +141,10 @@ def __init__(self, sh, *args, **kwargs): self.providers = {} self.mowertype = {} + self._refresh_token = '' + self._bearer = '' + self.token_expires = '' + # The following part of the __init__ method is only needed, if a webinterface is being implemented: # if plugin should start even without web interface @@ -162,15 +166,16 @@ def run(self): self.password = self.credentials.split(":")[1] # taken from Init of the plugin if (self.user != '' and self.password != ''): - self._auth() - self.logged_in = self._check_auth() + # self._auth() deprecated + self.logged_in = self._login2Bosch() # start the refresh timers self.scheduler_add('operating_data',self._get_operating_data,cycle = 300) self.scheduler_add('get_state', self._get_state, cycle = self.cycle) self.scheduler_add('alert', self.alert, cycle=300) self.scheduler_add('get_all_calendars', self._get_all_calendars, cycle=300) - self.scheduler_add('check_login_state', self._check_login_state, cycle=130) + #self.scheduler_add('check_login_state', self._check_login_state, cycle=130) + self.scheduler_add('refresh_token', self._getrefreshToken, cycle=self.token_expires-100) self.scheduler_add('device_data', self._device_data, cycle=120) self.scheduler_add('get_weather', self._get_weather, cycle=600) self.scheduler_add('get_next_time', self._get_next_time, cycle=300) @@ -188,7 +193,8 @@ def stop(self): self.scheduler_remove('get_state') self.scheduler_remove('alert') self.scheduler_remove('get_all_calendars') - self.scheduler_remove('check_login_state') + #self.scheduler_remove('check_login_state') + self.scheduler_remove('refresh_token') self.scheduler_remove('device_date') self.scheduler_remove('get_weather') self.scheduler_remove('get_next_time') @@ -830,9 +836,12 @@ def _delete_url(self, url, contextid=None, timeout=40, auth=None,nowait = True): myCouner += 1 time.sleep(2) - headers = { - 'x-im-context-id' : self.context_id - } + headers = {'accept-encoding' : 'gzip', + 'authorization' : 'Bearer '+ self._bearer, + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253' + } response = False try: response = requests.delete(url, headers=headers, auth=auth) @@ -861,16 +870,19 @@ def _get_url(self, url, contextid=None, timeout=40, auth=None): myCouner += 1 time.sleep(2) - headers = { - 'x-im-context-id' : self.context_id - } + headers = {'accept-encoding' : 'gzip', + 'authorization' : 'Bearer '+ self._bearer, + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253' + } response = False try: if auth == None: response = requests.get(url, headers=headers) else: response = requests.get(url, headers=headers, auth=auth) - self._log_communication('get ', url, response.status_code) + self._log_communication('GET ', url, response.status_code) except Exception as e: self.logger.warning("Problem fetching {}: {}".format(url, e)) return False @@ -902,12 +914,12 @@ def _post_url(self, url, contextid=None, body=None, timeout=2, auth = "", nowait myCouner += 1 time.sleep(2) - if (contextid != None and contextid != ""): - headers = { - 'x-im-context-id' : self.context_id - } - else: - headers = "" + headers = {'accept-encoding' : 'gzip', + 'authorization' : 'Bearer '+ self._bearer, + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253' + } response = False try: @@ -937,9 +949,12 @@ def _put_url(self, url, contextid=None, body=None, timeout=2): myCouner += 1 time.sleep(2) - headers = { - 'x-im-context-id' : contextid - } + headers = {'accept-encoding' : 'gzip', + 'authorization' : 'Bearer '+ self._bearer, + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253' + } response = False try: @@ -1072,6 +1087,449 @@ def _auth(self): self.logger.info("Serial received : {}".format(self.alm_sn)) self._log_communication('Auth ', 'Expiration time {}'.format(expiration_timestamp), str(auth_response)) + def _getrefreshToken(self): + myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/token' + mySession = requests.session() + mySession.headers['accept'] = 'application/json' + mySession.headers['accept-encoding'] = 'gzip' + mySession.headers['connection'] = 'Keep-Alive' + mySession.headers['content-type'] = 'application/x-www-form-urlencoded' + mySession.headers['host'] = 'prodindego.b2clogin.com' + mySession.headers['user-agent'] = 'Dalvik/2.1.0 (Linux; U; Android 11; sdk_gphone_x86_arm Build/RSR1.201013.001)' + params = { + "grant_type":"refresh_token", + "refresh_token": self._refresh_token + } + + response = requests.post(myUrl, data=params) + self._log_communication('POST ', myUrl, response.status_code) + + myJson = json.loads (response.content.decode()) + self._refresh_token = myJson['refresh_token'] + self._bearer = myJson['access_token'] + self.token_expires = myJson['expires_in'] + self.last_login_timestamp = datetime.timestamp(datetime.now()) + self.expiration_timestamp = self.last_login_timestamp + self.token_expires + + def _login2Bosch(self): + # Standardvalues + self.login_pending = True + code_challenge = 'iGz3HXMCebCh65NomBE5BbfSTBWE40xLew2JeSrDrF4' + code_verifier = '9aOBN3dvc634eBaj7F8iUnppHeqgUTwG7_3sxYMfpcjlIt7Uuv2n2tQlMLhsd0geWMNZPoryk_bGPmeZKjzbwA' + nonce = 'LtRKgCy_l1abdbKPuf5vhA' + myClientID = '65bb8c9d-1070-4fb4-aa95-853618acc876' # that the Client-ID for the Bosch-App + + myPerfPayload ={ + "navigation": { + "type": 0, + "redirectCount": 0 + }, + "timing": { + "connectStart": 1678187315976, + "navigationStart": 1678187315876, + "loadEventEnd": 1678187317001, + "domLoading": 1678187316710, + "secureConnectionStart": 1678187315994, + "fetchStart": 1678187315958, + "domContentLoadedEventStart": 1678187316973, + "responseStart": 1678187316262, + "responseEnd": 1678187316322, + "domInteractive": 1678187316973, + "domainLookupEnd": 1678187315958, + "redirectStart": 0, + "requestStart": 1678187316010, + "unloadEventEnd": 0, + "unloadEventStart": 0, + "domComplete": 1678187317001, + "domainLookupStart": 1678187315958, + "loadEventStart": 1678187317001, + "domContentLoadedEventEnd": 1678187316977, + "redirectEnd": 0, + "connectEnd": 1678187316002 + }, + "entries": [ + { + "name": "https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id=65bb8c9d-1070-4fb4-aa95-853618acc876&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce=LtRKgCy_l1abdbKPuf5vhA&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256".format(code_challenge), + "entryType": "navigation", + "startTime": 0, + "duration": 1125.3999999999849, + "initiatorType": "navigation", + "nextHopProtocol": "http/1.1", + "workerStart": 0, + "redirectStart": 0, + "redirectEnd": 0, + "fetchStart": 82.29999999997517, + "domainLookupStart": 82.29999999997517, + "domainLookupEnd": 82.29999999997517, + "connectStart": 99.99999999999432, + "connectEnd": 126.29999999998631, + "secureConnectionStart": 117.4999999999784, + "requestStart": 133.7999999999795, + "responseStart": 385.5999999999824, + "responseEnd": 445.699999999988, + "transferSize": 66955, + "encodedBodySize": 64581, + "decodedBodySize": 155950, + "serverTiming": [], + "workerTiming": [], + "unloadEventStart": 0, + "unloadEventEnd": 0, + "domInteractive": 1097.29999999999, + "domContentLoadedEventStart": 1097.29999999999, + "domContentLoadedEventEnd": 1100.999999999999, + "domComplete": 1125.2999999999815, + "loadEventStart": 1125.3999999999849, + "loadEventEnd": 1125.3999999999849, + "type": "navigate", + "redirectCount": 0 + }, + { + "name": "https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/unified.html", + "entryType": "resource", + "startTime": 1038.0999999999858, + "duration": 21.600000000006503, + "initiatorType": "xmlhttprequest", + "nextHopProtocol": "", + "workerStart": 0, + "redirectStart": 0, + "redirectEnd": 0, + "fetchStart": 1038.0999999999858, + "domainLookupStart": 0, + "domainLookupEnd": 0, + "connectStart": 0, + "connectEnd": 0, + "secureConnectionStart": 0, + "requestStart": 0, + "responseStart": 0, + "responseEnd": 1059.6999999999923, + "transferSize": 0, + "encodedBodySize": 0, + "decodedBodySize": 0, + "serverTiming": [], + "workerTiming": [] + }, + { + "name": "https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/bosch-header.png", + "entryType": "resource", + "startTime": 1312.7999999999815, + "duration": 7.900000000006457, + "initiatorType": "css", + "nextHopProtocol": "", + "workerStart": 0, + "redirectStart": 0, + "redirectEnd": 0, + "fetchStart": 1312.7999999999815, + "domainLookupStart": 0, + "domainLookupEnd": 0, + "connectStart": 0, + "connectEnd": 0, + "secureConnectionStart": 0, + "requestStart": 0, + "responseStart": 0, + "responseEnd": 1320.699999999988, + "transferSize": 0, + "encodedBodySize": 0, + "decodedBodySize": 0, + "serverTiming": [], + "workerTiming": [] + } + ], + "connection": { + "onchange": None, + "effectiveType": "4g", + "rtt": 150, + "downlink": 1.6, + "saveData": False, + "downlinkMax": None, + "type": "unknown", + "ontypechange": None + } + } + + myReqPayload = { + "pageViewId":'', + "pageId":"CombinedSigninAndSignup", + "trace":[ + { + "ac":"T005", + "acST":1678187316, + "acD":7 + }, + { + "ac":"T021 - URL:https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/unified.html", + "acST":1678187316, + "acD":119 + }, + { + "ac":"T019", + "acST":1678187317, + "acD":44 + }, + { + "ac":"T004", + "acST":1678187317, + "acD":19 + }, + { + "ac":"T003", + "acST":1678187317, + "acD":5 + }, + { + "ac":"T035", + "acST":1678187317, + "acD":0 + }, + { + "ac":"T030Online", + "acST":1678187317, + "acD":0 + }, + { + "ac":"T002", + "acST":1678187328, + "acD":0 + } + ] + } + # Create a session + mySession = requests.session() + + # Collect some Cookies + + url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id={}&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce=LtRKgCy_l1abdbKPuf5vhA&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256'.format(myClientID,code_challenge) + + myHeader = {'accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'accept-encoding' : 'gzip, deflate, br', + 'accept-language' : 'en-US', + 'connection' : 'keep-alive', + 'host' : 'prodindego.b2clogin.com', + 'user-agent' : 'Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86_arm) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36' + } + mySession.headers = myHeader + + response = mySession.get(url, allow_redirects=True ) + self._log_communication('GET ', url, response.status_code) + + myText= response.content.decode() + myText1 = myText[myText.find('"csrf"')+8:myText.find('"csrf"')+300] + myCsrf = (myText1[:myText1.find(',')-1]) + + myText1 = myText[myText.find('nonce'):myText.find('nonce')+40] + myNonce = myText1.split('"')[1] + + myText1 = myText[myText.find('pageViewId'):myText.find('pageViewId')+60] + myPageViewID = myText1.split('"')[2] + + myReqPayload['pageViewId']=myPageViewID + + mySession.headers['x-csrf-token'] = myCsrf + mySession.headers['referer'] = url + mySession.headers['origin'] = 'https://prodindego.b2clogin.com' + mySession.headers['host'] = 'prodindego.b2clogin.com' + mySession.headers['x-requested-with'] = 'XMLHttpRequest' + mySession.headers['content-length'] = str(len(json.dumps(myPerfPayload))) + mySession.headers['content-type'] = 'application/json; charset=UTF-8' + mySession.headers['accept-language'] = 'en-US,en;q=0.9' + + + myState = mySession.cookies['x-ms-cpim-trans'] + myCookie = json.loads(base64.b64decode(myState).decode()) + myNewState = '{"TID":"'+myCookie['C_ID']+'"}' + myNewState = base64.b64encode(myNewState.encode()).decode()[:-2] + #'{"TID":"8912c0e6-defb-4d58-858b-27d1cfbbe8f5"}' + #eyJUSUQiOiI4OTEyYzBlNi1kZWZiLTRkNTgtODU4Yi0yN2QxY2ZiYmU4ZjUifQ + + + myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/B2C_1A_signup_signin/client/perftrace?tx=StateProperties={}&p=B2C_1A_signup_signin'.format(myNewState) + response=mySession.post(myUrl,data=json.dumps(myPerfPayload)) + self._log_communication('GET ', myUrl, response.status_code) + + + myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/B2C_1A_signup_signin/api/CombinedSigninAndSignup/unified' + mySession.headers['accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' + mySession.headers['accept-encoding'] = 'gzip, deflate, br' + mySession.headers['upgrade-insecure-requests'] = '1' + mySession.headers['sec-fetch-mode'] = 'navigate' + mySession.headers['sec-fetch-dest'] = 'document' + mySession.headers['sec-fetch-user'] = '?1' + mySession.headers['sec-fetch-site'] = 'same-origin' + + del mySession.headers['content-length'] + del mySession.headers['content-type'] + del mySession.headers['x-requested-with'] + del mySession.headers['x-csrf-token'] + del mySession.headers['origin'] + + myParams = { + 'claimsexchange': 'BoschIDExchange', + 'csrf_token': myCsrf, + 'tx': 'StateProperties=' + myNewState, + 'p': 'B2C_1A_signup_signin', + 'diags': myReqPayload + } + # Get the redirect-URI + response = mySession.get(myUrl,allow_redirects=False,params=myParams) + self._log_communication('GET ', myUrl, response.status_code) + try: + if (response.status_code == 302): + myText = response.content.decode() + myText1 = myText[myText.find('href') + 6:] + myNewUrl = myText1.split('"')[0].replace('&','&') + else: + pass + except: + pass + + mySession.headers['sec-fetch-site'] = 'cross-site' + mySession.headers['host'] = 'identity.bosch.com' + + # Get the CIAMIDS + response = mySession.get(myNewUrl,allow_redirects=True) + self._log_communication('GET ', myNewUrl, response.status_code) + try: + if (response.history[0].status_code != 302): + pass + else: + myNewUrl = response.history[0].headers['location'] + except: + pass + + # Signin to Session + response = mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # Authorize -IDS + myNewUrl = response.headers['location'] + mySession.headers['host'] = 'identity-myprofile.bosch.com' + mySession.headers['upgrade-insecure-requests']='1' + cookie_obj = requests.cookies.create_cookie(domain="identity-myprofile.bosch.com",name=".AspNetCore.Identity.Application",value="CfDJ8BbTLtgL3GFMvpWDXN913TQqlMWfpnzYNGGcX0qV3_e1mcxyuYndGzcNXVwoAHCyvY3Ad_1bkYnLsg-J56IdLUNQVMguFnS_KWkPbzib4u6SQtdCZfbiIPV_ZUh4xK-Pd-LgJ61Fi4ljxbb4CewKJRAaDyOhS7KPUu68EVdzte3mEYGm2z8PeSvViW6cGgQeIIOcJ1G3f7XG_s2synfm4o6MDA49a1WnkBIk1kXBodq-vKYXZNMLHOtNGVNE2aZ_k5b9E4mGQVeuncw6SupEku9dCXgO0tRRFK0qUX-41JVrgQdz5v4c_4NB--i1U1b7LUmoZrTtkv0a5KcGPTGz9cZqV5D_Ki4p5uoQxZCmDBPbyecSe6xF3m4yGpEC6hTfrOEJR4LdX6mnppjnXMSc1Y9Pr0Lui3FGeBGuK8GyT4QXJ-pnFrLyF8dh6g2ovkeRvI8MlS5DLSLy_d0s2nOgUxVQPxDsVCxtIMJhE14tSUnC9oRDB_6YUxOqMTEJ_dFacHt-s4iLD2ClBLtA6MsDQcF5pYe4ZOt9zLMuLcoO1NqD3Ca0r00Y0qdkGFGvckp5Xqf7QndkcZxKMPE3GtfH8o6uMsFd7hs1xstxBlT2pgrp0fjjk5R8ugOzJDv-BXarCbjXTzLJtAMVYO4dzorJ7xnXAZDK4IczfXIgxZliwOnTCBvwGIx5CHZfnkYlfhS1PbOE0bwR-sqvJXCS8Jmh6BjmSPHcoKxWxJbLa_wok5HsYmOJgQhVE49WgwuBV88sFvoxpnK_pp1IRR0jFfnV4stT905lkd8hNj5D8o3aZ35sHZDuNPYEXFNUPDORoFnfHkNAP33r126a00n-fLLjaBhFa7W5PnPDaD-M-luVP7nIL-c2tlVon_XRZRC5KMzO4FuOqCeCFwsh3jTtpJk5_iUS4EpHvHT5ldZtRVShC2uzZQ63N_LWl5KZwVlWXPCaLECCZwsGfaAJz0HKDlC-vgXuWL7odJKInmIsi4BJeM9xe280pPDwD6FNUhSOAM2GZgCAW2jilScn5hA2pS1HsLD9yLV0-80Rk9UR9RmRt7USsIOf_7qFMnijAV3MZq9wNKt7ZTBDCI40dxQ1WCYSUV0") + mySession.cookies.set_cookie(cookie_obj) + response = mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # Get the login page with redirect URI + returnUrl = myNewUrl + myNewUrl='https://identity-myprofile.bosch.com/ids/login?ReturnUrl='+returnUrl + response=mySession.get(myNewUrl,allow_redirects=True) + self._log_communication('GET', myNewUrl, response.status_code) + myText = response.content.decode() + # find all the needed values + RequestVerificationToken = myText[myText.find('__RequestVerificationToken'):myText.find('__RequestVerificationToken')+300].split('"')[4] + postData = { + 'meta-information' : '', + 'uEmail' : self.user, + 'uPassword' : self.password, + 'ReturnUrl' : returnUrl[36:58]+'/callback'+returnUrl[58:], + '__RequestVerificationToken' : RequestVerificationToken + } + mySession.headers['content-type'] = 'application/x-www-form-urlencoded' + mySession.headers['sec-fetch-sites'] = 'same-origin' + mySession.headers['origin'] = '' + response=mySession.post(myNewUrl,data=postData,allow_redirects=True) + self._log_communication('POST ', myNewUrl, response.status_code) + + ######################################### + mySession.headers['pragma'] = 'no-cache' + mySession.headers['request-context'] = response.history[0].headers['request-context'] + mySession.headers['host'] = 'identity.bosch.com' + myNewUrl = response.history[1].headers['location'] + + # Collect next Cookie + response = mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + #Get Location for autorization + myNewUrl = 'https://identity.bosch.com/callback' + response=mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET', myNewUrl, response.status_code) + myNewUrl = response.headers['location'] + + #Get Authorize-Informations + response = mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # Get the post-Fields + myText= response.content.decode() + myCode = myText[myText.find('"code"')+14:myText.find('"code"')+300].split('"')[0] + mySessionState = myText[myText.find('"session_state"')+23:myText.find('"session_state"')+300].split('"')[0] + myState = myText[myText.find('"state"')+15:myText.find('"state"')+300].split('"')[0] + + request_body = {"code" : myCode, "state" : myState, "session_state=" : mySessionState } + + mySession.headers['host'] = 'prodindego.b2clogin.com' + mySession.headers['origin'] = 'https://identity.bosch.com' + mySession.headers['content-type'] = 'application/x-www-form-urlencoded' + mySession.headers['cache-control'] = 'max-age=0' + + del mySession.headers['pragma'] + del mySession.headers['request-context'] + + myNewUrl='https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/oauth2/authresp' + response = mySession.post(myNewUrl,data=request_body,allow_redirects=False) + self._log_communication('POST ', myNewUrl, response.status_code) + myNewUrl = response.headers['location'] + + myFinalCode = myNewUrl.split("code")[1].split("=")[1] + + + # Get the new Login-Page + url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id={}&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce=LtRKgCy_l1abdbKPuf5vhA&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256'.format(myClientID,code_challenge) + mySession.headers['host'] = 'prodindego.b2clogin.com' + del mySession.headers['content-type'] + del mySession.headers['origin'] + del mySession.headers['referer'] + response = mySession.get(url,allow_redirects=False) + self._log_communication('GET ', url, response.status_code) + + # Now Post for a token + mySession.close() + request_body = { + 'code' : myFinalCode, + 'grant_type' : 'authorization_code', + 'redirect_uri' : 'com.bosch.indegoconnect://login', + 'code_verifier' : code_verifier, + 'client_id' : myClientID + } + url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/token' + mySession = requests.session() + mySession.headers['accept'] = 'application/json' + mySession.headers['accept-encoding'] = 'gzip' + mySession.headers['connection'] = 'Keep-Alive' + mySession.headers['content-type'] = 'application/x-www-form-urlencoded' + mySession.headers['host'] = 'prodindego.b2clogin.com' + mySession.headers['user-agent'] = 'Dalvik/2.1.0 (Linux; U; Android 11; sdk_gphone_x86_arm Build/RSR1.201013.001)' + + response = mySession.post(url,data=request_body) + self._log_communication('POST ', url, response.status_code) + myJson = json.loads (response.content.decode()) + self._refresh_token = myJson['refresh_token'] + self._bearer = myJson['access_token'] + self.token_expires = myJson['expires_in'] + + + + url='https://api.indego-cloud.iot.bosch-si.com/api/v1/alms' + myHeader = {'accept-encoding' : 'gzip', + 'authorization' : 'Bearer '+ myJson['access_token'], + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253' + } + response = requests.get(url, headers=myHeader,allow_redirects=True ) + self._log_communication('GET ', url, response.status_code) + if (response.status_code == 200): + myJson = json.loads (response.content.decode()) + self.alm_sn = myJson[0]['alm_sn'] + self.login_pending = False + self.last_login_timestamp = datetime.timestamp(datetime.now()) + self.expiration_timestamp = self.last_login_timestamp + self.token_expires + return True + else: + return False + + + def _get_predictive_calendar(self): ''' GET diff --git a/indego4shng/plugin.yaml b/indego4shng/plugin.yaml index ce72c7a48..8b519f87e 100755 --- a/indego4shng/plugin.yaml +++ b/indego4shng/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: http://smarthomeng.de/user/plugins_doc/config/indego.html # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/966612-indego-connect - version: 3.0.2 # Plugin version + version: 4.0.0 # Plugin version sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance diff --git a/indego4shng/webif/static/img/garden.svg b/indego4shng/webif/static/img/garden.svg deleted file mode 100755 index 203ee06b7..000000000 --- a/indego4shng/webif/static/img/garden.svg +++ /dev/null @@ -1,5 +0,0 @@ -XSym -0045 -b7d40008153fcf068cc1330d47e0b2b6 -/var/www/html/smartVISU2.9/dropins/garden.svg - \ No newline at end of file diff --git a/indego4shng/webif/static/img/garden.svg b/indego4shng/webif/static/img/garden.svg new file mode 120000 index 000000000..6b11a6546 --- /dev/null +++ b/indego4shng/webif/static/img/garden.svg @@ -0,0 +1 @@ +/var/www/html/smartVISU2.9/dropins/garden.svg \ No newline at end of file From ae42c3ae91f80367f7557111675d9d1d4389b736 Mon Sep 17 00:00:00 2001 From: sisamiwe Date: Thu, 30 Mar 2023 22:49:10 +0200 Subject: [PATCH 174/178] DB_ADDON Plugin - Bugfix WebIF --- db_addon/webif/templates/index.html | 30 ++--------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/db_addon/webif/templates/index.html b/db_addon/webif/templates/index.html index 2661bd915..31b2dd3a2 100644 --- a/db_addon/webif/templates/index.html +++ b/db_addon/webif/templates/index.html @@ -159,13 +159,9 @@ title: '{{ _('dict/list') }}', targets: [1], "className": "dict" }, - { - title: '{{ _('count') }}', - targets: [2], "className": "count" - }, { title: '{{ _('content') }}', - targets: [3], "className": "content" + targets: [2], "className": "content" }].concat($.fn.dataTable.defaults.columnDefs), pageLength: webif_pagelength, pageResize: resize}); @@ -284,7 +280,7 @@ {{ item._path }} {{ item._type }} - {{ p.get_item_config(item._path)['attribute'] }} + {{ p.get_item_config(item._path)['db_addon_fct'] }} {{ _(p.get_item_config(item)['cycle']|string) }} {% if p.get_item_config(item)['startup'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %} {{ item._value | float }} @@ -303,112 +299,90 @@ {{ _('00_items') }} - {{ len(p.get_item_path_list('database_addon', True)) }} {{ p.get_item_path_list('database_addon', True) }} {{ _('02_admin_items') }} - {{ len(p.get_item_path_list('database_addon', 'admin')) }} {{ p.get_item_path_list('database_addon', 'admin') }} {{ _('10_daily_items') }} - {{ len(p._daily_items()) }} {{ p._daily_items() }} {{ _('11_weekly_items') }} - {{ len(p._weekly_items()) }} {{ p._weekly_items() }} {{ _('12_monthly_items') }} - {{ len(p._monthly_items()) }} {{ p._monthly_items() }} {{ _('13_yearly_items') }} - {{ len(p._yearly_items()) }} {{ p._yearly_items() }} {{ _('14_onchange_items') }} - {{ len(p._onchange_items()) }} {{ p._onchange_items() }} {{ _('15_startup_items') }} - {{ len(p._startup_items()) }} {{ p._startup_items() }} {{ _('17_database_items') }} - {{ len(p._database_items()) }} {{ p._database_items() }} {{ _('16_static_items') }} - {{ len(p._static_items()) }} {{ p._static_items() }} {{ _('32_item_cache') }} - {{ len(p.item_cache) }} {{ p.item_cache }} {{ _('20_tageswert_dict') }} - {{ len(p.current_values['day']) }} {{ p.current_values['day'] }} {{ _('21_wochenwert_dict') }} - {{ len(p.current_values['week']) }} {{ p.current_values['week'] }} {{ _('22_monatswert_dict') }} - {{ len(p.current_values['month']) }} {{ p.current_values['month'] }} {{ _('23_jahreswert_dict') }} - {{ len(p.current_values['year']) }} {{ p.current_values['year'] }} {{ _('24_vortagsendwert_dict') }} - {{ len(p.previous_values['day']) }} {{ p.previous_values['day'] }} {{ _('25_vorwochenendwert_dict') }} - {{ len(p.previous_values['week']) }} {{ p.previous_values['week'] }} {{ _('26_vormonatsendwert_dict') }} - {{ len(p.previous_values['month']) }} {{ p.previous_values['month'] }} {{ _('27_vorjahresendwert_dict') }} - {{ len(p.previous_values['year']) }} {{ p.previous_values['year'] }} {{ _('get_item_list') }} - {{ len(p.get_item_list('database_addon', True)) }} {{ p.get_item_list('database_addon', True) }} {{ _('_plg_item_dict') }} - {{ len(p._plg_item_dict) }} {{ p._plg_item_dict }} {{ _('work_item_queue_thread') }} - {{ len(p._plg_item_dict) }} {% if p.work_item_queue_thread != None %}{{ p.work_item_queue_thread.is_alive() }}{% endif %} From c88c0b594048df5bfe3401ec0b491e2226a99121 Mon Sep 17 00:00:00 2001 From: msinn Date: Fri, 31 Mar 2023 13:19:01 +0200 Subject: [PATCH 175/178] Removed documentation url from metadata (would result in duplicate buttons in plugin list --- zigbee2mqtt/plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigbee2mqtt/plugin.yaml b/zigbee2mqtt/plugin.yaml index fa485af40..7df947ef6 100755 --- a/zigbee2mqtt/plugin.yaml +++ b/zigbee2mqtt/plugin.yaml @@ -9,7 +9,7 @@ plugin: tester: Michael Wenzel # Who tests this plugin? state: develop # change to ready when done with development keywords: iot - documentation: http://smarthomeng.de/user/plugins/zigbee2mqtt/user_doc.html + documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1856775-support-thread-f%C3%BCr-das-zigbee2mqtt-plugin version: 1.1.2 # Plugin version From ce2c56deb96ef49033f492066b7401b340ffdb6b Mon Sep 17 00:00:00 2001 From: msinn Date: Fri, 31 Mar 2023 13:54:18 +0200 Subject: [PATCH 176/178] ical: Corrected fatal error in metadata --- ical/plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ical/plugin.yaml b/ical/plugin.yaml index ceb552767..a5cd7066f 100755 --- a/ical/plugin.yaml +++ b/ical/plugin.yaml @@ -14,7 +14,7 @@ plugin: ' maintainer: onkelandy, cmalo (mknx) - tester: ? + tester: '?' state: ready keywords: ical ics calendar # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page From 4bd7429c728f2a85ee62d57059ab20573c8423d1 Mon Sep 17 00:00:00 2001 From: msinn Date: Fri, 31 Mar 2023 14:10:16 +0200 Subject: [PATCH 177/178] onewire: Added a "sleep-time" for testing to improve io_cycle reading for parasite powered devices; Bumped version to 1.9.4 --- onewire/__init__.py | 5 +++-- onewire/plugin.yaml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/onewire/__init__.py b/onewire/__init__.py index 59266bf29..3b378bc60 100755 --- a/onewire/__init__.py +++ b/onewire/__init__.py @@ -43,7 +43,7 @@ class OneWire(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.9.3' + PLUGIN_VERSION = '1.9.4' _flip = {0: '1', False: '1', 1: '0', True: '0', '0': True, '1': False} @@ -304,12 +304,13 @@ def _io_cycle(self): value = (addr in entries) else: value = self._flip[self.owbase.read('/uncached' + path).decode()] + self.stopevent.wait(self._parasitic_power_wait) except ConnectionError as e: self.logger.warning(f"_io_cycle: 'raise' {self._ios[addr][key]['readerrors']}. problem connecting to {addr}-{key}, error: {e}") raise except Exception as e: # time.sleep(self._parasitic_power_wait) - self.stopevent.wait(self._parasitic_power_wait) + #self.stopevent.wait(self._parasitic_power_wait) self._ios[addr][key]['readerrors'] = self._ios[addr][key].get('readerrors', 0) + 1 if self._ios[addr][key]['readerrors'] % self.warn_after == 0: self.logger.warning(f"_io_cycle: {self._ios[addr][key]['readerrors']}. problem reading {addr}-{key}, error: {e}") diff --git a/onewire/plugin.yaml b/onewire/plugin.yaml index 519da5f1b..a74228138 100755 --- a/onewire/plugin.yaml +++ b/onewire/plugin.yaml @@ -11,7 +11,7 @@ plugin: keywords: 1wire onewire dallas ibutton sensor temperature humidity documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1493319-support-thread-zum-onewire-plugin - version: 1.9.3 # Plugin version + version: 1.9.4 # Plugin version sh_minversion: 1.9.3.5 # minimum shNG version to use this plugin multi_instance: True restartable: True From 8f117bdcf52f577357683f08935860b48d8d6b9c Mon Sep 17 00:00:00 2001 From: msinn Date: Fri, 31 Mar 2023 16:48:25 +0200 Subject: [PATCH 178/178] Set repo version to 1.9.5-master --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 8ae4907e8..389159c96 100755 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ def plugin_release(): - return '1.9.4.1' + return '1.9.5' def plugin_branch(): - return 'develop' + return 'master'