diff --git a/tasmota/__init__.py b/tasmota/__init__.py index fa5ba4b73..ce5a0545b 100644 --- a/tasmota/__init__.py +++ b/tasmota/__init__.py @@ -21,10 +21,10 @@ # ######################################################################### +from __future__ import annotations from datetime import datetime, timedelta -from lib.model.mqttplugin import * -# from lib.item import Items +from lib.model.mqttplugin import MqttPlugin from lib.item.item import Item from .webif import WebInterface @@ -34,7 +34,7 @@ class Tasmota(MqttPlugin): Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items """ - PLUGIN_VERSION = '1.5.2' + PLUGIN_VERSION = '1.6.0' LIGHT_MSG = ['HSBColor', 'Dimmer', 'Color', 'CT', 'Scheme', 'Fade', 'Speed', 'LedTable', 'White'] @@ -51,6 +51,8 @@ class Tasmota(MqttPlugin): 'SetOption125': 'ON', } + ZIGBEE_BRIDGE_IDENTIFIER = ('zigbee_bridge', 'zb-gw03') # typical friendly names of zigbee bridges + TASMOTA_ATTR_R_W = ['relay', 'hsb', 'white', 'ct', 'rf_send', 'rf_key_send', 'zb_permit_join', 'zb_forget', 'zb_ping', 'rf_key'] TASMOTA_ZB_ATTR_R_W = ['power', 'hue', 'sat', 'ct', 'dimmer', 'ct_k'] @@ -105,6 +107,7 @@ def __init__(self, sh): # Define properties self.tasmota_devices = {} # to hold tasmota device information for web interface + self.tasmota_zb_bridge_topics = set() # to hold all defined topics representing a zigbee bridge self.tasmota_zigbee_devices = {} # to hold tasmota zigbee device information for web interface self.topics_of_retained_messages = [] # to hold all topics of retained messages @@ -131,11 +134,11 @@ def run(self): # start subscription to all defined topics self.start_subscriptions() - self.logger.debug(f"Scheduler: 'check_online_status' created") + self.logger.debug("Scheduler: 'check_online_status' created") dt = self.shtime.now() + timedelta(seconds=(self.telemetry_period - 3)) self.scheduler_add('check_online_status', self.check_online_status, cycle=self.telemetry_period, next=dt) - self.logger.debug(f"Scheduler: 'add_tasmota_subscriptions' created") + self.logger.debug("Scheduler: 'add_tasmota_subscriptions' created") self.scheduler_add('add_tasmota_subscriptions', self.add_tasmota_subscriptions, cron='init+20') self.alive = True @@ -179,6 +182,11 @@ def parse_item(self, item): tasmota_sml_device = self.get_iattr_value(item.conf, 'tasmota_sml_device') tasmota_sml_attr = self.get_iattr_value(item.conf, 'tasmota_sml_attr') + # check for being Zigbee Bridge + if tasmota_zb_device == 'bridge': + self.logger.info(f"Tasmota Device with Topic {tasmota_topic} defined as Zigbee Bridge") + self.tasmota_zb_bridge_topics.add(tasmota_topic) + # handle tasmota if tasmota_attr: self.logger.info(f"Item={item.property.path} identified for Tasmota with tasmota_attr={tasmota_attr}") @@ -187,7 +195,7 @@ def parse_item(self, item): if tasmota_rf_details and '=' in tasmota_rf_details: var = tasmota_rf_details.split('=') if len(var) == 2: - tasmota_rf_details, tasmota_rf_key_param = var + tasmota_rf_details, _ = var if tasmota_attr == 'relay': if not tasmota_relay: @@ -266,7 +274,7 @@ def parse_item(self, item): return self.update_item - def update_item(self, item, caller: str = None, source: str = None, dest: str = None): + def update_item(self, item, caller: str|None = None, source: str|None = None, dest: str|None = None): """ Item has been updated @@ -295,25 +303,24 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = tasmota_zb_cluster = self.get_iattr_value(item.conf, 'tasmota_zb_cluster') # handle tasmota_admin - if tasmota_admin: - if tasmota_admin == 'delete_retained_messages' and bool(item()): - self.clear_retained_messages() - item(False, self.get_shortname()) + if tasmota_admin == 'delete_retained_messages' and bool(item()): + self.clear_retained_messages() + item(False, self.get_shortname()) # handle tasmota_attr elif tasmota_attr and tasmota_attr in self.TASMOTA_ATTR_R_W: - self.logger.info(f"update_item: {item.property.path}, item has been changed in SmartHomeNG outside of this plugin in {caller} with value {item()}") + self.logger.info(f"{item.property.path}, item has been changed in SmartHomeNG outside of this plugin in {caller} with value {item()}") value = item() link = { # 'attribute': (detail, data_type, bool_values, min_value, max_value) - 'relay': (f'Power', bool, ['OFF', 'ON'], None, None), + 'relay': ('Power', bool, ['OFF', 'ON'], None, None), 'hsb': ('HsbColor', list, None, None, None), 'white': ('White', int, None, 0, 120), 'ct': ('CT', int, None, 153, 500), 'rf_send': ('Backlog', dict, None, None, None), - 'rf_key_send': (f'RfKey', int, None, 1, 16), - 'rf_key': (f'RfKey', bool, None, None, None), + 'rf_key_send': ('RfKey', int, None, 1, 16), + 'rf_key': ('RfKey', bool, None, None, None), 'zb_permit_join': ('ZbPermitJoin', bool, ['0', '1'], None, None), 'zb_forget': ('ZbForget', bool, ['0', '1'], None, None), 'zb_ping': ('ZbPing', bool, ['0', '1'], None, None), @@ -326,7 +333,7 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = # check data type if not isinstance(value, data_type): - self.logger.warning(f"update_item: type of value {type(value)} for tasmota_attr={tasmota_attr} to be published, does not fit with expected type '{data_type}'. Abort publishing.") + self.logger.warning(f"type of value {type(value)} for tasmota_attr={tasmota_attr} to be published, does not fit with expected type '{data_type}'. Abort publishing.") return # check and correct if value is in allowed range @@ -358,7 +365,7 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = if all(k in rf_cmd for k in [x.lower() for x in self.RF_MSG]): value = f"RfSync {value['rfsync']}; RfLow {value['rflow']}; RfHigh {value['rfhigh']}; RfCode #{value['rfcode']}" else: - self.logger.debug(f"update_item: rf_send received but not with correct content; expected content is: {'RfSync': 12220, 'RfLow': 440, 'RfHigh': 1210, 'RfCode':'#F06104'}") + self.logger.debug(f"rf_send received but not with correct content; expected content is: {'RfSync': 12220, 'RfLow': 440, 'RfHigh': 1210, 'RfCode':'#F06104'}") return elif tasmota_attr == 'rf_key_send': @@ -374,13 +381,13 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = elif tasmota_attr == 'rf_key': if not tasmota_rf_details: - self.logger.warning(f"tasmota_rf_details not specified, no action taken.") + self.logger.warning("tasmota_rf_details not specified, no action taken.") return if tasmota_rf_details and '=' in tasmota_rf_details: var = tasmota_rf_details.split('=') if len(var) == 2: - tasmota_rf_details, tasmota_rf_key_param = var + tasmota_rf_details, _ = var else: return @@ -398,7 +405,7 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = return if value is not None: - self.publish_tasmota_topic('cmnd', tasmota_topic, detail, value, item, bool_values=bool_values) + return self.publish_tasmota_topic('cmnd', tasmota_topic, detail, value, item, bool_values=bool_values) # handle tasmota_zb_attr elif tasmota_zb_attr: @@ -437,7 +444,7 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = elif max_value and value > max_value: self.logger.info(f'Commanded value for {tasmota_zb_attr} above max value; set to allowed max value.') value = max_value - + # Konvertiere Wert if convert: value = convert(value) @@ -451,7 +458,7 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = self.logger.debug(f"payload={payload}") # publish command - self.publish_tasmota_topic('cmnd', tasmota_topic, detail, payload, item, bool_values=bool_values) + return self.publish_tasmota_topic('cmnd', tasmota_topic, detail, payload, item, bool_values=bool_values) else: self.logger.warning(f"update_item: {item.property.path}, trying to change item in SmartHomeNG that is read only in tasmota device (by {caller})") @@ -460,7 +467,7 @@ def update_item(self, item, caller: str = None, source: str = None, dest: str = # Callbacks ############################################################ - def on_mqtt_discovery_message(self, topic: str, payload: dict, qos: int = None, retain: bool = None) -> None: + def on_mqtt_discovery_message(self, topic: str, payload: dict, qos:int, retain: bool) -> None: """ Callback function to handle received discovery messages @@ -472,24 +479,24 @@ def on_mqtt_discovery_message(self, topic: str, payload: dict, qos: int = None, """ try: - self.logger.dbgmed(f"on_mqtt_discovery_message: topic {topic} = {payload}") + self.logger.dbgmed(f"topic {topic} = {payload}") self._handle_retained_message(topic, retain) try: (tasmota, discovery, device_id, msg_type) = topic.split('/') - self.logger.info(f"on_mqtt_discovery_message: device_id={device_id}, type={msg_type}, payload={payload}") + self.logger.info(f"device_id={device_id}, type={msg_type}, payload={payload}") except ValueError: self.logger.error(f"received topic {topic} is not in correct format.") return if not isinstance(payload, dict): - self.logger.debug(f'Payload not of type dict. Message will be discarded.') + self.logger.debug('Payload not of type dict. Message will be discarded.') return if msg_type == 'config': """ device_id = 2CF432CC2FC5 - + payload = { 'ip': '192.168.2.33', // IP address @@ -528,7 +535,7 @@ def on_mqtt_discovery_message(self, topic: str, payload: dict, qos: int = None, # if device is unknown, add it to dict if tasmota_topic not in self.tasmota_devices: - self.logger.info(f"New device based on Discovery Message found.") + self.logger.info("New device based on Discovery Message found.") self._add_new_device_to_tasmota_devices(tasmota_topic) # process decoding message and set device to status 'discovered' @@ -548,8 +555,8 @@ def on_mqtt_discovery_message(self, topic: str, payload: dict, qos: int = None, self.logger.warning(f"Device {device_name} discovered, but FullTopic of device does not match plugin setting!") # if zigbee bridge, process those - if 'zigbee_bridge' in device_name.lower(): - self.logger.info(f"Zigbee_Bridge discovered") + if any(x in device_name.lower() for x in self.ZIGBEE_BRIDGE_IDENTIFIER) or tasmota_topic in self.tasmota_zb_bridge_topics: + self.logger.info(f"Zigbee Bridge discovered as device {device_name}") self.tasmota_devices[tasmota_topic]['zigbee']['status'] = 'discovered' self._configure_zigbee_bridge_settings(tasmota_topic) self._discover_zigbee_bridge_devices(tasmota_topic) @@ -557,7 +564,7 @@ def on_mqtt_discovery_message(self, topic: str, payload: dict, qos: int = None, elif msg_type == 'sensors': """ device_id = 2CF432CC2FC5 - + payload = {'sn': {'Time': '2022-11-19T13:35:59', 'ENERGY': {'TotalStartTime': '2019-12-23T17:02:03', 'Total': 85.314, 'Yesterday': 0.0, 'Today': 0.0, 'Power': 0, 'ApparentPower': 0, 'ReactivePower': 0, 'Factor': 0.0, @@ -571,7 +578,7 @@ def on_mqtt_discovery_message(self, topic: str, payload: dict, qos: int = None, return if 'Time' in sensor_payload: - sensor_payload.pop('Time') + del sensor_payload['Time'] # find matching tasmota_topic tasmota_topic = None @@ -586,10 +593,10 @@ def on_mqtt_discovery_message(self, topic: str, payload: dict, qos: int = None, self._handle_sensor(tasmota_topic, '', sensor_payload) except Exception as e: - self.logger.exception(f"on_mqtt_discovery_message: Exception {e.__class__.__name__}: {e}") + self.logger.exception(f"Exception {e.__class__.__name__}: {e}") return - def on_mqtt_lwt_message(self, topic: str, payload: bool, qos: int = None, retain: bool = None) -> None: + def on_mqtt_lwt_message(self, topic: str, payload: bool, qos: int, retain: bool) -> None: """ Callback function to handle received lwt messages @@ -601,12 +608,12 @@ def on_mqtt_lwt_message(self, topic: str, payload: bool, qos: int = None, retain """ try: - self.logger.dbgmed(f"on_mqtt_lwt_message: topic {topic} = {payload}") + self.logger.dbgmed(f"topic {topic} = {payload}") self._handle_retained_message(topic, retain) try: (topic_type, tasmota_topic, info_topic) = topic.split('/') - self.logger.info(f"on_mqtt_lwt_message: topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") + self.logger.info(f"topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") except ValueError: self.logger.error(f"received topic {topic} is not in correct format.") return @@ -614,7 +621,7 @@ def on_mqtt_lwt_message(self, topic: str, payload: bool, qos: int = None, retain self.logger.info(f"Received LWT Message for {tasmota_topic} with value={payload} and retain={retain}") if payload: if tasmota_topic not in self.tasmota_devices: - self.logger.debug(f"New online device based on LWT Message discovered.") + self.logger.debug("New online device based on LWT Message discovered.") self._handle_new_discovered_device(tasmota_topic) self.tasmota_devices[tasmota_topic]['online_timeout'] = datetime.now() + timedelta(seconds=self.telemetry_period + 5) @@ -623,10 +630,10 @@ def on_mqtt_lwt_message(self, topic: str, payload: bool, qos: int = None, retain self._set_item_value(tasmota_topic, 'online', payload, info_topic) except Exception as e: - self.logger.exception(f"on_mqtt_lwt_message: Exception {e.__class__.__name__}: {e}") + self.logger.exception(f"Exception {e.__class__.__name__}: {e}") return - def on_mqtt_status0_message(self, topic: str, payload: dict, qos: int = None, retain: bool = None) -> None: + def on_mqtt_status0_message(self, topic: str, payload: dict, qos: int, retain: bool) -> None: """ Callback function to handle received messages @@ -637,9 +644,9 @@ def on_mqtt_status0_message(self, topic: str, payload: dict, qos: int = None, re """ - """ - Example payload - + """ + Example payload + payload = {'Status': {'Module': 75, 'DeviceName': 'ZIGBEE_Bridge01', 'FriendlyName': ['SONOFF_ZB1'], 'Topic': 'SONOFF_ZB1', 'ButtonTopic': '0', 'Power': 0, 'PowerOnState': 3, 'LedState': 1, 'LedMask': 'FFFF', 'SaveData': 1, 'SaveState': 1, 'SwitchTopic': '0', @@ -678,22 +685,22 @@ def on_mqtt_status0_message(self, topic: str, payload: dict, qos: int = None, re 'Wifi': {'AP': 1, 'SSId': 'WLAN-Access', 'BSSId': '38:10:D5:15:87:69', 'Channel': 1, 'Mode': '11n', 'RSSI': 50, 'Signal': -75, 'LinkCount': 1, 'Downtime': '0T00:00:03'}}} - + """ try: - self.logger.dbgmed(f"on_mqtt_status0_message: topic {topic} = {payload}") + self.logger.dbgmed(f"topic {topic} = {payload}") self._handle_retained_message(topic, retain) try: (topic_type, tasmota_topic, info_topic) = topic.split('/') - self.logger.info(f"on_mqtt_status0_message: topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") + self.logger.info(f"topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") except ValueError: self.logger.error(f"received topic {topic} is not in correct format.") return if not isinstance(payload, dict): - self.logger.debug(f'Payload not of type dict. Message will be discarded.') + self.logger.debug('Payload not of type dict. Message will be discarded.') return self.logger.info(f"Received Status0 Message for {tasmota_topic} with value={payload} and retain={retain}") @@ -715,42 +722,43 @@ def on_mqtt_status0_message(self, topic: str, payload: dict, qos: int = None, re pass # IP Address - ip = None - ip_eth = None try: ip = payload['StatusNET']['IPAddress'] except KeyError: - pass + ip = None + try: - ip_eth = payload['StatusNET'].get('Ethernet', {}).get('IPAddress') + ip_eth = payload['StatusNET']['Ethernet']['IPAddress'] except KeyError: - pass + ip_eth = None + + if ip == '0.0.0.0' and ip_eth: + ip = ip_eth if ip: - if ip_eth and ip == '0.0.0.0': - ip = ip_eth self.tasmota_devices[tasmota_topic]['ip'] = ip # Firmware try: self.tasmota_devices[tasmota_topic]['fw_ver'] = payload['StatusFWR']['Version'].split('(')[0] - except KeyError: + except (TypeError, KeyError): pass # MAC try: self.tasmota_devices[tasmota_topic]['mac'] = payload['StatusNET']['Mac'] - except KeyError: + except (TypeError, KeyError): pass # Module No try: self.tasmota_devices[tasmota_topic]['template'] = payload['Status']['Module'] - except KeyError: + except (TypeError, KeyError): pass # get detailed status using payload['StatusSTS'] status_sts = payload.get('StatusSTS') + # make sure, that dict is not empty if not status_sts and not isinstance(status_sts, dict): return @@ -763,7 +771,7 @@ def on_mqtt_status0_message(self, topic: str, payload: dict, qos: int = None, re self._handle_power(tasmota_topic, info_topic, status_sts) # Handling of RF messages - if any(item.startswith("Rf") for item in status_sts.keys()): + if any(item.startswith("R") for item in status_sts.keys()): self._handle_rf(tasmota_topic, info_topic, status_sts) # Handling of Wi-Fi @@ -776,14 +784,14 @@ def on_mqtt_status0_message(self, topic: str, payload: dict, qos: int = None, re # Handling of UptimeSec if 'UptimeSec' in status_sts: - self.logger.info(f"Received Message contains UptimeSec information.") + self.logger.info("Received Message contains UptimeSec information.") self._handle_uptime_sec(tasmota_topic, status_sts['UptimeSec']) except Exception as e: - self.logger.exception(f"on_mqtt_status0_message: Exception {e.__class__.__name__}: {e}") + self.logger.exception(f"Exception {e.__class__.__name__}: {e}") return - def on_mqtt_info_message(self, topic: str, payload: dict, qos: int = None, retain: bool = None) -> None: + def on_mqtt_info_message(self, topic: str, payload: dict, qos: int, retain: bool) -> None: """ Callback function to handle received messages @@ -795,23 +803,23 @@ def on_mqtt_info_message(self, topic: str, payload: dict, qos: int = None, retai """ try: - self.logger.dbgmed(f"on_mqtt_info_message: topic {topic} = {payload}") + self.logger.dbgmed(f"topic {topic} = {payload}") self._handle_retained_message(topic, retain) try: (topic_type, tasmota_topic, info_topic) = topic.split('/') - self.logger.info(f"on_mqtt_info_message: topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") + self.logger.info(f"topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") except ValueError: self.logger.error(f"received topic {topic} is not in correct format.") return if not isinstance(payload, dict): - self.logger.debug(f'Payload not of type dict. Message will be discarded.') + self.logger.debug('Payload not of type dict. Message will be discarded.') return if info_topic == 'INFO1': # payload={'Info1': {'Module': 'Sonoff Basic', 'Version': '11.0.0(tasmota)', 'FallbackTopic': 'cmnd/DVES_2EB8AE_fb/', 'GroupTopic': 'cmnd/tasmotas/'}} - self.logger.debug(f"Received Message decoded as INFO1 message.") + self.logger.debug("Received Message decoded as INFO1 message.") try: self.tasmota_devices[tasmota_topic]['fw_ver'] = payload['Info1']['Version'].split('(')[0] except KeyError: @@ -823,7 +831,7 @@ def on_mqtt_info_message(self, topic: str, payload: dict, qos: int = None, retai elif info_topic == 'INFO2': # payload={'Info2': {'WebServerMode': 'Admin', 'Hostname': 'SONOFF-B1-6318', 'IPAddress': '192.168.2.25'}} - self.logger.debug(f"Received Message decoded as INFO2 message.") + self.logger.debug("Received Message decoded as INFO2 message.") try: self.tasmota_devices[tasmota_topic]['ip'] = payload['Info2']['IPAddress'] except KeyError: @@ -831,7 +839,7 @@ def on_mqtt_info_message(self, topic: str, payload: dict, qos: int = None, retai elif info_topic == 'INFO3': # payload={'Info3': {'RestartReason': 'Software/System restart', 'BootCount': 1395}} - self.logger.debug(f"Received Message decoded as INFO3 message.") + self.logger.debug("Received Message decoded as INFO3 message.") try: restart_reason = payload['Info3']['RestartReason'] self.logger.info(f"Device {tasmota_topic} (IP={self.tasmota_devices[tasmota_topic]['ip']}) just startet. Reason={restart_reason}") @@ -839,10 +847,10 @@ def on_mqtt_info_message(self, topic: str, payload: dict, qos: int = None, retai pass except Exception as e: - self.logger.exception(f"on_mqtt_info_message: Exception {e.__class__.__name__}: {e}") + self.logger.exception(f"Exception {e.__class__.__name__}: {e}") return - def on_mqtt_message(self, topic: str, payload: dict, qos: int = None, retain: bool = None) -> None: + def on_mqtt_message(self, topic: str, payload: dict, qos: int, retain: bool) -> None: """ Callback function to handle received messages @@ -854,18 +862,18 @@ def on_mqtt_message(self, topic: str, payload: dict, qos: int = None, retain: bo """ try: - self.logger.dbgmed(f"on_mqtt_message: topic {topic} = {payload}") + self.logger.dbgmed(f"topic {topic} = {payload}") self._handle_retained_message(topic, retain) try: (topic_type, tasmota_topic, info_topic) = topic.split('/') - self.logger.info(f"on_mqtt_message: topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") + self.logger.info(f"topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") except ValueError: self.logger.error(f"received topic {topic} is not in correct format.") return if not isinstance(payload, dict): - self.logger.debug(f'Payload not of type dict. Message will be discarded.') + self.logger.debug('Payload not of type dict. Message will be discarded.') return # handle unknown device @@ -877,66 +885,67 @@ def on_mqtt_message(self, topic: str, payload: dict, qos: int = None, retain: bo # Handling of TelePeriod if 'TelePeriod' in payload: - self.logger.info(f"Received Message decoded as teleperiod message.") + self.logger.info("Received Message decoded as teleperiod message.") self._handle_teleperiod(tasmota_topic, payload['TelePeriod']) + # Handling of Module elif 'Module' in payload: - self.logger.info(f"Received Message decoded as Module message.") + self.logger.info("Received Message decoded as Module message.") self._handle_module(tasmota_topic, payload['Module']) # Handling of Light messages elif any([i in payload for i in self.LIGHT_MSG]): - self.logger.info(f"Received Message decoded as light message.") + self.logger.info("Received Message decoded as light message.") self._handle_lights(tasmota_topic, info_topic, payload) # Handling of Power messages elif any(item.startswith("POWER") for item in payload.keys()): - self.logger.info(f"Received Message decoded as power message.") + self.logger.info("Received Message decoded as power message.") self._handle_power(tasmota_topic, info_topic, payload) # Handling of RF messages payload={'Time': '2022-11-21T11:22:55', 'RfReceived': {'Sync': 10120, 'Low': 330, 'High': 980, 'Data': '3602B8', 'RfKey': 'None'}} elif 'RfReceived' in payload: - self.logger.info(f"Received Message decoded as RF message.") + self.logger.info("Received Message decoded as RF message.") self._handle_rf(tasmota_topic, info_topic, payload['RfReceived']) # Handling of Setting messages elif next(iter(payload)).startswith("SetOption"): # elif any(item.startswith("SetOption") for item in payload.keys()): - self.logger.info(f"Received Message decoded as Tasmota Setting message.") + self.logger.info("Received Message decoded as Tasmota Setting message.") self._handle_setting(tasmota_topic, payload) # Handling of Zigbee Bridge Config messages elif 'ZbConfig' in payload: - self.logger.info(f"Received Message decoded as Zigbee Config message.") + self.logger.info("Received Message decoded as Zigbee Config message.") self._handle_zbconfig(tasmota_topic, payload['ZbConfig']) # Handling of Zigbee Bridge Status messages elif any(item.startswith("ZbStatus") for item in payload.keys()): - self.logger.info(f"Received Message decoded as Zigbee ZbStatus message.") + self.logger.info("Received Message decoded as Zigbee ZbStatus message.") self._handle_zbstatus(tasmota_topic, payload) # Handling of Wi-Fi if 'Wifi' in payload: - self.logger.info(f"Received Message contains Wifi information.") + self.logger.info("Received Message contains Wifi information.") self._handle_wifi(tasmota_topic, payload['Wifi']) # Handling of Uptime if 'Uptime' in payload: - self.logger.info(f"Received Message contains Uptime information.") + self.logger.info("Received Message contains Uptime information.") self._handle_uptime(tasmota_topic, payload['Uptime']) # Handling of UptimeSec if 'UptimeSec' in payload: - self.logger.info(f"Received Message contains UptimeSec information.") + self.logger.info("Received Message contains UptimeSec information.") self._handle_uptime_sec(tasmota_topic, payload['UptimeSec']) # Handling of Button messages if any(item.startswith("Button") for item in payload.keys()): - self.logger.info(f"Received Message decoded as button message.") + self.logger.info("Received Message decoded as button message.") self._handle_button(tasmota_topic, info_topic, payload) elif info_topic == 'SENSOR': - self.logger.info(f"Received Message contains sensor information.") + self.logger.info("Received Message contains sensor information.") self._handle_sensor(tasmota_topic, info_topic, payload) else: @@ -949,10 +958,10 @@ def on_mqtt_message(self, topic: str, payload: dict, qos: int = None, retain: bo self._set_item_value(tasmota_topic, 'online', True, info_topic) except Exception as e: - self.logger.exception(f"on_mqtt_message: Exception {e.__class__.__name__}: {e}") + self.logger.exception(f"Exception {e.__class__.__name__}: {e}") return - def on_mqtt_power_message(self, topic: str, payload: bool, qos: int = None, retain: bool = None) -> None: + def on_mqtt_power_message(self, topic: str, payload: bool, qos: int, retain: bool) -> None: """ Callback function to handle received power messages @@ -964,12 +973,12 @@ def on_mqtt_power_message(self, topic: str, payload: bool, qos: int = None, ret """ try: - self.logger.dbgmed(f"on_mqtt_power_message: topic {topic} = {payload}") + self.logger.dbgmed(f"topic {topic} = {payload}") self._handle_retained_message(topic, retain) try: (topic_type, tasmota_topic, info_topic) = topic.split('/') - self.logger.info(f"on_mqtt_power_message: topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") + self.logger.info(f"topic_type={topic_type}, tasmota_topic={tasmota_topic}, info_topic={info_topic}, payload={payload}") except ValueError: self.logger.error(f"received topic {topic} is not in correct format.") return @@ -986,7 +995,7 @@ def on_mqtt_power_message(self, topic: str, payload: bool, qos: int = None, ret self.tasmota_devices[tasmota_topic]['relais'][info_topic] = payload except Exception as e: - self.logger.exception(f"on_mqtt_power_message: Exception {e.__class__.__name__}: {e}") + self.logger.exception(f"Exception {e.__class__.__name__}: {e}") return ############################################################ @@ -1003,12 +1012,12 @@ def _handle_sensor(self, device: str, function: str, payload: dict) -> None: """ # Handling of Zigbee Device Messages if 'ZbReceived' in payload: - self.logger.info(f"Received Message decoded as Zigbee Sensor message.") + self.logger.info("Received Message decoded as Zigbee Sensor message.") self._handle_sensor_zigbee(device, function, payload['ZbReceived']) # Handling of Energy Sensors elif 'ENERGY' in payload: - self.logger.info(f"Received Message decoded as Energy Sensor message.") + self.logger.info("Received Message decoded as Energy Sensor message.") self._handle_sensor_energy(device, function, payload['ENERGY']) # Handling of Environmental Sensors @@ -1017,18 +1026,18 @@ def _handle_sensor(self, device: str, function: str, payload: dict) -> None: # Handling of Analog Sensors elif 'ANALOG' in payload: - self.logger.info(f"Received Message decoded as ANALOG Sensor message.") + self.logger.info("Received Message decoded as ANALOG Sensor message.") self._handle_sensor_analog(device, function, payload['ANALOG']) # Handling of Sensors of ESP32 elif 'ESP32' in payload: - self.logger.info(f"Received Message decoded as ESP32 Sensor message.") + self.logger.info("Received Message decoded as ESP32 Sensor message.") self._handle_sensor_esp32(device, function, payload['ESP32']) # Handling of any other Sensor e.g. all SML devices else: if len(payload) == 2 and isinstance(payload[list(payload.keys())[1]], dict): # wenn payload 2 Einträge und der zweite Eintrag vom Typ dict - self.logger.info(f"Received Message decoded as other Sensor message (e.g. smartmeter).") + self.logger.info("Received Message decoded as other Sensor message (e.g. smartmeter).") sensor = list(payload.keys())[1] self._handle_sensor_other(device, sensor, function, payload[sensor]) @@ -1067,6 +1076,8 @@ def _handle_sensor_zigbee(self, device: str, function: str, payload: dict) -> No # Korrektur des LastSeenEpoch von Timestamp zu datetime if 'lastseenepoch' in zigbee_device_dict: zigbee_device_dict.update({'lastseenepoch': datetime.fromtimestamp(zigbee_device_dict['lastseenepoch'])}) + else: + zigbee_device_dict.update({'lastseenepoch': self.shtime.now()}) if 'batterylastseenepoch' in zigbee_device_dict: zigbee_device_dict.update({'batterylastseenepoch': datetime.fromtimestamp(zigbee_device_dict['batterylastseenepoch'])}) @@ -1242,7 +1253,7 @@ def _handle_power(self, device: str, function: str, payload: dict) -> None: :param payload: MQTT message payload """ - # payload = {"Time": "2022-11-21T12:56:34", "Uptime": "0T00:00:11", "UptimeSec": 11, "Heap": 27, "SleepMode": "Dynamic", "Sleep": 50, "LoadAvg": 19, "MqttCount": 0, "POWER1": "OFF", "POWER2": "OFF", "POWER3": "OFF", "POWER4": "OFF", "Wifi": {"AP": 1, "SSId": "WLAN-Access", "BSSId": "38:10:D5:15:87:69", "Channel": 1, "Mode": "11n", "RSSI": 82, "Signal": -59, "LinkCount": 1, "Downtime": "0T00:00:03"}} + # payload = {"Time": "2022-11-21T12:56:34", "Uptime": "0T00:00:11", "UptimeSec": 11, "Heap": 27, "SleepMode": "Dynamic", "Sleep": 50, "LoadAvg": 19, "MqttCount": 0, "POWER1": "OF", "POWER2": "OF", "POWER3": "OF", "POWER4": "OF", "Wifi": {"AP": 1, "SSId": "WLAN-Access", "BSSId": "38:10:D5:15:87:69", "Channel": 1, "Mode": "11n", "RSSI": 82, "Signal": -59, "LinkCount": 1, "Downtime": "0T00:00:03"}} power_dict = {key: val for key, val in payload.items() if key.startswith('POWER')} self.tasmota_devices[device]['relais'].update(power_dict) @@ -1267,7 +1278,7 @@ def _handle_button(self, device: str, function: str, payload: dict) -> None: for button in button_dict: button_index = 1 if len(button) == 6 else str(button[6:]) item_button = f'button.{button_index}' - self._set_item_value(device, item_button, button_dict[button].get('Action',None), function) + self._set_item_value(device, item_button, button_dict[button].get('Action'), function) def _handle_module(self, device: str, payload: dict) -> None: """ @@ -1294,7 +1305,7 @@ def _handle_rf(self, device: str, function: str, payload: dict) -> None: # payload = {'Sync': 10120, 'Low': 330, 'High': 980, 'Data': '3602B8', 'RfKey': 'None'} - self.logger.info(f"Received Message decoded as RF message.") + self.logger.info("Received Message decoded as RF message.") self.tasmota_devices[device]['rf']['rf_received'] = payload self._set_item_value(device, 'rf_recv', payload['Data'], function) @@ -1344,7 +1355,7 @@ def _handle_zbstatus1(self, device: str, zbstatus1: list) -> None: """ """ - zbstatus1 = [{'Device': '0x676D', 'Name': 'SNZB-02_01'}, + zbstatus1 = [{'Device': '0x676D', 'Name': 'SNZB-02_01'}, {'Device': '0xD4F3', 'Name': 'Fenster_01'} ] """ @@ -1357,9 +1368,8 @@ def _handle_zbstatus1(self, device: str, zbstatus1: list) -> None: if zigbee_device != '0x0000' and zigbee_device not in self.tasmota_zigbee_devices: self.logger.info(f"New Zigbee Device '{zigbee_device}'based on 'ZbStatus1'-Message from {device} discovered") self.tasmota_zigbee_devices[zigbee_device] = {} - - # request detailed information of all discovered zigbee devices - self._poll_zigbee_devices(device) + # request detailed information of all discovered zigbee devices + self._poll_zigbee_device(zigbee_device, device) def _handle_zbstatus23(self, device: str, zbstatus23: dict) -> None: """ @@ -1376,10 +1386,10 @@ def _handle_zbstatus23(self, device: str, zbstatus23: dict) -> None: 'Config': ['A01'], 'ZoneStatus': 29697, 'Reachable': True, 'BatteryPercentage': 100, 'BatteryLastSeenEpoch': 1668953504, 'LastSeen': 238, 'LastSeenEpoch': 1668953504, 'LinkQuality': 81}] - + zbstatus23 = [{'Device': '0x676D', 'Name': 'SNZB-02_01', 'IEEEAddr': '0x00124B00231E45B8', - 'ModelId': 'TH01', 'Manufacturer': 'eWeLink', 'Endpoints': [1], 'Config': ['T01'], - 'Temperature': 19.27, 'Humidity': 58.12, 'Reachable': True, 'BatteryPercentage': 73, + 'ModelId': 'TH01', 'Manufacturer': 'eWeLink', 'Endpoints': [1], 'Config': ['T01'], + 'Temperature': 19.27, 'Humidity': 58.12, 'Reachable': True, 'BatteryPercentage': 73, 'BatteryLastSeenEpoch': 1668953064, 'LastSeen': 610, 'LastSeenEpoch': 1668953064, 'LinkQuality': 66}] zbstatus23 = [{'Device': '0x0A22', 'IEEEAddr': '0xF0D1B800001571C5', 'ModelId': 'CLA60 RGBW Z3', @@ -1429,7 +1439,7 @@ def _handle_setting(self, device: str, payload: dict) -> None: if self.tasmota_devices[device]['zigbee']['setting'] == self.ZIGBEE_BRIDGE_DEFAULT_OPTIONS: self.tasmota_devices[device]['zigbee']['status'] = 'set' - self.logger.info(f'_handle_setting: Setting of Tasmota Zigbee Bridge successful.') + self.logger.info('_handle_setting: Setting of Tasmota Zigbee Bridge successful.') def _handle_teleperiod(self, tasmota_topic: str, teleperiod: dict) -> None: @@ -1450,7 +1460,7 @@ def _handle_uptime_sec(self, tasmota_topic: str, uptime_sec: int) -> None: ############################################################ def add_tasmota_subscriptions(self): - self.logger.info(f"Further tasmota_subscriptions for regular/cyclic messages will be added") + self.logger.info("Further tasmota_subscriptions for regular/cyclic messages will be added") for detail in ('STATE', 'SENSOR', 'RESULT'): self.add_tasmota_subscription('tele', '+', detail, 'dict', callback=self.on_mqtt_message) @@ -1474,7 +1484,7 @@ def check_online_status(self): else: self.logger.debug(f'check_online_status: Checking online status of {tasmota_topic} successful') - def add_tasmota_subscription(self, prefix: str, topic: str, detail: str, payload_type: str, bool_values: list = None, item: Item = None, callback=None) -> None: + def add_tasmota_subscription(self, prefix: str, topic: str, detail: str, payload_type: str, bool_values: list|None = None, item: Item|None = None, callback=None) -> None: """ build the topic in Tasmota style and add the subscription to mqtt @@ -1493,7 +1503,7 @@ def add_tasmota_subscription(self, prefix: str, topic: str, detail: str, payload tpc += detail self.add_subscription(tpc, payload_type, bool_values=bool_values, callback=callback) - def publish_tasmota_topic(self, prefix: str, topic: str, detail: str, payload, item: Item = None, qos: int = None, retain: bool = False, bool_values: list = None) -> None: + def publish_tasmota_topic(self, prefix: str, topic: str, detail: str, payload=None, item: Item|None = None, qos: int|None = None, retain: bool = False, bool_values: list|None = None) -> None: """ build the topic in Tasmota style and publish to mqtt @@ -1519,9 +1529,9 @@ def interview_all_devices(self): Interview known Tasmota Devices (defined in item.yaml and self discovered) """ - self.logger.info(f"Interview of all known tasmota devices started.") + self.logger.info("Interview of all known tasmota devices started.") - for device in self.tasmota_device(): + for device in self.get_tasmota_device_list(): self.logger.debug(f"Interview {device}.") self._interview_device(device) self.logger.debug(f"Set Telemetry period for {device}.") @@ -1533,8 +1543,7 @@ def clear_retained_messages(self, retained_msg=None): """ if retained_msg: - retained_msg_list = list() - retained_msg_list.append(retained_msg) + retained_msg_list = [retained_msg] else: retained_msg_list = self.topics_of_retained_messages @@ -1542,7 +1551,7 @@ def clear_retained_messages(self, retained_msg=None): self.logger.info(f"Clearing retained message for topic={topic}") self.publish_topic(topic=topic, payload="", retain=True) - def _interview_device(self, topic: str) -> None: + def _interview_device(self, topic: str) -> bool: """ ask for status info of each known tasmota_topic @@ -1551,6 +1560,7 @@ def _interview_device(self, topic: str) -> None: # self.logger.debug(f"run: publishing 'cmnd/{topic}/Status0'") self.publish_tasmota_topic('cmnd', topic, 'Status0', '') + return True # self.logger.debug(f"run: publishing 'cmnd/{topic}/State'") # self.publish_tasmota_topic('cmnd', topic, 'State', '') @@ -1558,7 +1568,7 @@ def _interview_device(self, topic: str) -> None: # self.logger.debug(f"run: publishing 'cmnd/{topic}/Module'") # self.publish_tasmota_topic('cmnd', topic, 'Module', '') - def _set_telemetry_period(self, topic: str) -> None: + def _set_telemetry_period(self, topic: str): """ sets telemetry period for given topic/device @@ -1605,9 +1615,9 @@ def _set_item_value(self, tasmota_topic: str, item_type: str, value, info_topic: elif tasmota_rf_key_param.lower() == 'false': value = True elif tasmota_rf_key_param.lower() == 'toggle': - value = not(item()) + value = not item() else: - self.logger.warning(f"Parameter of tasmota_rf_key unknown, Need to be 'True', 'False', 'Toggle'") + self.logger.warning("Parameter of tasmota_rf_key unknown, Need to be 'True', 'False', 'Toggle'") return # set item value @@ -1695,17 +1705,66 @@ def _get_device_dict_2_template(): # Zigbee ############################################################ - def _poll_zigbee_devices(self, device: str) -> None: + def _poll_zigbee_devices(self, zb_bridge: str = '') -> bool: """ Polls information of all discovered zigbee devices from dedicated Zigbee bridge - :param device: Zigbee bridge, where all Zigbee Devices shall be polled (equal to tasmota_topic) + :param zb_bridge: Zigbee bridge, where all Zigbee Devices shall be polled (equal to tasmota_topic) + + """ + if not zb_bridge: + zb_bridge = self.get_tasmota_device_w_zigbee() + + if not zb_bridge: + return False + + result = True + self.logger.info(f"Polling information of all discovered Zigbee devices for Zigbee-Bridge {zb_bridge}") + for zb_device in self.tasmota_zigbee_devices: + res = self._poll_zigbee_device(zb_device, zb_bridge) + self.logger.info(f"poll of {zb_device}: {res}") + if not res: + result = False + + return result + + def _poll_zigbee_device(self, zb_device: str, zb_bridge: str = '') -> bool: + """ + Polls information of zigbee devices from dedicated Zigbee bridge + + :param zb_bridge: Zigbee bridge, where Zigbee Devices is linked to + :param zb_device: Zigbee Device to be polled """ - self.logger.info(f"_poll_zigbee_devices: Polling information of all discovered Zigbee devices for zigbee_bridge {device}") - for zigbee_device in self.tasmota_zigbee_devices: - # self.logger.debug(f"_poll_zigbee_devices: publishing 'cmnd/{device}/ZbStatus3 {zigbee_device}'") - self.publish_tasmota_topic('cmnd', device, 'ZbStatus3', zigbee_device) + + if not zb_bridge: + zb_bridge = self.get_tasmota_device_w_zigbee() + + if not zb_bridge: + return False + + self.logger.info(f"_poll_zigbee_device: Polling information from {zb_device} via Zigbee-Bridge {zb_bridge}") + self.publish_tasmota_topic('cmnd', zb_bridge, 'ZbStatus3', zb_device) + return True + + def _ping_zigbee_device(self, zb_device: str, zb_bridge: str = '') -> bool: + """ + Ping zigbee devices from dedicated Zigbee bridge + + :param zb_bridge: Zigbee bridge, where Zigbee Devices is linked to + :param zb_device: Zigbee Device to be polled + + """ + + if not zb_bridge: + zb_bridge = self.get_tasmota_device_w_zigbee() + + if not zb_bridge: + return False + + self.logger.info(f"_ping_zigbee_device: Ping {zb_device} via Zigbee-Bridge {zb_bridge}") + self.publish_tasmota_topic('cmnd', zb_bridge, 'ZbPing', zb_device) + return True def _configure_zigbee_bridge_settings(self, device: str) -> None: """ @@ -1762,78 +1821,112 @@ def _handle_retained_message(self, topic: str, retain: bool) -> None: def log_level(self): return self.logger.getEffectiveLevel() + @property def retained_msg_count(self): - return self._broker.retained_messages + return self._broker['retained_messages'] - def tasmota_device(self): + def get_tasmota_device_list(self): return list(self.tasmota_devices.keys()) - def has_zigbee(self): + def get_tasmota_device_w_zigbee(self) -> str: for tasmota_topic in self.tasmota_devices: if self.tasmota_devices[tasmota_topic]['zigbee']: - return True - return False + return tasmota_topic + return '' + def _has_capability(self, func, tasmota_topic: str = ''): + """ + Checks if any Tasmota device has a certain capability. - def has_lights(self): - for tasmota_topic in self.tasmota_devices: - if self.tasmota_devices[tasmota_topic]['lights']: - return True - return False + Args: + tasmota_topic: Optional Tasmota topic to check specifically. - def has_rf(self): - for tasmota_topic in self.tasmota_devices: - if self.tasmota_devices[tasmota_topic]['rf']: - return True - return False + Returns: + True if any Tasmota device has Zigbee capabilities, False otherwise. + """ - def has_relais(self): - for tasmota_topic in self.tasmota_devices: - if self.tasmota_devices[tasmota_topic]['relais']: - return True - return False + if tasmota_topic and self.tasmota_devices.get(tasmota_topic, {}).get(func, False): + return True - def has_button(self): - for tasmota_topic in self.tasmota_devices: - if self.tasmota_devices[tasmota_topic]['button']: - return True - return False + return any(device.get(func, False) for device in self.tasmota_devices.values()) - def has_energy_sensor(self): - for tasmota_topic in self.tasmota_devices: - if 'ENERGY' in self.tasmota_devices[tasmota_topic]['sensors']: - return True - return False + def has_zigbee(self, tasmota_topic: str = ''): + return self._has_capability('zigbee', tasmota_topic) - def has_env_sensor(self): - for tasmota_topic in self.tasmota_devices: - if any([i in self.tasmota_devices[tasmota_topic]['sensors'] for i in self.ENV_SENSOR]): - return True - return False + def has_lights(self, tasmota_topic: str = ''): + return self._has_capability('lights', tasmota_topic) - def has_ds18b20_sensor(self): - for tasmota_topic in self.tasmota_devices: - if 'DS18B20' in self.tasmota_devices[tasmota_topic]['sensors']: - return True - return False + def has_rf(self, tasmota_topic: str = ''): + return self._has_capability('rf', tasmota_topic) - def has_am2301_sensor(self): - for tasmota_topic in self.tasmota_devices: - if 'AM2301' in self.tasmota_devices[tasmota_topic]['sensors']: - return True - return False + def has_relais(self, tasmota_topic: str = ''): + return self._has_capability('relais', tasmota_topic) - def has_sht3x_sensor(self): - for tasmota_topic in self.tasmota_devices: - if 'SHT3X' in self.tasmota_devices[tasmota_topic]['sensors']: - return True - return False + def has_button(self, tasmota_topic: str = ''): + return self._has_capability('button', tasmota_topic) - def has_other_sensor(self): - for tasmota_topic in self.tasmota_devices: - for sensor in self.tasmota_devices[tasmota_topic]['sensors']: + def _has_sensor(self, sensor: str, tasmota_topic: str = ''): + """ + Checks if any Tasmota device has a sensor. + + Args: + tasmota_topic: Optional Tasmota topic to check specifically. + + Returns: + True if any Tasmota device has a sensor, False otherwise. + """ + + if tasmota_topic: + return sensor in self.tasmota_devices.get(tasmota_topic, {}).get('sensors', {}) + + return any(sensor in device.get('sensors', {}) for device in self.tasmota_devices.values()) + + def has_energy_sensor(self, tasmota_topic: str = ''): + return self._has_sensor('ENERGY', tasmota_topic) + + def has_env_sensor(self, tasmota_topic: str = ''): + """ + Checks if any Tasmota device has an environmental sensor. + + Args: + tasmota_topic: Optional Tasmota topic to check specifically. + + Returns: + True if any Tasmota device has an environmental sensor, False otherwise. + """ + + if tasmota_topic: + return any(sensor in self.ENV_SENSOR for sensor in self.tasmota_devices.get(tasmota_topic, {}).get('sensors', [])) + + return any(any(sensor in self.ENV_SENSOR for sensor in device.get('sensors', [])) for device in self.tasmota_devices.values()) + + def has_ds18b20_sensor(self, tasmota_topic: str = ''): + return self._has_sensor('DS18B20', tasmota_topic) + + def has_am2301_sensor(self, tasmota_topic: str = ''): + return self._has_sensor('AM2301', tasmota_topic) + + def has_sht3x_sensor(self, tasmota_topic: str = ''): + return self._has_sensor('SHT3X', tasmota_topic) + + def has_other_sensor(self, tasmota_topic: str = ''): + """ + Checks if any Tasmota device has sensors other than those defined in self.SENSORS. + + Args: + tasmota_topic: Optional Tasmota topic to check specifically. + + Returns: + True if any device has sensors not in self.SENSORS, False otherwise. + """ + + if tasmota_topic: + for sensor in self.tasmota_devices.get(tasmota_topic, {}).get('sensors', []): if sensor not in self.SENSORS: return True - return False + + return any(any(sensor not in self.SENSORS for sensor in device.get('sensors', [])) + for device in self.tasmota_devices.values()) + ################################################################## # Utilities diff --git a/tasmota/locale.yaml b/tasmota/locale.yaml old mode 100755 new mode 100644 diff --git a/tasmota/plugin.yaml b/tasmota/plugin.yaml index c154faea9..afb904f5a 100644 --- a/tasmota/plugin.yaml +++ b/tasmota/plugin.yaml @@ -12,8 +12,8 @@ plugin: documentation: http://smarthomeng.de/user/plugins/tasmota/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1520293-support-thread-für-das-tasmota-plugin - version: 1.5.2 # Plugin version - sh_minversion: '1.9.3' # minimum shNG version to use this plugin + version: 1.6.0 # Plugin version + sh_minversion: 1.10.0.3 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) # py_minversion: # minimum Python version to use for this plugin multi_instance: True # plugin supports multi instance @@ -38,7 +38,6 @@ parameters: de: 'Zeitabstand in Sekunden in dem die Tasmota Devices Telemetrie Daten senden sollen' en: 'Timeperiod in seconds in which Tasmota devices shall send telemetry data' - item_attributes: tasmota_topic: type: str @@ -128,5 +127,3 @@ item_structs: NONE plugin_functions: NONE logic_parameters: NONE - - diff --git a/tasmota/user_doc.rst b/tasmota/user_doc.rst old mode 100755 new mode 100644 index 58ccec3cf..e1c2152c3 --- a/tasmota/user_doc.rst +++ b/tasmota/user_doc.rst @@ -60,6 +60,15 @@ folgenden Beispiel gezeigt: tasmota_zb_device: snzb02_01 tasmota_zb_attr: Temperature +Das Plugin versucht eine ZigBee-Bridge automatisch zu erkennen. Zudem kann diese auch per Attribut dediziert definiert werden: + +.. code-block:: yaml + + temp: + type: num + tasmota_topic: ZB-GW03_01 + tasmota_zb_device: bridge + Für die Nutzung von SML Devices über ein Tasmota-Gerät müssen in dem entsprechenden Item die drei Attribute ``tasmota_topic``, ``tasmota_sml_device`` und ``tasmota_sml_attr`` konfiguriert werden, wie im folgenden Beispiel gezeigt: @@ -147,7 +156,7 @@ Die folgenden Attribute (Werte für das Item-Attribut ``tasmota-attr``) sind bis * "rf_send": Zu sendende RF Daten bei Tasmota Device mit RF Sendemöglichkeit (SONOFF RF Bridge) -> dict {'RfSync': 12220, 'RfLow': 440, 'RfHigh': 1210, 'RfCode':'#F06104'}, r/w * "rf_key_send": Zu sendender RF-Key Tasmota Device mit RF Sendemöglichkeit (SONOFF RF Bridge) -> num [1-16], r/w * "rf_key_recv": Zu empfangender RF-Key Tasmota Device mit RF Sendemöglichkeit (SONOFF RF Bridge) -> num [1-16], r/w - * rf_key: 'RF Key' + * "rf_key": RF Key * "zb_permit_join": Schaltet das Pairing an der ZigBee Bridge ein/aus -> bool, r/w * "zb_forget": Löscht das Zigbee-Gerät aus dem Item Wert aus der Liste bekannter Geräte in der Zigbee-Bridge -> str, r/w * "zb_ping": Sendet ein Ping zum Zigbee-Gerät aus dem Item Wert -> str, r/w diff --git a/tasmota/user_doc/assets/webif_tab1.jpg b/tasmota/user_doc/assets/webif_tab1.jpg old mode 100755 new mode 100644 diff --git a/tasmota/user_doc/assets/webif_tab2.jpg b/tasmota/user_doc/assets/webif_tab2.jpg old mode 100755 new mode 100644 diff --git a/tasmota/user_doc/assets/webif_tab3.jpg b/tasmota/user_doc/assets/webif_tab3.jpg old mode 100755 new mode 100644 diff --git a/tasmota/user_doc/assets/webif_tab4.jpg b/tasmota/user_doc/assets/webif_tab4.jpg old mode 100755 new mode 100644 diff --git a/tasmota/user_doc/assets/webif_tab5.jpg b/tasmota/user_doc/assets/webif_tab5.jpg old mode 100755 new mode 100644 diff --git a/tasmota/user_doc/assets/webif_tab6.jpg b/tasmota/user_doc/assets/webif_tab6.jpg old mode 100755 new mode 100644 diff --git a/tasmota/webif/__init__.py b/tasmota/webif/__init__.py old mode 100755 new mode 100644 index 893d51ce3..49ad9d31e --- a/tasmota/webif/__init__.py +++ b/tasmota/webif/__init__.py @@ -66,23 +66,19 @@ def index(self, reload=None): """ self.plugin.get_broker_info() - pagelength = self.plugin.get_parameter_value('webif_pagelength') tmpl = self.tplenv.get_template('index.html') - items = self.plugin.get_item_list() - return tmpl.render(p=self.plugin, - webif_pagelength=pagelength, - items=items, - item_count=len(items), - plugin_shortname=self.plugin.get_shortname(), - plugin_version=self.plugin.get_version(), - plugin_info=self.plugin.get_info(), + webif_pagelength=self.plugin.get_parameter_value('webif_pagelength'), + item_count=len(self.plugin.get_item_list()), + zigbee = True if self.plugin.tasmota_zigbee_devices else False, + broker_config = self.plugin.broker_config, + full_topic = self.plugin.full_topic, maintenance=True if self.plugin.log_level == 10 else False, ) @cherrypy.expose - def get_data_html(self, dataSet=None): + def get_data_html(self, dataSet=None, params=None): """ Return data to update the webpage @@ -91,37 +87,94 @@ 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 + + self.logger.debug(f"get_data_html: {dataSet=}, {params=}") + + data = dict() + + if dataSet == "items_info": + data[dataSet] = {} + for item in self.plugin.get_item_list(): + item_data = { + 'value': item.property.value, + 'type': item.property.type, + 'topic': self.plugin.get_iattr_value(item.conf, 'tasmota_topic'), + 'relais': self._get_relay_value(item), + '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'), + } + data['items_info'][item.property.path] = item_data + + elif dataSet == "devices_info": + data[dataSet] = {} + for device_name, device_data in self.plugin.tasmota_devices.items(): + device_data = device_data.copy() + device_data.pop('discovery_config', None) + data[dataSet][device_name] = device_data + + elif dataSet == "zigbee_info": + data[dataSet] = self.plugin.tasmota_zigbee_devices.copy() + + elif dataSet == "broker_info": self.plugin.get_broker_info() - data = dict() - data['broker_info'] = self.plugin._broker - data['broker_uptime'] = self.plugin.broker_uptime() + data[dataSet] = self.plugin._broker.copy() + data[dataSet]['broker_uptime'] = self.plugin.broker_uptime() + + elif dataSet == "details_info": + data['devices_info'] = {} + for device_name, device_data in self.plugin.tasmota_devices.items(): + device_data = device_data.copy() + device_data.pop('discovery_config', None) + data['devices_info'][device_name] = device_data + data['zigbee_info'] = self.plugin.tasmota_zigbee_devices.copy() + + # return it as json the web page + try: + return json.dumps(data, default=str) + except Exception as e: + self.logger.error("get_data_html exception: {}".format(e)) + return {} - data['item_values'] = {} - for item in self.plugin.get_item_list(): - data['item_values'][item.property.path] = {} - data['item_values'][item.property.path]['value'] = item.property.value - data['item_values'][item.property.path]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') - data['item_values'][item.property.path]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') - - data['device_values'] = {} - for device in self.plugin.tasmota_devices: - data['device_values'][device] = {} - data['device_values'][device]['online'] = self.plugin.tasmota_devices[device].get('online', '-') - data['device_values'][device]['uptime'] = self.plugin.tasmota_devices[device].get('uptime', '-') - data['device_values'][device]['fw_ver'] = self.plugin.tasmota_devices[device].get('fw_ver', '-') - data['device_values'][device]['wifi_signal'] = self.plugin.tasmota_devices[device].get('wifi_signal', '-') - data['device_values'][device]['sensors'] = self.plugin.tasmota_devices[device].get('sensors', '-') - data['device_values'][device]['lights'] = self.plugin.tasmota_devices[device].get('lights', '-') - data['device_values'][device]['rf'] = self.plugin.tasmota_devices[device].get('rf', '-') - - data['tasmota_zigbee_devices'] = self.plugin.tasmota_zigbee_devices - - # return it as json the web page - try: - return json.dumps(data, default=str) - except Exception as e: - self.logger.error("get_data_html exception: {}".format(e)) - return {} - return + + @cherrypy.expose + def submit(self, cmd=None, params=None): + + self.logger.debug(f"submit: {cmd=}, {params=}") + result = None + + if cmd == "zbstatus": + result = self.plugin._poll_zigbee_devices() + + elif cmd == "tasmota_status": + result = self.plugin._interview_device(params) + + elif cmd == "zb_ping": + result = self.plugin._poll_zigbee_device(params) + + self.logger.debug(f"submit: {cmd=}, {params=} --> {result=}") + + 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') + + def _get_relay_value(self, item): + """ + Determines the relay value based on item configuration. + + Args: + item: The item object containing configuration data. + Returns: + The relay value as a string. + """ + + relay = self.plugin.get_iattr_value(item.conf, 'tasmota_relay') + if relay in ['1', '2', '3', '4', '5', '6', '7', '8']: + return relay + + if self.plugin.get_iattr_value(item.conf, 'tasmota_attr') == 'relay': + return "1" + + return "-" + \ No newline at end of file diff --git a/tasmota/webif/static/img/plugin_logo.svg b/tasmota/webif/static/img/plugin_logo.svg old mode 100755 new mode 100644 diff --git a/tasmota/webif/static/img/readme.txt b/tasmota/webif/static/img/readme.txt old mode 100755 new mode 100644 diff --git a/tasmota/webif/templates/index.html b/tasmota/webif/templates/index.html old mode 100755 new mode 100644 index 17f5e42f1..93aa78477 --- a/tasmota/webif/templates/index.html +++ b/tasmota/webif/templates/index.html @@ -1,764 +1,1408 @@ {% extends "base_plugin.html" %} + {% set logo_frame = false %} -{% set update_interval = [(((10 * item_count) / 1000) | round | int) * 1000, 5000]|max %} +{% set update_interval = 0 %} + + +{% set update_interval = (200 * (log_array | length)) %} + + +{% set dataSet = 'devices_info' %} + + +{% set update_params = item_id %} + + +{% set buttons = true %} + + +{% set autorefresh_buttons = true %} + + +{% set reload_button = true %} - + +{% set close_button = true %} + + +{% set row_count = false %} + + +{% set initial_update = false %} + + {% block pluginstyles %} - + {% endblock pluginstyles %} - + {% block pluginscripts %} - - + + + + + + + + + + + + + + + + + + + {% endblock pluginscripts %} + {% block headtable %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{_('Broker Host')}}{{ p.broker_config.host }}{{_('Broker Port')}}{{ p.broker_config.port }}
{{_('Benutzer')}}{{ p.broker_config.user }}{{_('Passwort')}} - {% if p.broker_config.password %} - {% for letter in p.broker_config.password %}*{% endfor %} - {% endif %} -
{{_('QoS')}}{{ p.broker_config.qos }}{{_('full_topic')}}{{ p.full_topic }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{_('Broker Host')}}{{ broker_config.host }}{{_('Broker Port')}}{{ broker_config.port }}{{_('Anzahl Geräte')}}{{ len(p.tasmota_devices) }}
{{_('Benutzer')}}{{ broker_config.user }}{{_('Passwort')}} + {% if broker_config.password %} + {% for letter in broker_config.password %}*{% endfor %} + {% endif %} + {{_('Anzahl Zigbee')}}{{ len(p.tasmota_zigbee_devices) }}
{{_('QoS')}}{{ broker_config.qos }}{{_('full_topic')}}{{ full_topic }}
{% endblock headtable %} - + + {% block buttons %} +
+ +
+
+ + +
{% endblock %} - + {% set tabcount = 6 %} - -{% if p.tasmota_items != [] %} - {% set start_tab = 1 %} + + +{% if not item_count %} + {% set start_tab = 6 %} {% endif %} + -{% if items != [] %} - {% set tab1title = _("" ~ plugin_shortname ~ " Items") %} +{% if item_count %} + {% set tab1title = _("" " Items") %} {% else %} {% set tab1title = "hidden" %} {% endif %} -{% set tab2title = _("" ~ plugin_shortname ~ " Devices") %} -{% set tab3title = _("" ~ plugin_shortname ~ " " ~ _('Details') ~ "") %} -{% set tab4title = _("" ~ plugin_shortname ~ " " ~ _('Zigbee Devices') ~ "") %} -{% set tab5title = _("" ~ " Broker Information") %} + +{% set tab2title = _("" " Devices") %} + +{% set tab3title = _("" ~ _('Sensor Data') ~ "") %} + +{% if zigbee %} + {% set tab4title = _("" ~ _('Zigbee Devices') ~ "") %} +{% else %} + {% set tab4title = "hidden" %} +{% endif %} + +{% set tab5title = _("" " Broker Information") %} + {% if maintenance %} - {% set tab6title = _("" ~ plugin_shortname ~ " " ~ _('Maintenance') ~ "") %} + {% set tab6title = _("" ~ _('Device Details') ~ "") %} {% else %} {% set tab6title = "hidden" %} {% endif %} - + {% block bodytab1 %} -
-

