├── LICENSE ├── NOTICE ├── README.md ├── apps └── hubitat-mqtt-link-app.groovy ├── drivers └── hubitat-mqtt-link-driver.groovy ├── packageManifest.json └── repository.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 jeubanks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ========================================================================= 2 | NOTICE file for use with, and corresponding to Section 4 of, 3 | the Apache License, Version 2.0. 4 | ========================================================================= 5 | 6 | MQTT Bridge 7 | 8 | Authors 9 | - st.john.johnson@gmail.com 10 | - jeremiah.wuenschel@gmail.com 11 | - john.eubanks@gmail.com 12 | 13 | Copyright 2016 14 | 15 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | in compliance with the License. You may obtain a copy of the License at: 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 21 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 22 | for the specific language governing permissions and limitations under the License. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hubitat Elevation MQTT Link 2 | 3 | ***System to share and control Hubitat Elevation device states in MQTT.*** 4 | 5 | MQTT Link is a derivative of [MQTT Bridge](https://github.com/jeubanks/hubitat-mqtt-bridge) for Hubitat released by jeubanks who derived it from [MQTT Bridge](https://github.com/stjohnjohnson/smartthings-mqtt-bridge) for SmartThings by stjohnjohnson. 6 | 7 | Each of the prior MQTT Bridge releases set out to fill a gap in SmartThings and Hubitat as each platform lacked a native MQTT client for which to interface with an MQTT broker. Both releases relied upon a separate, self-hosted nodejs _bridge_ app that ran outside of 8 | 9 | the platform and provided both a client to receive MQTT messages and a client to translate those MQTT messages to REST calls which were both platforms offered as integration points. 10 | 11 | Since that time the Hubitat platform has introduced an MQTT client capable of interfacing with an MQTT broker without the need for external bridges. 12 | 13 | The MQTT Link project builds upon the methods established in the prior works by refactoring the Driver code to utilize the built-in Hubitat MQTT client and to make improvements to the App code. 14 | 15 | A big thanks to stjohnjohnson, jeubanks and those to blazed the trails to make this project possible. 16 | 17 | ## MQTT 18 | 19 | The MQTT Link apps provide for transit of Hubitat device-specific messages to and from the configured MQTT broker. To remain versatile and lean, no assumptions or impositions were made about the consumers of the published events however, contracts were needed to ensure proper integration with those consumers. 20 | 21 | Following are details about the topic format and messages used to communicate to and from the hub devices. 22 | 23 | ### Topics 24 | 25 | The MQTT topics apply the following pattern. 26 | * prefix - Hardcoded to `hubitat` 27 | * hub name & id - Combines the hub location name with the hub id 28 | * normalized device id - Combines the device name and id 29 | * normalized capability - Provides 30 | 31 | Example: `hubitat/home-000d/hue-color-lamp-1-738/switch` 32 | 33 | ### Messages 34 | 35 | Each device has a set of capabilities, attributes and commands that it supports but not every device has support for all three areas. Triggered hub events are converted to a standardized message matching the event that occurred. For example, when light is turned on or off or a door is opened or closed, an MQTT message will be sent to the broker with details about the device and event so that consumers can take the appropriate action. 36 | 37 | See the Supported Capabilities section for details on message details for each capability. 38 | 39 | 40 | #### Outbound 41 | 42 | Messages resulting from hub events mostly report state changes in order to inform subscribers to those events. 43 | 44 | * Light was turned on 45 | 46 | Topic: `hubitat/home-000d/hue-color-lamp-1-738/switch` 47 | Message: `on` 48 | 49 | * Light was turned off 50 | 51 | Topic: `hubitat/home-000d/hue-color-lamp-1-738/switch` 52 | Message: `off` 53 | 54 | #### Inbound 55 | 56 | For those devices that support commands, MQTT messages can be authored be downstream consumers so that those commands are executed on the target device. 57 | 58 | * Message to lock the front door 59 | 60 | Topic: `hubitat/home-000d/august-pro-z-wave-lock-324/lock` 61 | Message: `lock` 62 | 63 | * Message to unlock the front door 64 | 65 | Topic: `hubitat/home-000d/august-pro-z-wave-lock-324/lock` 66 | Message: `unlock` 67 | 68 | 69 | ### Last Will 70 | 71 | When the client establishes a connection to the broker it sets an default `LWT` topic to `offline` and then pushes `online` shortly thereafter. 72 | 73 | In addition to `LWT`, the client also sends `UPTIME`, `FW` and `IP` containing uptime, current firmware version and IP address of the hub. 74 | 75 | ``` 76 | hubitat: 77 | home-000d: 78 | LWT: online 79 | FW: 2.2.0.126 80 | IP: 192.168.1.100 81 | UPTIME: 82815 82 | ``` 83 | ## Installation & Configuration 84 | 85 | MQTT Link consists of both a driver and app. Both must be installed and configured prior to their use. 86 | 87 | ### Driver 88 | 89 | The driver app must be installed first because the App depends upon it. The driver connects to the configured MQTT broker and sends out messages when new hub events occur and receives messages from external client events such has those from Home Assistant. 90 | 91 | The driver provides a number of commands that are useful for troubleshooting but they are not needed for normal operation of the driver code. 92 | 93 | * Connect - Connects to the configured broker 94 | * Disconnect - Disconnects from the configured broker 95 | * Device Notification - For internal use by the app 96 | 97 | The following commands allow for subscribing and publishing to MQTT topics. The driver automatically prefixes all topics with the following prefix within the code to ensure unique topics for each hub. 98 | 99 | `/hubitat/{hub-name}-{hub-id}/` 100 | e.g. 101 | `/hubitat/home-893/` 102 | 103 | * Subscribe - Subscribes to the provided topic. 104 | * e.g. `device` becomes `/hubitat/home-893/device` 105 | * Unsubscribe - Unsubscribe from the provided topic. 106 | * e.g `#` becomes `/hubitat/home-893/#` 107 | * Publish - Publish message to provided topic. 108 | * e.g. `switch` msg: `on` becomes `/hubitat/home-893/switch` msg: `on` 109 | 110 | Follow the procedure for installing user driver code on Hubitat and enter the following details. 111 | 112 | * MQTT Broker IP Address - Provide the IP address of the target MQTT broker 113 | * MQTT Broker Port - Provide the port for the broker. This is typically 1883 114 | * MQTT Broker Username - Provide username 115 | * MQTT Broker Password - Provide password 116 | * Type - MQTT Link Driver 117 | 118 | _optional_ 119 | 120 | * Send full payload messages on device events - When ON the driver will send a detailed payload of the fired event 121 | * Enable debug logging - When ON the driver will log debug statements for troubleshooting 122 | 123 | ### App 124 | 125 | The app is responsible for listening to subscribed hub events that it relays to the driver to publish to the MQTT broker. It also listens for inbound messages from the driver that it then translates to a hub event. 126 | 127 | Follow the procedure for installing apps code on Hubitat and specify the following details. 128 | 129 | #### Select Devices and Driver 130 | 131 | * Select devices - Expand and select the devices that the app should monitor for. Note that the capabilities for each of the selected devices are selected on the next page. 132 | * Notify this driver - Example and select the MQTT Link Driver device that was installed previously. 133 | 134 | _optional_ 135 | 136 | * Enable debug logging - When ON the driver will log debug statements for troubleshooting 137 | 138 | #### Device Capabilities 139 | 140 | Each of the devices chosen on the prior page are listed on this page and include a dropdown containing the capabilities associated with that device. This page also lists the normalized topic for the device. 141 | 142 | * Click to set - Expand and select the associated device capabilities that the app should monitor for. 143 | 144 | ## Supported Capabilities 145 | Following is an inclusive list of device capabilities, attributes and commands recognized by MQTT Link. 146 | 147 | Limited access to devices within each of these categories made it impossible to test each combination list. Please report any missing or erroneous details so that they can be corrected within the code. 148 | 149 | [Hubitat Capabilities List](https://docs.hubitat.com/index.php?title=Driver_Capability_List) | [SmartThings Capabilities List](https://docs.smartthings.com/en/latest/capabilities-reference.html) 150 | * Acceleration Sensor - accelerationSensor 151 | * acceleration 152 | * Alarm - alarm 153 | * alarm 154 | * siren 155 | * strobe 156 | * both 157 | * off 158 | * Audio Notification - audioNotification 159 | * - 160 | * playText 161 | * playTextAndRestore 162 | * playTextAndResume 163 | * playTrack 164 | * playTrackAndResume 165 | * playTrackAndRestore 166 | * Audio Volume - audioVolume 167 | * mute 168 | * mute 169 | * unmute 170 | * volume 171 | * setVolume 172 | * volumeUp 173 | * volumeDown 174 | * Battery - battery 175 | * battery 176 | * Carbon Dioxide Measurement - carbonDioxideMeasurement 177 | * carbonDioxide 178 | * Carbon Monoxide Detector - carbonMonoxideDetector 179 | * carbonMonoxide 180 | * Change Level - changeLevel 181 | * - 182 | * startLevelChange 183 | * stopLevelChange 184 | * Chime - chime 185 | * soundEffects 186 | * playSound 187 | * stop 188 | * soundName 189 | * status 190 | * Color Control - colorControl 191 | * color 192 | * setColor 193 | * hue 194 | * setHue 195 | * saturation 196 | * setSaturation 197 | * Color Mode - colorMode 198 | * colorMode 199 | * Color Temperature - colorTemperature 200 | * colorTemperature 201 | * Configuration - configuration 202 | * - 203 | * Consumable - consumable 204 | * consumableStatus 205 | * Contact Sensor - contactSensor 206 | * contact 207 | * Door Control - doorControl 208 | * door 209 | * open 210 | * close 211 | * DoubleTapable Button - doubleTapableButton 212 | * doubleTapped 213 | * Energy Meter - energyMeter 214 | * energy 215 | * Estimated Time Of Arrival - estimatedTimeOfArrival 216 | * eta 217 | * Fan Control - fanControl 218 | * speed 219 | * Filter Status - filterStatus 220 | * filterStatus 221 | * Health Check - healthCheck 222 | * checkInterval 223 | * Illuminance Measurement - illuminanceMeasurement 224 | * illuminance 225 | * Image Capture - imageCapture 226 | * image 227 | * Light Effects - lightEffects 228 | * effectName 229 | * lightEffects 230 | * setEffect 231 | * setNextEffect 232 | * setPreviousEffect 233 | * Location Mode - locationMode 234 | * mode 235 | * Lock Codes - lock 236 | * lock 237 | * lock 238 | * unlock 239 | * Lock Codes - lockCode 240 | * codeChanged 241 | * codeLength 242 | * lockCodes 243 | * deleteCode 244 | * getCodes 245 | * setCode 246 | * setCodeLength 247 | * maxCodes 248 | * Media Controller - mediaController 249 | * activities 250 | * currentActivity 251 | * Momentary - momentary 252 | * - 253 | * Motion Sensor - motionSensor 254 | * motion 255 | * active 256 | * inactive 257 | * Notification - notification 258 | * - 259 | * pH Measurement - pHMeasurement 260 | * pH 261 | * Power Meter - powerMeter 262 | * power 263 | * Power Source - powerSource 264 | * powerSource 265 | * Presence Sensor - presenceSensor 266 | * presence 267 | * present 268 | * not present 269 | * PressureMeasurement - pressureMeasurement 270 | * pressure 271 | * Refresh - refresh 272 | * - 273 | * Pushable Button - pushableButton 274 | * numberOfButtons 275 | * pushed 276 | * Relative Humidity Measurement - relativeHumidityMeasurement 277 | * humidity 278 | * ReleasableButton - releasableButton 279 | * released 280 | * Samsung TV - samsungTV 281 | * messageButton 282 | * mute 283 | * pictureMode 284 | * soundMode 285 | * switch 286 | * volume 287 | * mute 288 | * off 289 | * on 290 | * setPictureMode 291 | * setSoundMode 292 | * setVolume 293 | * showMessage 294 | * unmute 295 | * volumeDown 296 | * volumeUp 297 | * Security Keypad - securityKeypad 298 | * codeChanged 299 | * codeLength 300 | * lockCodes 301 | * maxCodes 302 | * securityKeypad 303 | * armAway 304 | * armHome 305 | * deleteCode 306 | * disarm 307 | * getCodes 308 | * setCode 309 | * setCodeLength 310 | * setEntryDelay 311 | * setExitDelay 312 | * Signal Strength - signalStrength 313 | * lqi 314 | * rssi 315 | * Sleep Sensor - sleepSensor 316 | * sleeping 317 | * Smoke Detector - smokeDetector 318 | * smoke 319 | * Sound Pressure Level - soundPressureLevel 320 | * soundPressureLevel 321 | * Sound Sensor - soundSensor 322 | * sound 323 | * Speech Recognition - speechRecognition 324 | * phraseSpoken 325 | * Speech Synthesis - speechSynthesis 326 | * - 327 | * Step Sensor - stepSensor 328 | * goal 329 | * steps 330 | * Switch Level - switchLevel 331 | * level 332 | * Switch - switch 333 | * switch 334 | * on 335 | * off 336 | * Tamper Alert - tamperAlert 337 | * tamper 338 | * Temperature Measurement - temperatureSensor 339 | * temperature 340 | * Thermostat Cooling Setpoint - thermostatCoolingSetpoint 341 | * coolingSetpoint 342 | * Thermostat Fan Mode - thermostatFanMode 343 | * thermostatFanMode 344 | * fanAuto 345 | * fanCirculate 346 | * fanOn 347 | * setThermostatFanMode 348 | * Thermostat Heating Setpoint - thermostatHeatingSetpoint 349 | * heatingSetpoint 350 | * Thermostat Mode - thermostatMode 351 | * thermostatMode 352 | * auto 353 | * cool 354 | * emergencyHeat 355 | * heat 356 | * off 357 | * setThermostatMode 358 | * Thermostat Operating State - thermostatOperatingState 359 | * thermostatOperatingState 360 | * Thermostat Schedule - thermostatSchedule 361 | * schedule 362 | * Three Axis - threeAxis 363 | * threeAxis 364 | * Timed Session - timedSession 365 | * sessionStatus 366 | * timeRemaining 367 | * setTimeRemaining 368 | * start 369 | * stop 370 | * pause 371 | * cancel 372 | * Tone - tone 373 | * - 374 | * TV - tv 375 | * channel 376 | * channelUp 377 | * channelDown 378 | * movieMode 379 | * picture 380 | * power 381 | * sound 382 | * volume 383 | * volumeUp 384 | * volumeDown 385 | * Temperature Measurement - temperatureMeasurement 386 | * temperature 387 | * Ultraviolet Index - ultravioletIndex 388 | * ultravioletIndex 389 | * Valve - valve 390 | * contact 391 | * open 392 | * closed 393 | * valve 394 | * open 395 | * closed 396 | * Video Camera - videoCamera 397 | * camera 398 | * flip 399 | * mute 400 | * mute 401 | * unmute 402 | * settings 403 | * statusMessage 404 | * on 405 | * off 406 | * Video Capture - videoCapture 407 | * clip 408 | * Window Shade - windowShades 409 | * windowShade 410 | * Water Sensor - waterSensor 411 | * water 412 | * Window Shade - windowShade 413 | * windowShade 414 | * close 415 | * open 416 | * presetPosition 417 | * ZW Multichannel - zwMultichannel 418 | * epEvent 419 | * epInfo 420 | 421 | ### Release Notes 422 | 423 | # Update in Release 1.0.0 424 | * BREAKING CHANGES 425 | * Replace spaces in hub name with dashes to prevent MQTT topic with spaces in the name. `hub name` becomes `hub-name` 426 | # Update in Release 0.3.0 427 | * Added support for all Hubitat Virtual Devices 428 | # Update in Release 0.2.1 429 | * Minor fix that added device attibute name to notification raised from the app to the driver 430 | # Update in Release 0.2.0 431 | * Added scheduled job that runs every minute that reads and publishes device state messages to MQTT 432 | # Update in Releadse 0.1.0 433 | * Initial release with Hubitat Package Manager support 434 | 435 | -------------------------------------------------------------------------------- /apps/hubitat-mqtt-link-app.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * MQTT Link 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2020 license@mydevbox.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | import groovy.json.JsonOutput 32 | import groovy.transform.Field 33 | 34 | public static String version() { return "v2.0.0" } 35 | public static String rootTopic() { return "hubitat" } 36 | 37 | definition( 38 | name: "MQTT Link", 39 | namespace: "mydevbox", 40 | author: "Chris Lawson, et al", 41 | description: "A link between Hubitat device events and MQTT Link Driver", 42 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections.png", 43 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@2x.png", 44 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@3x.png" 45 | ) 46 | 47 | preferences { 48 | page(name: "devicePage", nextPage: "capabilitiesPage", uninstall: true) { 49 | section("Select the hub devices that MQTT Link should monitor and control.", hideable: false) { 50 | input ( 51 | name: "selectedDevices", 52 | type: "capability.*", 53 | title: "Select devices", 54 | multiple: true, 55 | required: true, 56 | submitOnChange: false 57 | ) 58 | } 59 | section ("

Specify MQTT Link Driver device

") { 60 | paragraph "The MQTT Link Driver must be set up prior to the MQTT Link app otherwise the driver will not show up here." 61 | input ( 62 | name: "mqttLink", 63 | type: "capability.notification", 64 | title: "Notify this driver", 65 | required: true, 66 | multiple: false, 67 | submitOnChange: false 68 | ) 69 | } 70 | section("Debug Settings") { 71 | input("debugLogging", "bool", title: "Enable debug logging", required: false, default:false) 72 | } 73 | } 74 | page(name: "capabilitiesPage", install: true) 75 | } 76 | 77 | def capabilitiesPage() { 78 | def deprecatedCapabilities = ["Actuator","Beacon","Bridge","Bulb","Button","Garage Door Control", 79 | "Indicator","Light","Lock Only","Music Player","Outlet","Polling","Relay Switch", 80 | "Sensor","Shock Sensor","Thermostat Setpoint","Thermostat","Touch Sensor", 81 | "Configuration","Refresh"] 82 | dynamicPage(name: "capabilitiesPage") { 83 | section ("

Specify Exposed Capabilities per Device

") { 84 | paragraph """""".stripMargin() 89 | 90 | // Build normalized list of selected device names 91 | def selectedList = [] 92 | selectedDevices.each { 93 | device -> selectedList.add(normalizeId(device)) 94 | } 95 | state.selectedList = selectedList 96 | 97 | // Remove deselected device capabilities 98 | settings.each { setting -> 99 | if (setting.value.class == java.util.ArrayList) { 100 | if (!state.selectedList.contains(setting.key)) { 101 | app.removeSetting(setting.key) 102 | } 103 | } 104 | } 105 | 106 | // Build normalized list of selected device names 107 | def selectedLookup = [:] 108 | selectedDevices.each { 109 | device -> selectedLookup.put(normalizeId(device), device.getDisplayName()) 110 | } 111 | state.selectedLookup = selectedLookup 112 | 113 | // List selected devices with capabilities chooser 114 | selectedDevices.sort{x -> x.getDisplayName()}.each { device -> 115 | 116 | def selectedCapabilities = [] 117 | def deviceCapabilities = device.getCapabilities() 118 | 119 | deviceCapabilities.each { capability -> 120 | if (!deprecatedCapabilities.contains(capability.getName())) { 121 | selectedCapabilities.add(capability.getName()) 122 | } 123 | } 124 | 125 | def normalizeId = normalizeId(device) 126 | 127 | paragraph "
${device.getDisplayName()}
" 128 | 129 | input ( 130 | name: normalizeId, 131 | type: "enum", 132 | title: "", 133 | options: selectedCapabilities, 134 | multiple: true, 135 | submitOnChange: false 136 | ) 137 | paragraph "
Topic
${getTopicPrefix()}${normalizeId}

" 138 | } 139 | } 140 | } 141 | } 142 | 143 | // Massive lookup tree 144 | @Field CAPABILITY_MAP = [ 145 | "accelerationSensor": [ 146 | name: "Acceleration Sensor", 147 | capability: "capability.accelerationSensor", 148 | attributes: [ 149 | "acceleration" // ["inactive", "active"] 150 | ] 151 | ], 152 | "alarm": [ 153 | name: "Alarm", 154 | capability: "capability.alarm", 155 | attributes: [ 156 | "alarm" // ["strobe", "off", "both", "siren"] 157 | ], 158 | action: "actionAlarm" 159 | ], 160 | "audioNotification": [ 161 | name: "Audio Notification", 162 | capability: "capability.audioNotification", 163 | attributes: [ 164 | ], 165 | action: "actionAudioNotification" 166 | ], 167 | "audioVolume": [ 168 | name: "Audio Volume", 169 | capability: "capability.audioVolume", 170 | attributes: [ 171 | "mute", // ["unmuted", "muted"] 172 | "volume" // 0 - 100 173 | ], 174 | action: "actionAudioVolume" 175 | ], 176 | "battery": [ 177 | name: "Battery", 178 | capability: "capability.battery", 179 | attributes: [ 180 | "battery" // 0 - 100 181 | ] 182 | ], 183 | "carbonDioxideMeasurement": [ 184 | name: "Carbon Dioxide Measurement", 185 | capability: "capability.carbonDioxideMeasurement", 186 | attributes: [ 187 | "carbonDioxide" 188 | ] 189 | ], 190 | "carbonMonoxideDetector": [ 191 | name: "Carbon Monoxide Detector", 192 | capability: "capability.carbonMonoxideDetector", 193 | attributes: [ 194 | "carbonMonoxide" // 0 - 100 195 | ] 196 | ], 197 | "changeLevel": [ 198 | name: "Change Level", 199 | capability: "capability.changeLevel", 200 | attributes: [ 201 | ], 202 | action: "actionChangeLevel" 203 | ], 204 | "chime": [ 205 | name: "Chime", 206 | capability: "capability.chime", 207 | attributes: [ 208 | "soundEffects", // JSON_OBJ 209 | "soundName", // String 210 | "status" // ["playing", "stopped"] 211 | ], 212 | action: "actionChime" 213 | ], 214 | "colorControl": [ 215 | name: "Color Control", 216 | capability: "capability.colorControl", 217 | attributes: [ 218 | "RGB", // String 219 | "color", // String 220 | "colorName", // String 221 | "hue", // 0 - 100 222 | "saturation" // 0 - 100 223 | ], 224 | action: "actionColorControl" 225 | ], 226 | "colorMode": [ 227 | name: "Color Mode", 228 | capability: "capability.colorMode", 229 | attributes: [ 230 | "colorMode" // ["CT", "RGB"] 231 | ] 232 | ], 233 | "colorTemperature": [ 234 | name: "Color Temperature", 235 | capability: "capability.colorTemperature", 236 | attributes: [ 237 | "colorName", // String 238 | "colorTemperature" // 0 - 100 239 | ], 240 | action: "actionColorTemperature" 241 | ], 242 | "configuration": [ 243 | name: "Configuration", 244 | capability: "capability.configuration", 245 | attributes: [ 246 | ], 247 | action: "actionConfiguration" 248 | ], 249 | "consumable": [ 250 | name: "Consumable", 251 | capability: "capability.consumable", 252 | attributes: [ 253 | "consumableStatus" // ["missing", "order", "maintenance_required", "good", "replace"] 254 | ], 255 | action: "actionConsumable" 256 | ], 257 | "contactSensor": [ 258 | name: "Contact Sensor", 259 | capability: "capability.contactSensor", 260 | attributes: [ 261 | "contact" // ["closed", "open"] 262 | ] 263 | ], 264 | "doorControl": [ 265 | name: "Door Control", 266 | capability: "capability.doorControl", 267 | attributes: [ 268 | "door" // ["unknown", "closed", "open", "closing", "opening"] 269 | ], 270 | action: "actionOpenClose" 271 | ], 272 | "doubleTapableButton": [ 273 | name: "DoubleTapable Button", 274 | capability: "capability.doubleTapableButton", 275 | attributes: [ 276 | "doubleTapped" 277 | ] 278 | ], 279 | "energyMeter": [ 280 | name: "Energy Meter", 281 | capability: "capability.energyMeter", 282 | attributes: [ 283 | "energy" // 0 - 100 284 | ] 285 | ], 286 | "estimatedTimeOfArrival": [ 287 | name: "Estimated Time Of Arrival", 288 | capability: "capability.estimatedTimeOfArrival", 289 | attributes: [ 290 | "eta" // Date 291 | ] 292 | ], 293 | "fanControl": [ 294 | name: "Fan Control", 295 | capability: "capability.fanControl", 296 | attributes: [ 297 | "speed" // ["low","medium-low","medium","medium-high","high","on","off","auto"] 298 | ], 299 | action: "actionFanControl" 300 | ], 301 | "filterStatus": [ 302 | name: "Filter Status", 303 | capability: "capability.filterStatus", 304 | attributes: [ 305 | "filterStatus" // ["normal", "replace"] 306 | ] 307 | ], 308 | "garageDoorControl": [ 309 | name: "Garage Door Control", 310 | capability: "capability.garageDoorControl", 311 | attributes: [ 312 | "door" // ["unknown", "open", "closing", "closed", "opening"] 313 | ], 314 | action: "actionOpenClose" 315 | ], 316 | "healthCheck": [ 317 | name: "Health Check", 318 | capability: "capability.healthCheck", 319 | attributes: [ 320 | "checkInterval" // 0 - 100 321 | ], 322 | action: "actionHealthCheck" 323 | ], 324 | "holdableButton": [ 325 | name: "Holdable Button", 326 | capability: "capability.holdableButton", 327 | attributes: [ 328 | "held" // 0 - 100 329 | ] 330 | ], 331 | "illuminanceMeasurement": [ 332 | name: "Illuminance Measurement", 333 | capability: "capability.illuminanceMeasurement", 334 | attributes: [ 335 | "illuminance" // 0 - 100 336 | ] 337 | ], 338 | "imageCapture": [ 339 | name: "Image Capture", 340 | capability: "capability.imageCapture", 341 | attributes: [ 342 | "image" // String 343 | ], 344 | action: "actionImageCapture" 345 | ], 346 | "lightEffects": [ 347 | name: "Light Effects", 348 | capability: "capability.lightEffects", 349 | attributes: [ 350 | "effectName", // String 351 | "lightEffects" // JSON_OBJ 352 | ], 353 | action: "actionLightEffects" 354 | ], 355 | "locationMode": [ 356 | name: "Location Mode", 357 | capability: "capability.locationMode", 358 | attributes: [ 359 | "mode" 360 | ] 361 | ], 362 | "lock": [ 363 | name: "Lock Codes", 364 | capability: "capability.lock", 365 | attributes: [ 366 | "lock" // ["locked", "unlocked with timeout", "unlocked", "unknown"] 367 | ], 368 | action: "actionLock" 369 | ], 370 | "lockCodes": [ 371 | name: "Lock Codes", 372 | capability: "capability.lockCodes", 373 | attributes: [ 374 | "codeChanged", // ["added", "changed", "deleted", "failed"] 375 | "codeLength", 376 | "lockCodes", // JSON_OBJ 377 | "maxCodes" 378 | ], 379 | action: "actionLockCodes" 380 | ], 381 | "mediaController": [ 382 | name: "Media Controller", 383 | capability: "capability.mediaController", 384 | attributes: [ 385 | "activities", // JSON_OBJ 386 | "currentActivity" // String 387 | ], 388 | action: "actionMediaController" 389 | ], 390 | "momentary": [ 391 | name: "Momentary", 392 | capability: "capability.momentary", 393 | attributes: [ 394 | ], 395 | action: "actionMomentary" 396 | ], 397 | "motionSensor": [ 398 | name: "Motion Sensor", 399 | capability: "capability.motionSensor", 400 | attributes: [ 401 | "motion" // ["inactive", "active"] 402 | ] 403 | ], 404 | "notification": [ 405 | name: "Notification", 406 | capability: "capability.notification", 407 | attributes: [ 408 | ], 409 | action: "actionNotification" 410 | ], 411 | "pHMeasurement": [ 412 | name: "pH Measurement", 413 | capability: "capability.pHMeasurement", 414 | attributes: [ 415 | "pH" // 0 - 100 416 | ] 417 | ], 418 | "powerMeter": [ 419 | name: "Power Meter", 420 | capability: "capability.powerMeter", 421 | attributes: [ 422 | "power" // 0 - 100 423 | ] 424 | ], 425 | "powerSource": [ 426 | name: "Power Source", 427 | capability: "capability.powerSource", 428 | attributes: [ 429 | "powerSource" // ["battery", "dc", "mains", "unknown"] 430 | ] 431 | ], 432 | "presenceSensor": [ 433 | name: "Presence Sensor", 434 | capability: "capability.presenceSensor", 435 | attributes: [ 436 | "presence" // ["present", "not present"] 437 | ] 438 | ], 439 | "pressureMeasurement": [ 440 | name: "PressureMeasurement", 441 | capability: "capability.pressureMeasurement", 442 | attributes: [ 443 | "pressure" // 0 - 100 444 | ] 445 | ], 446 | "pushableButton": [ 447 | name: "Pushable Button", 448 | capability: "capability.pushableButton", 449 | attributes: [ 450 | "numberOfButtons", // 1 - # 451 | "pushed" // 1 - # 452 | ] 453 | ], 454 | "refresh": [ 455 | name: "Refresh", 456 | capability: "capability.refresh", 457 | attributes: [ 458 | ], 459 | action: "actionRefresh" 460 | ], 461 | "relativeHumidityMeasurement": [ 462 | name: "Relative Humidity Measurement", 463 | capability: "capability.relativeHumidityMeasurement", 464 | attributes: [ 465 | "humidity" // 0 - 100 466 | ] 467 | ], 468 | "releasableButton": [ 469 | name: "ReleasableButton", 470 | capability: "capability.releasableButton", 471 | attributes: [ 472 | "released" 473 | ] 474 | ], 475 | "samsungTV": [ 476 | name: "Samsung TV", 477 | capability: "capability.samsungTV", 478 | attributes: [ 479 | "messageButton", // JSON_OBJ 480 | "mute", // ["muted", "unknown", "unmuted"] 481 | "pictureMode", // ["unknown", "standard", "movie", "dynamic"] 482 | "soundMode", // ["speech", "movie", "unknown", "standard", "music"] 483 | "switch", // ["on", "off"] 484 | "volume" // 0 - 100 485 | ], 486 | action: "actionSamsungTV" 487 | ], 488 | "securityKeypad": [ 489 | name: "Security Keypad", 490 | capability: "capability.securityKeypad", 491 | attributes: [ 492 | "codeChanged", // ["added", "changed", "deleted", "failed"] 493 | "codeLength", 494 | "lockCodes", // JSON_OBJ 495 | "maxCodes", 496 | "securityKeypad" // ["disarmed", "armed home", "armed away", "unknown"] 497 | ], 498 | action: "actionSecurityKeypad" 499 | ], 500 | "signalStrength": [ 501 | name: "Signal Strength", 502 | capability: "capability.signalStrength", 503 | attributes: [ 504 | "lqi", // 0 - 100 505 | "rssi" // 0 - 100 506 | ] 507 | ], 508 | "sleepSensor": [ 509 | name: "Sleep Sensor", 510 | capability: "capability.sleepSensor", 511 | attributes: [ 512 | "sleeping" // ["not sleeping", "sleeping"] 513 | ] 514 | ], 515 | "smokeDetector": [ 516 | name: "Smoke Detector", 517 | capability: "capability.smokeDetector", 518 | attributes: [ 519 | "smoke" // ["clear", "tested", "detected"] 520 | ] 521 | ], 522 | "soundPressureLevel": [ 523 | name: "Sound Pressure Level", 524 | capability: "capability.soundPressureLevel", 525 | attributes: [ 526 | "soundPressureLevel" // 0 - 100 527 | ] 528 | ], 529 | "soundSensor": [ 530 | name: "Sound Sensor", 531 | capability: "capability.soundSensor", 532 | attributes: [ 533 | "sound" // ["detected", "not detected"] 534 | ] 535 | ], 536 | "speechRecognition": [ 537 | name: "Speech Recognition", 538 | capability: "capability.speechRecognition", 539 | attributes: [ 540 | "phraseSpoken" // String 541 | ] 542 | ], 543 | "speechSynthesis": [ 544 | name: "Speech Synthesis", 545 | capability: "capability.speechSynthesis", 546 | attributes: [ 547 | ], 548 | action: "actionSpeechSynthesis" 549 | ], 550 | "stepSensor": [ 551 | name: "Step Sensor", 552 | capability: "capability.stepSensor", 553 | attributes: [ 554 | "goal", // 0 - # 555 | "steps" // 0 - # 556 | ] 557 | ], 558 | "switch": [ 559 | name: "Switch", 560 | capability: "capability.switch", 561 | attributes: [ 562 | "switch" // ["on", "off"] 563 | ], 564 | action: "actionOnOff" 565 | ], 566 | "switchLevel": [ 567 | name: "Switch Level", 568 | capability: "capability.switchLevel", 569 | attributes: [ 570 | "level" // 0 - 100 571 | ], 572 | action: "actionSwitchLevel" 573 | ], 574 | "tv": [ 575 | name: "TV", 576 | capability: "capability.TV", 577 | attributes: [ 578 | "channel", // 0 - # 579 | "movieMode", // String 580 | "picture", // String 581 | "power", // String 582 | "sound", // String 583 | "volume" // 0 - 100 584 | ], 585 | action: "actionTV" 586 | ], 587 | "tamperAlert": [ 588 | name: "Tamper Alert", 589 | capability: "capability.tamperAlert", 590 | attributes: [ 591 | "tamper" // ["clear", "detected"] 592 | ] 593 | ], 594 | "temperatureSensor": [ 595 | name: "Temperature Measurement", 596 | capability: "capability.temperatureMeasurement", 597 | attributes: [ 598 | "temperature" // 0 - 100 599 | ] 600 | ], 601 | "temperatureMeasurement": [ 602 | name: "Temperature Measurement", 603 | capability: "capability.temperatureMeasurement", 604 | attributes: [ 605 | "temperature" // 0 - 100 606 | ] 607 | ], 608 | "thermostatCoolingSetpoint": [ 609 | name: "Thermostat Cooling Setpoint", 610 | capability: "capability.thermostatCoolingSetpoint", 611 | attributes: [ 612 | "coolingSetpoint" // 0 - 100 613 | ], 614 | action: "actionThermostatCoolingSetpoint" 615 | ], 616 | "thermostatFanMode": [ 617 | name: "Thermostat Fan Mode", 618 | capability: "capability.thermostatFanMode", 619 | attributes: [ 620 | "thermostatFanMode" // ["auto", "circulate", "on"] 621 | ], 622 | action: "actionThermostatFanMode" 623 | ], 624 | "thermostatHeatingSetpoint": [ 625 | name: "Thermostat Heating Setpoint", 626 | capability: "capability.thermostatHeatingSetpoint", 627 | attributes: [ 628 | "heatingSetpoint" // 0 - 100 629 | ], 630 | action: "actionThermostatHeatingSetpoint" 631 | ], 632 | "thermostatMode": [ 633 | name: "Thermostat Mode", 634 | capability: "capability.thermostatMode", 635 | attributes: [ 636 | "thermostatMode" // ["heat", "cool", "emergency heat", "auto", "off"] 637 | ], 638 | action: "actionThermostatMode" 639 | ], 640 | "thermostatOperatingState": [ 641 | name: "Thermostat Operating State", 642 | capability: "capability.thermostatOperatingState", 643 | attributes: [ 644 | "thermostatOperatingState" // ["vent economizer", "pending cool", "cooling", "heating", "pending heat", "fan only", "idle"] 645 | ] 646 | ], 647 | "thermostatSchedule": [ 648 | name: "Thermostat Schedule", 649 | capability: "capability.thermostatSchedule", 650 | attributes: [ 651 | "schedule" // JSON_OBJ 652 | ], 653 | action: "actionThermostatSchedule" 654 | ], 655 | "threeAxis": [ 656 | name: "Three Axis", 657 | capability: "capability.threeAxis", 658 | attributes: [ 659 | "threeAxis" // VECTOR3 660 | ] 661 | ], 662 | "timedSession": [ 663 | name: "Timed Session", 664 | capability: "capability.timedSession", 665 | attributes: [ 666 | "sessionStatus", // ["stopped", "canceled", "running", "paused"] 667 | "timeRemaining" // 0 - 100 668 | ], 669 | action: "actionTimedSession" 670 | ], 671 | "tone": [ 672 | name: "Tone", 673 | capability: "capability.tone", 674 | attributes: [ 675 | ], 676 | action: "actionTone" 677 | ], 678 | "ultravioletIndex": [ 679 | name: "Ultraviolet Index", 680 | capability: "capability.ultravioletIndex", 681 | attributes: [ 682 | "ultravioletIndex" // 0 - 100 683 | ] 684 | ], 685 | "valve": [ 686 | name: "Valve", 687 | capability: "capability.valve", 688 | attributes: [ 689 | "valve" // ["open", "closed"] 690 | ], 691 | action: "actionOpenClose" 692 | ], 693 | "videoCamera": [ 694 | name: "Video Camera", 695 | capability: "capability.videoCamera", 696 | attributes: [ 697 | "camera", // ["on", "off", "restarting", "unavailable"] 698 | "mute", // ["unmuted", "muted"] 699 | "settings", // JSON_OBJ 700 | "statusMessage" // String 701 | ], 702 | action: "actionVideoCamera" 703 | ], 704 | "videoCapture": [ 705 | name: "Video Capture", 706 | capability: "capability.videoCapture", 707 | attributes: [ 708 | "clip" // JSON_OBJ 709 | ], 710 | action: "actionVideoCapture" 711 | ], 712 | "voltageMeasurement": [ 713 | name: "Voltage Measurement", 714 | capability: "capability.voltageMeasurement", 715 | attributes: [ 716 | "voltage" // 0 - # 717 | ], 718 | action: "actionVideoCapture" 719 | ], 720 | "waterSensor": [ 721 | name: "Water Sensor", 722 | capability: "capability.waterSensor", 723 | attributes: [ 724 | "water" // ["wet", "dry"] 725 | ] 726 | ], 727 | "windowShades": [ 728 | name: "Window Shade", 729 | capability: "capability.windowShade", 730 | attributes: [ 731 | "windowShade" 732 | ], 733 | action: "actionWindowShade" 734 | ], 735 | "windowShade": [ 736 | name: "Window Shade", 737 | capability: "capability.windowShade", 738 | attributes: [ 739 | "position", // 0 - 100 740 | "windowShade" // ["opening", "partially open", "closed", "open", "closing", "unknown"] 741 | ], 742 | action: "actionWindowShade" 743 | ], 744 | "zwMultichannel": [ 745 | name: "ZW Multichannel", 746 | capability: "capability.zwMultichannel", 747 | attributes: [ 748 | "epEvent", // String 749 | "epInfo" // String 750 | ], 751 | action: "actionZwMultichannel" 752 | ] 753 | ] 754 | 755 | def installed() { 756 | debug("[a:installed] Installed with settings: ${settings}") 757 | 758 | runEvery15Minutes(initialize) 759 | runEvery1Minute(pingState) 760 | 761 | initialize() 762 | } 763 | 764 | def updated() { 765 | debug("[a:updated] Updated with settings: ${settings}") 766 | 767 | // Unsubscribe from all events 768 | unsubscribe() 769 | 770 | // Subscribe to stuff 771 | initialize() 772 | } 773 | 774 | def initialize() { 775 | debug("Initializing app...") 776 | 777 | // subscribe to mode/routine changes 778 | subscribe(location, "mode", inputHandler) 779 | subscribe(location, "routineExecuted", inputHandler) 780 | 781 | def attributes = [ 782 | notify: ["Contacts", "System"] 783 | ] 784 | 785 | settings.selectedDevices.each { device -> 786 | def normalizeId = normalizeId(device) 787 | 788 | settings[normalizeId].each { capability -> 789 | def capabilityCamel = lowerCamel(capability) 790 | def capabilitiesMap = CAPABILITY_MAP[capabilityCamel] 791 | 792 | capabilitiesMap["attributes"].each { attribute -> 793 | subscribe(device, attribute, inputHandler) 794 | } 795 | 796 | if (!attributes.containsKey(capabilityCamel)) { 797 | attributes[capabilityCamel] = [] 798 | } 799 | 800 | attributes[capabilityCamel].push(normalizeId) 801 | } 802 | } 803 | 804 | // Subscribe to new events from devices 805 | CAPABILITY_MAP.each { key, capability -> 806 | capability["attributes"].each { attribute -> 807 | subscribe(settings[key], attribute, inputHandler) 808 | } 809 | } 810 | 811 | // Subscribe to events from the mqttLink 812 | subscribe(mqttLink, "message", mqttLinkHandler) 813 | 814 | updateSubscription(attributes) 815 | } 816 | 817 | // Update the mqttLink's subscription 818 | def updateSubscription(attributes) { 819 | def json = new groovy.json.JsonOutput().toJson([ 820 | path: "/subscribe", 821 | body: [ 822 | devices: attributes 823 | ] 824 | ]) 825 | 826 | debug("[a:updateSubscription] Updating subscription: ${json}") 827 | 828 | mqttLink.deviceNotification(json) 829 | } 830 | 831 | // Receive an inbound event from the MQTT Link Driver 832 | def mqttLinkHandler(evt) { 833 | def json = new JsonSlurper().parseText(evt.value) 834 | debug("[a:mqttLinkHandler] Received inbound device event from MQTT Link Driver: ${json}") 835 | 836 | if (json.type == "notify") { 837 | sendNotificationEvent("${json.value}") 838 | return 839 | } else if (json.type == "modes") { 840 | actionModes(json.value) 841 | return 842 | } else if (json.type == "routines") { 843 | actionRoutines(json.value) 844 | return 845 | } 846 | 847 | def attribute = json.type 848 | def capability = CAPABILITY_MAP[attribute] 849 | def normalizedId = json.device.toString() 850 | def deviceName = state.selectedLookup[normalizedId] 851 | 852 | def selectedDevice = settings.selectedDevices.find { 853 | device -> (device.displayName == deviceName) 854 | } 855 | 856 | if (selectedDevice && settings[normalizedId] && capability["attributes"].contains(attribute)) { 857 | if (capability.containsKey("action")) { 858 | def action = capability["action"] 859 | json['action'] = action 860 | debug("[a:mqttLinkHandler] MQTT incoming target action: ${json}") 861 | // Yes, this is calling the method dynamically 862 | "$action"(selectedDevice, attribute, json.value) 863 | } 864 | } 865 | } 866 | 867 | // Receive an event from a device 868 | def inputHandler(evt) { 869 | 870 | // Incoming MQTT event will tigger a hub event which in-turn triggers a second call 871 | // to inputHandler. If the evt is a hub Event and not json, it is swallowed 872 | // to prevent triggering an outbound MQTT event for the incoming MQTT event. 873 | if (state.ignoreEvent 874 | && state.ignoreEvent.name == evt.displayName 875 | && state.ignoreEvent.type == evt.name 876 | && state.ignoreEvent.value == evt.value 877 | ) { 878 | debug("[a:inputHandler] Ignoring event: ${state.ignoreEvent}") 879 | state.ignoreEvent = false; 880 | } 881 | else { 882 | def json = new JsonOutput().toJson([ 883 | path: "/push", 884 | body: [ 885 | archivable: evt.archivable, 886 | date: evt.date, 887 | description: evt.description, 888 | descriptionText: evt.descriptionText, 889 | deviceId: evt.deviceId, 890 | deviceLabel: evt.displayName, 891 | displayed: evt.displayed, 892 | eventId: evt.id, 893 | hubId: evt.hubId, 894 | installedAppId: evt.installedAppId, 895 | isStateChange: evt.isStateChange, 896 | locationId: evt.locationId, 897 | name: evt.name, 898 | normalizedId: normalizedId(evt), 899 | source: evt.source, 900 | translatable: evt.translatable, 901 | type: evt.type, 902 | value: evt.value, 903 | unit: evt.unit, 904 | ] 905 | ]) 906 | 907 | debug("[a:inputHandler] Forwarding device event to driver: ${json}") 908 | mqttLink.deviceNotification(json) 909 | } 910 | } 911 | 912 | def pingState() { 913 | def pingList = [] 914 | settings.selectedDevices.each { device -> 915 | def deviceId = normalizeId(device) 916 | def attributes = device.getSupportedAttributes() 917 | def capabilities = device.getCapabilities() 918 | 919 | capabilities.each { capability -> 920 | 921 | def found = false 922 | settings[deviceId].find { cap -> 923 | if (cap == capability.name) { 924 | found = true 925 | return true 926 | } 927 | return false 928 | } 929 | 930 | if (found) { 931 | capability.getAttributes().each { attribute -> 932 | 933 | def attributeName = upperCamel(attribute.toString()) 934 | def currentValue = device."current${attributeName}" 935 | 936 | debug("[a:pingState] Sending state refresh: ${device}:${attribute}:${currentValue}") 937 | 938 | pingList.add([ 939 | normalizedId: deviceId, 940 | name: attribute.name, 941 | value: currentValue.toString(), 942 | pingRefresh: true 943 | ]) 944 | } 945 | } 946 | } 947 | } 948 | 949 | if (pingList.size > 0) { 950 | def json = new JsonOutput().toJson([ 951 | path: "/ping", 952 | body: pingList 953 | ]) 954 | 955 | mqttLink.deviceNotification(json) 956 | } 957 | 958 | } 959 | 960 | // ======================================================== 961 | // HELPERS 962 | // ======================================================== 963 | 964 | def getDeviceObj(id) { 965 | def found 966 | settings.allDevices.each { device -> 967 | if (device.getId() == id) { 968 | debug("[a:getDeviceObj] Found at $device for $id with id: ${device.id}") 969 | found = device 970 | } 971 | } 972 | return found 973 | } 974 | 975 | def getHubId() { 976 | def hub = location.hubs[0] 977 | def hubNameNormalized = normalize(hub.name) 978 | return "${hubNameNormalized}-${hub.hardwareID}".toLowerCase() 979 | } 980 | 981 | def getTopicPrefix() { 982 | return "${rootTopic()}/${getHubId()}/" 983 | } 984 | 985 | def upperCamel(str) { 986 | def c = str.charAt(0) 987 | return "${c.toUpperCase()}${str.substring(1)}".toString(); 988 | } 989 | 990 | def lowerCamel(str) { 991 | def c = str.charAt(0) 992 | return "${c.toLowerCase()}${str.substring(1)}".toString(); 993 | } 994 | 995 | def normalize(name) { 996 | return name.replaceAll("[^a-zA-Z0-9]+","-").toLowerCase() 997 | } 998 | 999 | def normalizeId(name, id) { 1000 | def normalizedName = normalize(name) 1001 | return "${normalizedName}-${id}".toString() 1002 | } 1003 | 1004 | def normalizeId(device) { 1005 | return normalizeId(device.displayName, device.id) 1006 | } 1007 | 1008 | def normalizedId(com.hubitat.hub.domain.Event evt) { 1009 | def deviceId = evt.deviceId 1010 | 1011 | if (!deviceId && evt.type == "LOCATION_MODE_CHANGE") { 1012 | return normalizeId(evt.displayName, "mode") 1013 | } 1014 | 1015 | return normalizeId(evt.displayName, deviceId) 1016 | } 1017 | 1018 | // ======================================================== 1019 | // LOGGING 1020 | // ======================================================== 1021 | 1022 | def debug(msg) { 1023 | if (debugLogging) { 1024 | log.debug msg 1025 | } 1026 | } 1027 | 1028 | def info(msg) { 1029 | log.info msg 1030 | } 1031 | 1032 | def warn(msg) { 1033 | log.warn msg 1034 | } 1035 | 1036 | def error(msg) { 1037 | log.error msg 1038 | } 1039 | 1040 | // ======================================================== 1041 | // ACTIONS 1042 | // ======================================================== 1043 | 1044 | // +---------------------------------+ 1045 | // | WARNING, BEYOND HERE BE DRAGONS | 1046 | // +---------------------------------+ 1047 | // These are the functions that handle incoming messages from MQTT. 1048 | // I tried to put them in closures but apparently SmartThings Groovy sandbox 1049 | // restricts you from running closures from an object (it's not safe). 1050 | // -- 1051 | // John E - Note there isn't the same sandbox for Hubitat. So heed 1052 | // the original warning. 1053 | 1054 | def actionAirConditionerMode(device, attribute, value) { 1055 | device.setAirConditionerMode(value) 1056 | } 1057 | 1058 | def actionAlarm(device, attribute, value) { 1059 | switch (value) { 1060 | case "both": 1061 | device.both() 1062 | break 1063 | case "off": 1064 | device.off() 1065 | break 1066 | case "siren": 1067 | device.siren() 1068 | break 1069 | case "strobe": 1070 | device.strobe() 1071 | break 1072 | } 1073 | } 1074 | 1075 | def actionAudioMute(device, attribute, value) { 1076 | device.setMute(value) 1077 | } 1078 | 1079 | def actionAudioNotification(device, attribute, value) { 1080 | //value0: URI/URL of track to play 1081 | //value1: Volume level (0 to 100) 1082 | def (texttrackuri, volumelevel) = value.split(',') 1083 | switch (attribute) { 1084 | case "playText": 1085 | device.playText(texttrackuri, volumelevel) 1086 | break 1087 | case "playTextAndRestore": 1088 | device.playTextAndRestore(texttrackuri, volumelevel) 1089 | break 1090 | case "playTextAndResume": 1091 | device.playTextAndResume(texttrackuri, volumelevel) 1092 | break 1093 | case "playTrack": 1094 | device.playTrack(texttrackuri, volumelevel) 1095 | break 1096 | case "playTrackAndResume": 1097 | device.playTrackAndResume(texttrackuri, volumelevel) 1098 | break 1099 | case "playTrackAndRestore": 1100 | device.playTrackAndRestore(texttrackuri, volumelevel) 1101 | break 1102 | } 1103 | } 1104 | 1105 | def actionAudioVolume(device, attribute, value) { 1106 | switch (attribute) { 1107 | case "mute": 1108 | device.mute() 1109 | break 1110 | case "setVolume": 1111 | device.setVolume(value) 1112 | break 1113 | case "unmute": 1114 | device.unmute() 1115 | break 1116 | case "volumeUp": 1117 | device.volumeUp() 1118 | break 1119 | case "volumeDown": 1120 | device.volumeDown() 1121 | break 1122 | } 1123 | } 1124 | 1125 | def actionColorControl(device, attribute, value) { 1126 | switch (attribute) { 1127 | case "setColor": 1128 | def values = value.split(',') 1129 | def colormap = ["hue": values[0] as int, "saturation": values[1] as int] 1130 | 1131 | if (values[2]) { 1132 | colormap["level"] = values[2] as int 1133 | } 1134 | 1135 | device.setColor(colormap) 1136 | break 1137 | case "setHue": 1138 | device.setHue(value as int) 1139 | break 1140 | case "setSaturation": 1141 | device.setSaturation(value as int) 1142 | break 1143 | } 1144 | } 1145 | 1146 | def actionChangeLevel(device, attribute, value) { 1147 | switch (attribute) { 1148 | case "startLevelChange": 1149 | device.startLevelChange(value) 1150 | break 1151 | case "stopLevelChange": 1152 | device.stopLevelChange() 1153 | break 1154 | } 1155 | } 1156 | 1157 | def actionChime(device, attribute, value) { 1158 | switch (attribute) { 1159 | case "playSound": 1160 | device.playSound(value) 1161 | break 1162 | case "stop": 1163 | device.stop() 1164 | break 1165 | } 1166 | } 1167 | 1168 | def actionColorTemperature(device, attribute, value) { 1169 | device.setColorTemperature(value as int) 1170 | } 1171 | 1172 | def actionConfiguration(device, attribute, value) { 1173 | // device.configure() 1174 | } 1175 | 1176 | def actionConsumable(device, attribute, value) { 1177 | device.setConsumableStatus(value) 1178 | } 1179 | 1180 | def actionFanControl(device, attribute, value) { 1181 | // value: speed - ENUM ["low","medium-low","medium","medium-high","high","on","off","auto"] 1182 | device.setSpeed(value) 1183 | } 1184 | 1185 | def actionHealthCheck(device, attribute, value) { 1186 | device.ping() 1187 | } 1188 | 1189 | def actionImageCapture(device, attribute, value) { 1190 | device.take() 1191 | } 1192 | 1193 | def actionLightEffects(device, attribute, value) { 1194 | switch (value) { 1195 | case "setEffect": 1196 | device.setEffect(value) 1197 | break 1198 | case "setNextEffect": 1199 | device.setNextEffect() 1200 | break 1201 | case "setPreviousEffect": 1202 | device.setPreviousEffect() 1203 | break 1204 | } 1205 | } 1206 | 1207 | def actionLock(device, attribute, value) { 1208 | if (value == "lock") { 1209 | device.lock() 1210 | } else if (value == "unlock") { 1211 | device.unlock() 1212 | } 1213 | } 1214 | 1215 | def actionLockCodes(device, attribute, value) { 1216 | // codeposition required (NUMBER) - Code position number 1217 | // pincode required (STRING) - Numeric PIN code 1218 | // name optional (STRING) - Name for this lock code 1219 | switch (value) { 1220 | case "deleteCode": 1221 | device.deleteCode(value) 1222 | break 1223 | case "getCodes": 1224 | device.getCodes() 1225 | break 1226 | case "setCode": 1227 | def (codeposition, pincode, name) = value.split(",") 1228 | device.setCode(codeposition, pincode, name) 1229 | break 1230 | case "setCodeLength": 1231 | device.setCodeLength() 1232 | break 1233 | } 1234 | } 1235 | 1236 | def actionMediaController(device, attribute, value) { 1237 | switch (value) { 1238 | case "getAllActivities": 1239 | device.getAllActivities() 1240 | break 1241 | case "getCurrentActivity": 1242 | device.getCurrentActivity() 1243 | break 1244 | case "startActivity": 1245 | device.startActivity(value) 1246 | break 1247 | } 1248 | } 1249 | 1250 | def actionPlaybackShuffle(device, attribute, value) { 1251 | device.setPlaybackShuffle(value) 1252 | } 1253 | 1254 | def actionMomentary(device, attribute, value) { 1255 | device.push() 1256 | } 1257 | 1258 | def actionNotification(device, attribute, value) { 1259 | device.deviceNotification(value) 1260 | } 1261 | 1262 | def actionSamsungTV(device, attribute, value) { 1263 | switch (value) { 1264 | case "mute": 1265 | device.mute() 1266 | break 1267 | case "off": 1268 | device.off() 1269 | break 1270 | case "on": 1271 | device.on() 1272 | break 1273 | case "setPictureMode": 1274 | device.setPictureMode(value) 1275 | break 1276 | case "setSoundMode": 1277 | device.setSoundMode(value) 1278 | break 1279 | case "setVolume": 1280 | device.setVolume(value) 1281 | break 1282 | case "showMessage": 1283 | device.showMessage(value) 1284 | break 1285 | case "unmute": 1286 | device.unmute() 1287 | break 1288 | case "volumeDown": 1289 | device.volumeDown() 1290 | break 1291 | case "volumeUp": 1292 | device.volumeUp() 1293 | break 1294 | } 1295 | } 1296 | 1297 | def actionSecurityKeypad(device, attribute, value) { 1298 | // codeposition required (NUMBER) - Code position number 1299 | // pincode required (STRING) - Numeric PIN code 1300 | // name optional (STRING) - Name for this lock code 1301 | switch (value) { 1302 | case "armAway": 1303 | device.armAway() 1304 | break 1305 | case "armHome": 1306 | device.armHome() 1307 | break 1308 | case "deleteCode": 1309 | device.deleteCode(value) 1310 | break 1311 | case "disarm": 1312 | device.disarm(value) 1313 | break 1314 | case "getCodes": 1315 | device.getCodes() 1316 | break 1317 | case "setCode": 1318 | def (codeposition, pincode, name) = value.split(",") 1319 | device.setCode(codeposition, pincode, name) 1320 | break 1321 | case "setCodeLength": 1322 | device.setCodeLength(value) 1323 | break 1324 | case "setEntryDelay": 1325 | device.setEntryDelay(value) 1326 | break 1327 | case "setExitDelay": 1328 | device.setExitDelay(value) 1329 | break 1330 | } 1331 | } 1332 | 1333 | def actionSpeechSynthesis(device, attribute, value) { 1334 | device.speak(value) 1335 | } 1336 | 1337 | def actionSwitchLevel(device, attribute, value) { 1338 | device.setLevel(value as int) 1339 | } 1340 | 1341 | def actionTimedSession(device, attribute, value) { 1342 | switch (attribute) { 1343 | case "cancel": 1344 | device.cancel() 1345 | break 1346 | case "pause": 1347 | device.pause() 1348 | break 1349 | case "setTimeRemaining": 1350 | device.setTimeRemaining(value) 1351 | break 1352 | case "start": 1353 | device.start() 1354 | break 1355 | case "stop": 1356 | device.stop() 1357 | break 1358 | } 1359 | } 1360 | 1361 | def actionTone(device, attribute, value) { 1362 | device.beep() 1363 | } 1364 | 1365 | def actionTV(device, attribute, value) { 1366 | switch (attribute) { 1367 | case "channelDown": 1368 | device.channelDown() 1369 | break 1370 | case "channelUp": 1371 | device.channelUp() 1372 | break 1373 | case "volumeDown": 1374 | device.volumeDown() 1375 | break 1376 | case "volumeUp": 1377 | device.volumeUp() 1378 | break 1379 | } 1380 | } 1381 | 1382 | def actionThermostatCoolingSetpoint(device, attribute, value) { 1383 | device.setCoolingSetpoint(value) 1384 | } 1385 | 1386 | def actionThermostatFanMode(device, attribute, value) { 1387 | switch (attribute) { 1388 | case "fanAuto": 1389 | device.fanAuto() 1390 | break 1391 | case "fanCirculate": 1392 | device.fanCirculate() 1393 | break 1394 | case "fanOn": 1395 | device.fanOn() 1396 | break 1397 | case "setThermostatFanMode": 1398 | device.setThermostatFanMode(value) 1399 | break 1400 | } 1401 | } 1402 | 1403 | def actionThermostatHeatingSetpoint(device, attribute, value) { 1404 | device.setHeatingSetpoint(value) 1405 | } 1406 | 1407 | def actionThermostatMode(device, attribute, value) { 1408 | switch (attribute) { 1409 | case "auto": 1410 | device.auto() 1411 | break 1412 | case "cool": 1413 | device.cool() 1414 | break 1415 | case "emergencyHeat": 1416 | device.emergencyHeat() 1417 | break 1418 | case "heat": 1419 | device.heat() 1420 | break 1421 | case "off": 1422 | device.off() 1423 | break 1424 | case "setThermostatMode": 1425 | device.setThermostatMode(value) 1426 | break 1427 | } 1428 | } 1429 | 1430 | def actionThermostatSchedule(device, attribute, value) { 1431 | device.setSchedule(value) 1432 | } 1433 | 1434 | def actionVideoCamera(device, attribute, value) { 1435 | switch (attribute) { 1436 | case "flip": 1437 | device.flip() 1438 | break 1439 | case "mute": 1440 | device.mute() 1441 | break 1442 | case "off": 1443 | device.off() 1444 | break 1445 | case "on": 1446 | device.on() 1447 | break 1448 | case "unmute": 1449 | device.unmute() 1450 | break 1451 | } 1452 | } 1453 | 1454 | def actionVideoCapture(device, attribute, value) { 1455 | // capture(DATE, DATE, DATE) 1456 | device.capture(value) 1457 | } 1458 | 1459 | def actionWindowShade(device, attribute, value) { 1460 | switch (attribute) { 1461 | case "close": 1462 | device.close(value) 1463 | break 1464 | case "open": 1465 | device.open() 1466 | break 1467 | case "setPosition": 1468 | device.setPosition(value) 1469 | break 1470 | } 1471 | } 1472 | 1473 | def actionZwMultichannel(device, attribute, value) { 1474 | switch (attribute) { 1475 | case "enableEpEvents": 1476 | device.enableEpEvents(value) 1477 | break 1478 | case "epCmd": 1479 | def (num, str) = value.split(",") 1480 | device.epCmd(num, str) 1481 | break 1482 | } 1483 | } 1484 | 1485 | /* 1486 | * Generic Actions 1487 | * Routines & Modes Actions 1488 | */ 1489 | 1490 | def actionOpenClose(device, attribute, value) { 1491 | if (value == "open") { 1492 | device.open() 1493 | } else if (value == "close") { 1494 | device.close() 1495 | } 1496 | } 1497 | 1498 | def actionOnOff(device, attribute, value) { 1499 | if (value == "off") { 1500 | device.off() 1501 | } else if (value == "on") { 1502 | device.on() 1503 | } 1504 | } 1505 | 1506 | def actionRoutines(value) { 1507 | location.helloHome?.execute(value) 1508 | } 1509 | 1510 | def actionModes(value) { 1511 | if (location.mode != value) { 1512 | if (location.modes?.find{it.name == value}) { 1513 | location.setMode(value) 1514 | } else { 1515 | warn("[actionModes] unknown mode: ${value}") 1516 | } 1517 | } 1518 | } -------------------------------------------------------------------------------- /drivers/hubitat-mqtt-link-driver.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * MQTT Link Driver 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2020 license@mydevbox.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | import groovy.json.JsonOutput 32 | 33 | public static String version() { return "v2.0.0" } 34 | public static String rootTopic() { return "hubitat" } 35 | 36 | //hubitat / {hub-name} / { device-name } / { device-capability } / STATE 37 | 38 | metadata { 39 | definition( 40 | name: "MQTT Link Driver", 41 | namespace: "mydevbox", 42 | author: "Chris Lawson, et al", 43 | description: "A link between MQTT broker and MQTT Link app", 44 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections.png", 45 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@2x.png", 46 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@3x.png" 47 | ) { 48 | capability "Notification" 49 | 50 | preferences { 51 | input( 52 | name: "brokerIp", 53 | type: "string", 54 | title: "MQTT Broker IP Address", 55 | description: "e.g. 192.168.1.200", 56 | required: true, 57 | displayDuringSetup: true 58 | ) 59 | input( 60 | name: "brokerPort", 61 | type: "string", 62 | title: "MQTT Broker Port", 63 | description: "e.g. 1883", 64 | required: true, 65 | displayDuringSetup: true 66 | ) 67 | 68 | input( 69 | name: "brokerUser", 70 | type: "string", 71 | title: "MQTT Broker Username", 72 | description: "e.g. mqtt_user", 73 | required: false, 74 | displayDuringSetup: true 75 | ) 76 | input( 77 | name: "brokerPassword", 78 | type: "password", 79 | title: "MQTT Broker Password", 80 | description: "e.g. ^L85er1Z7g&%2En!", 81 | required: false, 82 | displayDuringSetup: true 83 | ) 84 | input( 85 | name: "sendPayload", 86 | type: "bool", 87 | title: "Send full payload messages on device events", 88 | required: false, 89 | default: false 90 | ) 91 | input( 92 | name: "debugLogging", 93 | type: "bool", 94 | title: "Enable debug logging", 95 | required: false, 96 | default: false 97 | ) 98 | } 99 | 100 | // Provided for broker setup and troubleshooting 101 | command "publish", [[name:"topic*",type:"STRING", title:"test",description:"Topic"],[name:"message",type:"STRING", description:"Message"]] 102 | command "subscribe",[[name:"topic*",type:"STRING", description:"Topic"]] 103 | command "unsubscribe",[[name:"topic*",type:"STRING", description:"Topic"]] 104 | command "connect" 105 | command "disconnect" 106 | } 107 | } 108 | 109 | void initialize() { 110 | debug("Initializing driver...") 111 | 112 | try { 113 | interfaces.mqtt.connect(getBrokerUri(), 114 | "hubitat_${getHubId()}", 115 | settings?.brokerUser, 116 | settings?.brokerPassword, 117 | lastWillTopic: "${getTopicPrefix()}LWT", 118 | lastWillQos: 0, 119 | lastWillMessage: "offline", 120 | lastWillRetain: true) 121 | 122 | // delay for connection 123 | pauseExecution(1000) 124 | 125 | } catch(Exception e) { 126 | error("[d:initialize] ${e}") 127 | } 128 | } 129 | 130 | // ======================================================== 131 | // MQTT COMMANDS 132 | // ======================================================== 133 | 134 | def publish(topic, payload) { 135 | publishMqtt(topic, payload) 136 | } 137 | 138 | def subscribe(topic) { 139 | if (notMqttConnected()) { 140 | connect() 141 | } 142 | 143 | debug("[d:subscribe] full topic: ${getTopicPrefix()}${topic}") 144 | interfaces.mqtt.subscribe("${getTopicPrefix()}${topic}") 145 | } 146 | 147 | def unsubscribe(topic) { 148 | if (notMqttConnected()) { 149 | connect() 150 | } 151 | 152 | debug("[d:unsubscribe] full topic: ${getTopicPrefix()}${topic}") 153 | interfaces.mqtt.unsubscribe("${getTopicPrefix()}${topic}") 154 | } 155 | 156 | def connect() { 157 | initialize() 158 | connected() 159 | } 160 | 161 | def disconnect() { 162 | try { 163 | interfaces.mqtt.disconnect() 164 | disconnected() 165 | } catch(e) { 166 | warn("Disconnection from broker failed", ${e.message}) 167 | if (interfaces.mqtt.isConnected()) connected() 168 | } 169 | } 170 | 171 | // ======================================================== 172 | // MQTT LINK APP MESSAGE HANDLER 173 | // ======================================================== 174 | 175 | // Device event notification from MQTT Link app via mqttLink.deviceNotification() 176 | def deviceNotification(message) { 177 | debug("[d:deviceNotification] Received message from MQTT Link app: '${message}'") 178 | 179 | 180 | def slurper = new JsonSlurper() 181 | def parsed = slurper.parseText(message) 182 | 183 | // Scheduled event in MQTT Broker app that renews device topic subs 184 | if (parsed.path == '/subscribe') { 185 | deviceSubscribe(parsed) 186 | } 187 | 188 | // Device event 189 | if (parsed.path == '/push') { 190 | sendDeviceEvent(parsed.body) 191 | } 192 | 193 | // Device state refresh 194 | if (parsed.path == '/ping') { 195 | if (mqttConnected) { 196 | connected() 197 | } 198 | 199 | parsed.body.each { device -> 200 | sendDeviceEvent(device) 201 | } 202 | } 203 | } 204 | 205 | def deviceSubscribe(message) { 206 | 207 | // Clear all prior subsciptions 208 | if (message.update) { 209 | unsubscribe("#") 210 | } 211 | 212 | message.body.devices.each { key, capability -> 213 | capability.each { attribute -> 214 | def normalizedAttrib = normalize(attribute) 215 | def topic = "${normalizedAttrib}/cmd/${key}".toString() 216 | 217 | debug("[d:deviceSubscribe] topic: ${topic} attribute: ${attribute}") 218 | subscribe(topic) 219 | } 220 | } 221 | } 222 | 223 | def sendDeviceEvent(message) { 224 | topic = "${message.normalizedId}/" 225 | 226 | // Send command value only 227 | publishMqtt("${topic}${message.name}", message.value) 228 | 229 | if (message.pingRefresh) { 230 | return 231 | } 232 | 233 | if (settings.sendPayload) { 234 | // Send detailed event object 235 | publishMqtt("${topic}payload", JsonOutput.toJson(message)) 236 | } 237 | } 238 | 239 | // ======================================================== 240 | // MQTT METHODS 241 | // ======================================================== 242 | 243 | // Parse incoming message from the MQTT broker 244 | def parse(String event) { 245 | def message = interfaces.mqtt.parseMessage(event) 246 | def (name, hub, device, cmd, type) = message.topic.tokenize( '/' ) 247 | 248 | // ignore all msgs that aren't commands 249 | if (cmd != 'cmd') return 250 | 251 | debug("[d:parse] Received MQTT message: ${message}") 252 | 253 | def json = new groovy.json.JsonOutput().toJson([ 254 | device: device, 255 | type: type, 256 | value: message.payload 257 | ]) 258 | 259 | return createEvent(name: "message", value: json, displayed: false) 260 | } 261 | 262 | def mqttClientStatus(status) { 263 | debug("[d:mqttClientStatus] status: ${status}") 264 | } 265 | 266 | def publishMqtt(topic, payload, qos = 0, retained = false) { 267 | if (notMqttConnected()) { 268 | debug("[d:publishMqtt] not connected") 269 | initialize() 270 | } 271 | 272 | def pubTopic = "${getTopicPrefix()}${topic}" 273 | 274 | try { 275 | interfaces.mqtt.publish("${pubTopic}", payload, qos, retained) 276 | debug("[d:publishMqtt] topic: ${pubTopic} payload: ${payload}") 277 | 278 | } catch (Exception e) { 279 | error("[d:publishMqtt] Unable to publish message: ${e}") 280 | } 281 | } 282 | 283 | // ======================================================== 284 | // ANNOUNCEMENTS 285 | // ======================================================== 286 | 287 | def connected() { 288 | debug("[d:connected] Connected to broker") 289 | sendEvent (name: "connectionState", value: "connected") 290 | announceLwtStatus("online") 291 | } 292 | 293 | def disconnected() { 294 | debug("[d:disconnected] Disconnected from broker") 295 | sendEvent (name: "connectionState", value: "disconnected") 296 | announceLwtStatus("offline") 297 | } 298 | 299 | def announceLwtStatus(String status) { 300 | publishMqtt("LWT", status) 301 | publishMqtt("FW", "${location.hub.firmwareVersionString}") 302 | publishMqtt("IP", "${location.hub.localIP}") 303 | publishMqtt("UPTIME", "${location.hub.uptime}") 304 | } 305 | 306 | // ======================================================== 307 | // HELPERS 308 | // ======================================================== 309 | 310 | def normalize(name) { 311 | return name.replaceAll("[^a-zA-Z0-9]+","-").toLowerCase() 312 | } 313 | 314 | def getBrokerUri() { 315 | return "tcp://${settings?.brokerIp}:${settings?.brokerPort}" 316 | } 317 | 318 | def getHubId() { 319 | def hub = location.hubs[0] 320 | def hubNameNormalized = normalize(hub.name) 321 | return "${hubNameNormalized}-${hub.hardwareID}".toLowerCase() 322 | } 323 | 324 | def getTopicPrefix() { 325 | return "${rootTopic()}/${getHubId()}/" 326 | } 327 | 328 | def mqttConnected() { 329 | return interfaces.mqtt.isConnected() 330 | } 331 | 332 | def notMqttConnected() { 333 | return !mqttConnected() 334 | } 335 | 336 | // ======================================================== 337 | // LOGGING 338 | // ======================================================== 339 | 340 | def debug(msg) { 341 | if (debugLogging) { 342 | log.debug msg 343 | } 344 | } 345 | 346 | def info(msg) { 347 | log.info msg 348 | } 349 | 350 | def warn(msg) { 351 | log.warn msg 352 | } 353 | 354 | def error(msg) { 355 | log.error msg 356 | } -------------------------------------------------------------------------------- /packageManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "MQTT Link", 3 | "author": "Chris Lawson", 4 | "version": "2.0.0", 5 | "minimumHEVersion": "2.1.2", 6 | "dateReleased": "2020-05-24", 7 | "documentationLink": "https://github.com/mydevbox/hubitat-mqtt-link/blob/master/README.md", 8 | "communityLink": "https://community.hubitat.com/t/release-hubitat-mqtt-link/41846", 9 | "apps": [ 10 | { 11 | "id": "a8a93d71-a62a-4c44-b286-42e8a425eea5", 12 | "name": "MQTT Link", 13 | "namespace": "mydevbox", 14 | "location": "https://raw.githubusercontent.com/mydevbox/hubitat-mqtt-link/master/apps/hubitat-mqtt-link-app.groovy", 15 | "required": true, 16 | "oauth": false 17 | } 18 | ], 19 | "drivers": [ 20 | { 21 | "id": "fd41655b-e22c-496e-9895-11792402a7c0", 22 | "name": "MQTT Link Driver", 23 | "namespace": "mydevbox", 24 | "location": "https://raw.githubusercontent.com/mydevbox/hubitat-mqtt-link/master/drivers/hubitat-mqtt-link-driver.groovy", 25 | "required": true 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Chris Lawson", 3 | "gitHubUrl": "https://github.com/mydevbox", 4 | "packages": [ 5 | { 6 | "name": "MQTT Link", 7 | "category": "Integrations", 8 | "location": "https://raw.githubusercontent.com/mydevbox/hubitat-mqtt-link/master/packageManifest.json", 9 | "description": "System to share and control Hubitat Elevation device states in MQTT" 10 | } 11 | ] 12 | } --------------------------------------------------------------------------------