// Not licensed for onward /re-distribution for any reason and in any form - see license
import java.security.MessageDigest
metadata {
definition (name: "MQTT Client", namespace: "ukusa", author: "Kevin Hawkins",importUrl: "https://raw.githubusercontent.com/xAPPO/MQTT/beta3/MQTT%20Client%20driver") {
capability "Initialize"
capability "Presence Sensor"
//capability "Indicator"
command "publishMsg", ["String","String"]
command "subscribeTopic",["String"]
command "unsubscribeTopic",["String"]
command "subscribeWildcardTopic",["String"]
//command "getTopic",["String"] // This is intended to recover the payload for use (e.g. testing or appending) within the application and then unsubscribe
//command "createChild", ["String"] // not using device children currently
//command "deleteChild", ["String"]
//command "setStateTopic", ["String","String"]
//command "setCommandTopic", ["String","String"]
//command "ack", ["Integer"]
//command "setEventDelay",["Integer"]
command "reset"
command "connect"
command "disconnect"
//command "simulateError",["String"]
//command "setStateVar", ["String","String"]
//command "setLogLevel", ["Integer"]
attribute "Sequence", "number"
attribute "Subscribes", "number"
attribute "waiting","string"
// attribute "RXTopic", "string"
// attribute "OnOffDev", "string"
// attribute "DimDev", "string"
// attribute "Status", "string"
//attribute "OnOff", "string"
}
/*
preferences {
input name: "MQTTBroker", type: "text", title: "MQTT Broker Address", description: "e.g. tcp://192.168.1.17:1883", required: true, displayDuringSetup: true
input name: "username", type: "text", title: "MQTT Username", description: "(blank if none)", required: false, displayDuringSetup: true
input name: "password", type: "password", title: "MQTT Password", description: "(blank if none)", required: false, displayDuringSetup: true
//input name: "clientID", type: "text", title: "MQTT Client ID", description: "(blank for auto)",defaultValue: "Hubitat Elevation", required: false, displayDuringSetup: true
//input name: "spare", type: "text", title: "MQTT user", description: "User (blank if none)", required: false, displayDuringSetup: true
//input name: "Retain", type: "bool", title: "Retain published states", required: true, defaultValue: false
}
*/
}
//import groovy.transform.Field // TODO needed ?
def installed() {
log ("installed...", "WARN")
initialize()
}
def updated() {
log ("MQTT client updated...", "INFO")
//state.clear()
initialize()
}
def uninstalled() {
log ("disconnecting from mqtt", "INFO")
interfaces.mqtt.disconnect()
}
def initialize() {
state.myHub=false
unschedule (heartbeat)
state.seqNum=0
state.seq=0
state.subs=0
state.count=0
//state.ack=[]
//state.maxImmediate=0
//state.maxComplete=0
state.topic0="DoNoTmatch"
if (MD5(location.hubs[0].getZigbeeId())=="202cb962ac59075b964b07152d234b70") state.myHub=true
heartbeat()
schedule("0/20 * * * * ? *", heartbeat)
version="beta 3d"
// sendEvent (name: "Status", value: "disconnected" , isStateChange: true)
sendEvent (name: "presence", value: "not present")
sendEvent (name: "Sequence", value: state.seq++)
state.waiting = "none"
sendEvent (name: "waiting", value: "none")
sendEvent (name: "Subscribes", value: state.subs++)
state.continue=false
if (state.normHubName==null) state.normHubName = "temporary"
//connect()
}
def connect() {
log ("Initialise MQTT","DEBUG")
//state.connectionAttempts = 0
mqttStatus=false
mqttStatus=mqttConnect()
while(!mqttStatus) {
log ("MQTT connect failed attempt:[$state.connectionAttempts], try again in 10 secs","INFO")
pauseExecution(10000) // 10 second pause // TODO refine with increasing periods
mqttStatus=mqttConnect()
}
mqttState=interfaces.mqtt.isConnected()
if (mqttState) {
log ( "Connected to MQTT broker ${state.MQTTBroker}","BLUE")
subscribeTopic ('homie/'+state.normHubName+'/$implementation/heartbeat')
//sendEvent(name: "MQTTStatus", value: "Connect", isStateChange: true) // allow continue
parent.mqttStatus ("Connect")
state.connectionAttempts = 0
state.seq2=0
}
else log ("MQTT not connected","ERROR")
}
void setLogLevel (level) {
if (level<6) {
state.logLevel=level.toInteger()
log ("Log level set to " + level,"INFO")
log ("## Restarted ##","WARN")
}
}
void setUserName(String username) {
state.username=username
}
void setPassword(String password) {
state.password=password
}
void setBrokerAddress(String broker) {
state.MQTTBroker=broker
}
void setHubName (String normHubName,String hubName) {
state.normHubName=normHubName
}
void setHAStatestreamTopic(String HATopic) {
state.HAStatestreamTopic = HATopic
log ("HAStatestreamTopic is " + HATopic,"INFO")
}
void setHomieDevice(String hDev) {
state.homieDeviceDiscovery = hDev
log ("homie discovery is for device: " + hDev, "INFO")
}
boolean mqttConnect() {
//try {
def mqttInt = interfaces.mqtt
log ( "MQTT> client ${version} ${state.normHubName}","INFO")
try{
log ("Connecting as Hubitat_${state.normHubName} to MQTT broker ${settings?.MQTTBroker}", "INFO")
mqttInt.connect(state.MQTTBroker, "Hubitat_${state.normHubName}", state.username,state.password, lastWillTopic: "Hubitat/${state.normHubName}/LWT", lastWillQos: 0, lastWillMessage: "I died")//give it a chance to start
pauseExecution(1000)
mqttState=mqttInt.isConnected()
if (mqttState) {
mqttInt.publish("homie/${state.normHubName}/" + '$fw/client',"${version}",1,true)
mqttInt.subscribe ('homie/'+state.normHubName+'/$implementation/heartbeat')
// sendEvent (name: "Status", value: "connected" , isStateChange: true)
sendEvent (name: "presence", value: "present")
return (true)
}
else return (false)
}
catch(e) {
log ("MQTT initialise error: ${e.message}", "WARN")
sendEvent (name: "presence", value: "not present")
state.connectionAttempts ++
return (false)
}
/* }
catch(e) {
log ("MQTT initialise failed: ${e.message}", "ERROR")
state.connectionAttempts ++
sendEvent (name: "presence", value: "not present")
log ("No MQTT connection", "ERROR")
return (false)
} */
}
void mqttClientStatus(String message) {
status=message.take(6)
mqttState=interfaces.mqtt.isConnected()
if (status=="Error:") {
try {
interfaces.mqtt.disconnect() // clears buffers
}
catch (e)
{
}
log ("Broker: ${message} Will restart in 5 seconds","ERROR")
runIn (5,"adviseStatus")
// sendEvent(name: "MQTTStatus", value: status, data: [message:message], isStateChange: true) // will reset
// cant allow client to reconnect itself as it will have no subscriptions (unless not clean sesssion)
// runIn (5,"reset")
}
else {
log ("Broker: ${message}","INFO")
mqttState=interfaces.mqtt.isConnected()
if (mqttState) {
log ("MQTT broker connected","INFO")
subscribeTopic ('homie/'+state.normHubName+'/$implementation/heartbeat')
sendEvent (name: "presence", value: "present")
parent.mqttStatus(status)
//sendEvent(name: "MQTTStatus", value: status, data: [message:message], isStateChange: true) // allow continue
// need to advise app have a connection to allow it to continue.
}
}
}
void adviseStatus() {
log ("Advising app that broker is down","WARN")
parent.mqttStatus ("Error")
//sendEvent(name: "MQTTStatus", value: "Error:", data: [message:"reset"], isStateChange: true) // will reset
}
def publishMsg(String topic, String payload, int qos, String retained) { // overload for RM compatibility
publishMsg(topic, payload, qos, retained.toBoolean())
}
def publishMsg(String topic, String payload,int qos = 1, boolean retained = false, boolean suppress = false ) {
mqttState=interfaces.mqtt.isConnected()
if (!mqttState) {
log ("Dropping message - no MQTT connection","WARN")
return
}
if (suppress == true){
log ("Suppress: $topic $payload","TRACE")
if (state.myHub) log("Suppressed: $topic = $payload","LOG")
return // this reduces the messages posted to homie topic to bare minimum although this will no longer be homie compliant
}
if (payload==null) {
log("Publish payload is null for topic ${topic}","ERROR")
return
}
if (topic==null){
log("Publish topic is null for payload ${payload}","ERROR")
return
}
qos=1 //enforce FTTB
interfaces.mqtt.publish(topic, payload, qos, retained)
sendEvent (name: "Sequence", value: state.seq++)
}
def subscribeTopic (String s) {
if (s.startsWith('homie/development/$fw/')) {
log ("MQTT subscribing to: " + s, "LOG")
if (s.endsWith("name")) setStateWaiting ("name")
else if (s.endsWith("version")) setStateWaiting ("version")
else if (s.endsWith("client")) setStateWaiting ("client")
log ("state.waiting is now $state.waiting","DEBUG")
}
else {
log ("MQTT subscribing to: " + s, "INFO")
}
mqttState=interfaces.mqtt.isConnected()
if (mqttState){
interfaces.mqtt.subscribe(s,1)
sendEvent (name: "Subscribes", value: state.subs++)
}
else log("Dropping subscription - no MQTT connection","WARN")
}
def setStateWaiting (sw) { // had to use this method as was not working when directly set above.
state.waiting = sw
sendEvent (name: "waiting", value: sw)
return
}
def getStateWaiting () {
return (state.waiting)
}
def unsubscribeTopic (String s) {
mqttState=interfaces.mqtt.isConnected()
log ("MQTT unsubscribing from: " + s, "INFO")
if (mqttState){
interfaces.mqtt.unsubscribe(s)
sendEvent (name: "Subscribes", value: --state.subs)
}
else log("Dropping unsubscribe - no MQTT connection","WARN")
}
def subscribeWildcardTopic (String s) {
mqttState=interfaces.mqtt.isConnected()
log ("MQTT subscribing to: " + s, "DEBUG")
if (mqttState){
topics=s.split('/')
topicCounts=topics.size()
state.topic0=topics[0]
state.topic1=topics[1]
state.topic2=topics[2]
//state.topic3=topics[3]
interfaces.mqtt.subscribe(s,1)
sendEvent (name: "Subscribes", value: state.subs++)
pauseExecution(60)
interfaces.mqtt.unsubscribe(s)
log ("MQTT unsubscribing from: " + s, "INFO")
}
else log("Dropping subscription - no MQTT connection","WARN")
}
def testme (){
log.error "howdy"
return ("hello")
}
def getTopic (String s) {
mqttState=interfaces.mqtt.isConnected()
state.continue=false
log ("Checking on ${s}","ERROR")
if (mqttState) {
interfaces.mqtt.subscribe(s)
sendEvent (name: "Subscribes", value: state.subs++)
pause (1800)// TODO adjust based on actual response
notfoundevt(s)
}
}
def notfoundevt(String s){
if (state.continue==true){
state.continue = false
log ("Already exists ${s}","WARN")
return
}
else {
log ("Time out on getTopic ${s}","WARN")
sendEvent(name: "getTopic", value: '#NoNe#', data: [state: "", topic: s], isStateChange: true)
}
}
def heartbeat (seqNum=0) {
mqttState=interfaces.mqtt.isConnected()
if (mqttState) interfaces.mqtt.publish("homie/${state.normHubName}" + '/$implementation/heartbeat',state.seqNum.toString() + "," + device.currentWaiting,1,true)
log ("Tx: heartbeat [$state.seqNum] $device.currentWaiting", "DEBUG")
state.seqNum++
}
def bufferEmpty(i){
log ("MQTT message queue idle","ERROR")
}
def parse(String description) {
runIn (30, "bufferEmpty")
topicFull=interfaces.mqtt.parseMessage(description).topic
def topic=topicFull.split('/')
def topicCount=topic.size()
def payloadFull=interfaces.mqtt.parseMessage(description).payload
def payload=payloadFull.split(',')
//log (topicFull + " " + payload, "INFO")
//
if (payloadFull.startsWith('{') && payloadFull.endsWith('}')) json="true" else json="false"
if (topicFull.startsWith('homie/'+state.normHubName+'/$implementation/heartbeat')) {
//log ("Rx: heartbeat [${payload[0]}] $device.currentWaiting","DEBUG")
if (device.currentWaiting != "none"){
if (device.currentWaiting == "complete") {
return
}
else log ("Waiting for $device.currentWaiting [$state.waiting]","WARN")
num1=payload[0].toInteger()
num2=state.seqNum
if (num1==num2-1) {
rxMsg= "MQTT>RX:[$state.seq2] [${interfaces.mqtt.parseMessage(description).payload}] ${interfaces.mqtt.parseMessage(description).topic}"
if (payload[1]==device.currentWaiting) log ("Waiting for $device.currentWaiting .. - but it appears there is no queue:-) ","RED")
version="beta 1"
if (device.currentWaiting=="name") publishMsg("homie/${state.normHubName}/" +'$fw/name','beta',1,true)
else if (device.currentWaiting=="client") publishMsg("homie/${state.normHubName}/" + '$fw/client',"${version}",1,true)
else if (device.currentWaiting=="version") publishMsg("homie/${state.normHubName}/" +'$fw/version','1',1,true)
//If this fails I could just force forwards
}
}
}
else if (topicFull.startsWith('homie/'+state.normHubName+'/$fw')){
rxMsg= "MQTT>RX:[$state.seq2] [${interfaces.mqtt.parseMessage(description).payload}] ${interfaces.mqtt.parseMessage(description).topic}"
log ( " " + rxMsg,"KH")
}
//else log ( " " + rxMsg,"GREEN") // will need to create rxMsg again if re-enable
// TODO make more use of topicCount for topic validation - some topics that currently match have further subTopics
// Potential bug TODO - if we set up a virtual device to import to HE and it matches here 'early' it wont be onwardly passed to HE e.g. within homie topic
else if (topic[0]==state.topic0){
if ((topic[1]==state.topic1) || (state.topic1=='+')|| (state.topic1=='#')){
if ((topic[2]==state.topic2) || (state.topic2=='+') || (state.topic2=='#')){
//if ((topic[3]==state.topic3) || (state.topic3=='*')){
parent.wildcardTopics(interfaces.mqtt.parseMessage(description).topic)
log ("Wildcard Match : got some topic details back from ${topic} : ${payload}", "INFO")
//def evt44 = createEvent(name: "WildcardTopics", value: interfaces.mqtt.parseMessage(description).topic,data:[seq: sequence], isStateChange: true)
return // evt44
// }
}
}
}
if (topic[0]==state.HAStatestreamTopic) { //HA
if (topic[1]=='status') { // HA LWT
if (payload[0]=="online") {
log ("Home Assistant [${state.HAStatestreamTopic}] is ONLINE","INFO")
parent.synchDevices () // state.HAStatestreamTopic is not used
//def evt23 = createEvent(name: "HASynch", value: state.HAStatestreamTopic, isStateChange: true)
// return evt23
}
else if (payload[0]=="offline") {
log ("Home Assistant [${state.HAStatestreamTopic}] is OFFLINE","INFO")
}
return
}
else if (topic[1]=='sensor') {
if (topic[3]=='state'){
//log ("RX::Sensor state:: " + topic[2] + " is " + payload[0], "TRACE")
parent.HASensorEventP(topic[2],payload[0], topic[1], true)
//def evt10 = createEvent(name: "Sensor", value: topic[2], data: [status: payload[0],seq: sequence, type: topic[1], update: true], isStateChange: true) //, key2: payload])
return //evt10
}
else if (topic[3]=='device_class'){
log ("### [" + payload[0] +"] type for HA Sensor Device ${topic[2]}", "TRACE")
parent.HASensorTypeP(topic[2],deQuote(payload[0]),topic[1])
//def evt11= createEvent(name: "HASensorType", value: topic[2], data: [payload: deQuote(payload[0]), type: topic[1],seq: sequence], isStateChange: true)
return //evt11
}
else if (topic[3]=='friendly_name'){
log ("Device ${topic[2]} is a HA sensor and is called " +payload[0], "TRACE")
parent.HASensorFriendlyP (topic[2],deQuote(payload[0]),topic[1]) // TODO This returns a label so is different
//def evt11= createEvent(name: "HASensorDev", value: topic[2], data: [label: deQuote(payload[0]), type: topic[1], seq: state.sequence], isStateChange: true)
return //evt11
}
else if (topic[3]=='unit_of_measurement'){
parent.HASensorUnitP (topic[2],deQuote(payload[0]))
//def evt11= createEvent(name: "SensorUnit", value: topic[2], data: [payload: deQuote(payload[0]),seq: state.sequence], isStateChange: true)
return //evt11
}
}
else if (topic[1]=='binary_sensor') {
if (topic[3]=='state'){
//log ("RX::Binary Sensor state:: " + topic[2] + " is " + payload[0], "TRACE")
parent.inputBooleanEventP(topic[2],deQuote(payload[0]),topic[1])
//def evt10 = createEvent(name: "BinarySensor", value: topic[2], data: [status: payload[0],seq: sequence, type: topic[1], update: true], isStateChange: true) //, key2: payload])
return // evt10
}
else if (topic[3]=='friendly_name'){
parent.LabelDeviceP("MQTT:HA_"+topic[2],deQuote(payload[0]))
return // evt19
}
else if (topic[3]=='device_class'){
parent.HABinarySensorDevP(topic[2],deQuote(payload[0]),topic[1])
return // evt18
}
}
else if (topic[1]=='switch') {
if (topic[3]=='friendly_name') {
//log ("Device ${topic[2]} is a HA switch and is called " +payload[0], "TRACE")
parent.HASwitchFriendlyP (topic[2],deQuote(payload[0]),topic[1])
//def evt3= createEvent(name: "HASwitchDev", value: topic[2], data: [label: deQuote(payload[0]), type: topic[1], seq: sequence], isStateChange: true)
return //evt3
}
else if (topic[3]=='state'){
parent.onoffEventP (topic[2],payload[0],topic[1]) // not passing update atm as always true ??
//def evt4 = createEvent(name: "OnOff", value: topic[2], data: [status: payload[0], type: topic[1], seq: sequence, update: true], isStateChange: true) //, key2: payload])
return // evt4
}
parent.HAUnknown (topic[2],payload[0])
//def evt57 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0]],seq: sequence, isStateChange: true) //, key2: payload]
log ("Unhandled HA (a) switch MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
return // evt57
}
else if (topic[1]=='light') {
if (topic[3]=='friendly_name')
{
log ("Device ${topic[2]} is a HA light and is called " +payload[0] + " " + friendlyName,"TRACE")
parent.HALightFriendlyP (topic[2],deQuote(payload[0]),topic[1])
//def evt5= createEvent(name: "HALightDev", value: topic[2], data: [label: deQuote(friendlyName), type: topic[1],seq: sequence], isStateChange: true)
return //evt5
}
else if (topic[3]=='state'){
parent.onoffEventP (topic[2],payload[0],topic[1]) // not passing update atm as always true ??
//def evt6 = createEvent(name: "OnOff", value: topic[2], data: [status: payload[0],seq: sequence, type: topic[1], update: true], isStateChange: true) //, key2: payload])
return //evt6
}
else if (topic[3]=='brightness'){
parent.dimEventP (topic[2],payload[0],topic[1],255) // maxLevel passed as 255 always ??
//def evt9 = createEvent(name: "Dim", value: topic[2], data: [level: payload[0]], maxLevel: "255", type: topic[1], isStateChange: true)
return // evt9
}
log ("Unhandled HA (c) light MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
//def evt62 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
return // evt62
} // not a HA light
else if (topic[1]=='group') {
if (topic[3]=='friendly_name') {
log ("Device ${topic[2]} is a HA group and is called " +payload[0], "TRACE")
parent.HAGroupFriendlyP (topic[2],deQuote(payload[0]),topic[1])
//def evt3= createEvent(name: "HAGroupDev", value: topic[2], data: [label: deQuote(payload[0]), type: topic[1],seq: sequence], isStateChange: true)
return // evt3
}
else if (topic[3]=='state'){
//def evt4 = createEvent(name: "Group", value: topic[2], data: [status: payload[0],seq: sequence, type: topic[1], update:true], isStateChange: true) //, key2: payload])
parent.groupEvent (topic[2],payload[0],topic[1])
//log ("Device ${topic[2]} is a HA group and is turned " +payload[0], "TRACE")
return // evt4
}
log ("Unhandled HA (b) group MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
//def evt58 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0]], isStateChange: true) //, key2: payload]
return // evt58
}
else if (topic[1]=='input_boolean') {
if (topic[3]=='friendly_name') {
parent.HAInputBooleanFriendlyP (topic[2],deQuote(payload[0]),topic[1])
log ("Device ${topic[2]} is a HA input boolean and is called " +payload[0], "TRACE")
//def evt3= createEvent(name: "HAInputBooleanDev", value: topic[2], data: [label: deQuote(payload[0]), type: topic[1],seq: sequence], isStateChange: true)
return // evt3
}
else if (topic[3]=='state'){
//def evt4 = createEvent(name: "InputBoolean", value: topic[2], data: [status: payload[0],type: topic[1],seq: sequence,update: true,topic: topicFull], isStateChange: true) //, key2: payload])
log ("Device ${topic[2]} is a HA input boolean and is " +payload[0], "TRACE")
parent.inputBooleanEventP(topic[2],payload[0],topic[1],topicFull)
return// evt4
}
log ("Unhandled HA (b) input_boolean MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
//def evt58 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
return // evt58
}
else if (topic[1]=='climate') {
if (topic[3]=='friendly_name') {
parent.HAClimateFriendlyP(topic[2],deQuote(payload[0]),topic[1])
log ("Device ${topic[2]} is a HA climate device and is called " +payload[0], "TRACE")
//def evt3= createEvent(name: "HAClimateDev", value: topic[2], data: [label: deQuote(payload[0]),seq: sequence], isStateChange: true)
return // evt3
}
else if (topic[3]=='state'){
parent.HAClimateEventP(topic[2],payload[0],topic[1])
//def evt4 = createEvent(name: "HAClimateDev", value: topic[2], data: [status: payload[0],update: true, type: topic[1], seq: sequence], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG - r-eenable
log ("Device ${topic[2]} is a HA climate device and is " +payload[0], "TRACE")
return // evt4
}
log ("Unhandled HA (b) climate MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
//def evt59 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
return // evt59
}
else if (topic[1]=='device_tracker') {
if (topic[3]=='friendly_name') {
parent.HADeviceTrackerFriendlyP(topic[2],deQuote(payload[0]),topic[1])
log ("Device ${topic[2]} is a HA tracker device and is called " +payload[0], "TRACE")
//def evt3= createEvent(name: "HADeviceTrackerDev", value: topic[2], data: [label: deQuote(payload[0]), type: topic[1],seq: sequence], isStateChange: true)
return // evt3
}
else if (topic[3]=='state'){
parent.parent.inputBooleanEventP(topic[2],payload[0],topic[1],'') //topicfull not used here ?
//def evt4 = createEvent(name: "InputBoolean", value: topic[2], data: [status: payload[0], type: topic[1], update: true], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG
//log ("Device ${topic[2]} is a HA input boolean and is " +payload[0], "TRACE")
return // evt4
}
log ("Unhandled HA (b) device_tracker MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
//def evt60 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0], type: topic[1], seq: sequence], isStateChange: true) //, key2: payload]
return // evt60
}
else if (topic[1]=='cover') {
if (topic[3]=='friendly_name') {
parent.HACoverFriendlyP(topic[2],deQuote(payload[0]),topic[1])
log ("Device ${topic[2]} is a HA cover and is called " +payload[0], "TRACE")
//def evt3= createEvent(name: "HACoverDev", value: topic[2], data: [label: deQuote(payload[0]),seq: sequence, type: topic[1]], isStateChange: true)
return //evt3
}
// Blind works off current_position only and sets switch and windowShade and level/position to be the same
/*
else if (topic[3]=='state'){
def evt4 = createEvent(name: "Cover", value: topic[2], data: [status: payload[0], type: topic[1], update: true], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG
log ("Device ${topic[2]} is a HA cover and is " +payload[0], "TRACE")
return evt4
}
*/
else if (topic[3]=='current_position'){
parent.dimEventP(topic[2],payload[0],topic[1])
//def evt4 = createEvent(name: "Dim", value: topic[2], data: [level: payload[0],seq: sequence, type: topic[1]], isStateChange: true) //, key2: payload]) // Dim works because cover accepts both set level and set position
log ("Device ${topic[2]} is a HA cover and is at position " +payload[0], "TRACE")
return //evt4
}
log ("Unhandled HA (b) cover MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
//def evt61 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0]],seq: sequence, isStateChange: true) //, key2: payload]
return // evt61
}
else if (topic[1]=='locks') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
log ("Device ${topic[2]} is a HA lock and is called " +payload[0], "TRACE")
parent.HALockFriendlyP(topic[2],deQuote(payload[0]),topic[1])
//def evt3= createEvent(name: "HALockDev", value: topic[2], data: [label: deQuote(payload[0])],seq: sequence, type: topic[1], isStateChange: true)
return // evt3
}
else if (topic[3]=='state'){
parent.inputBooleanEventP(topic[2],payload[0],topic[1],topicFull)
//def evt4 = createEvent(name: "InputBoolean", value: topic[2], data: [status: payload[0],seq: sequence, type: topic[1], update: true], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG
//log ("Device ${topic[2]} is a HA lock and is " +payload[0], "TRACE")
return // evt4
}
log ("Unhandled HA (b) lock MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
//def evt61 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
return // evt61
}
else if (topic[1]=='person') {
if (topic[3]=='friendly_name') {
parent.HAPresenceFriendlyP(topic[2],deQuote(payload[0]),topic[1])
log ("Device ${topic[2]} is a HA person and is called " +payload[0], "TRACE")
//def evt43= createEvent(name: "HAPerson", value: topic[2], data: [label: deQuote(payload[0]), type: topic[1]], isStateChange: true)
return // evt43
}
else if (topic[3]=='state'){
parent.PresenceEventP(topic[2],payload[0],topic[1])
//def evt44 = createEvent(name: "person", value: topic[2], data: [status: payload[0],seq: sequence, type: topic[1], update: true], isStateChange: true) //, key2: payload])
//log ("Device ${topic[2]} is a HA person and is " +payload[0], "TRACE")
return // evt44
}
log ("Unhandled HA (b) person MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
// def evt63 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
return //evt63
}
log ("Unhandled (d) HA MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
parent.HAUnknown (topic[2],payload[0])
// def evt64 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
return // evt64
} // not HA
// ################################################## homie ###############################################
else if (topic[0]=="homie" && topic[1]== "${state.normHubName}"){ //local - look for incoming homie 'set' commands that control Hubitat devices here
log ("Received this message from homie " + topic + " " + payload, "TRACE")
state.seq2++
sequence=state.seq2.toString()
if (topicCount==6) { // rather than these why not just check last topic - endsWith("/set")
if ((topic[5]=="set")&&(topic[4]=="rgb")) {
parent.commandP(topic[2],payload[0],payload,payloadFull,topic[3],interfaces.mqtt.parseMessage(description),json)
//def evt12 = createEvent(name: "Command", value: topic[2], data: [state: payload[0], payload: payload, seq: sequence, cmd: topic[3],topic: interfaces.mqtt.parseMessage(description)], isStateChange: true)
log ("Received this " + payload + " " + payload.size(), "TRACE")
return //evt12
}
}
else if (topicCount==5) {
if (topic[4]=="set") {
parent.commandP(topic[2],payload[0],payload,payloadFull,topic[3],interfaces.mqtt.parseMessage(description),json)
//def evt12 = createEvent(name: "Command", value: topic[2], data: [state: payload[0], payload: payload,seq: sequence, cmd: topic[3],topic: interfaces.mqtt.parseMessage(description)], isStateChange: true)
log ("Received this " + payload + " " + payload.size(), "TRACE")
return //evt12
}
else {
log ("Received unexpected message from homie " + topic + " " + payload, "WARN")
}
}
else if (topicCount==4) {
if (topic[3]=='$properties'){ // This will be a response to a 'getMQTT' for the $properties of an inbuilt device to append to.
//state.seq2++
//sequence=state.seq2.toString()
// for first node will be blank so never get here- need to init elsewhere
log ("UNSUBSCRIBE from ${interfaces.mqtt.parseMessage(description).topic}","DEBUG")
unsubscribe(interfaces.mqtt.parseMessage(description).topic)
parent.topicPayload (topic[2],payload,interfaces.mqtt.parseMessage(description).topic)
//sendEvent (name: "Subscribes", value: --state.subs)
log ("Timeout cancelled as got a property response ${payload} ${payload[0]} from ${topic}","DEBUG")
//def evt23 = createEvent(name: "getTopic", value: topic[2], data: [state: payload,seq: sequence, topic: interfaces.mqtt.parseMessage(description).topic], isStateChange: true)
return //evt23
}
if (topic[2]=='$fw') { // This is a random (non updating) topic used to pace the progress of subscriptions
if (topic[3]=='name') {
//log ("============ homie Discovery has completed ============","INFO")
if (device.currentWaiting=="name") {
log ("============ Asking HA Discovery to start ============","INFO")
sendEvent(name: "HADiscoverStart", value: 'HAstart',isStateChange: true)
setStateWaiting ("none")
sendEvent (name: "Waiting", value: "none")
unsubscribeTopic ('homie/'+state.normHubName+'/$fw/name')
}
else {
log ("Received pacing 'name' but expected $device.currentWaiting", "KH")
log ("====== Regardless still asking HA Discovery to start ========","INFO")
sendEvent(name: "HADiscoverStart", value: 'HAstart',isStateChange: true)
}
}//name
else if (topic[3]=='client') {
if (device.currentWaiting=="client") {
setStateWaiting ("version")
sendEvent (name: "Waiting", value: "version")
log ("============ Unsubscribing from HA friendly_names ============","INFO")
sendEvent(name: "unsubscribeFriendly", value: "true",isStateChange: true)
unsubscribeTopic ('homie/'+state.normHubName+'/$fw/client')
}
else {
log ("Received pacing 'client' but expected $device.currentWaiting", "KH")
log ("======= Regardless still unsubscribing from HA friendly_names ========","INFO")
sendEvent(name: "unsubscribeFriendly", value: "true",isStateChange: true)
}
} //client
else if (topic[3]=='version') {
log ("============ HA Discovery has completed =============","INFO")
if (device.currentWaiting=="version") {
log ("============ Asking Startup Summary to run ============","INFO")
setStateWaiting ("complete")
sendEvent (name: "Waiting", value: "complete")
sendEvent(name: "HADiscoverStart", value: 'complete',isStateChange: true)
unsubscribeTopic ('homie/'+state.normHubName+'/$fw/version')
}
else {
log ("Received pacing 'version' but expected $device.currentWaiting", "KH")
log ("======== Regardless HA Discovery has completed =============","INFO")
log ("======== Regardless asking Startup Summary to run ============","INFO")
sendEvent(name: "HADiscoverStart", value: 'complete',isStateChange: true)
setStateWaiting ("complete")
}
} //version
} // not $fw
} //topiccount 4
} // not local homie message
else if (topic[0]=="homie" && topic[1]== state.homieDeviceDiscovery) { // remote subscribed homie)
log ("homie discovery","TRACE")
if (topic[2]=='$nodes') { // maybe pass this directly to the input selector ??
log ("============= ${payload.size()} homie entries for device ${topic[1]} =============",, "INFO")
return
}
if (topic[3]=='onoff') {
//state.seq2++
//sequence=state.seq2.toString()
parent.onoffEventP (topic[2],payload[0],topic[1]) // TODO This may need attention as I'm not passing the 'topic' value below but method doesn't seem to use it ?
//def evt1 = createEvent(name: "OnOff", value: topic[2], data: [status: payload[0],seq: sequence, topic:interfaces.mqtt.parseMessage(description).topic], isStateChange: true) //, key2: payload])
return //evt1
}
else if (topic[3]=='dim' && topicCount==4) {
parent.dimEventP (topic[2],payload[0],topic[1],255) //TODO check see below
// TODO check if this level handling is problematic assuming 1.0 ?
// float convertedNumber = Float.parseFloat(payload[0])*100
// int intLevel = convertedNumber = convertedNumber.round()
//state.seq2++
//sequence=state.seq2.toString()
//adjLevel=intLevel.toString()
//ToDo - check .. why did we pass topic below or state - we didnt use them ??
//def evt2 = createEvent(name: "Dim", value: topic[2], data: [state: "level", seq: sequence, level: payload[0], topic: interfaces.mqtt.parseMessage(description).topic], isStateChange: true)
return // evt2
}
else if ((topic[3]=='dim') && (topic[4]=='$format') && (topicCount==5)) {
parent.formatP (topic[2], payload[0]) // ,interfaces.mqtt.parseMessage(description).topic)
//def evt2 = createEvent(name: "Format", value: topic[2], data: [state: "format", seq: sequence, format: payload[0], topic: interfaces.mqtt.parseMessage(description).topic], isStateChange: true)
return //evt2
}
else if (topic[3]=="$properties") {
log ("New $properties discovery" + interfaces.mqtt.parseMessage(description).payload + " for " + topic, "LOG")
}
else if (topic[3]=='$type') {
//log ("Got a type of " + payload[0] + " for " + topic, "INFO")
state.seq2++
sequence=state.seq2.toString()
switch (payload[0]) {
case 'switch':
case 'socket':
parent.homieOnOffDev (topic[2],'onoff')
//def evt33 = createEvent(name: "OnOffDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'light':
case 'RGBT light':
case 'RGB light':
case 'CT light':
parent.homieDimDev (topic[2],'dim')
//evt33 = createEvent(name: "DimDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'sensor':
parent.homieSensorDev (topic[2],'sensor')
//evt33 = createEvent(name: "SensorDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'thermostat':
//evt33 = createEvent(name: "HEUnknown", value: topic[2], data: [seq: sequence], isStateChange: true)
parent.homieUnknownDev (topic[2],payload[0])
break
case 'button':
parent.homieButtonDev (topic[2],'button')
//evt33 = createEvent(name: "ButtonDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'variable':
parent.homieVariableDev (topic[2],'variable')
//evt33 = createEvent(name: "VariableDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'lock':
parent.homieLockCapability (topic[2],'lock')
break
default:
parent.homieUnknownDev (topic[2],payload[0])
//evt33 = createEvent(name: "HEUnknown", value: topic[2], data: [seq: sequence], isStateChange: true)
break
}
log ("Registering homie device ${topic[2]} of type ${payload[0]} ","DEBUG")
//return evt33 // TODO - need to sure it exists .. OK to return from here inside an else ? isStateChange: true needed otherwise if no change to value: event is sent
}
else if (topic[3]=='$name') {
parent.LabelDeviceP ("MQTT:homie_"+topic[2],payload[0])
//state.seq2++
//sequence=state.seq2.toString()
//log("Received homie name from MQTT ${topic[2]} " + status, "TRACE")
//def evt0 = createEvent(name: "LabelDevice", value: "MQTT:homie_"+topic[2], data: [label: payload[0],seq: sequence], isStateChange: true)
return // evt0
}
else if (topicCount==5)
{
if (topic[4]=='$unit') {
//state.seq2++
//sequence=state.seq2.toString()
log ( "--$state.seq2-- - mS","DEBUG")
log("Received homie UOM from MQTT ${topic[2]} " + status, "TRACE")
}
}
else log ("Unhandled" + topic,"KH")
} // end remote homie
else if (topic[0]=='shellies')
{
//if (topic[2] == "online") log.error "Found Shelly device ${topic[1]}" // This is not a retained or periodically updated topic and used for LWT
if (topic[2] == "online" | topic[2] == "onlineRet") {
//if (payload[0] == "true") {
parent.ShellyDev(topic[1],payload[0])
//log ("Found Shelly device ${topic[1]}", "LOG")
// We need to determine how many relays and other endpoints there are
//def evt47 = createEvent(name: "ShellyDevice", value: topic[1],data:[seq: sequence], isStateChange: true)
//def evt47 = createEvent(name: "ShellyRelayDev", value: topic[1], isStateChange: true)
return // evt47
//}
//else if (payload[0] == "false") log ("Shelly device ${topic[1]} is offline", "LOG")
}
else if (topicCount>3) {
if (topic[3] == "input") {
//log.info "Sonoff Input"
}
else if (topic[3] == "relay"){
//log.info "Sonoff Relay"
}
}
return
}
else { // unhandled parse messages arrive here
//state.seq2++
sequence=state.seq2.toString()
top=interfaces.mqtt.parseMessage(description).topic
if (!top.startsWith("SHAC/")) log ("ad hoc MQTT parse $top ${payload[0]}", "LOG")
log ("ad hoc MQTT parse $top ${payload[0]}", "TRACE")
parent.lookupP(payload[0],payloadFull,interfaces.mqtt.parseMessage(description).topic, json) // replacement for event call
// if (json=="true") sendEvent (name: "Lookup", value: interfaces.mqtt.parseMessage(description).payload, data: [topic: interfaces.mqtt.parseMessage(description).topic,seq: sequence,json: json], isStateChange: true)
// else sendEvent (name: "Lookup", value: payload[0], data: [topic: interfaces.mqtt.parseMessage(description).topic,seq: sequence,json: json], isStateChange: true)
}
json="false"
}
def deQuote (s) {
s=s.replaceAll('^\"|\"$', "")
return s
}
def reset()
{
try {
interfaces.mqtt.disconnect()
log ("Disconnected MQTT", "INFO")
} catch(e) {
log ("Disconnecting : ${e.message}", "WARN")
}
log ("Resetting MQTT connection", "INFO")
initialize()
connect()
}
def disconnect()
{
try {
interfaces.mqtt.disconnect()
log ("Disconnected MQTT", "INFO")
sendEvent (name: "presence", value: "not present")
} catch(e) {
log ("Disconnecting failed : ${e.message}", "WARN")
}
}
def simulateError (type) {
try {
interfaces.mqtt.disconnect()
log ("Force Disconnected MQTT", "INFO")
} catch(e) {
log ("Disconnecting : ${e.message}", "WARN")
}
parent.mqttStatus ("Error")
}
def setStateVar(var,value,value2=null) { //DEPRECATED // Better to expose as attributes
return
if (var == "logLevel"){
state.logLevel = value.toInteger()
log ("## Restarted ##","WARN")
log ("Log Level set to " + value,"INFO")
}
else if (var == "homieDevice"){
state.homieDeviceDiscovery = value
log ("homie discovery is for device: " + value, "INFO")
}
else if (var == "HAStatestreamTopic"){
state.HAStatestreamTopic = value
log ("HAStatestreamTopic is " + value,"INFO")
}
/* else if (var == "connectionAttempts"){
state.connectionAttempts = value
log ("ConnectionAttempts reset to " + value ,"INFO")
}
else if (var == "normHubName") {
state.normHubName = value
state.hubName=value2
log ("Normalised ${state.hubName} hub name is ${state.normHubName}","INFO")
}
*/
else if (var == "MQTTmyStatus") {
sendEvent (name: "presence", value: value)
log ("Reporting broker status as ${value}","INFO")
}
else log ("Tried to set a non supported state variable in MQTT client ${var}","ERROR")
}
def pause(millis) {
pauseExecution(millis.toInteger())
}
def MD5(String s){
if (s==null) s='123'
MessageDigest.getInstance("MD5").digest(s.bytes).encodeHex().toString()
}
def log(data, type) {
data = "MQTT> ${data ?: ''}"
if (determineLogLevel(type) >= state.logLevel) {
switch (type?.toUpperCase()) {
case "TRACE":
log.trace "${data}"
break
case "DEBUG":
log.debug "${data}"
break
case "INFO":
log.info "${data}"
break
case "WARN":
log.warn "${data}"
break
case "ERROR":
log.error "${data}"
break
case "DISABLED":
break
case "BLUE":
log.info "${data}"
break
case "RED":
log.info "${data}"
break
case "ORANGE":
log.info "${data}"
break
case "GREEN":
log.info "${data}"
break
case "YELLOW":
log.info "${data}"
break
case "KH":
if (state.myHub) log.trace "${data}"
break
case "LOG":
if (state.myHub) log.trace "${data}"
break
default:
log.error "MQTT -- ${device.label} -- Invalid Log Setting"
}
}
}
private determineLogLevel(data) {
switch (data?.toUpperCase()) {
case "TRACE":
return 0
break
case "DEBUG":
return 1
break
case "INFO":
return 2
break
case "WARN":
return 3
break
case "ERROR":
return 4
break
case "DISABLED":
return 5
break
case "RED":
case "BLUE":
case "GREEN":
case "YELLOW":
case "ORANGE":
case "LOG":
case "KH":
return 6
break
default:
return 1
}
}