Item Information

- - - - - - - - - - - - - - - {% for item in items %} - - - - - - - {% if p.get_iattr_value(item.conf, 'tasmota_relay') is in ['1', '2', '3', '4', '5', '6', '7', '8'] %} - - {% elif p.get_iattr_value(item.conf, 'tasmota_attr') == 'relay' %} - - {% else %} - - {% endif %} - - - - {% endfor %} - -
{{ _('Item') }}{{ _('Typ') }}{{ _('Wert') }}{{ _('Tasmota Topic') }}{{ _('Relais') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item._path }}{{ item._type }}{{ item() }}{{ p.get_iattr_value(item.conf, 'tasmota_topic') }}{{ p.get_iattr_value(item.conf, 'tasmota_relay') }}1-{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
-
+

Item Information

+ +
{% endblock %} {% block bodytab2 %} -
-

Device Information

- - - - - - - - - - - - - - - - - - - {% for device in p.tasmota_devices %} - {% if 'fw_ver' in p.tasmota_devices[device] %} - - - - - - - - - - - - {% if p.tasmota_devices[device]['wifi_signal'] %} - - {% else %} - - {% endif %} - - - {% endif %} - {% endfor %} - -
{{ _('Tasmota Topic') }}{{ _('Online') }}{{ _('Friendy Name') }}{{ _('Mac Adresse') }}{{ _('IP Adresse') }}{{ _('Uptime') }}{{ _('Sensor Type') }}{{ _('Firmware') }}{{ _('Module') }}{{ _('Wifi') }}{{ _('Details') }}
{{ device }}{{ p.tasmota_devices[device].online }}{{ p.tasmota_devices[device].friendly_name }}{{ p.tasmota_devices[device].mac }}{{ p.tasmota_devices[device].ip }}{{ p.tasmota_devices[device].uptime }} - {% if p.tasmota_devices[device]['sensors'] != {} %} - {% for key in p.tasmota_devices[device]['sensors'] %} - {{ key }} - {%if not loop.last%}, {%endif%} - {% endfor %} - {% else %} - - - {% endif %} - {{ p.tasmota_devices[device].fw_ver }}{{ p.tasmota_devices[device].module }}{{ p.tasmota_devices[device].wifi_signal }} dBm - - {% for entry in p.tasmota_devices[device]['discovery_config'] %} - - - - - {% endfor %} -
{{ entry }}:{{ p.tasmota_devices[device]['discovery_config'][entry] }}
-
- -
+

Device Information

+ +
{% endblock %} {% block bodytab3 %} -
-{% if p.has_energy_sensor() %} -

ENERGY SENSORS

- - - - - - - - - - - - - - - - - {% for device in p.tasmota_devices %} - {% if p.tasmota_devices[device]['sensors']['ENERGY'] %} - - - - - - - - - - - - - {% endif %} - {% endfor %} - -
{{ _('Tasmota Topic') }}{{ _('Spannung') }}{{ _('Strom') }}{{ _('Leistung') }}{{ _('Heute') }}{{ _('Gestern') }}{{ _('Gesamt') }}{{ _('Gesamt - Startzeit') }}
{{ device }}{{ p.tasmota_devices[device]['sensors']['ENERGY']['voltage'] }}V.{{ p.tasmota_devices[device]['sensors']['ENERGY']['current'] }}A.{{ p.tasmota_devices[device]['sensors']['ENERGY']['power'] }}W{{ p.tasmota_devices[device]['sensors']['ENERGY']['today'] }}kWh{{ p.tasmota_devices[device]['sensors']['ENERGY']['yesterday'] }}kWh{{ p.tasmota_devices[device]['sensors']['ENERGY']['total'] }}kWh{{ p.tasmota_devices[device]['sensors']['ENERGY']['total_starttime'] }}
-
-
-{% endif %} + {% if p.has_energy_sensor() %} +

ENERGY SENSORS

+ +
-{% if p.has_env_sensor() %} -

ENVIRONMENTAL SENSORS

- - - - - - - - - - - - - {% if p.has_ds18b20_sensor() %} - {% for device in p.tasmota_devices %} - {% if p.tasmota_devices[device]['sensors'] %} - {% if p.tasmota_devices[device]['sensors']['DS18B20'] %} - - - - - - - - - {% endif %} - {% endif %} - {% endfor %} - {% endif %} - {% if p.has_am2301_sensor() or p.has_sht3x_sensor() %} - {% for device in p.tasmota_devices %} - {% if p.tasmota_devices[device]['sensors'] %} - {% if p.tasmota_devices[device]['sensors']['AM2301'] %} - - - - - - - - - {% endif %} - {% if p.tasmota_devices[device]['sensors']['SHT3X'] %} - - - - - - - - - {% endif %} - {% endif %} - {% endfor %} - {% endif %} - -
{{ _('Tasmota Topic') }}{{ _('Temperatur') }}{{ _('Luftfeuchtigkeit') }}{{ _('Taupunkt') }}{{ _('1w-ID') }}
{{ device }}{{ p.tasmota_devices[device]['sensors']['DS18B20'].temperature }}°C.--{{ p.tasmota_devices[device]['sensors']['DS18B20'].id }}
{{ device }}{{ p.tasmota_devices[device]['sensors']['AM2301'].temperature }}°C.{{ p.tasmota_devices[device]['sensors']['AM2301'].humidity }}%rH.{{ p.tasmota_devices[device]['sensors']['AM2301'].dewpoint }}°C.-
{{ device }}{{ p.tasmota_devices[device]['sensors']['SHT3X'].temperature }}°C.{{ p.tasmota_devices[device]['sensors']['SHT3X'].humidity }}%rH.{{ p.tasmota_devices[device]['sensors']['SHT3X'].dewpoint }}°C.-
-
-
-{% endif %} +
+
+ {% endif %} -{% if p.has_other_sensor() %} -

OTHER SENSORS

- - - - - - - - - - {% for device in p.tasmota_devices %} - {% for sensor in p.tasmota_devices[device]['sensors'] %} - {% if sensor not in p.SENSORS %} - - - - - - {% endif %} - {% endfor %} - {% endfor %} - -
{{ _('Sensor') }}{{ _('Sensor Details') }}
{{ sensor }}{{ p.tasmota_devices[device]['sensors'][sensor] }}
-{% endif %} + {% if p.has_env_sensor() %} +

ENVIRONMENTAL SENSORS

+ +
-{% if p.has_lights() %} -

LIGHTS

- - - - - - - - - - - - - - - - - {% for device in p.tasmota_devices %} - {% if p.tasmota_devices[device]['lights'] %} - - - - - - - - - - - - - {% endif %} - {% endfor %} - -
{{ _('Tasmota Topic') }}{{ _('HSB') }}{{ _('Dimmer') }}{{ _('Color') }}{{ _('CT') }}{{ _('Scheme') }}{{ _('Fade') }}{{ _('Speed') }}{{ _('LED-Table') }}
{{ device }}{{ p.tasmota_devices[device]['lights'].hsb }}.{{ p.tasmota_devices[device]['lights'].dimmer }}.{{ p.tasmota_devices[device]['lights'].color }}.{{ p.tasmota_devices[device]['lights'].ct }}.{{ p.tasmota_devices[device]['lights'].scheme }}.{{ p.tasmota_devices[device]['lights'].fade }}.{{ p.tasmota_devices[device]['lights'].speed }}.{{ p.tasmota_devices[device]['lights'].ledtable }}.
- -
-
-{% endif %} +
+
+ {% endif %} -{% if p.has_rf() %} -

RF

- - - - - - - - - - - - {% for device in p.tasmota_devices %} - {% if p.tasmota_devices[device]['rf'] %} - - - - - - - - {% endif %} - {% endfor %} - -
{{ _('Tasmota Topic') }}{{ _('RF-Received') }}{{ _('RF-Send Result') }}{{ _('RF-Key Result') }}
{{ device }}{{ p.tasmota_devices[device]['rf'].rf_received }}{{ p.tasmota_devices[device]['rf'].rf_send_result }}{{ p.tasmota_devices[device]['rf'].rfkey_result }}
- -
-
-{% endif %} -
+ {% if p.has_other_sensor() %} +

OTHER SENSORS

+ +
+ +
+
+ {% endif %} + + {% if p.has_lights() %} +

LIGHTS

+ +
+ +
+
+ {% endif %} + + {% if p.has_rf() %} +

RF

+ +
+ +
+
+ {% endif %} {% endblock %} {% block bodytab4 %} -
-

Zigbee Information

- - - - - - - - - - - - - - - - {% for device in p.tasmota_zigbee_devices %} - - - - - - - - - - - - {% endfor %} - -
{{ _('Device ID') }}{{ _('IEEEAddr') }}{{ _('Hersteller') }}{{ _('ModelId') }}{{ _('LinkQuality') }}{{ _('Battery %') }}{{ _('LastSeen') }}{{ _('Data') }}
{{ device }}{{ p.tasmota_zigbee_devices[device]['ieeeaddr'] }}{{ p.tasmota_zigbee_devices[device]['manufacturer'] }}{{ p.tasmota_zigbee_devices[device]['modelid'] }}{{ p.tasmota_zigbee_devices[device]['linkquality'] }}{{ p.tasmota_zigbee_devices[device]['batterypercentage'] }}{{ p.tasmota_zigbee_devices[device]['lastseenepoch'] }}{{ p.tasmota_zigbee_devices[device] }}
-
+

Zigbee Information

+ + + +
{% endblock %} {% block bodytab5 %} -
-

Broker Information

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if p.broker_monitoring %} - - - - - {% endif %} - - - -
{{ _('Merkmal') }}{{ _('Wert') }}
{{ 'Broker Version' }}{{ p._broker.version }}
{{ 'Active Clients' }}{{ p._broker.active_clients }}
{{ 'Subscriptions' }}{{ p._broker.subscriptions }}
{{ 'Messages stored' }}{{ p._broker.stored_messages }}
{{ 'Retained Messages' }}{{ p._broker.retained_messages }}
{{ _('Laufzeit') }}{{ p.broker_uptime() }}
- {% if p.broker_monitoring %} -
-
-

