├── 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 | }
--------------------------------------------------------------------------------