Skip to content

Commit 5fb16c2

Browse files
committed
merged pull request 27
influxdb pooling: add ability to pool db writes codersaur#27
1 parent d6a200f commit 5fb16c2

File tree

1 file changed

+71
-66
lines changed

1 file changed

+71
-66
lines changed

smartapps/influxdb-logger/influxdb-logger.groovy

+71-66
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,17 @@ preferences {
6868
input "prefDatabaseUser", "text", title: "Username", required: false
6969
input "prefDatabasePass", "text", title: "Password", required: false
7070
}
71-
71+
7272
section("Polling:") {
7373
input "prefSoftPollingInterval", "number", title:"Soft-Polling interval (minutes)", defaultValue: 10, required: true
7474
}
75-
75+
7676
section("System Monitoring:") {
7777
input "prefLogModeEvents", "bool", title:"Log Mode Events?", defaultValue: true, required: true
7878
input "prefLogHubProperties", "bool", title:"Log Hub Properties?", defaultValue: true, required: true
7979
input "prefLogLocationProperties", "bool", title:"Log Location Properties?", defaultValue: true, required: true
8080
}
81-
81+
8282
section("Devices To Monitor:") {
8383
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
8484
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
@@ -107,8 +107,8 @@ preferences {
107107
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
108108
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
109109
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
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
112112
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
113113
input "tamperAlerts", "capability.tamperAlert", title: "Tamper Alerts", multiple: true, required: false
114114
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
@@ -137,7 +137,7 @@ preferences {
137137
def installed() {
138138
state.installedAt = now()
139139
state.loggingLevelIDE = 5
140-
log.debug "${app.label}: Installed with settings: ${settings}"
140+
log.debug "${app.label}: Installed with settings: ${settings}"
141141
}
142142

143143
/**
@@ -151,11 +151,11 @@ def uninstalled() {
151151

152152
/**
153153
* updated()
154-
*
154+
*
155155
* Runs when app settings are changed.
156-
*
156+
*
157157
* 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
158+
* Builds state.deviceAttributes which describes the attributes that will be monitored for each device collection
159159
* (used by manageSubscriptions() and softPoll()).
160160
* Refreshes scheduling and subscriptions.
161161
**/
@@ -164,24 +164,24 @@ def updated() {
164164

165165
// Update internal state:
166166
state.loggingLevelIDE = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3
167-
167+
168168
// Database config:
169169
state.databaseHost = settings.prefDatabaseHost
170170
state.databasePort = settings.prefDatabasePort
171171
state.databaseName = settings.prefDatabaseName
172172
state.databaseUser = settings.prefDatabaseUser
173-
state.databasePass = settings.prefDatabasePass
174-
173+
state.databasePass = settings.prefDatabasePass
174+
175175
state.path = "/write?db=${state.databaseName}"
176-
state.headers = [:]
176+
state.headers = [:]
177177
state.headers.put("HOST", "${state.databaseHost}:${state.databasePort}")
178178
state.headers.put("Content-Type", "application/x-www-form-urlencoded")
179179
if (state.databaseUser && state.databasePass) {
180180
state.headers.put("Authorization", encodeCredentialsBasic(state.databaseUser, state.databasePass))
181181
}
182182

183183
// 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
184+
// Note, the collection names are stored as strings. Adding references to the actual collection
185185
// objects causes major issues (possibly memory issues?).
186186
state.deviceAttributes = []
187187
state.deviceAttributes << [ devices: 'accelerometers', attributes: ['acceleration']]
@@ -211,8 +211,8 @@ def updated() {
211211
state.deviceAttributes << [ devices: 'sleepSensors', attributes: ['sleeping']]
212212
state.deviceAttributes << [ devices: 'smokeDetectors', attributes: ['smoke']]
213213
state.deviceAttributes << [ devices: 'soundSensors', attributes: ['sound']]
214-
state.deviceAttributes << [ devices: 'spls', attributes: ['soundPressureLevel']]
215-
state.deviceAttributes << [ devices: 'switches', attributes: ['switch']]
214+
state.deviceAttributes << [ devices: 'spls', attributes: ['soundPressureLevel']]
215+
state.deviceAttributes << [ devices: 'switches', attributes: ['switch']]
216216
state.deviceAttributes << [ devices: 'switchLevels', attributes: ['level']]
217217
state.deviceAttributes << [ devices: 'tamperAlerts', attributes: ['tamper']]
218218
state.deviceAttributes << [ devices: 'temperatures', attributes: ['temperature']]
@@ -228,7 +228,7 @@ def updated() {
228228
// Configure Scheduling:
229229
state.softPollingInterval = settings.prefSoftPollingInterval.toInteger()
230230
manageSchedules()
231-
231+
232232
// Configure Subscriptions:
233233
manageSubscriptions()
234234
}
@@ -239,18 +239,18 @@ def updated() {
239239

240240
/**
241241
* handleAppTouch(evt)
242-
*
242+
*
243243
* Used for testing.
244244
**/
245245
def handleAppTouch(evt) {
246246
logger("handleAppTouch()","trace")
247-
247+
248248
softPoll()
249249
}
250250

251251
/**
252252
* handleModeEvent(evt)
253-
*
253+
*
254254
* Log Mode changes.
255255
**/
256256
def handleModeEvent(evt) {
@@ -266,18 +266,24 @@ def handleModeEvent(evt) {
266266
/**
267267
* handleEvent(evt)
268268
*
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/
269+
* parseEvent then post to InfluxDB.
277270
**/
278271
def handleEvent(evt) {
279272
logger("handleEvent(): $evt.displayName($evt.name:$evt.unit) $evt.value","info")
280-
273+
data = parseEvent(evt)
274+
postToInfluxDB(data)
275+
}
276+
277+
/**
278+
* parseEvent(evt)
279+
*
280+
* Parses event data to send to InfluxDB.
281+
* - Escapes and quotes string values.
282+
* - Calculates logical binary values where string values can be
283+
* represented as binary values (e.g. contact: closed = 1, open = 0)
284+
*
285+
**/
286+
def parseEvent(evt) {
281287
// Build data string to send to InfluxDB:
282288
// Format: <measurement>[,<tag_name>=<tag_value>] field=<field_value>
283289
// If value is an integer, it must have a trailing "i"
@@ -297,9 +303,9 @@ def handleEvent(evt) {
297303
def unit = escapeStringForInfluxDB(evt.unit)
298304
def value = escapeStringForInfluxDB(evt.value)
299305
def valueBinary = ''
300-
306+
301307
def data = "${measurement},deviceId=${deviceId},deviceName=${deviceName},groupId=${groupId},groupName=${groupName},hubId=${hubId},hubName=${hubName},locationId=${locationId},locationName=${locationName}"
302-
308+
303309
// Unit tag and fields depend on the event type:
304310
// Most string-valued attributes can be translated to a binary value too.
305311
if ('acceleration' == evt.name) { // acceleration: Calculate a binary value (active = 1, inactive = 0)
@@ -419,7 +425,7 @@ def handleEvent(evt) {
419425
else if ('thermostatOperatingState' == evt.name) { // thermostatOperatingState: Calculate a binary value (heating = 1, <any other value> = 0)
420426
unit = 'thermostatOperatingState'
421427
value = '"' + value + '"'
422-
valueBinary = ('idle' == evt.value) ? '0i' : '1i'
428+
valueBinary = ('idle' == evt.value) ? '0i' : '1i'
423429
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
424430
}
425431
else if ('thermostatSetpointMode' == evt.name) { // thermostatSetpointMode: Calculate a binary value (followSchedule = 0, <any other value> = 1)
@@ -474,18 +480,15 @@ def handleEvent(evt) {
474480
}
475481
// Catch any other event with a string value that hasn't been handled:
476482
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")
483+
logger("parseEvent(): Found a string value that's not explicitly handled: Device Name: ${deviceName}, Event Name: ${evt.name}, Value:${evt.value}","warn")
478484
value = '"' + value + '"'
479485
data += ",unit=${unit} value=${value}"
480486
}
481487
// Catch any other general numerical event (carbonDioxide, power, energy, humidity, level, temperature, ultravioletIndex, voltage, etc).
482488
else {
483489
data += ",unit=${unit} value=${value}"
484490
}
485-
486-
// Post data to InfluxDB:
487-
postToInfluxDB(data)
488-
491+
return data
489492
}
490493

491494

@@ -497,41 +500,43 @@ def handleEvent(evt) {
497500
* softPoll()
498501
*
499502
* Executed by schedule.
500-
*
503+
*
501504
* Forces data to be posted to InfluxDB (even if an event has not been triggered).
502505
* Doesn't poll devices, just builds a fake event to pass to handleEvent().
503506
*
504507
* Also calls LogSystemProperties().
505508
**/
506509
def softPoll() {
507510
logger("softPoll()","trace")
508-
511+
509512
logSystemProperties()
510-
513+
511514
// Iterate over each attribute for each device, in each device collection in deviceAttributes:
512515
def devs // temp variable to hold device collection.
516+
def eventsData = []
513517
state.deviceAttributes.each { da ->
514518
devs = settings."${da.devices}"
515519
if (devs && (da.attributes)) {
516520
devs.each { d ->
517521
da.attributes.each { attr ->
518522
if (d.hasAttribute(attr) && d.latestState(attr)?.value != null) {
519523
logger("softPoll(): Softpolling device ${d} for attribute: ${attr}","info")
520-
// Send fake event to handleEvent():
521-
handleEvent([
522-
name: attr,
524+
// Event list to sent to InfluxDB
525+
eventsData.add(parseEvent([
526+
name: attr,
523527
value: d.latestState(attr)?.value,
524528
unit: d.latestState(attr)?.unit,
525529
device: d,
526530
deviceId: d.id,
527531
displayName: d.displayName
528-
])
532+
]))
529533
}
530534
}
531535
}
532536
}
533537
}
534-
538+
// InfluxDB needs to be a newline delimited string
539+
postToInfluxDB(eventsData.join('\n'))
535540
}
536541

537542
/**
@@ -571,7 +576,7 @@ def logSystemProperties() {
571576
def hubIP = '"' + escapeStringForInfluxDB(h.localIP) + '"'
572577
def hubStatus = '"' + escapeStringForInfluxDB(h.status) + '"'
573578
def batteryInUse = ("false" == h.hub.getDataValue("batteryInUse")) ? "0i" : "1i"
574-
def hubUptime = h.hub.getDataValue("uptime") + 'i'
579+
def hubUptime = ("null" == h.hub.getDataValue("uptime")) ? (h.hub.getDataValue("uptime") + "i") : "0i"
575580
def zigbeePowerLevel = h.hub.getDataValue("zigbeePowerLevel") + 'i'
576581
def zwavePowerLevel = '"' + escapeStringForInfluxDB(h.hub.getDataValue("zwavePowerLevel")) + '"'
577582
def firmwareVersion = '"' + escapeStringForInfluxDB(h.firmwareVersionString) + '"'
@@ -597,7 +602,7 @@ def logSystemProperties() {
597602
**/
598603
def postToInfluxDB(data) {
599604
logger("postToInfluxDB(): Posting data to InfluxDB: Host: ${state.databaseHost}, Port: ${state.databasePort}, Database: ${state.databaseName}, Data: [${data}]","debug")
600-
605+
601606
try {
602607
def hubAction = new physicalgraph.device.HubAction(
603608
[
@@ -609,15 +614,15 @@ def postToInfluxDB(data) {
609614
null,
610615
[ callback: handleInfluxResponse ]
611616
)
612-
617+
613618
sendHubCommand(hubAction)
614619
}
615620
catch (Exception e) {
616621
logger("postToInfluxDB(): Exception ${e} on ${hubAction}","error")
617622
}
618623

619624
// For reference, code that could be used for WAN hosts:
620-
// def url = "http://${state.databaseHost}:${state.databasePort}/write?db=${state.databaseName}"
625+
// def url = "http://${state.databaseHost}:${state.databasePort}/write?db=${state.databaseName}"
621626
// try {
622627
// httpPost(url, data) { response ->
623628
// if (response.status != 999 ) {
@@ -626,7 +631,7 @@ def postToInfluxDB(data) {
626631
// log.debug "Response contentType: ${response.contentType}"
627632
// }
628633
// }
629-
// } catch (e) {
634+
// } catch (e) {
630635
// logger("postToInfluxDB(): Something went wrong when posting: ${e}","error")
631636
// }
632637
}
@@ -649,8 +654,8 @@ def handleInfluxResponse(physicalgraph.device.HubResponse hubResponse) {
649654

650655
/**
651656
* manageSchedules()
652-
*
653-
* Configures/restarts scheduled tasks:
657+
*
658+
* Configures/restarts scheduled tasks:
654659
* softPoll() - Run every {state.softPollingInterval} minutes.
655660
**/
656661
private manageSchedules() {
@@ -659,7 +664,7 @@ private manageSchedules() {
659664
// Generate a random offset (1-60):
660665
Random rand = new Random(now())
661666
def randomOffset = 0
662-
667+
663668
// softPoll:
664669
try {
665670
unschedule(softPoll)
@@ -673,26 +678,26 @@ private manageSchedules() {
673678
logger("manageSchedules(): Scheduling softpoll to run every ${state.softPollingInterval} minutes (offset of ${randomOffset} seconds).","trace")
674679
schedule("${randomOffset} 0/${state.softPollingInterval} * * * ?", "softPoll")
675680
}
676-
681+
677682
}
678683

679684
/**
680685
* manageSubscriptions()
681-
*
686+
*
682687
* Configures subscriptions.
683688
**/
684689
private manageSubscriptions() {
685690
logger("manageSubscriptions()","trace")
686691

687692
// Unsubscribe:
688693
unsubscribe()
689-
694+
690695
// Subscribe to App Touch events:
691696
subscribe(app,handleAppTouch)
692-
697+
693698
// Subscribe to mode events:
694699
if (prefLogModeEvents) subscribe(location, "mode", handleModeEvent)
695-
700+
696701
// Subscribe to device attributes (iterate over each attribute for each device collection in state.deviceAttributes):
697702
def devs // dynamic variable holding device collection.
698703
state.deviceAttributes.each { da ->
@@ -754,9 +759,9 @@ private encodeCredentialsBasic(username, password) {
754759
* escapeStringForInfluxDB()
755760
*
756761
* 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.
762+
*
763+
* If a tag key, tag value, or field key contains a space, comma, or an equals sign = it must
764+
* be escaped using the backslash character \. Backslash characters do not need to be escaped.
760765
* Commas and spaces will also need to be escaped for measurements, though equals signs = do not.
761766
*
762767
* Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/
@@ -770,7 +775,7 @@ private escapeStringForInfluxDB(str) {
770775
//str = str.replaceAll("'", "_") // Replace apostrophes with underscores.
771776
}
772777
else {
773-
str = 'null'
778+
str = '0'
774779
}
775780
return str
776781
}
@@ -779,10 +784,10 @@ private escapeStringForInfluxDB(str) {
779784
* getGroupName()
780785
*
781786
* Get the name of a 'Group' (i.e. Room) from its ID.
782-
*
787+
*
783788
* This is done manually as there does not appear to be a way to enumerate
784789
* groups from a SmartApp currently.
785-
*
790+
*
786791
* GroupIds can be obtained from the SmartThings IDE under 'My Locations'.
787792
*
788793
* See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830
@@ -793,5 +798,5 @@ private getGroupName(id) {
793798
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Kitchen'}
794799
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Lounge'}
795800
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Office'}
796-
else {return 'Unknown'}
797-
}
801+
else {return 'Unknown'}
802+
}

0 commit comments

Comments
 (0)