Update turn-on-by-zip-code.groovy
[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
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
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 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)
705             }
706         }
707     }
708 }
709
710 /**
711  *  logger()
712  *
713  *  Wrapper function for all logging.
714  **/
715 private logger(msg, level = "debug") {
716
717     switch(level) {
718         case "error":
719             if (state.loggingLevelIDE >= 1) log.error msg
720             break
721
722         case "warn":
723             if (state.loggingLevelIDE >= 2) log.warn msg
724             break
725
726         case "info":
727             if (state.loggingLevelIDE >= 3) log.info msg
728             break
729
730         case "debug":
731             if (state.loggingLevelIDE >= 4) log.debug msg
732             break
733
734         case "trace":
735             if (state.loggingLevelIDE >= 5) log.trace msg
736             break
737
738         default:
739             log.debug msg
740             break
741     }
742 }
743
744 /**
745  *  encodeCredentialsBasic()
746  *
747  *  Encode credentials for HTTP Basic authentication.
748  **/
749 private encodeCredentialsBasic(username, password) {
750     return "Basic " + "${username}:${password}".encodeAsBase64().toString()
751 }
752
753 /**
754  *  escapeStringForInfluxDB()
755  *
756  *  Escape values to InfluxDB.
757  *  
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.
761  *
762  *  Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/
763  **/
764 private escapeStringForInfluxDB(str) {
765     if (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.
771     }
772     else {
773         str = 'null'
774     }
775     return str
776 }
777
778 /**
779  *  getGroupName()
780  *
781  *  Get the name of a 'Group' (i.e. Room) from its ID.
782  *  
783  *  This is done manually as there does not appear to be a way to enumerate
784  *  groups from a SmartApp currently.
785  * 
786  *  GroupIds can be obtained from the SmartThings IDE under 'My Locations'.
787  *
788  *  See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830
789  **/
790 private getGroupName(id) {
791
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'}    
797 }