# MQTT discovery plugin # """ MQTT discovery, compatible with home-assistant.

Specify MQTT server and port.

Automatically creates Domoticz device entries for all discovered devices.
""" import Domoticz # from Domoticz import Devices # Used for local debugging without Domoticz # from Domoticz import Settings # Used for local debugging without Domoticz from datetime import datetime from itertools import count, filterfalse import json import re import time import traceback class MqttClient: Address = "" Port = "" mqttConn = None isConnected = False mqttConnectedCb = None mqttDisconnectedCb = None mqttPublishCb = None def __init__( self, destination, port, mqttConnectedCb, mqttDisconnectedCb, mqttPublishCb, mqttSubackCb, ): Domoticz.Debug("MqttClient::__init__") self.Address = destination self.Port = port self.mqttConnectedCb = mqttConnectedCb self.mqttDisconnectedCb = mqttDisconnectedCb self.mqttPublishCb = mqttPublishCb self.mqttSubackCb = mqttSubackCb self.Open() def __str__(self): Domoticz.Debug("MqttClient::__str__") if self.mqttConn != None: return str(self.mqttConn) else: return "None" def Open(self): Domoticz.Debug("MqttClient::Open") if self.mqttConn != None: self.Close() self.isConnected = False self.mqttConn = Domoticz.Connection( Name=self.Address, Transport="TCP/IP", Protocol="MQTT", Address=self.Address, Port=self.Port, ) self.mqttConn.Connect() def Connect(self): Domoticz.Debug("MqttClient::Connect") if self.mqttConn == None: self.Open() else: ID = ( "Domoticz_" + Parameters["Key"] + "_" + str(Parameters["HardwareID"]) + "_" + str(int(time.time())) ) Domoticz.Log("MQTT CONNECT ID: '" + ID + "'") self.mqttConn.Send({"Verb": "CONNECT", "ID": ID}) def Ping(self): Domoticz.Debug("MqttClient::Ping") if self.mqttConn == None or not self.isConnected: self.Open() else: self.mqttConn.Send({"Verb": "PING"}) def Publish(self, topic, payload, retain=0): Domoticz.Log("MqttClient::Publish " + topic + " (" + payload + ")") if self.mqttConn == None or not self.isConnected: self.Open() else: self.mqttConn.Send( { "Verb": "PUBLISH", "Topic": topic, "Payload": bytearray(payload, "utf-8"), "Retain": retain, } ) def Subscribe(self, topics): Domoticz.Debug("MqttClient::Subscribe") subscriptionlist = [] for topic in topics: subscriptionlist.append({"Topic": topic, "QoS": 0}) if self.mqttConn == None or not self.isConnected: self.Open() else: self.mqttConn.Send({"Verb": "SUBSCRIBE", "Topics": subscriptionlist}) def Close(self): Domoticz.Log("MqttClient::Close") # TODO: Disconnect from server self.mqttConn = None self.isConnected = False def onConnect(self, Connection, Status, Description): Domoticz.Debug("MqttClient::onConnect") if Status == 0: Domoticz.Log( "Successful connect to: " + Connection.Address + ":" + Connection.Port ) self.Connect() else: Domoticz.Log( "Failed to connect to: " + Connection.Address + ":" + Connection.Port + ", Description: " + Description ) def onDisconnect(self, Connection): Domoticz.Log( "MqttClient::onDisonnect Disconnected from: " + Connection.Address + ":" + Connection.Port ) self.Close() # TODO: Reconnect? if self.mqttDisconnectedCb != None: self.mqttDisconnectedCb() def onMessage(self, Connection, Data): topic = "" if "Topic" in Data: topic = Data["Topic"] payloadStr = "" if "Payload" in Data: payloadStr = Data["Payload"].decode("utf8", "replace") payloadStr = str(payloadStr.encode("unicode_escape")) # Domoticz.Debug("MqttClient::onMessage called for connection: '"+Connection.Name+"' type:'"+Data['Verb']+"' topic:'"+topic+"' payload:'" + payloadStr + "'") if Data["Verb"] == "CONNACK": self.isConnected = True if self.mqttConnectedCb != None: self.mqttConnectedCb() if Data["Verb"] == "SUBACK": if self.mqttSubackCb != None: self.mqttSubackCb() if Data["Verb"] == "PUBLISH": if self.mqttPublishCb != None: self.mqttPublishCb(topic, Data["Payload"]) CONF_DEVICE = "device" TOPIC_BASE = "~" ABBREVIATIONS = { "aux_cmd_t": "aux_command_topic", "aux_stat_tpl": "aux_state_template", "aux_stat_t": "aux_state_topic", "avty_t": "availability_topic", "away_mode_cmd_t": "away_mode_command_topic", "away_mode_stat_tpl": "away_mode_state_template", "away_mode_stat_t": "away_mode_state_topic", "bri_cmd_t": "brightness_command_topic", "bri_scl": "brightness_scale", "bri_stat_t": "brightness_state_topic", "bri_val_tpl": "brightness_value_template", "clr_temp_cmd_tpl": "color_temp_command_template", "bat_lev_t": "battery_level_topic", "bat_lev_tpl": "battery_level_template", "chrg_t": "charging_topic", "chrg_tpl": "charging_template", "clr_temp_cmd_t": "color_temp_command_topic", "clr_temp_stat_t": "color_temp_state_topic", "clr_temp_val_tpl": "color_temp_value_template", "cln_t": "cleaning_topic", "cln_tpl": "cleaning_template", "cmd_t": "command_topic", "curr_temp_t": "current_temperature_topic", "dev": "device", "dev_cla": "device_class", "dock_t": "docked_topic", "dock_tpl": "docked_template", "err_t": "error_topic", "err_tpl": "error_template", "fanspd_t": "fan_speed_topic", "fanspd_tpl": "fan_speed_template", "fanspd_lst": "fan_speed_list", "fx_cmd_t": "effect_command_topic", "fx_list": "effect_list", "fx_stat_t": "effect_state_topic", "fx_val_tpl": "effect_value_template", "exp_aft": "expire_after", "fan_mode_cmd_t": "fan_mode_command_topic", "fan_mode_stat_tpl": "fan_mode_state_template", "fan_mode_stat_t": "fan_mode_state_topic", "frc_upd": "force_update", "hold_cmd_t": "hold_command_topic", "hold_stat_tpl": "hold_state_template", "hold_stat_t": "hold_state_topic", "ic": "icon", "init": "initial", "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "max_temp": "max_temp", "min_temp": "min_temp", "mode_cmd_t": "mode_command_topic", "mode_stat_tpl": "mode_state_template", "mode_stat_t": "mode_state_topic", "name": "name", "on_cmd_type": "on_command_type", "opt": "optimistic", "osc_cmd_t": "oscillation_command_topic", "osc_stat_t": "oscillation_state_topic", "osc_val_tpl": "oscillation_value_template", "pl_arm_away": "payload_arm_away", "pl_arm_home": "payload_arm_home", "pl_avail": "payload_available", "pl_cls": "payload_close", "pl_disarm": "payload_disarm", "pl_hi_spd": "payload_high_speed", "pl_lock": "payload_lock", "pl_lo_spd": "payload_low_speed", "pl_med_spd": "payload_medium_speed", "pl_not_avail": "payload_not_available", "pl_off": "payload_off", "pl_on": "payload_on", "pl_open": "payload_open", "pl_osc_off": "payload_oscillation_off", "pl_osc_on": "payload_oscillation_on", "pl_stop": "payload_stop", "pl_unlk": "payload_unlock", "pow_cmd_t": "power_command_topic", "ret": "retain", "rgb_cmd_tpl": "rgb_command_template", "rgb_cmd_t": "rgb_command_topic", "rgb_stat_t": "rgb_state_topic", "rgb_val_tpl": "rgb_value_template", "send_cmd_t": "send_command_topic", "send_if_off": "send_if_off", "set_pos_tpl": "set_position_template", "set_pos_t": "set_position_topic", "spd_cmd_t": "speed_command_topic", "spd_stat_t": "speed_state_topic", "spd_val_tpl": "speed_value_template", "spds": "speeds", "stat_clsd": "state_closed", "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", "stat_t": "state_topic", "stat_val_tpl": "state_value_template", "sup_feat": "supported_features", "swing_mode_cmd_t": "swing_mode_command_topic", "swing_mode_stat_tpl": "swing_mode_state_template", "swing_mode_stat_t": "swing_mode_state_topic", "temp_cmd_t": "temperature_command_topic", "temp_stat_tpl": "temperature_state_template", "temp_stat_t": "temperature_state_topic", "tilt_clsd_val": "tilt_closed_value", "tilt_cmd_t": "tilt_command_topic", "tilt_inv_stat": "tilt_invert_state", "tilt_max": "tilt_max", "tilt_min": "tilt_min", "tilt_opnd_val": "tilt_opened_value", "tilt_status_opt": "tilt_status_optimistic", "tilt_status_t": "tilt_status_topic", "t": "topic", "uniq_id": "unique_id", "unit_of_meas": "unit_of_measurement", "val_tpl": "value_template", "whit_val_cmd_t": "white_value_command_topic", "whit_val_scl": "white_value_scale", "whit_val_stat_t": "white_value_state_topic", "whit_val_tpl": "white_value_template", "xy_cmd_t": "xy_command_topic", "xy_stat_t": "xy_state_topic", "xy_val_tpl": "xy_value_template", } DEVICE_ABBREVIATIONS = { "cns": "connections", "ids": "identifiers", "name": "name", "mf": "manufacturer", "mdl": "model", "sw": "sw_version", } class BasePlugin: # MQTT settings mqttClient = None mqttserveraddress = "" mqttserverport = "" debugging = "Normal" cachedDeviceNames = {} options = { "addDiscoveredDeviceUsed": True, # Newly discovered devices added as "used" (visible in swithces tab) or not (only visible in devices list) "updateRSSI": False, # Store Tasmota RSSI "updateVCC": False, } # Store Tasmota VCC as battery level def copyDevices(self): # self.cachedDevices = copy.deepcopy(Devices) for k, Device in Devices.items(): self.cachedDeviceNames[k] = Device.Name def deviceStr(self, unit): name = "" if unit in Devices: name = Devices[unit].Name return format(unit, "03d") + "/" + name def getUnit(self, device): unit = -1 for k, dev in Devices.items(): if dev == device: unit = k return unit def onStart(self): # Parse options self.debugging = Parameters["Mode6"] DumpConfigToLog() if self.debugging == "Verbose+": Domoticz.Debugging(2 + 4 + 8 + 16 + 64) if self.debugging == "Verbose": Domoticz.Debugging(2 + 4 + 8 + 16 + 64) if self.debugging == "Debug": Domoticz.Debugging(2 + 4 + 8) self.mqttserveraddress = Parameters["Address"].replace(" ", "") self.mqttserverport = Parameters["Port"].replace(" ", "") self.discoverytopic = Parameters["Mode2"] self.ignoredtopics = Parameters["Mode4"].split(",") options = "" try: options = json.loads(Parameters["Mode3"]) except ValueError: options = Parameters["Mode3"] if type(options) == str or type(options) == int: # JSON decoding failed, check for deprecated used/unused setting # # Domoticz.Log( "Warning: could not load plugin options '" + Parameters["Mode3"] + "' as JSON object" ) try: if int(options) == 0: self.options["addDiscoveredDeviceUsed"] = False if int(options) == 1: self.options["addDiscoveredDeviceUsed"] = True except ValueError: # Options not a valid int pass elif type(options) == dict: self.options.add(options) Domoticz.Log("Plugin options: " + str(self.options)) # Enable heartbeat Domoticz.Heartbeat(10) # Connect to MQTT server self.prefixpos = 0 self.topicpos = 0 self.discoverytopiclist = self.discoverytopic.split("/") self.mqttClient = MqttClient( self.mqttserveraddress, self.mqttserverport, self.onMQTTConnected, self.onMQTTDisconnected, self.onMQTTPublish, self.onMQTTSubscribed, ) self.copyDevices() def onConnect(self, Connection, Status, Description): self.mqttClient.onConnect(Connection, Status, Description) def onDisconnect(self, Connection): self.mqttClient.onDisconnect(Connection) def onMessage(self, Connection, Data): self.mqttClient.onMessage(Connection, Data) def onMQTTConnected(self): Domoticz.Debug("onMQTTConnected") self.mqttClient.Subscribe(self.getTopics()) def onMQTTDisconnected(self): Domoticz.Debug("onMQTTDisconnected") def onMQTTPublish(self, topic, rawmessage): validJSON = False message = "" try: message = json.loads(rawmessage.decode("utf8")) validJSON = True except ValueError: message = rawmessage.decode("utf8") topiclist = topic.split("/") if self.debugging == "Verbose" or self.debugging == "Verbose+": DumpMQTTMessageToLog(topic, rawmessage, "onMQTTPublish: ") if topic in self.ignoredtopics: Domoticz.Debug( "Topic: '" + topic + "' included in ignored topics, message ignored" ) return if topic.startswith(self.discoverytopic): discoverytopiclen = len(self.discoverytopiclist) # Discovery topic format: # //[/]/ if ( len(topiclist) == discoverytopiclen + 3 or len(topiclist) == discoverytopiclen + 4 ): component = topiclist[discoverytopiclen] if len(topiclist) == discoverytopiclen + 3: node_id = "" object_id = topiclist[discoverytopiclen + 1] action = topiclist[discoverytopiclen + 2] else: node_id = topiclist[discoverytopiclen + 1] object_id = topiclist[discoverytopiclen + 2] action = topiclist[discoverytopiclen + 3] # Sensor support if (component == "sensor") and (node_id != ""): object_id = node_id if ( validJSON and action == "config" and ( "command_topic" in message or "state_topic" in message or "cmd_t" in message or "stat_t" in message ) ): # Do expansion of the message payload = dict(message) for key in list(payload.keys()): abbreviated_key = key key = ABBREVIATIONS.get(key, key) payload[key] = payload.pop(abbreviated_key) if CONF_DEVICE in payload: device = payload[CONF_DEVICE] for key in list(device.keys()): abbreviated_key = key key = DEVICE_ABBREVIATIONS.get(key, key) device[key] = device.pop(abbreviated_key) base = payload.pop(TOPIC_BASE, None) if base: for key, value in payload.items(): if isinstance(value, str) and value: if value[0] == TOPIC_BASE and key.endswith("_topic"): payload[key] = "{}{}".format(base, value[1:]) if value[-1] == TOPIC_BASE and key.endswith("_topic"): payload[key] = "{}{}".format(value[:-1], base) # Add / update the device self.updateDeviceSettings(object_id, component, payload) else: matchingDevices = self.getDevices(topic=topic) for device in matchingDevices: self.updateSwitch(device, topic, message) # Try to update availability self.updateAvailability(device, topic, message) # Try to update sensor self.updateSensor(device, topic, message) # TODO: Try to update binary sensor # self.updateBinarySensor(device, topic, message) # TODO: Try to update tasmota status self.updateTasmotaStatus(device, topic, message) # Special handling of Tasmota STATE message topic2, matches = re.subn(r"\/STATUS\d+$", "/STATE", topic) if matches > 0: topic2, matches = re.subn(r"\/stat\/", "/tele/", topic2) if matches > 0: matchingDevices = self.getDevices(topic=topic2) for device in matchingDevices: # Try to update tasmota settings self.updateTasmotaSettings(device, topic, message) def onMQTTSubscribed(self): # (Re)subscribed, refresh device info Domoticz.Debug("onMQTTSubscribed") matchingDevices = self.getDevices(hasconfigkey="tasmota_tele_topic") topics = set() for device in matchingDevices: # Refresh Tasmota specific data try: configdict = json.loads(device.Options["config"]) cmnd_topic = re.sub( r"^tele\/", "cmnd/", configdict["tasmota_tele_topic"] ) # Replace tele with cmnd cmnd_topic = re.sub( r"\/tele\/", "/cmnd/", cmnd_topic ) # Replace tele with cmnd cmnd_topic = re.sub(r"\/STATE", "", cmnd_topic) # Remove '/STATE' if cmnd_topic not in topics: self.refreshConfiguration(cmnd_topic) topics.add(cmnd_topic) except (ValueError, KeyError, TypeError) as e: # Domoticz.Error("onMQTTSubscribed: Error: " + str(e)) Domoticz.Error(traceback.format_exc()) # ==========================================================DASHBOARD COMMAND============================================================= def onCommand(self, Unit, Command, Level, sColor): Domoticz.Log( "onCommand " + self.deviceStr(Unit) + ": Command: '" + str(Command) + "', Level: " + str(Level) + ", Color:" + str(sColor) ) if Unit in Devices: try: # TODO: Make sure the relevant command topic exists configdict = json.loads(Devices[Unit].Options["config"]) if Command == "Set Level" and "set_position_topic" in configdict: self.mqttClient.Publish( configdict["set_position_topic"], str(Level) ) elif Command == "Set Brightness" or Command == "Set Level": self.mqttClient.Publish( configdict["brightness_command_topic"], str(Level) ) elif Command == "On": payload = "ON" if "payload_on" in configdict: payload = configdict["payload_on"] elif "payload_close" in configdict: payload = configdict["payload_close"] self.mqttClient.Publish(configdict["command_topic"], payload) elif Command == "Off": payload = "OFF" if "payload_off" in configdict: payload = configdict["payload_off"] elif "payload_open" in configdict: payload = configdict["payload_open"] self.mqttClient.Publish(configdict["command_topic"], payload) elif Command == "Stop": payload = "STOP" if "payload_stop" in configdict: payload = configdict["payload_stop"] self.mqttClient.Publish(configdict["command_topic"], payload) elif Command == "Set Color": try: Color = json.loads(sColor) except (ValueError, KeyError, TypeError) as e: Domoticz.Error( "onCommand: Illegal color: '" + str(sColor) + "'" ) # TODO: This is not really correct, should check color mode r = int(Color["r"] * Level / 100) g = int(Color["g"] * Level / 100) b = int(Color["b"] * Level / 100) cw = int(Color["cw"] * Level / 100) ww = int(Color["ww"] * Level / 100) if ( "rgb_command_topic" in configdict and "brightness_command_topic" in configdict ): self.mqttClient.Publish( configdict["rgb_command_topic"], format(r, "02x") + format(g, "02x") + format(b, "02x") + format(cw, "02x") + format(ww, "02x"), ) self.mqttClient.Publish( configdict["brightness_command_topic"], str(Level) ) elif ( "color_temp_command_topic" in configdict and "brightness_command_topic" in configdict ): self.mqttClient.Publish( configdict["color_temp_command_topic"], str(Color["t"] * (500 - 153) / 255 + 153), ) self.mqttClient.Publish( configdict["brightness_command_topic"], str(Level) ) except (ValueError, KeyError, TypeError) as e: Domoticz.Error("onCommand: Error: " + str(e)) else: Domoticz.Debug("Device not found, ignoring command") def onDeviceAdded(self, Unit): Domoticz.Log("onDeviceAdded " + self.deviceStr(Unit)) self.copyDevices() # TODO: Update subscribed topics def onDeviceModified(self, Unit): Domoticz.Log("onDeviceModified " + self.deviceStr(Unit)) if Unit in Devices and Devices[Unit].Name != self.cachedDeviceNames[Unit]: Domoticz.Log( "Device name changed, new name: " + Devices[Unit].Name + ", old name: " + self.cachedDeviceNames[Unit] ) Device = Devices[Unit] try: configdict = json.loads(Device.Options["config"]) if ( "tasmota_tele_topic" in configdict and Device.SwitchType != 9 ): # Do not set friendly name for button, they don't have their own friendly name # Tasmota device! device_nbr = "" m = re.match(r".*_(\d)$", str(Device.Options["devicename"])) if m: device_nbr = m.group(1) cmnd_topic = re.sub( r"\/POWER\d?", "", configdict["command_topic"] ) # Remove '/POWER' self.mqttClient.Publish( cmnd_topic + "/FriendlyName" + str(device_nbr), Device.Name ) except (ValueError, KeyError, TypeError) as e: Domoticz.Debug("onDeviceModified: Error: " + str(e)) pass self.copyDevices() def onDeviceRemoved(self, Unit): Domoticz.Log("onDeviceRemoved " + self.deviceStr(Unit)) if Unit in Devices and "devicename" in Devices[Unit].Options: Device = Devices[Unit] # Clear retained topic devicetype = "" if ( Device.Type == 0xF4 and Device.SubType == 0x49 # pTypeGeneralSwitch and Device.SwitchType == 0 # sSwitchGeneralSwitch ): # OnOff devicetype = "switch" elif ( Device.Type == 0xF4 and Device.SubType == 0x49 # pTypeGeneralSwitch and Device.SwitchType == 7 # sSwitchGeneralSwitch ): # Dimmer devicetype = "light" elif Device.Type == 0xF1: # pTypeColorSwitch devicetype = "light" elif ( Device.Type == 0xF4 and Device.SubType == 0x49 # pTypeGeneralSwitch and ( # sSwitchGeneralSwitch (Device.SwitchType == 3) or (Device.SwitchType == 15) # Blind (up/down buttons) or ( # Venetian blinds EU (up/down/stop buttons) Device.SwitchType == 13 ) ) ): # Blinds Percentage devicetype = "blinds" elif ( Device.Type == 0xF4 and Device.SubType == 0x49 # pTypeGeneralSwitch and Device.SwitchType == 9 # sSwitchGeneralSwitch ): # STYPE_PushOn devicetype = "binary_sensor" elif self.isMQTTSensor(Device): devicetype = "sensor" if devicetype: topic = ( self.discoverytopic + "/" + devicetype + "/" + Devices[Unit].Options["devicename"] + "/config" ) Domoticz.Log("Clearing topic '" + topic + "'") self.mqttClient.Publish(topic, "", 1) self.copyDevices() # TODO: Update subscribed topics def onHeartbeat(self): Domoticz.Debug("Heartbeating...") # Reconnect if connection has dropped if self.mqttClient.mqttConn is None or ( not self.mqttClient.mqttConn.Connecting() and not self.mqttClient.mqttConn.Connected() or not self.mqttClient.isConnected ): Domoticz.Debug("Reconnecting") self.mqttClient.Open() else: self.mqttClient.Ping() # Timing out sensors # Domoticz.Debug( "OnHeartbeat: Settings " + str( Settings ) ) now = datetime.now() update_timeout = int(Settings["SensorTimeout"]) # Domoticz.Debug( "OnHeartbeat: " + str( Devices.items() ) + " " + str( type( Devices ) ) ) for k, device in Devices.items(): if self.isMQTTSensor(device): if len(device.LastUpdate) > 0: # Domoticz.Debug( "OnHeartbeat: Device " + device.Name + ", Last update " + str( device.LastUpdate ) ) # Workaround of Python issue https://bugs.python.org/issue27400 last_update = None try: last_update = datetime.strptime( device.LastUpdate, "%Y-%m-%d %H:%M:%S" ) except TypeError: last_update = datetime.fromtimestamp( time.mktime( time.strptime(device.LastUpdate, "%Y-%m-%d %H:%M:%S") ) ) time_delta = now - last_update # Domoticz.Debug( "OnHeartbeat: " + device.Name + ", Time delta " + str( time_delta ) + ", Timeout " + str( update_timeout ) + ", Actual " + str( time_delta.total_seconds() / 60 ) ) if (time_delta.total_seconds() / 60) >= update_timeout: if device.TimedOut == 0: device.Update( nValue=device.nValue, sValue=device.sValue, TimedOut=1 ) # , SuppressTriggers=True) Domoticz.Status( self.deviceStr(self.getUnit(device)) + ": Offline for more than " + str(update_timeout) + " minutes, Setting TimedOut: 1" ) self.copyDevices() else: # Domoticz.Debug( "OnHeartbeat: Device " + device.Name + " already timed out, do nothing" ) pass # Pull configuration and status from tasmota device def refreshConfiguration(self, Topic): Domoticz.Debug("refreshConfiguration for device with topic: '" + Topic + "'") # Refresh relay / dimmer configuration self.mqttClient.Publish(Topic + "/Status", "11") # Refresh sensor configuration # self.mqttClient.Publish(Topic+"/Status",'10') # Refresh IP configuration self.mqttClient.Publish(Topic + "/Status", "5") # Returns list of topics to subscribe to def getTopics(self): topics = set() for key, Device in Devices.items(): # Domoticz.Debug("getTopics: '" + str(Device.Options) +"'") try: configdict = json.loads(Device.Options["config"]) # Domoticz.Debug("getTopics: '" + str(configdict) +"'") for key, value in configdict.items(): # Domoticz.Debug("getTopics: key:'" + str(key) +"' value: '" + str(value) + "'") try: # if key.endswith('_topic'): if ( key == "availability_topic" or key == "state_topic" or key == "brightness_state_topic" or key == "rgb_state_topic" or key == "color_temp_state_topic" or key == "position_topic" ): topics.add(value) except (TypeError) as e: Domoticz.Error("getTopics: Error: " + str(e)) pass if "tasmota_tele_topic" in configdict: # Subscribe to all Tasmota state topics state_topic = re.sub( r"^tele\/", "stat/", configdict["tasmota_tele_topic"] ) # Replace tele with stat state_topic = re.sub( r"\/tele\/", "/stat/", state_topic ) # Replace tele with stat state_topic = re.sub( r"\/STATE", "/#", state_topic ) # Replace '/STATE' with /# topics.add(state_topic) except (ValueError, KeyError, TypeError) as e: Domoticz.Error("getTopics: Error: " + str(e)) pass topics.add(self.discoverytopic + "/#") Domoticz.Debug("getTopics: '" + str(topics) + "'") return list(topics) # Returns list of matching devices def getDevices( self, key="", configkey="", hasconfigkey="", value="", config="", topic="", type="", channel="", ): Domoticz.Debug( "getDevices key: '" + key + "' configkey: '" + configkey + "' hasconfigkey: '" + hasconfigkey + "' value: '" + value + "' config: '" + config + "' topic: '" + topic + "'" ) matchingDevices = set() if key != "": for k, Device in Devices.items(): try: if Device.Options[key] == value: matchingDevices.add(Device) except (ValueError, KeyError) as e: pass if configkey != "": for k, Device in Devices.items(): try: configdict = json.loads(Device.Options["config"]) if configdict[configkey] == value: matchingDevices.add(Device) except (ValueError, KeyError) as e: pass elif hasconfigkey != "": for k, Device in Devices.items(): try: configdict = json.loads(Device.Options["config"]) if hasconfigkey in configdict: matchingDevices.add(Device) except (ValueError, KeyError) as e: pass elif config != "": for k, Device in Devices.items(): try: if Device.Options["config"] == config: matchingDevices.add(Device) except KeyError: pass elif topic != "": for k, Device in Devices.items(): try: configdict = json.loads(Device.Options["config"]) for key, value in configdict.items(): if value == topic: matchingDevices.add(Device) except (ValueError, KeyError) as e: pass Domoticz.Debug("getDevices found " + str(len(matchingDevices)) + " devices") return list(matchingDevices) def makeDevice(self, devicename, TypeName, switchTypeDomoticz, config): iUnit = next( filterfalse(set(Devices).__contains__, count(1)) ) # First unused 'Unit' Domoticz.Log("Creating device with unit: " + str(iUnit)) Options = {"config": json.dumps(config), "devicename": devicename} # DeviceName = topic+' - '+type DeviceName = config["name"] Domoticz.Device( Name=DeviceName, Unit=iUnit, TypeName=TypeName, Switchtype=switchTypeDomoticz, Options=Options, Used=self.options["addDiscoveredDeviceUsed"], ).Create() def makeDeviceRaw(self, devicename, Type, Subtype, switchTypeDomoticz, config): iUnit = next( filterfalse(set(Devices).__contains__, count(1)) ) # First unused 'Unit' Domoticz.Log("Creating device with unit: " + str(iUnit)) Options = {"config": json.dumps(config), "devicename": devicename} # DeviceName = topic+' - '+type DeviceName = config["name"] Domoticz.Device( Name=DeviceName, Unit=iUnit, Type=Type, Subtype=Subtype, Switchtype=switchTypeDomoticz, Options=Options, Used=self.options["addDiscoveredDeviceUsed"], ).Create() def isDeviceIgnored(self, config): ignore = False for ignoredtopic in self.ignoredtopics: for key, value in config.items(): if key.endswith("_topic"): if value.startswith(ignoredtopic): ignore = True Domoticz.Debug("isDeviceIgnored: " + str(ignore)) return ignore def addTasmotaTopics(self, config): isTasmota = False # TODO: Something smarter to detect Tasmota device try: # if "/cmnd/" in config["command_topic"] and "/POWER" in config["command_topic"] and "/tele/" in config["availability_topic"] and "/LWT" in config["availability_topic"]: if ( ( ( "/stat/" in config["state_topic"] and "/RESULT" in config["state_topic"] ) or ( "/cmnd/" in config["state_topic"] and "/POWER" in config["state_topic"] ) or ( "/tele/" in config["state_topic"] and "/STATE" in config["state_topic"] ) ) and "/tele/" in config["availability_topic"] and "/LWT" in config["availability_topic"] ): isTasmota = True Domoticz.Debug("addTasmotaTopics: isTasmota: " + str(isTasmota)) if isTasmota: statetopic = config["availability_topic"].replace("/LWT", "/STATE") Domoticz.Debug("addTasmotaTopics: statetopic: " + statetopic) config["tasmota_tele_topic"] = statetopic except (ValueError, KeyError) as e: pass # =============================================================DEVICE CONFIG============================================================== def updateDeviceSettings(self, devicename, devicetype, config): Domoticz.Debug( "updateDeviceSettings: devicename: '" + devicename + "' devicetype: '" + devicetype + "' config: '" + str(config) + "'" ) TypeName = "" Type = 0 Subtype = 0 switchTypeDomoticz = 0 # OnOff if (devicetype == "light" or devicetype == "switch") and ( "brightness_command_topic" in config or "color_temp_command_topic" in config or "rgb_command_topic" in config ): Domoticz.Debug("updateDeviceSettings: devicetype == 'light'") switchTypeDomoticz = 7 # Dimmer rgbww = 0 if "white_value_command_topic" in config: rgbww = 1 if "color_temp_command_topic" in config: rgbww = 2 if "rgb_command_topic" in config: rgbww = rgbww + 3 if rgbww == 2: # WW Type = 0xF1 # pTypeColorSwitch Subtype = 0x08 # sTypeColor_CW_WW elif rgbww == 3: # RGB Type = 0xF1 # pTypeColorSwitch Subtype = 0x02 # sTypeColor_RGB elif rgbww == 4: # RGBW Type = 0xF1 # pTypeColorSwitch Subtype = 0x06 # sTypeColor_RGB_W_Z elif rgbww == 5: # RGBWW Type = 0xF1 # pTypeColorSwitch Subtype = 0x07 # sTypeColor_RGB_CW_WW_Z else: TypeName = "Switch" Type = 0xF4 # pTypeGeneralSwitch Subtype = 0x49 # sSwitchGeneralSwitch elif ( devicetype == "switch" or devicetype == "light" ): # Switch or light without dimming/color/color temperature Domoticz.Debug("updateDeviceSettings: devicetype == 'switch'") TypeName = "Switch" Type = 0xF4 # pTypeGeneralSwitch Subtype = 0x49 # sSwitchGeneralSwitch elif devicetype == "binary_sensor": TypeName = "Switch" Type = 0xF4 # pTypeGeneralSwitch Subtype = 0x49 # sSwitchGeneralSwitch switchTypeDomoticz = 9 # STYPE_PushOn elif (devicetype == "cover") and ("set_position_topic" in config): Type = 0xF4 # pTypeGeneralSwitch Subtype = 0x49 # sSwitchGeneralSwitch switchTypeDomoticz = 13 # Blinds percent elif devicetype == "cover": TypeName = "Switch" Type = 0xF4 # pTypeGeneralSwitch Subtype = 0x49 # sSwitchGeneralSwitch switchTypeDomoticz = ( 15 # STYPE_Blinds Venetian-type with UP / DOWN / STOP buttons ) elif devicetype == "sensor": Domoticz.Debug("updateDeviceSettings: devicetype == 'sensor'") if "device_class" in config: if config["device_class"] == "temperature": Type = 0x50 # pTypeTemp RFXTrx.h Subtype = 0x01 # LaCrosse TX3 elif config["device_class"] == "humidity": Type = 0x52 # pTypeTempHum Subtype = 0x01 # La Crosse matchingDevices = self.getDevices(key="devicename", value=devicename) if len(matchingDevices) == 0: Domoticz.Log( "updateDeviceSettings: Did not find device with key='devicename', value = '" + devicename + "'" ) # Unknown device Domoticz.Log( "updateDeviceSettings: TypeName: '" + TypeName + "' Type: " + str(Type) ) if TypeName != "": self.addTasmotaTopics(config) if not self.isDeviceIgnored(config): self.makeDevice(devicename, TypeName, switchTypeDomoticz, config) # Update subscription list self.mqttClient.Subscribe(self.getTopics()) elif Type != 0: self.addTasmotaTopics(config) if not self.isDeviceIgnored(config): self.makeDeviceRaw( devicename, Type, Subtype, switchTypeDomoticz, config ) # Update subscription list self.mqttClient.Subscribe(self.getTopics()) else: # TODO: What do if len(matchingDevices) > 1? device = matchingDevices[0] self.addTasmotaTopics(config) oldconfigdict = {} try: oldconfigdict = json.loads(device.Options["config"]) except (ValueError, KeyError, TypeError) as e: pass # Correction for subsequent messages if self.isMQTTSensor(device): config["value_template"] = oldconfigdict["value_template"] if device.Type == 0x52 and Type == 0x50: Type = 0 # Reset, no change Subtype = 0 if Type != 0 and ( device.Type != Type or device.SubType != Subtype or device.SwitchType != switchTypeDomoticz or oldconfigdict != config ): Domoticz.Log( "updateDeviceSettings: " + self.deviceStr(self.getUnit(device)) + ": Device settings not matching, updating Type, SubType, Switchtype and Options['config']" ) Domoticz.Log( "updateDeviceSettings: device.Type: " + str(device.Type) + "->" + str(Type) + ", device.SubType: " + str(device.SubType) + "->" + str(Subtype) + ", device.SwitchType: " + str(device.SwitchType) + "->" + str(switchTypeDomoticz) + ", device.Options['config']: " + str(oldconfigdict) + " -> " + str(config) ) nValue = device.nValue sValue = device.sValue Options = dict(device.Options) Options["config"] = json.dumps(config) device.Update( nValue=nValue, sValue=sValue, Type=Type, Subtype=Subtype, Switchtype=switchTypeDomoticz, Options=Options, SuppressTriggers=True, ) self.copyDevices() # ==========================================================UPDATE STATUS from MQTT============================================================== def isMQTTSensor(self, Device): return ( (Device.Type == 0x50) or (Device.Type == 0x52) ) and ( # pTypeTemp or pTypeTemHum (Device.SubType == 0x05) or (Device.SubType == 0x01) ) # La Cross Temp_Hum combined def updateSensor(self, device, topic, message): Domoticz.Debug( "updateSensor topic: '" + topic + "' message: '" + str(message) + "'" ) nValue = device.nValue # 0 sValue = device.sValue # -1 isTeleTopic = False # Tasmota tele topic updatedevice = False bat = 255 rss = 12 result = False try: devicetopics = [] configdict = json.loads(device.Options["config"]) for key, value in configdict.items(): if value == topic: devicetopics.append(key) if ( "state_topic" in devicetopics or "tasmota_tele_topic" in devicetopics ): # Switch status is present in Tasmota tele/STAT message if "state_topic" in devicetopics: Domoticz.Debug( "UpdateSensor: Got state_topic " + configdict["state_topic"] ) if "tasmota_tele_topic" in devicetopics: Domoticz.Debug( "UpdateSensor: Got tasmota_tele_topic " + configdict["tasmota_tele_topic"] ) if "tasmota_tele_topic" in devicetopics: isTeleTopic = ( True # Suppress device triggers for periodic tele/STAT message ) if self.isMQTTSensor(device): result = True Domoticz.Debug( "updateSensor: value_template '" + configdict["value_template"] + "'" ) value_template = "Temperature" msg = message # Domoticz.Debug("updateSensor: MSG: " + str(msg)) m = re.match( r"^{{[\s]*value_json\.*(.+)[\s]*}}$", configdict["value_template"], ) if m != None: value_template = m.group(1).strip().strip("'").strip('"') m = re.split(r"\[(.*?)\]", value_template) # Domoticz.Debug("updateSensor: value_template '" + value_template + "'") if m != None: # Domoticz.Debug( "updateSensor: M1:" + str( m )) if len(m) > 1: value_template = ( m[len(m) - 2].strip().strip("'").strip('"') ) # Domoticz.Debug("updateSensor: value_template '" + value_template + "'") m = m[1 : len(m) - 2] # Domoticz.Debug("updateSensor: M2:" + str( m ) ) for value in m: val = value.strip().strip("'").strip('"') # Domoticz.Debug("updateSensor: VAL: " + str(val)) if len(val) > 0: msg = msg[val] # Domoticz.Debug("updateSensor: MSG: " + str(msg)) Domoticz.Debug( "updateSensor: Matched template '" + value_template + "', Message: " + str(msg) ) temp = None try: temp = float(msg[value_template]) except (ValueError, KeyError, TypeError): pass hum = None if value_template != "Humidity": try: hum = float(msg["Humidity"]) except (ValueError, KeyError, TypeError): pass try: bat = int(round(float(msg["Battery"]))) if (bat < 0) or (bat > 100): bat = 255 except (ValueError, KeyError, TypeError): pass try: rss = int(round(float(msg["RSSI"]))) rss = int(round((140 + rss) / 10)) except (ValueError, KeyError, TypeError): pass Domoticz.Debug( "updateSensor: Temperature: " + str(temp) + ", Humidity: " + str(hum) + ", Battery level: " + str(bat) + ", RSSI: " + str(rss) ) if temp != None: updatedevice = True sValue = str(temp) # Always transmit temperature if hum != None: wet = 1 if hum >= 70: wet = 3 elif hum <= 40: wet = 2 sValue = sValue + ";" + str(hum) + ";" + str(wet) except (ValueError, KeyError) as e: pass if updatedevice: # Do not update if we got Tasmota periodic state update and state has not changed if not isTeleTopic or nValue != device.nValue or sValue != device.sValue: Domoticz.Log( "updateSensor: " + self.deviceStr(self.getUnit(device)) + ": Topic: '" + str(topic) + " 'Setting nValue: " + str(device.nValue) + "->" + str(nValue) + ", sValue: '" + str(device.sValue) + "'->'" + str(sValue) + "'" ) device.Update( nValue=nValue, sValue=sValue, BatteryLevel=bat, SignalLevel=rss, TimedOut=0, ) self.copyDevices() return result def updateSwitch(self, device, topic, message): # Domoticz.Debug("updateSwitch topic: '" + topic + "' switchNo: " + str(switchNo) + " key: '" + key + "' message: '" + str(message) + "'") nValue = device.nValue # 0 sValue = device.sValue # -1 isTeleTopic = False # Tasmota tele topic updatedevice = False updatecolor = False try: Color = json.loads(device.Color) except (ValueError, KeyError, TypeError) as e: Color = {} pass try: devicetopics = [] configdict = json.loads(device.Options["config"]) for key, value in configdict.items(): if value == topic: devicetopics.append(key) if ( "state_topic" in devicetopics or "tasmota_tele_topic" in devicetopics ): # Switch status is present in Tasmota tele/STAT message if "state_topic" in devicetopics: Domoticz.Debug("Got state_topic") if "tasmota_tele_topic" in devicetopics: Domoticz.Debug("Got tasmota_tele_topic") if "tasmota_tele_topic" in devicetopics: isTeleTopic = ( True # Suppress device triggers for periodic tele/STAT message ) if "value_template" in configdict: m = re.match( r"^{{value_json\.(.+)}}$", configdict["value_template"] ) if m: value_template = m.group(1) Domoticz.Debug( "updateSwitch: value_template: '" + value_template + "'" ) if value_template in message: Domoticz.Debug( "updateSwitch: message[value_template]: '" + message[value_template] + "'" ) payload = message[value_template] if ( "payload_off" in configdict and payload == configdict["payload_off"] ): updatedevice = True nValue = 0 if ( "payload_on" in configdict and payload == configdict["payload_on"] ): updatedevice = True nValue = 1 else: Domoticz.Debug("updateSwitch: message[value_template]: '-'") else: Domoticz.Debug( "updateSwitch: unsupported value_template: '" + configdict["value_template"] + "'" ) else: Domoticz.Debug("updateSwitch: No value_template") payload = message if ( ( "payload_off" in configdict and payload == configdict["payload_off"] ) or ( "state_open" in configdict and payload == configdict["state_open"] ) or "payload_off" not in configdict and "state_open" not in configdict and payload == "OFF" ): updatedevice = True nValue = 0 if ( ( "payload_on" in configdict and payload == configdict["payload_on"] ) or ( "state_close" in configdict and payload == configdict["state_close"] ) or "payload_on" not in configdict and "state_close" not in configdict and payload == "ON" ): updatedevice = True nValue = 1 if ( ( "payload_stop" in configdict and payload == configdict["payload_stop"] ) or ( "state_stop" in configdict and payload == configdict["state_stop"] ) or "payload_stop" not in configdict and "state_stop" not in configdict and payload == "STOP" ): updatedevice = True nValue = 17 # state = STOP in blinds Domoticz.Debug("updateSwitch: nValue: '" + str(nValue) + "'") if "brightness_state_topic" in devicetopics: Domoticz.Debug("updateSwitch: Got brightness_state_topic") if "brightness_value_template" in configdict: m = re.match( r"^{{value_json\.(.+)}}$", configdict["brightness_value_template"], ) if m: brightness_value_template = m.group(1) Domoticz.Debug( "updateSwitch: brightness_value_template: '" + brightness_value_template + "'" ) if brightness_value_template in message: Domoticz.Debug( "updateSwitch: message[brightness_value_template]: '" + str(message[brightness_value_template]) + "'" ) payload = message[brightness_value_template] brightness_scale = 255 if "brightness_scale" in configdict: brightness_scale = configdict["brightness_scale"] sValue = payload * 100 / brightness_scale else: Domoticz.Debug( "updateSwitch: message[brightness_value_template]: '-'" ) else: Domoticz.Debug( "updateSwitch: unsupported template: '" + configdict["brightness_value_template"] + "'" ) else: payload = int(message) brightness_scale = 255 if "brightness_scale" in configdict: brightness_scale = configdict["brightness_scale"] sValue = int(payload * 100 / brightness_scale) Domoticz.Debug("updateSwitch: sValue: '" + str(sValue) + "'") updatedevice = True if "position_topic" in devicetopics: payload = message sValue = payload nValue = 0 Domoticz.Log("updateSwitch: sValue: '" + str(sValue) + "'") updatedevice = True if "rgb_state_topic" in devicetopics: Domoticz.Debug("updateSwitch: Got rgb_state_topic") if "rgb_value_template" in configdict: m = re.match( r"^{{value_json\.(.+)}}$", configdict["rgb_value_template"] ) if m: rgb_value_template = m.group(1) Domoticz.Debug( "updateSwitch: rgb_value_template: '" + rgb_value_template + "'" ) if rgb_value_template in message: Domoticz.Debug( "updateSwitch: message[rgb_value_template]: '" + str(message[rgb_value_template]) + "'" ) payload = message[rgb_value_template] if ( len(payload) == 6 or len(payload) == 8 or len(payload) == 10 ): updatecolor = True # TODO check contents of cw/ww and set mode accordingly Color["m"] = 3 # RGB Color["t"] = 0 Color["r"] = int(payload[0:2], 16) Color["g"] = int(payload[2:4], 16) Color["b"] = int(payload[4:6], 16) Color["cw"] = 0 Color["ww"] = 0 Domoticz.Debug( "updateSwitch: Color: " + json.dumps(Color) ) else: Domoticz.Debug( "updateSwitch: message[rgb_value_template]: '-'" ) else: Domoticz.Debug( "updateSwitch: unsupported template: '" + configdict["rgb_value_template"] + "'" ) else: # TODO: test # payload = message # brightness_scale = 255 # if "brightness_scale" in configdict: # brightness_scale = configdict['brightness_scale'] # sValue = payload * 100 / brightness_scale Domoticz.Debug("updateSwitch: sValue: '" + str(sValue) + "'") elif "color_temp_state_topic" in devicetopics: Domoticz.Debug("updateSwitch: Got color_temp_state_topic") if "color_temp_value_template" in configdict: m = re.match( r"^{{value_json\.(.+)}}$", configdict["color_temp_value_template"], ) if m: color_temp_value_template = m.group(1) Domoticz.Debug( "updateSwitch: color_temp_value_template: '" + color_temp_value_template + "'" ) if color_temp_value_template in message: Domoticz.Debug( "updateSwitch: message[color_temp_value_template]: '" + str(message[color_temp_value_template]) + "'" ) payload = message[color_temp_value_template] updatecolor = True Color["m"] = 2 # Color temperature Color["t"] = int(255 * (int(payload) - 153) / (500 - 153)) Domoticz.Debug("updateSwitch: Color: " + json.dumps(Color)) else: Domoticz.Debug( "updateSwitch: message[color_temp_value_template]: '-'" ) else: Domoticz.Debug( "updateSwitch: unsupported template: '" + configdict["color_temp_value_template"] + "'" ) else: # TODO: test # payload = message # brightness_scale = 255 # if "brightness_scale" in configdict: # brightness_scale = configdict['brightness_scale'] # sValue = payload * 100 / brightness_scale Domoticz.Debug("updateSwitch: sValue: '" + str(sValue) + "'") except (ValueError, KeyError) as e: pass if updatedevice: if updatecolor: # Do not update if we got Tasmota periodic state update and state has not changed if ( not isTeleTopic or nValue != device.nValue or sValue != device.sValue ): Domoticz.Log( self.deviceStr(self.getUnit(device)) + ": Topic: '" + str(topic) + " 'Setting nValue: " + str(device.nValue) + "->" + str(nValue) + ", sValue: '" + str(device.sValue) + "'->'" + str(sValue) + "', color: '" + device.Color + "'->'" + json.dumps(Color) + "'" ) device.Update( nValue=nValue, sValue=str(sValue), Color=json.dumps(Color) ) self.copyDevices() else: # Do not update if we got Tasmota periodic state update and state has not changed if ( not isTeleTopic or nValue != device.nValue or sValue != device.sValue ): Domoticz.Log( self.deviceStr(self.getUnit(device)) + ": Topic: '" + str(topic) + " 'Setting nValue: " + str(device.nValue) + "->" + str(nValue) + ", sValue: '" + str(device.sValue) + "'->'" + str(sValue) + "'" ) device.Update(nValue=nValue, sValue=str(sValue)) self.copyDevices() def updateAvailability(self, device, topic, message): TimedOut = 0 updatedevice = False try: devicetopics = [] configdict = json.loads(device.Options["config"]) for key, value in configdict.items(): if value == topic: devicetopics.append(key) if "availability_topic" in devicetopics: Domoticz.Debug("updateAvailability: Got availability_topic") if "availability_template" in configdict: m = re.match( r"^{{value_json\.(.+)}}$", configdict["availability_template"] ) availability_template = m.group(1) Domoticz.Debug( "updateAvailability: availability_template: '" + availability_template + "'" ) if availability_template in message: Domoticz.Debug( "updateAvailability: message[availability_template]: '" + message[availability_template] + "'" ) payload = message[availability_template] if payload == configdict["payload_available"]: updatedevice = True TimedOut = 0 if payload == configdict["payload_not_available"]: updatedevice = True TimedOut = 1 Domoticz.Debug( "updateAvailability: TimedOut: '" + str(TimedOut) + "'" ) else: Domoticz.Debug( "updateAvailability: message[availability_template]: '-'" ) else: payload = message if payload == configdict["payload_available"]: updatedevice = True TimedOut = 0 if payload == configdict["payload_not_available"]: updatedevice = True TimedOut = 1 Domoticz.Debug( "updateAvailability: TimedOut: '" + str(TimedOut) + "'" ) except (ValueError, KeyError) as e: pass if updatedevice: nValue = device.nValue sValue = device.sValue Domoticz.Log( self.deviceStr(self.getUnit(device)) + ": Setting TimedOut: '" + str(TimedOut) + "'" ) device.Update( nValue=nValue, sValue=sValue, TimedOut=TimedOut, SuppressTriggers=True ) self.copyDevices() def updateTasmotaStatus(self, device, topic, message): # Domoticz.Debug("updateTasmotaStatus topic: '" + topic + "' message: '" + str(message) + "'") nValue = device.nValue sValue = device.sValue updatedevice = False Vcc = 0 RSSI = 0 try: devicetopics = [] configdict = json.loads(device.Options["config"]) for key, value in configdict.items(): if value == topic: devicetopics.append(key) if "tasmota_tele_topic" in devicetopics: Domoticz.Debug("updateAvailability: Got tasmota_tele_topic") if "Vcc" in message and self.options["updateVCC"]: Vcc = int(message["Vcc"] * 10) Domoticz.Debug( "updateAvailability: Set battery level to: " + str(Vcc) + " was:" + str(device.BatteryLevel) ) updatedevice = True if ( "Wifi" in message and "RSSI" in message["Wifi"] and self.options["updateRSSI"] ): RSSI = int(message["Wifi"]["RSSI"]) Domoticz.Debug( "updateAvailability: Set SignalLevel to: " + str(RSSI) + " was:" + str(device.SignalLevel) ) updatedevice = True if updatedevice and ( device.SignalLevel != RSSI or device.BatteryLevel != Vcc ): Domoticz.Log( self.deviceStr(self.getUnit(device)) + ": Setting SignalLevel: '" + str(RSSI) + "', BatteryLevel: '" + str(Vcc) + "'" ) device.Update( nValue=nValue, sValue=sValue, SignalLevel=RSSI, BatteryLevel=Vcc, SuppressTriggers=True, ) self.copyDevices() except (ValueError, KeyError) as e: pass def updateTasmotaSettings(self, device, topic, message): Domoticz.Debug( "updateTasmotaSettings " + self.deviceStr(self.getUnit(device)) + " topic: '" + topic + "' message: '" + str(message) + "'" ) nValue = device.nValue sValue = device.sValue updatedevice = False IPAddress = "" Description = "" try: devicetopics = [] configdict = json.loads(device.Options["config"]) if topic.endswith("STATUS5"): if "StatusNET" in message and "IPAddress" in message["StatusNET"]: IPAddress = message["StatusNET"]["IPAddress"] cmnd_topic = re.sub( r"^tele\/", "cmnd/", configdict["tasmota_tele_topic"] ) # Replace tele with cmnd cmnd_topic = re.sub( r"\/tele\/", "/cmnd/", cmnd_topic ) # Replace tele with cmnd cmnd_topic = re.sub( r"\/STATE\d?", "", cmnd_topic ) # Remove '/STATE' Description = "IP: " + IPAddress + ", Topic: " + cmnd_topic updatedevice = True if updatedevice and (device.Description != Description): Domoticz.Log( "updateTasmotaSettings updating description from: '" + device.Description + "' to: '" + Description + "'" ) device.Update( nValue=nValue, sValue=sValue, Description=Description, SuppressTriggers=True, ) self.copyDevices() except (ValueError, KeyError) as e: pass global _plugin _plugin = BasePlugin() def onStart(): global _plugin _plugin.onStart() def onConnect(Connection, Status, Description): global _plugin _plugin.onConnect(Connection, Status, Description) def onDisconnect(Connection): global _plugin _plugin.onDisconnect(Connection) def onMessage(Connection, Data): global _plugin _plugin.onMessage(Connection, Data) def onCommand(Unit, Command, Level, Color): global _plugin _plugin.onCommand(Unit, Command, Level, Color) def onDeviceAdded(Unit): global _plugin _plugin.onDeviceAdded(Unit) def onDeviceModified(Unit): global _plugin _plugin.onDeviceModified(Unit) def onDeviceRemoved(Unit): global _plugin _plugin.onDeviceRemoved(Unit) def onHeartbeat(): global _plugin _plugin.onHeartbeat() def DumpConfigToLog(): for x in Parameters: if Parameters[x] != "": Domoticz.Log("'" + x + "':'" + str(Parameters[x]) + "'") Domoticz.Log("Device count: " + str(len(Devices))) for x in Devices: Domoticz.Log("Device: " + str(x) + " - " + str(Devices[x])) Domoticz.Log("Device LastLevel: " + str(Devices[x].LastLevel)) Domoticz.Log("Device Color: " + str(Devices[x].Color)) Domoticz.Log("Device Options: " + str(Devices[x].Options)) return def DumpMQTTMessageToLog(topic, rawmessage, prefix=""): message = rawmessage.decode("utf8", "replace") message = str(message.encode("unicode_escape")) Domoticz.Log(prefix + topic + ":" + message)