# 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 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] 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: # Try to update switch state self.updateSwitch(device, topic, message) # Try to update availability self.updateAvailability(device, topic, message) # TODO: 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 # pTypeGeneralSwitch Device.SubType == 0x49 and # sSwitchGeneralSwitch Device.SwitchType == 0): # OnOff devicetype = 'switch' elif (Device.Type == 0xf4 and # pTypeGeneralSwitch Device.SubType == 0x49 and # sSwitchGeneralSwitch Device.SwitchType == 7): # Dimmer devicetype = 'light' elif Device.Type == 0xf1: # pTypeColorSwitch devicetype = 'light' elif (Device.Type == 0xf4 and # pTypeGeneralSwitch Device.SubType == 0x49 and # sSwitchGeneralSwitch ((Device.SwitchType == 3) or # Blind (up/down buttons) (Device.SwitchType == 15) or # Venetian blinds EU (up/down/stop buttons) (Device.SwitchType == 13))): # Blinds Percentage devicetype = 'blinds' elif (Device.Type == 0xf4 and # pTypeGeneralSwitch Device.SubType == 0x49 and # sSwitchGeneralSwitch Device.SwitchType == 9): # STYPE_PushOn devicetype = 'binary_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() # 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"])) and "/tele/" in config["availability_topic"] and "/LWT" in config["availability_topic"]: isTasmota = True Domoticz.Debug("isTasmota: " + str(isTasmota)) if isTasmota: statetopic = config["availability_topic"].replace("/LWT", "/STATE") Domoticz.Debug("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("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("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 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 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 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']) value_template = m.group(1) Domoticz.Debug("value_template: '" + value_template + "'") if value_template in message: Domoticz.Debug("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("message[value_template]: '-'") else: Domoticz.Debug("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("nValue: '" + str(nValue) + "'") if "brightness_state_topic" in devicetopics: Domoticz.Debug("Got brightness_state_topic") if "brightness_value_template" in configdict: m = re.match(r"^{{value_json\.(.+)}}$", configdict['brightness_value_template']) brightness_value_template = m.group(1) Domoticz.Debug("brightness_value_template: '" + brightness_value_template + "'") if brightness_value_template in message: Domoticz.Debug("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("message[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("sValue: '" + str(sValue) + "'") updatedevice = True if "position_topic" in devicetopics: payload = message sValue = payload nValue = 0 Domoticz.Log("sValue: '" + str(sValue) + "'") updatedevice = True if "rgb_state_topic" in devicetopics: Domoticz.Debug("Got rgb_state_topic") if "rgb_value_template" in configdict: m = re.match(r"^{{value_json\.(.+)}}$", configdict['rgb_value_template']) rgb_value_template = m.group(1) Domoticz.Debug("rgb_value_template: '" + rgb_value_template + "'") if rgb_value_template in message: Domoticz.Debug("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("Color: "+json.dumps(Color)) else: Domoticz.Debug("message[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("sValue: '" + str(sValue) + "'") elif "color_temp_state_topic" in devicetopics: Domoticz.Debug("Got color_temp_state_topic") if "color_temp_value_template" in configdict: m = re.match(r"^{{value_json\.(.+)}}$", configdict['color_temp_value_template']) color_temp_value_template = m.group(1) Domoticz.Debug("color_temp_value_template: '" + color_temp_value_template + "'") if color_temp_value_template in message: Domoticz.Debug("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("Color: "+json.dumps(Color)) else: Domoticz.Debug("message[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("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("Got availability_topic") if "availability_template" in configdict: m = re.match(r"^{{value_json\.(.+)}}$", configdict['availability_template']) availability_template = m.group(1) Domoticz.Debug("availability_template: '" + availability_template + "'") if availability_template in message: Domoticz.Debug("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("TimedOut: '" + str(TimedOut) + "'") else: Domoticz.Debug("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("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("Got tasmota_tele_topic") if "Vcc" in message and self.options['updateVCC']: Vcc = int(message["Vcc"]*10) Domoticz.Debug("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("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)