From 3a559503423e8d922ad81751936fdc88a8a8987f Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Mon, 9 Jul 2018 06:16:44 -0500 Subject: [PATCH 01/10] Added actionOpenClosed for contactSensors (simulated) --- smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy index 2c13d2d..e28a946 100644 --- a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy +++ b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy @@ -103,7 +103,8 @@ import groovy.transform.Field capability: "capability.contactSensor", attributes: [ "contact" - ] + ], + action: "actionOpenClosed" ], "doorControl": [ name: "Door Control", @@ -611,11 +612,17 @@ def actionColor(device, attribute, value) { } } +//Action for contactSensors,doorControl,garageDoors,valve and windowShades. +//contactSensors don't have action but a simulated contact sensor can hence the hasCommand() check. def actionOpenClosed(device, attribute, value) { if (value == "open") { - device.open() + if (device.hasCommand("open")) { + device.open() + } } else if (value == "closed") { - device.close() + if (device.hasCommand("close")) { + device.close() + } } } @@ -721,4 +728,4 @@ def actionTimedSession(device, attribute, value) { if (attribute == "timeRemaining") { device.setTimeRemaining(value) } -} +} \ No newline at end of file From 62dda1a7c3559093ffae1a1972c7b7e58cdac867 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Wed, 16 Jan 2019 20:39:40 -0600 Subject: [PATCH 02/10] Don't add duplicate subscription --- server.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index ca8d43b..84376bd 100644 --- a/server.js +++ b/server.js @@ -195,8 +195,12 @@ function handleSubscribeEvent (req, res) { subscriptions = []; Object.keys(req.body.devices).forEach(function (property) { req.body.devices[property].forEach(function (device) { - subscriptions.push(getTopicFor(device, property, TOPIC_COMMAND)); - subscriptions.push(getTopicFor(device, property, TOPIC_WRITE_STATE)); + var cmdTopic = getTopicFor(device, property, TOPIC_COMMAND); + var writeTopic = getTopicFor(device, property, TOPIC_WRITE_STATE); + subscriptions.push(cmdTopic); + if (writeTopic !== cmdTopic) { + subscriptions.push(writeTopic); + } }); }); @@ -258,7 +262,7 @@ function parseMQTTMessage (topic, message) { device = pieces[0], property = pieces[1], topicReadState = getTopicFor(device, property, TOPIC_READ_STATE), - topicWriteState = getTopicFor(device, property, TOPIC_WRITE_STATE), + //topicWriteState = getTopicFor(device, property, TOPIC_WRITE_STATE), topicSwitchState = getTopicFor(device, 'switch', TOPIC_READ_STATE), topicLevelCommand = getTopicFor(device, 'level', TOPIC_COMMAND); From 05f5db72b8c80f5db9651f9bfef7c35a324c3315 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Wed, 16 Jan 2019 20:55:24 -0600 Subject: [PATCH 03/10] Remove unused variable --- server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server.js b/server.js index 84376bd..aaa337b 100644 --- a/server.js +++ b/server.js @@ -262,7 +262,6 @@ function parseMQTTMessage (topic, message) { device = pieces[0], property = pieces[1], topicReadState = getTopicFor(device, property, TOPIC_READ_STATE), - //topicWriteState = getTopicFor(device, property, TOPIC_WRITE_STATE), topicSwitchState = getTopicFor(device, 'switch', TOPIC_READ_STATE), topicLevelCommand = getTopicFor(device, 'level', TOPIC_COMMAND); From 7a08fb45f6b82b1cdab985c605848565399db851 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Sat, 2 Feb 2019 08:00:49 -0600 Subject: [PATCH 04/10] Fixed white spaces --- smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy index e28a946..41e8d3e 100644 --- a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy +++ b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy @@ -616,13 +616,13 @@ def actionColor(device, attribute, value) { //contactSensors don't have action but a simulated contact sensor can hence the hasCommand() check. def actionOpenClosed(device, attribute, value) { if (value == "open") { - if (device.hasCommand("open")) { - device.open() - } + if (device.hasCommand("open")) { + device.open() + } } else if (value == "closed") { if (device.hasCommand("close")) { - device.close() - } + device.close() + } } } @@ -728,4 +728,4 @@ def actionTimedSession(device, attribute, value) { if (attribute == "timeRemaining") { device.setTimeRemaining(value) } -} \ No newline at end of file +} From bcd9c88e76512a9207ccd539dbc452e6753b95c3 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Thu, 15 Aug 2019 06:57:41 -0500 Subject: [PATCH 05/10] Only setting command if piece[2] is present and matches commandSuffix --- server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index aaa337b..1e04b81 100644 --- a/server.js +++ b/server.js @@ -1,3 +1,4 @@ + /*jslint node: true */ 'use strict'; @@ -296,7 +297,8 @@ function parseMQTTMessage (topic, message) { name: device, type: property, value: contents, - command: (!pieces[2] || pieces[2] && pieces[2] === config.mqtt[SUFFIX_COMMAND]) + //command: (!pieces[2] || pieces[2] && pieces[2] === config.mqtt[SUFFIX_COMMAND]) + command: (pieces[2] && pieces[2] === config.mqtt[SUFFIX_COMMAND]) || false } }, function (error, resp) { if (error) { From 47165adc8b8aae4110b6ca2d6a27162f9daf8387 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Thu, 15 Aug 2019 07:04:29 -0500 Subject: [PATCH 06/10] Removed garageDoors entry since it has been deprecated in favor of doorControl. Added "opensensor", "closedsensor", "switch", "notify" to doorControl Allowing "notify" to be sent on devices. --- smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy index 41e8d3e..0ee0c8f 100644 --- a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy +++ b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy @@ -110,7 +110,8 @@ import groovy.transform.Field name: "Door Control", capability: "capability.doorControl", attributes: [ - "door" + "door", + "opensensor", "closedsensor", "switch", "notify" ], action: "actionOpenClosed" ], @@ -121,14 +122,14 @@ import groovy.transform.Field "energy" ] ], - "garageDoors": [ + /*"garageDoors": [ name: "Garage Door Control", capability: "capability.garageDoorControl", attributes: [ "door" ], action: "actionOpenClosed" - ], + ],*/ "illuminanceMeasurement": [ name: "Illuminance Measurement", capability: "capability.illuminanceMeasurement", @@ -514,10 +515,11 @@ def bridgeHandler(evt) { if (json.type == "notify") { if (json.name == "Contacts") { sendNotificationToContacts("${json.value}", recipients) - } else { + return + } else if (json.name == "System") { sendNotificationEvent("${json.value}") + return } - return } // @NOTE this is stored AWFUL, we need a faster lookup table @@ -612,8 +614,6 @@ def actionColor(device, attribute, value) { } } -//Action for contactSensors,doorControl,garageDoors,valve and windowShades. -//contactSensors don't have action but a simulated contact sensor can hence the hasCommand() check. def actionOpenClosed(device, attribute, value) { if (value == "open") { if (device.hasCommand("open")) { @@ -728,4 +728,4 @@ def actionTimedSession(device, attribute, value) { if (attribute == "timeRemaining") { device.setTimeRemaining(value) } -} +} \ No newline at end of file From 6b6773e64c74f4613f96c06ed6716c0d66fd4630 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Sun, 15 Dec 2019 06:29:56 -0600 Subject: [PATCH 07/10] Adjusted logging --- server.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 1e04b81..dfb9e13 100644 --- a/server.js +++ b/server.js @@ -256,7 +256,7 @@ function getTopicFor (device, property, type) { */ function parseMQTTMessage (topic, message) { var contents = message.toString(); - winston.info('Incoming message from MQTT: %s = %s', topic, contents); + //winston.info('Incoming message from MQTT: %s = %s', topic, contents); // Remove the preface from the topic before splitting it var pieces = topic.substr(config.mqtt.preface.length + 1).split('/'), @@ -268,12 +268,13 @@ function parseMQTTMessage (topic, message) { // Deduplicate only if the incoming message topic is the same as the read state topic if (topic === topicReadState) { - if (history[topic] === contents) { + if (history[topic] === contents) { winston.info('Skipping duplicate message from: %s = %s', topic, contents); return; } } history[topic] = contents; + winston.info('Incoming message from MQTT: %s = %s', topic, contents); // If sending level data and the switch is off, don't send anything // SmartThings will turn the device on (which is confusing) From f3be0b900c3b4c84fde5161346b1b3f34d944329 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Sun, 15 Dec 2019 06:39:52 -0600 Subject: [PATCH 08/10] Remove "switch" custom attribute from "Door Control", invoke action as fallback if setStatus is absent --- smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy index 0ee0c8f..193dc67 100644 --- a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy +++ b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy @@ -111,7 +111,7 @@ import groovy.transform.Field capability: "capability.doorControl", attributes: [ "door", - "opensensor", "closedsensor", "switch", "notify" + "opensensor", "closedsensor", "notify" ], action: "actionOpenClosed" ], @@ -529,11 +529,18 @@ def bridgeHandler(evt) { settings[key].each {device -> if (device.displayName == json.name) { if (json.command == false) { + //setStatus is exposed by Espurna Garage Door Device Handler V2 + //invoke action as fallback if setStatus is absent if (device.getSupportedCommands().any {it.name == "setStatus"}) { log.debug "Setting state ${json.type} = ${json.value}" device.setStatus(json.type, json.value) state.ignoreEvent = json; } + else if (capability.containsKey("action")) { + def action = capability["action"] + // Yes, this is calling the method dynamically + "$action"(device, json.type, json.value) + } } else { if (capability.containsKey("action")) { From 4b9fc5e85d859ac95d7ef451586db91bdb5e3dfc Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Mon, 4 May 2020 05:52:34 -0500 Subject: [PATCH 09/10] Removed capabilities for which I don't have any devices Changed door to door, notify to match EspurnaGarageDoorDeviceHandlerV2 Force sent device battery status when initialized --- .../stj/mqtt-bridge.src/mqtt-bridge.groovy | 274 +++--------------- 1 file changed, 35 insertions(+), 239 deletions(-) diff --git a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy index 193dc67..39a5da1 100644 --- a/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy +++ b/smartapps/stj/mqtt-bridge.src/mqtt-bridge.groovy @@ -4,6 +4,7 @@ * Authors * - st.john.johnson@gmail.com * - jeremiah.wuenschel@gmail.com + * - Indu Prakash * * Copyright 2016 * @@ -44,13 +45,7 @@ import groovy.transform.Field "battery" ] ], - "beacon": [ - name: "Beacon", - capability: "capability.beacon", - attributes: [ - "presence" - ] - ], + "button": [ name: "Button", capability: "capability.button", @@ -58,38 +53,7 @@ import groovy.transform.Field "button" ] ], - "carbonDioxideMeasurement": [ - name: "Carbon Dioxide Measurement", - capability: "capability.carbonDioxideMeasurement", - attributes: [ - "carbonDioxide" - ] - ], - "carbonMonoxideDetector": [ - name: "Carbon Monoxide Detector", - capability: "capability.carbonMonoxideDetector", - attributes: [ - "carbonMonoxide" - ] - ], - "colorControl": [ - name: "Color Control", - capability: "capability.colorControl", - attributes: [ - "hue", - "saturation", - "color" - ], - action: "actionColor" - ], - "colorTemperature": [ - name: "Color Temperature", - capability: "capability.colorTemperature", - attributes: [ - "colorTemperature" - ], - action: "actionColorTemperature" - ], + "consumable": [ name: "Consumable", capability: "capability.consumable", @@ -110,8 +74,7 @@ import groovy.transform.Field name: "Door Control", capability: "capability.doorControl", attributes: [ - "door", - "opensensor", "closedsensor", "notify" + "door", "notify" ], action: "actionOpenClosed" ], @@ -130,13 +93,6 @@ import groovy.transform.Field ], action: "actionOpenClosed" ],*/ - "illuminanceMeasurement": [ - name: "Illuminance Measurement", - capability: "capability.illuminanceMeasurement", - attributes: [ - "illuminance" - ] - ], "imageCapture": [ name: "Image Capture", capability: "capability.imageCapture", @@ -152,22 +108,7 @@ import groovy.transform.Field ], action: "actionLevel" ], - "lock": [ - name: "Lock", - capability: "capability.lock", - attributes: [ - "lock" - ], - action: "actionLock" - ], - "mediaController": [ - name: "Media Controller", - capability: "capability.mediaController", - attributes: [ - "activities", - "currentActivity" - ] - ], + "motionSensors": [ name: "Motion Sensor", capability: "capability.motionSensor", @@ -176,25 +117,6 @@ import groovy.transform.Field ], action: "actionActiveInactive" ], - "musicPlayer": [ - name: "Music Player", - capability: "capability.musicPlayer", - attributes: [ - "status", - "level", - "trackDescription", - "trackData", - "mute" - ], - action: "actionMusicPlayer" - ], - "pHMeasurement": [ - name: "pH Measurement", - capability: "capability.pHMeasurement", - attributes: [ - "pH" - ] - ], "powerMeters": [ name: "Power Meter", capability: "capability.powerMeter", @@ -232,43 +154,7 @@ import groovy.transform.Field "shock" ] ], - "signalStrength": [ - name: "Signal Strength", - capability: "capability.signalStrength", - attributes: [ - "lqi", - "rssi" - ] - ], - "sleepSensor": [ - name: "Sleep Sensor", - capability: "capability.sleepSensor", - attributes: [ - "sleeping" - ] - ], - "smokeDetector": [ - name: "Smoke Detector", - capability: "capability.smokeDetector", - attributes: [ - "smoke" - ] - ], - "soundSensor": [ - name: "Sound Sensor", - capability: "capability.soundSensor", - attributes: [ - "sound" - ] - ], - "stepSensor": [ - name: "Step Sensor", - capability: "capability.stepSensor", - attributes: [ - "steps", - "goal" - ] - ], + "switches": [ name: "Switch", capability: "capability.switch", @@ -277,13 +163,7 @@ import groovy.transform.Field ], action: "actionOnOff" ], - "soundPressureLevel": [ - name: "Sound Pressure Level", - capability: "capability.soundPressureLevel", - attributes: [ - "soundPressureLevel" - ] - ], + "tamperAlert": [ name: "Tamper Alert", capability: "capability.tamperAlert", @@ -365,15 +245,6 @@ import groovy.transform.Field "threeAxis" ] ], - "timedSession": [ - name: "Timed Session", - capability: "capability.timedSession", - attributes: [ - "timeRemaining", - "sessionStatus" - ], - action: "actionTimedSession" - ], "touchSensor": [ name: "Touch Sensor", capability: "capability.touchSensor", @@ -381,14 +252,7 @@ import groovy.transform.Field "touch" ] ], - "valve": [ - name: "Valve", - capability: "capability.valve", - attributes: [ - "contact" - ], - action: "actionOpenClosed" - ], + "voltageMeasurement": [ name: "Voltage Measurement", capability: "capability.voltageMeasurement", @@ -402,14 +266,6 @@ import groovy.transform.Field attributes: [ "water" ] - ], - "windowShades": [ - name: "Window Shade", - capability: "capability.windowShade", - attributes: [ - "windowShade" - ], - action: "actionOpenClosed" ] ] @@ -478,6 +334,9 @@ def initialize() { // Update the bridge updateSubscription() + + sendBatteryStatuses() + runIn(5, sendBatteryStatuses) } // Update the bridge"s subscription @@ -581,6 +440,30 @@ def inputHandler(evt) { } } + +def sendBatteryStatuses() { + //https://docs.smartthings.com/en/latest/ref-docs/device-ref.html + def devicesWithBattery = settings["battery"] + log.debug "sendBatteryStatuses ${devicesWithBattery}" + devicesWithBattery.each{device-> + sendBatteryStatus(device) + } +} + +def sendBatteryStatus(device) { + def json = new JsonOutput().toJson([ + path: "/push", + body: [ + name: device.displayName, + value: device.currentState("battery"), + type: "battery" + ] + ]) + + log.debug "sendBatteryStatus: ${json}" + bridge.deviceNotification(json) +} + // +---------------------------------+ // | WARNING, BEYOND HERE BE DRAGONS | // +---------------------------------+ @@ -605,22 +488,6 @@ def actionAlarm(device, attribute, value) { } } -def actionColor(device, attribute, value) { - switch (attribute) { - case "hue": - device.setHue(value as float) - break - case "saturation": - device.setSaturation(value as float) - break - case "color": - def values = value.split(',') - def colormap = ["hue": values[0] as float, "saturation": values[1] as float] - device.setColor(colormap) - break - } -} - def actionOpenClosed(device, attribute, value) { if (value == "open") { if (device.hasCommand("open")) { @@ -649,47 +516,6 @@ def actionActiveInactive(device, attribute, value) { } } -def actionThermostat(device, attribute, value) { - switch(attribute) { - case "heatingSetpoint": - device.setHeatingSetpoint(value) - break - case "coolingSetpoint": - device.setCoolingSetpoint(value) - break - case "thermostatMode": - device.setThermostatMode(value) - break - case "thermostatFanMode": - device.setThermostatFanMode(value) - break - } -} - -def actionMusicPlayer(device, attribute, value) { - switch(attribute) { - case "level": - device.setLevel(value) - break - case "mute": - if (value == "muted") { - device.mute() - } else if (value == "unmuted") { - device.unmute() - } - break - case "status": - if (device.getSupportedCommands().any {it.name == "setStatus"}) { - device.setStatus(value) - } - break - } -} - -def actionColorTemperature(device, attribute, value) { - device.setColorTemperature(value as int) -} - def actionLevel(device, attribute, value) { device.setLevel(value as int) } @@ -705,34 +531,4 @@ def actionPresence(device, attribute, value) { def actionConsumable(device, attribute, value) { device.setConsumableStatus(value) -} - -def actionLock(device, attribute, value) { - if (value == "locked") { - device.lock() - } else if (value == "unlocked") { - device.unlock() - } -} - -def actionCoolingThermostat(device, attribute, value) { - device.setCoolingSetpoint(value) -} - -def actionThermostatFan(device, attribute, value) { - device.setThermostatFanMode(value) -} - -def actionHeatingThermostat(device, attribute, value) { - device.setHeatingSetpoint(value) -} - -def actionThermostatMode(device, attribute, value) { - device.setThermostatMode(value) -} - -def actionTimedSession(device, attribute, value) { - if (attribute == "timeRemaining") { - device.setTimeRemaining(value) - } } \ No newline at end of file From 8aed2e189242cb8e61b471411b8dacd075010ef0 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Tue, 6 Oct 2020 05:39:30 -0500 Subject: [PATCH 10/10] Retry if connection fails --- server.js | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/server.js b/server.js index dfb9e13..40ad398 100644 --- a/server.js +++ b/server.js @@ -268,13 +268,13 @@ function parseMQTTMessage (topic, message) { // Deduplicate only if the incoming message topic is the same as the read state topic if (topic === topicReadState) { - if (history[topic] === contents) { + if (history[topic] === contents) { winston.info('Skipping duplicate message from: %s = %s', topic, contents); return; } } history[topic] = contents; - winston.info('Incoming message from MQTT: %s = %s', topic, contents); + winston.info('Incoming message from MQTT: %s = %s', topic, contents); // If sending level data and the switch is off, don't send anything // SmartThings will turn the device on (which is confusing) @@ -292,25 +292,38 @@ function parseMQTTMessage (topic, message) { contents = history[topicLevelCommand]; } - request.post({ + var json = { + name: device, + type: property, + value: contents, + //command: (!pieces[2] || pieces[2] && pieces[2] === config.mqtt[SUFFIX_COMMAND]) + command: (pieces[2] && pieces[2] === config.mqtt[SUFFIX_COMMAND]) || false + }; + + retryPost(json, true); +} + +function retryPost(json, retry) { + request.post({ url: 'http://' + callback, - json: { - name: device, - type: property, - value: contents, - //command: (!pieces[2] || pieces[2] && pieces[2] === config.mqtt[SUFFIX_COMMAND]) - command: (pieces[2] && pieces[2] === config.mqtt[SUFFIX_COMMAND]) || false - } + json: json }, function (error, resp) { if (error) { - // @TODO handle the response from SmartThings - winston.error('Error from SmartThings Hub: %s', error.toString()); - winston.error(JSON.stringify(error, null, 4)); - winston.error(JSON.stringify(resp, null, 4)); + if ((error.code === "ETIMEDOUT") && retry) { + winston.error('Error connecting SmartThings, retrying'); + retryPost(json); + } + else { + // @TODO handle the response from SmartThings + winston.error('Error from SmartThings Hub: %s', error.toString()); + winston.error(JSON.stringify(error, null, 4)); + winston.error(JSON.stringify(resp, null, 4)); + } } }); } + // Main flow async.series([ function loadFromDisk (next) {