// Not licensed for onward /re-distribution for any reason and in any form - see license
import java.security.MessageDigest
metadata {
// v0.5
definition (name: "MQTT Client", namespace: "ukusa", author: "Kevin Hawkins",importUrl: "https://raw.githubusercontent.com/xAPPO/MQTT/beta/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 "setStateVar", ["String","String"]
command "setLogLevel", ["Integer"]
attribute "Sequence", "integer"
attribute "Subscribes", "integer"
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() {
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())=="ed9bb17f2f9c2e9b721e1df63cd383e8") state.myHub=true else state.myHub=false
heartbeat()
schedule("0/20 * * * * ? *", heartbeat)
version="beta 1a"
if (state.delay==null) state.delay=250 // inter state.maxImmediate event pacing in mS
// 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 ${settings?.MQTTBroker}","BLUE")
subscribeTopic ('homie/'+state.normHubName+'/$implementation/heartbeat')
state.connectionAttempts = 0
state.seq2=0
}
else log ("MQTT not connected","ERROR")
}
void setEventDelay (delaymS) {
state.delay = delaymS
}
void setLogLevel (level) {
if (level<6) {
state.logLevel=level
log ("Log Level set to " + level,"INFO")
}
}
boolean mqttConnect() {
try {
def mqttInt = interfaces.mqtt
log ( "MQTT> client ${version} initialising","INFO")
try{
log ("Connecting as Hubitat_${state.normHubName} to MQTT broker ${settings?.MQTTBroker}", "INFO")
mqttInt.connect(settings?.MQTTBroker, "Hubitat_${state.normHubName}", settings?.username,settings?.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)
}
//state.delay=250 //delay between events - increase if you have a lot of discovered MQTT devices > 100
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 (4,"adviseStatus")
// sendEvent(name: "MQTTStatus", value: status, data: [message:message], isStateChange: true) // will reset
runIn (5,"reset")
}
else {
log ("Broker: ${message}","KH")
mqttState=interfaces.mqtt.isConnected()
if (mqttState) {
log ("MQTT broker connected","INFO")
subscribeTopic ('homie/'+state.normHubName+'/$implementation/heartbeat')
sendEvent (name: "presence", value: "present")
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")
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) ("Suppressed: $topic = $payload")
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 ack(seq) {
roundTime = now() - state.dispatch
if (seq!=null){
if (seq[0] != '@') {
log ("==$seq== $roundTime mS","DEBUG")
if (roundTime > state.maxImmediate) state.maxImmediate = roundTime
}
else {
log ( " ==$seq== $roundTime mS","DEBUG")
if (roundTime > state.maxComplete) state.maxComplete = roundTime
}
}
else log ("##null##","DEBUG")
}
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) {
delay = state.delay
runIn (30, "bufferEmpty")
topicFull=interfaces.mqtt.parseMessage(description).topic
def topic=topicFull.split('/')
def topicCount=topic.size()
def payload=interfaces.mqtt.parseMessage(description).payload.split(',')
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=='*')){
state.seq2++
sequence=state.seq2.toString()
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
ack=false
if (payload[0]=="online") {
log ("Home Assistant [${state.HAStatestreamTopic}] is ONLINE","INFO")
def evt23 = createEvent(name: "HASynch", value: state.HAStatestreamTopic, isStateChange: true)
//state.dispatch = now()
return evt23
}
else if (payload[0]=="offline") {
log ("Home Assistant [${state.HAStatestreamTopic}] is OFFLINE","INFO")
}
return
}
else if (topic[1]=='sensor') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='state'){
//log ("RX::Sensor state:: " + topic[2] + " is " + payload[0], "TRACE")
def evt10 = createEvent(name: "Sensor", value: topic[2], data: [status: payload[0],seq: sequence, update: true], isStateChange: true) //, key2: payload])
pause (delay) // pace
state.dispatch = now()
return evt10
}
else if (topic[3]=='device_class'){
log ("### [" + payload[0] +"] type for HA Sensor Device ${topic[2]}", "TRACE")
def evt11= createEvent(name: "HASensorType", value: topic[2], data: [payload: deQuote(payload[0]),seq: sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt11
}
else if (topic[3]=='friendly_name'){
log ("Device ${topic[2]} is a HA sensor and is called " +payload[0], "TRACE")
def evt11= createEvent(name: "HASensorDev", value: topic[2], data: [label: deQuote(payload[0]),seq: state.sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt11
}
else if (topic[3]=='unit_of_measurement'){
def evt11= createEvent(name: "SensorUnit", value: topic[2], data: [payload: deQuote(payload[0]),seq: state.sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt11
}
}
else if (topic[1]=='binary_sensor') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='state'){
//log ("RX::Binary Sensor state:: " + topic[2] + " is " + payload[0], "TRACE")
def evt10 = createEvent(name: "BinarySensor", value: topic[2], data: [status: payload[0],seq: sequence, update: true], isStateChange: true) //, key2: payload])
pause (delay) // pace
state.dispatch = now()
return evt10
}
else if (topic[3]=='friendly_name'){
log ("Device ${topic[2]} is a HA binary sensor and is called " +payload[0], "TRACE")
def evt19 = createEvent(name: "LabelDevice", value: "MQTT:HA_"+topic[2], data: [label: deQuote(payload[0]),seq: sequence], isStateChange: true)
//return evt19
// just renames the device as creation now from 'device_class // need to ensure device_class arrives first so driver is correct when created
//def evt19= createEvent([name: "HABinarySensorDev", value: topic[2], data: [label: payload[0]]])
pause (delay) // pace // pace // maybe extend this if needed to ensure above
state.dispatch = now()
return evt19
}
else if (topic[3]=='device_class'){
log ("### [" + payload[0] +"] type for HA Binary Sensor Device ${topic[2]}", "TRACE")
//log ("### Device ${topic[2]} is a HA binary sensor and is of type " +payload[0], "INFO")
// create device from here as need device_class to select driver
def evt18= createEvent(name: "HABinarySensorDev", value: topic[2], data: [type: deQuote(payload[0]),seq: sequence], isStateChange: true) // TODO handle in app
pause (delay) // pace
state.dispatch = now()
return evt18
}
}
else if (topic[1]=='switch') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
log ("Device ${topic[2]} is a HA switch and is called " +payload[0], "TRACE")
def evt3= createEvent(name: "HASwitchDev", value: topic[2], data: [label: deQuote(payload[0]),seq: sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt3
}
else if (topic[3]=='state'){
def evt4 = createEvent(name: "OnOff", value: topic[2], data: [status: payload[0], type: topic[1], seq: sequence, update: true], isStateChange: true) //, key2: payload])
pause (delay) // pace
state.dispatch = now()
return evt4
}
pause (delay) // pace
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")
state.dispatch = now()
return evt57
}
else if (topic[1]=='group') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
log ("Device ${topic[2]} is a HA group and is called " +payload[0], "TRACE")
def evt3= createEvent(name: "HAGroupDev", value: topic[2], data: [label: deQuote(payload[0]),seq: sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt3
}
else if (topic[3]=='state'){
def evt4 = createEvent(name: "Group", value: topic[2], data: [status: payload[0],seq: sequence, update:true], isStateChange: true) //, key2: payload])
pause (delay) // pace
//log ("Device ${topic[2]} is a HA group and is turned " +payload[0], "TRACE")
state.dispatch = now()
return evt4
}
log ("Unhandled HA (b) group MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
def evt58 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0]], isStateChange: true) //, key2: payload]
state.dispatch = now()
return evt58
}
else if (topic[1]=='input_boolean') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
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]),seq: sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
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])
pause (delay) // pace
log ("Device ${topic[2]} is a HA input boolean and is " +payload[0], "TRACE")
state.dispatch = now()
return evt4
}
log ("Unhandled HA (b) input_boolean MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
def evt58 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
state.dispatch = now()
return evt58
}
else if (topic[1]=='climate') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
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)
pause (delay) // pace
state.dispatch = now()
return evt3
}
else if (topic[3]=='state'){
def evt4 = createEvent(name: "InputBoolean", value: topic[2], data: [status: payload[0],update: true, seq: sequence], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG - r-eenable
pause (delay) // pace
log ("Device ${topic[2]} is a HA climate device and is " +payload[0], "TRACE")
state.dispatch = now()
return evt4
}
log ("Unhandled HA (b) climate MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
def evt59 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
state.dispatch = now()
return evt59
}
else if (topic[1]=='device_tracker') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
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]),seq: sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt3
}
else if (topic[3]=='state'){
def evt4 = createEvent(name: "InputBoolean", value: topic[2], data: [status: payload[0], update: true], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG
pause (delay) // pace
//log ("Device ${topic[2]} is a HA input boolean and is " +payload[0], "TRACE")
state.dispatch = now()
return evt4
}
log ("Unhandled HA (b) device_tracker MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
def evt60 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
state.dispatch = now()
return evt60
}
else if (topic[1]=='cover') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
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], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt3
}
else if (topic[3]=='state'){
def evt4 = createEvent(name: "Cover", value: topic[2], data: [status: payload[0], update: true], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG
pause (delay) // pace
log ("Device ${topic[2]} is a HA cover and is " +payload[0], "TRACE")
state.dispatch = now()
return evt4
}
else if (topic[3]=='current_position'){
def evt4 = createEvent(name: "Dim", value: topic[2], data: [level: payload[0]],seq: sequence, isStateChange: true) //, key2: payload]) // Dim works because cover accepts both set level and set position
pause (delay) // pace
log ("Device ${topic[2]} is a HA cover and is at position " +payload[0], "TRACE")
state.dispatch = now()
return evt4
}
log ("Unhandled HA (b) cover MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
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")
def evt3= createEvent(name: "HALockDev", value: topic[2], data: [label: deQuote(payload[0])],seq: sequence, isStateChange: true)
pause (state.delay) // pace
state.dispatch = now()
return evt3
}
else if (topic[3]=='state'){
def evt4 = createEvent(name: "InputBoolean", value: topic[2], data: [status: payload[0],seq: sequence, update: true], isStateChange: true) //, key2: payload]) // TODO CHECK WRONG
pause (delay) // pace
//log ("Device ${topic[2]} is a HA lock and is " +payload[0], "TRACE")
state.dispatch = now()
return evt4
}
log ("Unhandled HA (b) lock MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
def evt61 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
state.dispatch = now()
return evt61
}
else if (topic[1]=='light') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name')
{
friendlyName=payload[0]
if (friendlyName[0]=='"') // remove double quotes
{
friendlyName=friendlyName.substring(1)
friendlyName=friendlyName.substring(0, friendlyName.length() - 1)
}
log ("Device ${topic[2]} is a HA light and is called " +payload[0] + " " + friendlyName,"TRACE")
def evt5= createEvent(name: "HALightDev", value: topic[2], data: [label: deQuote(friendlyName),seq: sequence], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt5
}
else if (topic[3]=='state'){
def evt6 = createEvent(name: "OnOff", value: topic[2], data: [status: payload[0],seq: sequence, update: true], isStateChange: true) //, key2: payload])
pause (delay) // pace
state.dispatch = now()
return evt6
}
else if (topic[3]=='brightness'){
def evt9 = createEvent(name: "Dim", value: topic[2], data: [level: payload[0]], maxLevel: "255", isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt9
}
log ("Unhandled HA (c) light MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
def evt62 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
state.dispatch = now()
return evt62
} // not a HA light
else if (topic[1]=='person') {
state.seq2++
sequence=state.seq2.toString()
if (topic[3]=='friendly_name') {
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])], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt43
}
else if (topic[3]=='state'){
def evt44 = createEvent(name: "person", value: topic[2], data: [status: payload[0],seq: sequence, update: true], isStateChange: true) //, key2: payload])
pause (delay) // pace
//log ("Device ${topic[2]} is a HA input boolean and is " +payload[0], "TRACE")
state.dispatch = now()
return evt44
}
log ("Unhandled HA (b) person MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
def evt63 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
pause (delay) // pace
state.dispatch = now()
return evt63
}
log ("Unhandled (d) HA MQTT parse ${topic[0]} ${topic[1]} ${topic[2]} ${topic[3]} - ${payload[0]}", "WARN")
pause (delay) // pace
def evt64 = createEvent(name: "HAUnknown", value: topic[2], data: [status: payload[0],seq: sequence], isStateChange: true) //, key2: payload]
state.dispatch = now()
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")) {
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")
pause (delay) // pace
state.dispatch = now()
return evt12
}
}
else if (topicCount==5) {
if (topic[4]=="set") {
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")
pause (delay) // pace
state.dispatch = now()
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)
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)
pause (delay) // pace
state.dispatch = now()
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()
def evt1 = createEvent(name: "OnOff", value: topic[2], data: [status: payload[0],seq: sequence, topic:interfaces.mqtt.parseMessage(description).topic], isStateChange: true) //, key2: payload])
pause (delay) // pace
state.dispatch = now()
return evt1
}
else if (topic[3]=='dim' && topicCount==4) {
// 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()
def evt2 = createEvent(name: "Dim", value: topic[2], data: [state: "level", seq: sequence, level: payload[0], topic: interfaces.mqtt.parseMessage(description).topic], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt2
}
else if ((topic[3]=='dim') && (topic[4]=='$format') && (topicCount==5)) {
state.seq2++
sequence=state.seq2.toString()
def evt2 = createEvent(name: "Format", value: topic[2], data: [state: "format", seq: sequence, format: payload[0], topic: interfaces.mqtt.parseMessage(description).topic], isStateChange: true)
pause (delay) // pace
state.dispatch = now()
return evt2
}
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':
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':
evt33 = createEvent(name: "DimDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'sensor':
evt33 = createEvent(name: "SensorDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'thermostat':
log ("homie " + payload[0] +" type not handled yet for device: "+ topic,"WARN")
evt33 = createEvent(name: "HEUnknown", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'socket':
evt33 = createEvent(name: "OnOffDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'button':
evt33 = createEvent(name: "ButtonDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
case 'variable':
evt33 = createEvent(name: "VariableDev", value: topic[2], data: [seq: sequence], isStateChange: true)
break
default:
log ("homie " + payload[0] +" type not handled for device: "+ topic[2],"WARN")
evt33 = createEvent(name: "HEUnknown", value: topic[2], data: [seq: sequence], isStateChange: true)
break
}
pause (delay) // pace //TODO optimise (remove hopefully)
log ("Registering homie device ${topic[2]} of type ${payload[0]} ","DEBUG")
pause (delay) //pace
state.dispatch = now()
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') {
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)
pause (delay) // pace
state.dispatch = now()
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")
pause (delay) // pace
state.dispatch = now()
}
}
else log ("Unhandled" + topic,"WARN")
} // 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") {
state.seq2++
sequence=state.seq2.toString()
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)
pause (delay) // pace
state.dispatch = now()
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()
log ("ad hoc MQTT parse ${interfaces.mqtt.parseMessage(description).topic} ${payload[0]}", "KH")
pause (delay) // pace
state.dispatch = now()
sendEvent (name: "Lookup", value: payload[0], data: [topic: interfaces.mqtt.parseMessage(description).topic,seq: sequence,], isStateChange: true)
}
}
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 setStateVar(var,value,value2=null) { //DEPRECATED // Better to expose as attributes
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
}
}