1 /*****************************************************************************************************************
2 * Copyright David Lomas (codersaur)
4 * Name: InfluxDB Logger
10 * Source: https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
12 * Author: David Lomas (codersaur)
14 * Description: A SmartApp to log SmartThings device states to an InfluxDB database.
16 * For full information, including installation instructions, exmples, and version history, see:
17 * https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
19 * IMPORTANT - To enable the resolution of groupNames (i.e. room names), you must manually insert the group IDs
20 * into the getGroupName() command code at the end of this file.
23 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
24 * in compliance with the License. You may obtain a copy of the License at:
26 * http://www.apache.org/licenses/LICENSE-2.0
28 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
29 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
30 * for the specific language governing permissions and limitations under the License.
31 *****************************************************************************************************************/
33 name: "InfluxDB Logger",
34 namespace: "codersaur",
35 author: "David Lomas (codersaur)",
36 description: "Log SmartThings device states to InfluxDB",
38 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
39 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
40 iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
45 //input "prefDebugMode", "bool", title: "Enable debug logging?", defaultValue: true, displayDuringSetup: true
47 name: "configLoggingLevelIDE",
48 title: "IDE Live Logging Level:\nMessages with this level and higher will be logged to the IDE.",
59 displayDuringSetup: true,
64 section ("InfluxDB Database:") {
65 input "prefDatabaseHost", "text", title: "Host", defaultValue: "10.10.10.10", required: true
66 input "prefDatabasePort", "text", title: "Port", defaultValue: "8086", required: true
67 input "prefDatabaseName", "text", title: "Database Name", defaultValue: "", required: true
68 input "prefDatabaseUser", "text", title: "Username", required: false
69 input "prefDatabasePass", "text", title: "Password", required: false
73 input "prefSoftPollingInterval", "number", title:"Soft-Polling interval (minutes)", defaultValue: 10, required: true
76 section("System Monitoring:") {
77 input "prefLogModeEvents", "bool", title:"Log Mode Events?", defaultValue: true, required: true
78 input "prefLogHubProperties", "bool", title:"Log Hub Properties?", defaultValue: true, required: true
79 input "prefLogLocationProperties", "bool", title:"Log Location Properties?", defaultValue: true, required: true
82 section("Devices To Monitor:") {
83 input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
84 input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
85 input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
86 input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
87 input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
88 input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
89 input "co2s", "capability.carbonDioxideMeasurement", title: "Carbon Dioxide Detectors", multiple: true, required: false
90 input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
91 input "consumables", "capability.consumable", title: "Consumables", multiple: true, required: false
92 input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
93 input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
94 input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
95 input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
96 input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
97 input "locks", "capability.lock", title: "Locks", multiple: true, required: false
98 input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
99 input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
100 input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
101 input "phMeters", "capability.pHMeasurement", title: "pH Meters", multiple: true, required: false
102 input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
103 input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
104 input "pressures", "capability.sensor", title: "Pressure Sensors", multiple: true, required: false
105 input "shockSensors", "capability.shockSensor", title: "Shock Sensors", multiple: true, required: false
106 input "signalStrengthMeters", "capability.signalStrength", title: "Signal Strength Meters", multiple: true, required: false
107 input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
108 input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
109 input "soundSensors", "capability.soundSensor", title: "Sound Sensors", multiple: true, required: false
110 input "spls", "capability.soundPressureLevel", title: "Sound Pressure Level Sensors", multiple: true, required: false
111 input "switches", "capability.switch", title: "Switches", multiple: true, required: false
112 input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
113 input "tamperAlerts", "capability.tamperAlert", title: "Tamper Alerts", multiple: true, required: false
114 input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
115 input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
116 input "threeAxis", "capability.threeAxis", title: "Three-axis (Orientation) Sensors", multiple: true, required: false
117 input "touchs", "capability.touchSensor", title: "Touch Sensors", multiple: true, required: false
118 input "uvs", "capability.ultravioletIndex", title: "UV Sensors", multiple: true, required: false
119 input "valves", "capability.valve", title: "Valves", multiple: true, required: false
120 input "volts", "capability.voltageMeasurement", title: "Voltage Meters", multiple: true, required: false
121 input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
122 input "windowShades", "capability.windowShade", title: "Window Shades", multiple: true, required: false
128 /*****************************************************************************************************************
129 * SmartThings System Commands:
130 *****************************************************************************************************************/
135 * Runs when the app is first installed.
138 state.installedAt = now()
139 state.loggingLevelIDE = 5
140 log.debug "${app.label}: Installed with settings: ${settings}"
146 * Runs when the app is uninstalled.
149 logger("uninstalled()","trace")
155 * Runs when app settings are changed.
157 * Updates device.state with input values and other hard-coded values.
158 * Builds state.deviceAttributes which describes the attributes that will be monitored for each device collection
159 * (used by manageSubscriptions() and softPoll()).
160 * Refreshes scheduling and subscriptions.
163 logger("updated()","trace")
165 // Update internal state:
166 state.loggingLevelIDE = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3
169 state.databaseHost = settings.prefDatabaseHost
170 state.databasePort = settings.prefDatabasePort
171 state.databaseName = settings.prefDatabaseName
172 state.databaseUser = settings.prefDatabaseUser
173 state.databasePass = settings.prefDatabasePass
175 state.path = "/write?db=${state.databaseName}"
177 state.headers.put("HOST", "${state.databaseHost}:${state.databasePort}")
178 state.headers.put("Content-Type", "application/x-www-form-urlencoded")
179 if (state.databaseUser && state.databasePass) {
180 state.headers.put("Authorization", encodeCredentialsBasic(state.databaseUser, state.databasePass))
183 // Build array of device collections and the attributes we want to report on for that collection:
184 // Note, the collection names are stored as strings. Adding references to the actual collection
185 // objects causes major issues (possibly memory issues?).
186 state.deviceAttributes = []
187 state.deviceAttributes << [ devices: 'accelerometers', attributes: ['acceleration']]
188 state.deviceAttributes << [ devices: 'alarms', attributes: ['alarm']]
189 state.deviceAttributes << [ devices: 'batteries', attributes: ['battery']]
190 state.deviceAttributes << [ devices: 'beacons', attributes: ['presence']]
191 state.deviceAttributes << [ devices: 'buttons', attributes: ['button']]
192 state.deviceAttributes << [ devices: 'cos', attributes: ['carbonMonoxide']]
193 state.deviceAttributes << [ devices: 'co2s', attributes: ['carbonDioxide']]
194 state.deviceAttributes << [ devices: 'colors', attributes: ['hue','saturation','color']]
195 state.deviceAttributes << [ devices: 'consumables', attributes: ['consumableStatus']]
196 state.deviceAttributes << [ devices: 'contacts', attributes: ['contact']]
197 state.deviceAttributes << [ devices: 'doorsControllers', attributes: ['door']]
198 state.deviceAttributes << [ devices: 'energyMeters', attributes: ['energy']]
199 state.deviceAttributes << [ devices: 'humidities', attributes: ['humidity']]
200 state.deviceAttributes << [ devices: 'illuminances', attributes: ['illuminance']]
201 state.deviceAttributes << [ devices: 'locks', attributes: ['lock']]
202 state.deviceAttributes << [ devices: 'motions', attributes: ['motion']]
203 state.deviceAttributes << [ devices: 'musicPlayers', attributes: ['status','level','trackDescription','trackData','mute']]
204 state.deviceAttributes << [ devices: 'peds', attributes: ['steps','goal']]
205 state.deviceAttributes << [ devices: 'phMeters', attributes: ['pH']]
206 state.deviceAttributes << [ devices: 'powerMeters', attributes: ['power','voltage','current','powerFactor']]
207 state.deviceAttributes << [ devices: 'presences', attributes: ['presence']]
208 state.deviceAttributes << [ devices: 'pressures', attributes: ['pressure']]
209 state.deviceAttributes << [ devices: 'shockSensors', attributes: ['shock']]
210 state.deviceAttributes << [ devices: 'signalStrengthMeters', attributes: ['lqi','rssi']]
211 state.deviceAttributes << [ devices: 'sleepSensors', attributes: ['sleeping']]
212 state.deviceAttributes << [ devices: 'smokeDetectors', attributes: ['smoke']]
213 state.deviceAttributes << [ devices: 'soundSensors', attributes: ['sound']]
214 state.deviceAttributes << [ devices: 'spls', attributes: ['soundPressureLevel']]
215 state.deviceAttributes << [ devices: 'switches', attributes: ['switch']]
216 state.deviceAttributes << [ devices: 'switchLevels', attributes: ['level']]
217 state.deviceAttributes << [ devices: 'tamperAlerts', attributes: ['tamper']]
218 state.deviceAttributes << [ devices: 'temperatures', attributes: ['temperature']]
219 state.deviceAttributes << [ devices: 'thermostats', attributes: ['temperature','heatingSetpoint','coolingSetpoint','thermostatSetpoint','thermostatMode','thermostatFanMode','thermostatOperatingState','thermostatSetpointMode','scheduledSetpoint','optimisation','windowFunction']]
220 state.deviceAttributes << [ devices: 'threeAxis', attributes: ['threeAxis']]
221 state.deviceAttributes << [ devices: 'touchs', attributes: ['touch']]
222 state.deviceAttributes << [ devices: 'uvs', attributes: ['ultravioletIndex']]
223 state.deviceAttributes << [ devices: 'valves', attributes: ['contact']]
224 state.deviceAttributes << [ devices: 'volts', attributes: ['voltage']]
225 state.deviceAttributes << [ devices: 'waterSensors', attributes: ['water']]
226 state.deviceAttributes << [ devices: 'windowShades', attributes: ['windowShade']]
228 // Configure Scheduling:
229 state.softPollingInterval = settings.prefSoftPollingInterval.toInteger()
232 // Configure Subscriptions:
233 manageSubscriptions()
236 /*****************************************************************************************************************
238 *****************************************************************************************************************/
241 * handleAppTouch(evt)
245 def handleAppTouch(evt) {
246 logger("handleAppTouch()","trace")
252 * handleModeEvent(evt)
256 def handleModeEvent(evt) {
257 logger("handleModeEvent(): Mode changed to: ${evt.value}","info")
259 def locationId = escapeStringForInfluxDB(location.id)
260 def locationName = escapeStringForInfluxDB(location.name)
261 def mode = '"' + escapeStringForInfluxDB(evt.value) + '"'
262 def data = "_stMode,locationId=${locationId},locationName=${locationName} mode=${mode}"
269 * Builds data to send to InfluxDB.
270 * - Escapes and quotes string values.
271 * - Calculates logical binary values where string values can be
272 * represented as binary values (e.g. contact: closed = 1, open = 0)
275 * - http://docs.smartthings.com/en/latest/capabilities-reference.html
276 * - https://docs.influxdata.com/influxdb/v0.10/guides/writing_data/
278 def handleEvent(evt) {
279 logger("handleEvent(): $evt.displayName($evt.name:$evt.unit) $evt.value","info")
281 // Build data string to send to InfluxDB:
282 // Format: <measurement>[,<tag_name>=<tag_value>] field=<field_value>
283 // If value is an integer, it must have a trailing "i"
284 // If value is a string, it must be enclosed in double quotes.
285 def measurement = evt.name
287 def deviceId = escapeStringForInfluxDB(evt.deviceId)
288 def deviceName = escapeStringForInfluxDB(evt.displayName)
289 def groupId = escapeStringForInfluxDB(evt?.device.device.groupId)
290 def groupName = escapeStringForInfluxDB(getGroupName(evt?.device.device.groupId))
291 def hubId = escapeStringForInfluxDB(evt?.device.device.hubId)
292 def hubName = escapeStringForInfluxDB(evt?.device.device.hub.toString())
293 // Don't pull these from the evt.device as the app itself will be associated with one location.
294 def locationId = escapeStringForInfluxDB(location.id)
295 def locationName = escapeStringForInfluxDB(location.name)
297 def unit = escapeStringForInfluxDB(evt.unit)
298 def value = escapeStringForInfluxDB(evt.value)
301 def data = "${measurement},deviceId=${deviceId},deviceName=${deviceName},groupId=${groupId},groupName=${groupName},hubId=${hubId},hubName=${hubName},locationId=${locationId},locationName=${locationName}"
303 // Unit tag and fields depend on the event type:
304 // Most string-valued attributes can be translated to a binary value too.
305 if ('acceleration' == evt.name) { // acceleration: Calculate a binary value (active = 1, inactive = 0)
306 unit = 'acceleration'
307 value = '"' + value + '"'
308 valueBinary = ('active' == evt.value) ? '1i' : '0i'
309 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
311 else if ('alarm' == evt.name) { // alarm: Calculate a binary value (strobe/siren/both = 1, off = 0)
313 value = '"' + value + '"'
314 valueBinary = ('off' == evt.value) ? '0i' : '1i'
315 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
317 else if ('button' == evt.name) { // button: Calculate a binary value (held = 1, pushed = 0)
319 value = '"' + value + '"'
320 valueBinary = ('pushed' == evt.value) ? '0i' : '1i'
321 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
323 else if ('carbonMonoxide' == evt.name) { // carbonMonoxide: Calculate a binary value (detected = 1, clear/tested = 0)
324 unit = 'carbonMonoxide'
325 value = '"' + value + '"'
326 valueBinary = ('detected' == evt.value) ? '1i' : '0i'
327 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
329 else if ('consumableStatus' == evt.name) { // consumableStatus: Calculate a binary value ("good" = 1, "missing"/"replace"/"maintenance_required"/"order" = 0)
330 unit = 'consumableStatus'
331 value = '"' + value + '"'
332 valueBinary = ('good' == evt.value) ? '1i' : '0i'
333 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
335 else if ('contact' == evt.name) { // contact: Calculate a binary value (closed = 1, open = 0)
337 value = '"' + value + '"'
338 valueBinary = ('closed' == evt.value) ? '1i' : '0i'
339 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
341 else if ('door' == evt.name) { // door: Calculate a binary value (closed = 1, open/opening/closing/unknown = 0)
343 value = '"' + value + '"'
344 valueBinary = ('closed' == evt.value) ? '1i' : '0i'
345 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
347 else if ('lock' == evt.name) { // door: Calculate a binary value (locked = 1, unlocked = 0)
349 value = '"' + value + '"'
350 valueBinary = ('locked' == evt.value) ? '1i' : '0i'
351 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
353 else if ('motion' == evt.name) { // Motion: Calculate a binary value (active = 1, inactive = 0)
355 value = '"' + value + '"'
356 valueBinary = ('active' == evt.value) ? '1i' : '0i'
357 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
359 else if ('mute' == evt.name) { // mute: Calculate a binary value (muted = 1, unmuted = 0)
361 value = '"' + value + '"'
362 valueBinary = ('muted' == evt.value) ? '1i' : '0i'
363 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
365 else if ('presence' == evt.name) { // presence: Calculate a binary value (present = 1, not present = 0)
367 value = '"' + value + '"'
368 valueBinary = ('present' == evt.value) ? '1i' : '0i'
369 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
371 else if ('shock' == evt.name) { // shock: Calculate a binary value (detected = 1, clear = 0)
373 value = '"' + value + '"'
374 valueBinary = ('detected' == evt.value) ? '1i' : '0i'
375 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
377 else if ('sleeping' == evt.name) { // sleeping: Calculate a binary value (sleeping = 1, not sleeping = 0)
379 value = '"' + value + '"'
380 valueBinary = ('sleeping' == evt.value) ? '1i' : '0i'
381 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
383 else if ('smoke' == evt.name) { // smoke: Calculate a binary value (detected = 1, clear/tested = 0)
385 value = '"' + value + '"'
386 valueBinary = ('detected' == evt.value) ? '1i' : '0i'
387 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
389 else if ('sound' == evt.name) { // sound: Calculate a binary value (detected = 1, not detected = 0)
391 value = '"' + value + '"'
392 valueBinary = ('detected' == evt.value) ? '1i' : '0i'
393 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
395 else if ('switch' == evt.name) { // switch: Calculate a binary value (on = 1, off = 0)
397 value = '"' + value + '"'
398 valueBinary = ('on' == evt.value) ? '1i' : '0i'
399 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
401 else if ('tamper' == evt.name) { // tamper: Calculate a binary value (detected = 1, clear = 0)
403 value = '"' + value + '"'
404 valueBinary = ('detected' == evt.value) ? '1i' : '0i'
405 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
407 else if ('thermostatMode' == evt.name) { // thermostatMode: Calculate a binary value (<any other value> = 1, off = 0)
408 unit = 'thermostatMode'
409 value = '"' + value + '"'
410 valueBinary = ('off' == evt.value) ? '0i' : '1i'
411 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
413 else if ('thermostatFanMode' == evt.name) { // thermostatFanMode: Calculate a binary value (<any other value> = 1, off = 0)
414 unit = 'thermostatFanMode'
415 value = '"' + value + '"'
416 valueBinary = ('off' == evt.value) ? '0i' : '1i'
417 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
419 else if ('thermostatOperatingState' == evt.name) { // thermostatOperatingState: Calculate a binary value (heating = 1, <any other value> = 0)
420 unit = 'thermostatOperatingState'
421 value = '"' + value + '"'
422 valueBinary = ('heating' == evt.value) ? '1i' : '0i'
423 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
425 else if ('thermostatSetpointMode' == evt.name) { // thermostatSetpointMode: Calculate a binary value (followSchedule = 0, <any other value> = 1)
426 unit = 'thermostatSetpointMode'
427 value = '"' + value + '"'
428 valueBinary = ('followSchedule' == evt.value) ? '0i' : '1i'
429 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
431 else if ('threeAxis' == evt.name) { // threeAxis: Format to x,y,z values.
433 def valueXYZ = evt.value.split(",")
434 def valueX = valueXYZ[0]
435 def valueY = valueXYZ[1]
436 def valueZ = valueXYZ[2]
437 data += ",unit=${unit} valueX=${valueX}i,valueY=${valueY}i,valueZ=${valueZ}i" // values are integers.
439 else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, "" = 0)
441 value = '"' + value + '"'
442 valueBinary = ('touched' == evt.value) ? '1i' : '0i'
443 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
445 else if ('optimisation' == evt.name) { // optimisation: Calculate a binary value (active = 1, inactive = 0)
446 unit = 'optimisation'
447 value = '"' + value + '"'
448 valueBinary = ('active' == evt.value) ? '1i' : '0i'
449 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
451 else if ('windowFunction' == evt.name) { // windowFunction: Calculate a binary value (active = 1, inactive = 0)
452 unit = 'windowFunction'
453 value = '"' + value + '"'
454 valueBinary = ('active' == evt.value) ? '1i' : '0i'
455 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
457 else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, <any other value> = 0)
459 value = '"' + value + '"'
460 valueBinary = ('touched' == evt.value) ? '1i' : '0i'
461 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
463 else if ('water' == evt.name) { // water: Calculate a binary value (wet = 1, dry = 0)
465 value = '"' + value + '"'
466 valueBinary = ('wet' == evt.value) ? '1i' : '0i'
467 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
469 else if ('windowShade' == evt.name) { // windowShade: Calculate a binary value (closed = 1, <any other value> = 0)
471 value = '"' + value + '"'
472 valueBinary = ('closed' == evt.value) ? '1i' : '0i'
473 data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
475 // Catch any other event with a string value that hasn't been handled:
476 else if (evt.value ==~ /.*[^0-9\.,-].*/) { // match if any characters are not digits, period, comma, or hyphen.
477 logger("handleEvent(): Found a string value that's not explicitly handled: Device Name: ${deviceName}, Event Name: ${evt.name}, Value: ${evt.value}","warn")
478 value = '"' + value + '"'
479 data += ",unit=${unit} value=${value}"
481 // Catch any other general numerical event (carbonDioxide, power, energy, humidity, level, temperature, ultravioletIndex, voltage, etc).
483 data += ",unit=${unit} value=${value}"
486 // Post data to InfluxDB:
492 /*****************************************************************************************************************
494 *****************************************************************************************************************/
499 * Executed by schedule.
501 * Forces data to be posted to InfluxDB (even if an event has not been triggered).
502 * Doesn't poll devices, just builds a fake event to pass to handleEvent().
504 * Also calls LogSystemProperties().
507 logger("softPoll()","trace")
509 logSystemProperties()
511 // Iterate over each attribute for each device, in each device collection in deviceAttributes:
512 def devs // temp variable to hold device collection.
513 state.deviceAttributes.each { da ->
514 devs = settings."${da.devices}"
515 if (devs && (da.attributes)) {
517 da.attributes.each { attr ->
518 if (d.hasAttribute(attr) && d.latestState(attr)?.value != null) {
519 logger("softPoll(): Softpolling device ${d} for attribute: ${attr}","info")
520 // Send fake event to handleEvent():
523 value: d.latestState(attr)?.value,
524 unit: d.latestState(attr)?.unit,
527 displayName: d.displayName
538 * logSystemProperties()
540 * Generates measurements for SmartThings system (hubs and locations) properties.
542 def logSystemProperties() {
543 logger("logSystemProperties()","trace")
545 def locationId = '"' + escapeStringForInfluxDB(location.id) + '"'
546 def locationName = '"' + escapeStringForInfluxDB(location.name) + '"'
548 // Location Properties:
549 if (prefLogLocationProperties) {
551 def tz = '"' + escapeStringForInfluxDB(location.timeZone.ID) + '"'
552 def mode = '"' + escapeStringForInfluxDB(location.mode) + '"'
553 def hubCount = location.hubs.size()
554 def times = getSunriseAndSunset()
555 def srt = '"' + times.sunrise.format("HH:mm", location.timeZone) + '"'
556 def sst = '"' + times.sunset.format("HH:mm", location.timeZone) + '"'
558 def data = "_stLocation,locationId=${locationId},locationName=${locationName},latitude=${location.latitude},longitude=${location.longitude},timeZone=${tz} mode=${mode},hubCount=${hubCount}i,sunriseTime=${srt},sunsetTime=${sst}"
561 logger("logSystemProperties(): Unable to log Location properties: ${e}","error")
566 if (prefLogHubProperties) {
567 location.hubs.each { h ->
569 def hubId = '"' + escapeStringForInfluxDB(h.id) + '"'
570 def hubName = '"' + escapeStringForInfluxDB(h.name) + '"'
571 def hubIP = '"' + escapeStringForInfluxDB(h.localIP) + '"'
572 def hubStatus = '"' + escapeStringForInfluxDB(h.status) + '"'
573 def batteryInUse = ("false" == h.hub.getDataValue("batteryInUse")) ? "0i" : "1i"
574 def hubUptime = h.hub.getDataValue("uptime") + 'i'
575 def zigbeePowerLevel = h.hub.getDataValue("zigbeePowerLevel") + 'i'
576 def zwavePowerLevel = '"' + escapeStringForInfluxDB(h.hub.getDataValue("zwavePowerLevel")) + '"'
577 def firmwareVersion = '"' + escapeStringForInfluxDB(h.firmwareVersionString) + '"'
579 def data = "_stHub,locationId=${locationId},locationName=${locationName},hubId=${hubId},hubName=${hubName},hubIP=${hubIP} "
580 data += "status=${hubStatus},batteryInUse=${batteryInUse},uptime=${hubUptime},zigbeePowerLevel=${zigbeePowerLevel},zwavePowerLevel=${zwavePowerLevel},firmwareVersion=${firmwareVersion}"
583 logger("logSystemProperties(): Unable to log Hub properties: ${e}","error")
594 * Posts data to InfluxDB.
596 * Uses hubAction instead of httpPost() in case InfluxDB server is on the same LAN as the Smartthings Hub.
598 def postToInfluxDB(data) {
599 logger("postToInfluxDB(): Posting data to InfluxDB: Host: ${state.databaseHost}, Port: ${state.databasePort}, Database: ${state.databaseName}, Data: [${data}]","debug")
602 def hubAction = new physicalgraph.device.HubAction(
607 headers: state.headers
610 [ callback: handleInfluxResponse ]
613 sendHubCommand(hubAction)
615 catch (Exception e) {
616 logger("postToInfluxDB(): Exception ${e} on ${hubAction}","error")
619 // For reference, code that could be used for WAN hosts:
620 // def url = "http://${state.databaseHost}:${state.databasePort}/write?db=${state.databaseName}"
622 // httpPost(url, data) { response ->
623 // if (response.status != 999 ) {
624 // log.debug "Response Status: ${response.status}"
625 // log.debug "Response data: ${response.data}"
626 // log.debug "Response contentType: ${response.contentType}"
630 // logger("postToInfluxDB(): Something went wrong when posting: ${e}","error")
635 * handleInfluxResponse()
637 * Handles response from post made in postToInfluxDB().
639 /*def handleInfluxResponse(physicalgraph.device.HubResponse hubResponse) {
640 if(hubResponse.status >= 400) {
641 logger("postToInfluxDB(): Something went wrong! Response from InfluxDB: Headers: ${hubResponse.headers}, Body: ${hubResponse.body}","error")
646 /*****************************************************************************************************************
647 * Private Helper Functions:
648 *****************************************************************************************************************/
653 * Configures/restarts scheduled tasks:
654 * softPoll() - Run every {state.softPollingInterval} minutes.
656 private manageSchedules() {
657 logger("manageSchedules()","trace")
659 // Generate a random offset (1-60):
660 Random rand = new Random(now())
668 // logger("manageSchedules(): Unschedule failed!","error")
671 if (state.softPollingInterval > 0) {
672 randomOffset = rand.nextInt(60)
673 logger("manageSchedules(): Scheduling softpoll to run every ${state.softPollingInterval} minutes (offset of ${randomOffset} seconds).","trace")
674 schedule("${randomOffset} 0/${state.softPollingInterval} * * * ?", "softPoll")
680 * manageSubscriptions()
682 * Configures subscriptions.
684 private manageSubscriptions() {
685 logger("manageSubscriptions()","trace")
690 // Subscribe to App Touch events:
691 subscribe(app,handleAppTouch)
693 // Subscribe to mode events:
694 if (prefLogModeEvents) subscribe(location, "mode", handleModeEvent)
696 // Subscribe to device attributes (iterate over each attribute for each device collection in state.deviceAttributes):
697 def devs // dynamic variable holding device collection.
698 state.deviceAttributes.each { da ->
699 devs = settings."${da.devices}"
700 if (devs && (da.attributes)) {
701 da.attributes.each { attr ->
702 logger("manageSubscriptions(): Subscribing to attribute: ${attr}, for devices: ${da.devices}","info")
703 // There is no need to check if all devices in the collection have the attribute.
704 subscribe(devs, attr, handleEvent)
713 * Wrapper function for all logging.
715 private logger(msg, level = "debug") {
719 if (state.loggingLevelIDE >= 1) log.error msg
723 if (state.loggingLevelIDE >= 2) log.warn msg
727 if (state.loggingLevelIDE >= 3) log.info msg
731 if (state.loggingLevelIDE >= 4) log.debug msg
735 if (state.loggingLevelIDE >= 5) log.trace msg
745 * encodeCredentialsBasic()
747 * Encode credentials for HTTP Basic authentication.
749 private encodeCredentialsBasic(username, password) {
750 return "Basic " + "${username}:${password}".encodeAsBase64().toString()
754 * escapeStringForInfluxDB()
756 * Escape values to InfluxDB.
758 * If a tag key, tag value, or field key contains a space, comma, or an equals sign = it must
759 * be escaped using the backslash character \. Backslash characters do not need to be escaped.
760 * Commas and spaces will also need to be escaped for measurements, though equals signs = do not.
762 * Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/
764 private escapeStringForInfluxDB(str) {
766 str = str.replaceAll(" ", "\\\\ ") // Escape spaces.
767 str = str.replaceAll(",", "\\\\,") // Escape commas.
768 str = str.replaceAll("=", "\\\\=") // Escape equal signs.
769 str = str.replaceAll("\"", "\\\\\"") // Escape double quotes.
770 //str = str.replaceAll("'", "_") // Replace apostrophes with underscores.
781 * Get the name of a 'Group' (i.e. Room) from its ID.
783 * This is done manually as there does not appear to be a way to enumerate
784 * groups from a SmartApp currently.
786 * GroupIds can be obtained from the SmartThings IDE under 'My Locations'.
788 * See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830
790 private getGroupName(id) {
792 if (id == null) {return 'Home'}
793 else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Kitchen'}
794 else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Lounge'}
795 else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Office'}
796 else {return 'Unknown'}