e56fbccd75cf908893f18c1addb8d3f9c84081ee
[smartapps.git] / third-party / influxdb-logger.groovy
1 /*****************************************************************************************************************
2  *  Copyright David Lomas (codersaur)
3  *
4  *  Name: InfluxDB Logger
5  *
6  *  Date: 2017-04-03
7  *
8  *  Version: 1.11
9  *
10  *  Source: https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
11  *
12  *  Author: David Lomas (codersaur)
13  *
14  *  Description: A SmartApp to log SmartThings device states to an InfluxDB database.
15  *
16  *  For full information, including installation instructions, exmples, and version history, see:
17  *   https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
18  *
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.
21  *
22  *  License:
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:
25  *
26  *      http://www.apache.org/licenses/LICENSE-2.0
27  *
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  *****************************************************************************************************************/
32 definition(
33     name: "InfluxDB Logger",
34     namespace: "codersaur",
35     author: "David Lomas (codersaur)",
36     description: "Log SmartThings device states to InfluxDB",
37     category: "My Apps",
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")
41
42 preferences {
43
44     section("General:") {
45         //input "prefDebugMode", "bool", title: "Enable debug logging?", defaultValue: true, displayDuringSetup: true
46         input (
47                 name: "configLoggingLevelIDE",
48                 title: "IDE Live Logging Level:\nMessages with this level and higher will be logged to the IDE.",
49                 type: "enum",
50                 options: [
51                     "0" : "None",
52                     "1" : "Error",
53                     "2" : "Warning",
54                     "3" : "Info",
55                     "4" : "Debug",
56                     "5" : "Trace"
57                 ],
58                 defaultValue: "3",
59             displayDuringSetup: true,
60                 required: false
61         )
62     }
63
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
70     }
71     
72     section("Polling:") {
73         input "prefSoftPollingInterval", "number", title:"Soft-Polling interval (minutes)", defaultValue: 10, required: true
74     }
75     
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
80     }
81     
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//1
90         input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
91         input "consumables", "capability.consumable", title: "Consumables", multiple: true, required: false//2
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//3
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//4
106         input "signalStrengthMeters", "capability.signalStrength", title: "Signal Strength Meters", multiple: true, required: false//5
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//6
110         input "spls", "capability.soundPressureLevel", title: "Sound Pressure Level Sensors", multiple: true, required: false//7
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//8
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//9
119         input "valves", "capability.valve", title: "Valves", multiple: true, required: false
120         input "volts", "capability.voltageMeasurement", title: "Voltage Meters", multiple: true, required: false//10
121         input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
122         input "windowShades", "capability.windowShade", title: "Window Shades", multiple: true, required: false//11
123     }
124
125 }
126
127
128 /*****************************************************************************************************************
129  *  SmartThings System Commands:
130  *****************************************************************************************************************/
131
132 /**
133  *  installed()
134  *
135  *  Runs when the app is first installed.
136  **/
137 def installed() {
138     state.installedAt = now()
139     state.loggingLevelIDE = 5
140     log.debug "${app.label}: Installed with settings: ${settings}" 
141 }
142
143 /**
144  *  uninstalled()
145  *
146  *  Runs when the app is uninstalled.
147  **/
148 def uninstalled() {
149     logger("uninstalled()","trace")
150 }
151
152 /**
153  *  updated()
154  * 
155  *  Runs when app settings are changed.
156  * 
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.
161  **/
162 def updated() {
163     logger("updated()","trace")
164
165     // Update internal state:
166     state.loggingLevelIDE = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3
167     
168     // Database config:
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 
174     
175     state.path = "/write?db=${state.databaseName}"
176     state.headers = [:] 
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))
181     }
182
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']]
227
228     // Configure Scheduling:
229     state.softPollingInterval = settings.prefSoftPollingInterval.toInteger()
230     manageSchedules()
231     
232     // Configure Subscriptions:
233     manageSubscriptions()
234 }
235
236 /*****************************************************************************************************************
237  *  Event Handlers:
238  *****************************************************************************************************************/
239
240 /**
241  *  handleAppTouch(evt)
242  * 
243  *  Used for testing.
244  **/
245 def handleAppTouch(evt) {
246     logger("handleAppTouch()","trace")
247     
248     softPoll()
249 }
250
251 /**
252  *  handleModeEvent(evt)
253  * 
254  *  Log Mode changes.
255  **/
256 def handleModeEvent(evt) {
257     logger("handleModeEvent(): Mode changed to: ${evt.value}","info")
258
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}"
263     postToInfluxDB(data)
264 }
265
266 /**
267  *  handleEvent(evt)
268  *
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)
273  * 
274  *  Useful references: 
275  *   - http://docs.smartthings.com/en/latest/capabilities-reference.html
276  *   - https://docs.influxdata.com/influxdb/v0.10/guides/writing_data/
277  **/
278 def handleEvent(evt) {
279     logger("handleEvent(): $evt.displayName($evt.name:$evt.unit) $evt.value","info")
280     
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
286     // tags:
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)
296
297     def unit = escapeStringForInfluxDB(evt.unit)
298     def value = escapeStringForInfluxDB(evt.value)
299     def valueBinary = ''
300     
301     def data = "${measurement},deviceId=${deviceId},deviceName=${deviceName},groupId=${groupId},groupName=${groupName},hubId=${hubId},hubName=${hubName},locationId=${locationId},locationName=${locationName}"
302     
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}"
310     }
311     else if ('alarm' == evt.name) { // alarm: Calculate a binary value (strobe/siren/both = 1, off = 0)
312         unit = 'alarm'
313         value = '"' + value + '"'
314         valueBinary = ('off' == evt.value) ? '0i' : '1i'
315         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
316     }
317     else if ('button' == evt.name) { // button: Calculate a binary value (held = 1, pushed = 0)
318         unit = 'button'
319         value = '"' + value + '"'
320         valueBinary = ('pushed' == evt.value) ? '0i' : '1i'
321         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
322     }
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}"
328     }
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}"
334     }
335     else if ('contact' == evt.name) { // contact: Calculate a binary value (closed = 1, open = 0)
336         unit = 'contact'
337         value = '"' + value + '"'
338         valueBinary = ('closed' == evt.value) ? '1i' : '0i'
339         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
340     }
341     else if ('door' == evt.name) { // door: Calculate a binary value (closed = 1, open/opening/closing/unknown = 0)
342         unit = 'door'
343         value = '"' + value + '"'
344         valueBinary = ('closed' == evt.value) ? '1i' : '0i'
345         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
346     }
347     else if ('lock' == evt.name) { // door: Calculate a binary value (locked = 1, unlocked = 0)
348         unit = 'lock'
349         value = '"' + value + '"'
350         valueBinary = ('locked' == evt.value) ? '1i' : '0i'
351         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
352     }
353     else if ('motion' == evt.name) { // Motion: Calculate a binary value (active = 1, inactive = 0)
354         unit = 'motion'
355         value = '"' + value + '"'
356         valueBinary = ('active' == evt.value) ? '1i' : '0i'
357         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
358     }
359     else if ('mute' == evt.name) { // mute: Calculate a binary value (muted = 1, unmuted = 0)
360         unit = 'mute'
361         value = '"' + value + '"'
362         valueBinary = ('muted' == evt.value) ? '1i' : '0i'
363         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
364     }
365     else if ('presence' == evt.name) { // presence: Calculate a binary value (present = 1, not present = 0)
366         unit = 'presence'
367         value = '"' + value + '"'
368         valueBinary = ('present' == evt.value) ? '1i' : '0i'
369         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
370     }
371     else if ('shock' == evt.name) { // shock: Calculate a binary value (detected = 1, clear = 0)
372         unit = 'shock'
373         value = '"' + value + '"'
374         valueBinary = ('detected' == evt.value) ? '1i' : '0i'
375         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
376     }
377     else if ('sleeping' == evt.name) { // sleeping: Calculate a binary value (sleeping = 1, not sleeping = 0)
378         unit = 'sleeping'
379         value = '"' + value + '"'
380         valueBinary = ('sleeping' == evt.value) ? '1i' : '0i'
381         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
382     }
383     else if ('smoke' == evt.name) { // smoke: Calculate a binary value (detected = 1, clear/tested = 0)
384         unit = 'smoke'
385         value = '"' + value + '"'
386         valueBinary = ('detected' == evt.value) ? '1i' : '0i'
387         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
388     }
389     else if ('sound' == evt.name) { // sound: Calculate a binary value (detected = 1, not detected = 0)
390         unit = 'sound'
391         value = '"' + value + '"'
392         valueBinary = ('detected' == evt.value) ? '1i' : '0i'
393         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
394     }
395     else if ('switch' == evt.name) { // switch: Calculate a binary value (on = 1, off = 0)
396         unit = 'switch'
397         value = '"' + value + '"'
398         valueBinary = ('on' == evt.value) ? '1i' : '0i'
399         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
400     }
401     else if ('tamper' == evt.name) { // tamper: Calculate a binary value (detected = 1, clear = 0)
402         unit = 'tamper'
403         value = '"' + value + '"'
404         valueBinary = ('detected' == evt.value) ? '1i' : '0i'
405         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
406     }
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}"
412     }
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}"
418     }
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}"
424     }
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}"
430     }
431     else if ('threeAxis' == evt.name) { // threeAxis: Format to x,y,z values.
432         unit = 'threeAxis'
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.
438     }
439     else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, "" = 0)
440         unit = 'touch'
441         value = '"' + value + '"'
442         valueBinary = ('touched' == evt.value) ? '1i' : '0i'
443         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
444     }
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}"
450     }
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}"
456     }
457     else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, <any other value> = 0)
458         unit = 'touch'
459         value = '"' + value + '"'
460         valueBinary = ('touched' == evt.value) ? '1i' : '0i'
461         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
462     }
463     else if ('water' == evt.name) { // water: Calculate a binary value (wet = 1, dry = 0)
464         unit = 'water'
465         value = '"' + value + '"'
466         valueBinary = ('wet' == evt.value) ? '1i' : '0i'
467         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
468     }
469     else if ('windowShade' == evt.name) { // windowShade: Calculate a binary value (closed = 1, <any other value> = 0)
470         unit = 'windowShade'
471         value = '"' + value + '"'
472         valueBinary = ('closed' == evt.value) ? '1i' : '0i'
473         data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
474     }
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}"
480     }
481     // Catch any other general numerical event (carbonDioxide, power, energy, humidity, level, temperature, ultravioletIndex, voltage, etc).
482     else {
483         data += ",unit=${unit} value=${value}"
484     }
485     
486     // Post data to InfluxDB:
487     postToInfluxDB(data)
488
489 }
490
491
492 /*****************************************************************************************************************
493  *  Main Commands:
494  *****************************************************************************************************************/
495
496 /**
497  *  softPoll()
498  *
499  *  Executed by schedule.
500  * 
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().
503  *
504  *  Also calls LogSystemProperties().
505  **/
506 def softPoll() {
507     logger("softPoll()","trace")
508     
509     logSystemProperties()
510     
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)) {
516             devs.each { d ->
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():
521                         handleEvent([
522                             name: attr, 
523                             value: d.latestState(attr)?.value,
524                             unit: d.latestState(attr)?.unit,
525                             device: d,
526                             deviceId: d.id,
527                             displayName: d.displayName
528                         ])
529                     }
530                 }
531             }
532         }
533     }
534
535 }
536
537 /**
538  *  logSystemProperties()
539  *
540  *  Generates measurements for SmartThings system (hubs and locations) properties.
541  **/
542 def logSystemProperties() {
543     logger("logSystemProperties()","trace")
544
545     def locationId = '"' + escapeStringForInfluxDB(location.id) + '"'
546     def locationName = '"' + escapeStringForInfluxDB(location.name) + '"'
547
548         // Location Properties:
549     if (prefLogLocationProperties) {
550         try {
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) + '"'
557
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}"
559             postToInfluxDB(data)
560         } catch (e) {
561                     logger("logSystemProperties(): Unable to log Location properties: ${e}","error")
562         }
563         }
564
565         // Hub Properties:
566     if (prefLogHubProperties) {
567         location.hubs.each { h ->
568                 try {
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) + '"'
578
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}"
581                 postToInfluxDB(data)
582             } catch (e) {
583                                 logger("logSystemProperties(): Unable to log Hub properties: ${e}","error")
584                 }
585         }
586
587         }
588
589 }
590
591 /**
592  *  postToInfluxDB()
593  *
594  *  Posts data to InfluxDB.
595  *
596  *  Uses hubAction instead of httpPost() in case InfluxDB server is on the same LAN as the Smartthings Hub.
597  **/
598 def postToInfluxDB(data) {
599     logger("postToInfluxDB(): Posting data to InfluxDB: Host: ${state.databaseHost}, Port: ${state.databasePort}, Database: ${state.databaseName}, Data: [${data}]","debug")
600     
601    /* try {
602         def hubAction = new physicalgraph.device.HubAction(
603                 [
604                 method: "POST",
605                 path: state.path,
606                 body: data,
607                 headers: state.headers
608             ],
609             null,
610             [ callback: handleInfluxResponse ]
611         )
612                 
613         sendHubCommand(hubAction)
614     }
615     catch (Exception e) {
616                 logger("postToInfluxDB(): Exception ${e} on ${hubAction}","error")
617     }*/
618
619     // For reference, code that could be used for WAN hosts:
620     // def url = "http://${state.databaseHost}:${state.databasePort}/write?db=${state.databaseName}" 
621     //    try {
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}"
627     //            }
628     //      }
629     //  } catch (e) {   
630     //      logger("postToInfluxDB(): Something went wrong when posting: ${e}","error")
631     //  }
632 }
633
634 /**
635  *  handleInfluxResponse()
636  *
637  *  Handles response from post made in postToInfluxDB().
638  **/
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")
642     }
643 }*/
644
645
646 /*****************************************************************************************************************
647  *  Private Helper Functions:
648  *****************************************************************************************************************/
649
650 /**
651  *  manageSchedules()
652  * 
653  *  Configures/restarts scheduled tasks: 
654  *   softPoll() - Run every {state.softPollingInterval} minutes.
655  **/
656 private manageSchedules() {
657         logger("manageSchedules()","trace")
658
659     // Generate a random offset (1-60):
660     Random rand = new Random(now())
661     def randomOffset = 0
662     
663     // softPoll:
664     try {
665         unschedule(softPoll)
666     }
667     catch(e) {
668         // logger("manageSchedules(): Unschedule failed!","error")
669     }
670
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")
675     }
676     
677 }
678
679 /**
680  *  manageSubscriptions()
681  * 
682  *  Configures subscriptions.
683  **/
684 private manageSubscriptions() {
685         logger("manageSubscriptions()","trace")
686
687     // Unsubscribe:
688     unsubscribe()
689     
690     // Subscribe to App Touch events:
691     subscribe(app,handleAppTouch)
692     
693     // Subscribe to mode events:
694     if (prefLogModeEvents) subscribe(location, "mode", handleModeEvent)
695     
696     subscribe(accelerometers, "acceleration", handleEvent)
697     subscribe(alarms, "alarms", handleEvent)
698     subscribe(batteries, "batteries", handleEvent)
699     subscribe(beacons, "beacons", handleEvent)
700     subscribe(buttons, "buttons", handleEvent)
701     subscribe(cos, "carbonMonoxide", handleEvent)
702     subscribe(cos, "carbonDioxide", handleEvent)
703     subscribe(colors, "hue", handleEvent)
704     subscribe(colors, "saturation", handleEvent)
705     subscribe(colors, "color", handleEvent)
706     subscribe(consumables, "consumableStatus", handleEvent)
707     subscribe(contacts, "contact", handleEvent)
708     subscribe(doorsControllers, "door", handleEvent)
709     subscribe(energyMeters, "energy", handleEvent)
710     subscribe(humidities, "humidity", handleEvent)
711     subscribe(illuminances, "illuminance", handleEvent)
712     subscribe(locks, "lock", handleEvent)
713     subscribe(motions, "motion", handleEvent)
714     subscribe(musicPlayers, "status", handleEvent)
715     subscribe(musicPlayers, "level", handleEvent)
716     subscribe(musicPlayers, "trackDescription", handleEvent)
717     subscribe(musicPlayers, "trackData", handleEvent)
718     subscribe(musicPlayers, "mute", handleEvent)
719     subscribe(peds, "steps", handleEvent)
720     subscribe(peds, "goal", handleEvent)
721     subscribe(phMeters, "pH", handleEvent)
722     subscribe(powerMeters, "power", handleEvent)
723     subscribe(powerMeters, "voltage", handleEvent)
724     subscribe(powerMeters, "current", handleEvent)
725     subscribe(powerMeters, "powerFactor", handleEvent)
726     subscribe(presences, "presence", handleEvent)
727     subscribe(pressures, "pressure", handleEvent)
728     subscribe(shockSensors, "shock", handleEvent)
729     subscribe(signalStrengthMeters, "lqi", handleEvent)
730     subscribe(signalStrengthMeters, "rssi", handleEvent)
731     subscribe(sleepSensors, "sleeping", handleEvent)
732     subscribe(smokeDetectors, "smoke", handleEvent)
733     subscribe(soundSensors, "sound", handleEvent)
734     subscribe(spls, "soundPressureLevel", handleEvent)
735     subscribe(switches, "switch", handleEvent)
736     subscribe(switchLevels, "level", handleEvent)
737     subscribe(tamperAlerts, "tamper", handleEvent)
738     subscribe(temperatures, "temperature", handleEvent)
739     subscribe(thermostats, "temperature", handleEvent)
740     subscribe(thermostats, "heatingSetpoint", handleEvent)
741     subscribe(thermostats, "coolingSetpoint", handleEvent)
742     subscribe(thermostats, "thermostatSetpoint", handleEvent)
743     subscribe(thermostats, "thermostatMode", handleEvent)
744     subscribe(thermostats, "thermostatFanMode", handleEvent)
745     subscribe(thermostats, "thermostatOperatingState", handleEvent)
746     subscribe(thermostats, "thermostatSetpointMode", handleEvent)
747     subscribe(thermostats, "scheduledSetpoint", handleEvent)
748     subscribe(thermostats, "optimisation", handleEvent)
749     subscribe(thermostats, "windowFunction", handleEvent)
750     subscribe(threeAxis, "threeAxis", handleEvent)
751     subscribe(touchs, "touch", handleEvent)
752     subscribe(uvs, "ultravioletIndex", handleEvent)
753     subscribe(valves, "contact", handleEvent)
754     subscribe(volts, "voltage", handleEvent)
755     subscribe(waterSensors, "water", handleEvent)
756     subscribe(windowShades, "windowShade", handleEvent)
757 }
758
759 /**
760  *  logger()
761  *
762  *  Wrapper function for all logging.
763  **/
764 private logger(msg, level = "debug") {
765
766     switch(level) {
767         case "error":
768             if (state.loggingLevelIDE >= 1) log.error msg
769             break
770
771         case "warn":
772             if (state.loggingLevelIDE >= 2) log.warn msg
773             break
774
775         case "info":
776             if (state.loggingLevelIDE >= 3) log.info msg
777             break
778
779         case "debug":
780             if (state.loggingLevelIDE >= 4) log.debug msg
781             break
782
783         case "trace":
784             if (state.loggingLevelIDE >= 5) log.trace msg
785             break
786
787         default:
788             log.debug msg
789             break
790     }
791 }
792
793 /**
794  *  encodeCredentialsBasic()
795  *
796  *  Encode credentials for HTTP Basic authentication.
797  **/
798 private encodeCredentialsBasic(username, password) {
799     return "Basic " + "${username}:${password}".encodeAsBase64().toString()
800 }
801
802 /**
803  *  escapeStringForInfluxDB()
804  *
805  *  Escape values to InfluxDB.
806  *  
807  *  If a tag key, tag value, or field key contains a space, comma, or an equals sign = it must 
808  *  be escaped using the backslash character \. Backslash characters do not need to be escaped. 
809  *  Commas and spaces will also need to be escaped for measurements, though equals signs = do not.
810  *
811  *  Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/
812  **/
813 private escapeStringForInfluxDB(str) {
814     if (str) {
815         str = str.replaceAll(" ", "\\\\ ") // Escape spaces.
816         str = str.replaceAll(",", "\\\\,") // Escape commas.
817         str = str.replaceAll("=", "\\\\=") // Escape equal signs.
818         str = str.replaceAll("\"", "\\\\\"") // Escape double quotes.
819         //str = str.replaceAll("'", "_")  // Replace apostrophes with underscores.
820     }
821     else {
822         str = 'null'
823     }
824     return str
825 }
826
827 /**
828  *  getGroupName()
829  *
830  *  Get the name of a 'Group' (i.e. Room) from its ID.
831  *  
832  *  This is done manually as there does not appear to be a way to enumerate
833  *  groups from a SmartApp currently.
834  * 
835  *  GroupIds can be obtained from the SmartThings IDE under 'My Locations'.
836  *
837  *  See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830
838  **/
839 private getGroupName(id) {
840
841     if (id == null) {return 'Home'}
842     else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Kitchen'}
843     else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Lounge'}
844     else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Office'}
845     else {return 'Unknown'}    
846 }