Broker Monitor

- - - - - - - - - - - - - - - - - - - - - -
{{ _('Message Durchsatz') }}{{ _('letzte Minute') }}{{ _('letzte 5 Min.') }}{{ _('letzte 15 Min.') }}
{{ _('Durchschnittlich Messages je Minute empfangen') }}     {{ p._broker.msg_rcv_1min }}     {{ p._broker.msg_rcv_5min }}     {{ p._broker.msg_rcv_15min }}
{{ _('Durchschnittlich Messages je Minute gesendet') }}     {{ p._broker.msg_snt_1min }}     {{ p._broker.msg_snt_5min }}     {{ p._broker.msg_snt_15min }}
-{% endif %} -
+

Broker Information

+ +
+ + {% if p.broker_monitoring %} +
+
+

Broker Monitor

+ +
+ {% endif %} {% endblock %} {% block bodytab6 %} -
- - - - - - - - - {% for device in p.tasmota_devices %} - - - - - {% endfor %} - -
{{ _('Tasmota Device') }}{{ _('Tasmota Device Details') }}
{{ device }}{{ p.tasmota_devices[device] }}
-
- -
-
- -
- - - - - - - - - {% for item in p.get_item_list() %} - - - - - {% endfor %} - -
{{ _('Tasmota Items') }}{{ _('Tasmota Item Config') }}
{{ item }}{{ p.get_item_config(item) }}
-
- -
-
- -
- - - - - - - - - {% for device in p.tasmota_zigbee_devices %} - - - - - {% endfor %} - -
{{ _('Zigbee Device') }}{{ _('Zigbee Device Details') }}
{{ device }}{{ p.tasmota_zigbee_devices[device] }}
-
-{% endblock %} - +

Tasmota Device Details

+ +
+
+
+

Tasmota Zigbee-Device Details

+ +
+{% endblock %} \ No newline at end of file