@@ -68,17 +68,17 @@ preferences {
68
68
input " prefDatabaseUser" , " text" , title : " Username" , required : false
69
69
input " prefDatabasePass" , " text" , title : " Password" , required : false
70
70
}
71
-
71
+
72
72
section(" Polling:" ) {
73
73
input " prefSoftPollingInterval" , " number" , title :" Soft-Polling interval (minutes)" , defaultValue : 10 , required : true
74
74
}
75
-
75
+
76
76
section(" System Monitoring:" ) {
77
77
input " prefLogModeEvents" , " bool" , title :" Log Mode Events?" , defaultValue : true , required : true
78
78
input " prefLogHubProperties" , " bool" , title :" Log Hub Properties?" , defaultValue : true , required : true
79
79
input " prefLogLocationProperties" , " bool" , title :" Log Location Properties?" , defaultValue : true , required : true
80
80
}
81
-
81
+
82
82
section(" Devices To Monitor:" ) {
83
83
input " accelerometers" , " capability.accelerationSensor" , title : " Accelerometers" , multiple : true , required : false
84
84
input " alarms" , " capability.alarm" , title : " Alarms" , multiple : true , required : false
@@ -107,8 +107,8 @@ preferences {
107
107
input " sleepSensors" , " capability.sleepSensor" , title : " Sleep Sensors" , multiple : true , required : false
108
108
input " smokeDetectors" , " capability.smokeDetector" , title : " Smoke Detectors" , multiple : true , required : false
109
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
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
112
input " switchLevels" , " capability.switchLevel" , title : " Switch Levels" , multiple : true , required : false
113
113
input " tamperAlerts" , " capability.tamperAlert" , title : " Tamper Alerts" , multiple : true , required : false
114
114
input " temperatures" , " capability.temperatureMeasurement" , title : " Temperature Sensors" , multiple : true , required : false
@@ -137,7 +137,7 @@ preferences {
137
137
def installed () {
138
138
state. installedAt = now()
139
139
state. loggingLevelIDE = 5
140
- log. debug " ${ app.label} : Installed with settings: ${ settings} "
140
+ log. debug " ${ app.label} : Installed with settings: ${ settings} "
141
141
}
142
142
143
143
/**
@@ -151,11 +151,11 @@ def uninstalled() {
151
151
152
152
/**
153
153
* updated()
154
- *
154
+ *
155
155
* Runs when app settings are changed.
156
- *
156
+ *
157
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
158
+ * Builds state.deviceAttributes which describes the attributes that will be monitored for each device collection
159
159
* (used by manageSubscriptions() and softPoll()).
160
160
* Refreshes scheduling and subscriptions.
161
161
**/
@@ -164,24 +164,24 @@ def updated() {
164
164
165
165
// Update internal state:
166
166
state. loggingLevelIDE = (settings. configLoggingLevelIDE) ? settings. configLoggingLevelIDE. toInteger() : 3
167
-
167
+
168
168
// Database config:
169
169
state. databaseHost = settings. prefDatabaseHost
170
170
state. databasePort = settings. prefDatabasePort
171
171
state. databaseName = settings. prefDatabaseName
172
172
state. databaseUser = settings. prefDatabaseUser
173
- state. databasePass = settings. prefDatabasePass
174
-
173
+ state. databasePass = settings. prefDatabasePass
174
+
175
175
state. path = " /write?db=${ state.databaseName} "
176
- state. headers = [:]
176
+ state. headers = [:]
177
177
state. headers. put(" HOST" , " ${ state.databaseHost} :${ state.databasePort} " )
178
178
state. headers. put(" Content-Type" , " application/x-www-form-urlencoded" )
179
179
if (state. databaseUser && state. databasePass) {
180
180
state. headers. put(" Authorization" , encodeCredentialsBasic(state. databaseUser, state. databasePass))
181
181
}
182
182
183
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
184
+ // Note, the collection names are stored as strings. Adding references to the actual collection
185
185
// objects causes major issues (possibly memory issues?).
186
186
state. deviceAttributes = []
187
187
state. deviceAttributes << [ devices : ' accelerometers' , attributes : [' acceleration' ]]
@@ -211,8 +211,8 @@ def updated() {
211
211
state. deviceAttributes << [ devices : ' sleepSensors' , attributes : [' sleeping' ]]
212
212
state. deviceAttributes << [ devices : ' smokeDetectors' , attributes : [' smoke' ]]
213
213
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' ]]
216
216
state. deviceAttributes << [ devices : ' switchLevels' , attributes : [' level' ]]
217
217
state. deviceAttributes << [ devices : ' tamperAlerts' , attributes : [' tamper' ]]
218
218
state. deviceAttributes << [ devices : ' temperatures' , attributes : [' temperature' ]]
@@ -228,7 +228,7 @@ def updated() {
228
228
// Configure Scheduling:
229
229
state. softPollingInterval = settings. prefSoftPollingInterval. toInteger()
230
230
manageSchedules()
231
-
231
+
232
232
// Configure Subscriptions:
233
233
manageSubscriptions()
234
234
}
@@ -239,18 +239,18 @@ def updated() {
239
239
240
240
/**
241
241
* handleAppTouch(evt)
242
- *
242
+ *
243
243
* Used for testing.
244
244
**/
245
245
def handleAppTouch (evt ) {
246
246
logger(" handleAppTouch()" ," trace" )
247
-
247
+
248
248
softPoll()
249
249
}
250
250
251
251
/**
252
252
* handleModeEvent(evt)
253
- *
253
+ *
254
254
* Log Mode changes.
255
255
**/
256
256
def handleModeEvent (evt ) {
@@ -266,18 +266,24 @@ def handleModeEvent(evt) {
266
266
/**
267
267
* handleEvent(evt)
268
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/
269
+ * parseEvent then post to InfluxDB.
277
270
**/
278
271
def handleEvent (evt ) {
279
272
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 ) {
281
287
// Build data string to send to InfluxDB:
282
288
// Format: <measurement>[,<tag_name>=<tag_value>] field=<field_value>
283
289
// If value is an integer, it must have a trailing "i"
@@ -297,9 +303,9 @@ def handleEvent(evt) {
297
303
def unit = escapeStringForInfluxDB(evt. unit)
298
304
def value = escapeStringForInfluxDB(evt. value)
299
305
def valueBinary = ' '
300
-
306
+
301
307
def data = " ${ measurement} ,deviceId=${ deviceId} ,deviceName=${ deviceName} ,groupId=${ groupId} ,groupName=${ groupName} ,hubId=${ hubId} ,hubName=${ hubName} ,locationId=${ locationId} ,locationName=${ locationName} "
302
-
308
+
303
309
// Unit tag and fields depend on the event type:
304
310
// Most string-valued attributes can be translated to a binary value too.
305
311
if (' acceleration' == evt. name) { // acceleration: Calculate a binary value (active = 1, inactive = 0)
@@ -419,7 +425,7 @@ def handleEvent(evt) {
419
425
else if (' thermostatOperatingState' == evt. name) { // thermostatOperatingState: Calculate a binary value (heating = 1, <any other value> = 0)
420
426
unit = ' thermostatOperatingState'
421
427
value = ' "' + value + ' "'
422
- valueBinary = (' idle' == evt. value) ? ' 0i' : ' 1i'
428
+ valueBinary = (' idle' == evt. value) ? ' 0i' : ' 1i'
423
429
data + = " ,unit=${ unit} value=${ value} ,valueBinary=${ valueBinary} "
424
430
}
425
431
else if (' thermostatSetpointMode' == evt. name) { // thermostatSetpointMode: Calculate a binary value (followSchedule = 0, <any other value> = 1)
@@ -474,18 +480,15 @@ def handleEvent(evt) {
474
480
}
475
481
// Catch any other event with a string value that hasn't been handled:
476
482
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" )
478
484
value = ' "' + value + ' "'
479
485
data + = " ,unit=${ unit} value=${ value} "
480
486
}
481
487
// Catch any other general numerical event (carbonDioxide, power, energy, humidity, level, temperature, ultravioletIndex, voltage, etc).
482
488
else {
483
489
data + = " ,unit=${ unit} value=${ value} "
484
490
}
485
-
486
- // Post data to InfluxDB:
487
- postToInfluxDB(data)
488
-
491
+ return data
489
492
}
490
493
491
494
@@ -497,41 +500,43 @@ def handleEvent(evt) {
497
500
* softPoll()
498
501
*
499
502
* Executed by schedule.
500
- *
503
+ *
501
504
* Forces data to be posted to InfluxDB (even if an event has not been triggered).
502
505
* Doesn't poll devices, just builds a fake event to pass to handleEvent().
503
506
*
504
507
* Also calls LogSystemProperties().
505
508
**/
506
509
def softPoll () {
507
510
logger(" softPoll()" ," trace" )
508
-
511
+
509
512
logSystemProperties()
510
-
513
+
511
514
// Iterate over each attribute for each device, in each device collection in deviceAttributes:
512
515
def devs // temp variable to hold device collection.
516
+ def eventsData = []
513
517
state. deviceAttributes. each { da ->
514
518
devs = settings. " ${ da.devices} "
515
519
if (devs && (da. attributes)) {
516
520
devs. each { d ->
517
521
da. attributes. each { attr ->
518
522
if (d. hasAttribute(attr) && d. latestState(attr)?. value != null ) {
519
523
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,
523
527
value : d. latestState(attr)?. value,
524
528
unit : d. latestState(attr)?. unit,
525
529
device : d,
526
530
deviceId : d. id,
527
531
displayName : d. displayName
528
- ])
532
+ ]))
529
533
}
530
534
}
531
535
}
532
536
}
533
537
}
534
-
538
+ // InfluxDB needs to be a newline delimited string
539
+ postToInfluxDB(eventsData. join(' \n ' ))
535
540
}
536
541
537
542
/**
@@ -571,7 +576,7 @@ def logSystemProperties() {
571
576
def hubIP = ' "' + escapeStringForInfluxDB(h. localIP) + ' "'
572
577
def hubStatus = ' "' + escapeStringForInfluxDB(h. status) + ' "'
573
578
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 "
575
580
def zigbeePowerLevel = h. hub. getDataValue(" zigbeePowerLevel" ) + ' i'
576
581
def zwavePowerLevel = ' "' + escapeStringForInfluxDB(h. hub. getDataValue(" zwavePowerLevel" )) + ' "'
577
582
def firmwareVersion = ' "' + escapeStringForInfluxDB(h. firmwareVersionString) + ' "'
@@ -597,7 +602,7 @@ def logSystemProperties() {
597
602
**/
598
603
def postToInfluxDB (data ) {
599
604
logger(" postToInfluxDB(): Posting data to InfluxDB: Host: ${ state.databaseHost} , Port: ${ state.databasePort} , Database: ${ state.databaseName} , Data: [${ data} ]" ," debug" )
600
-
605
+
601
606
try {
602
607
def hubAction = new physicalgraph.device.HubAction (
603
608
[
@@ -609,15 +614,15 @@ def postToInfluxDB(data) {
609
614
null ,
610
615
[ callback : handleInfluxResponse ]
611
616
)
612
-
617
+
613
618
sendHubCommand(hubAction)
614
619
}
615
620
catch (Exception e) {
616
621
logger(" postToInfluxDB(): Exception ${ e} on ${ hubAction} " ," error" )
617
622
}
618
623
619
624
// 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}"
621
626
// try {
622
627
// httpPost(url, data) { response ->
623
628
// if (response.status != 999 ) {
@@ -626,7 +631,7 @@ def postToInfluxDB(data) {
626
631
// log.debug "Response contentType: ${response.contentType}"
627
632
// }
628
633
// }
629
- // } catch (e) {
634
+ // } catch (e) {
630
635
// logger("postToInfluxDB(): Something went wrong when posting: ${e}","error")
631
636
// }
632
637
}
@@ -649,8 +654,8 @@ def handleInfluxResponse(physicalgraph.device.HubResponse hubResponse) {
649
654
650
655
/**
651
656
* manageSchedules()
652
- *
653
- * Configures/restarts scheduled tasks:
657
+ *
658
+ * Configures/restarts scheduled tasks:
654
659
* softPoll() - Run every {state.softPollingInterval} minutes.
655
660
**/
656
661
private manageSchedules () {
@@ -659,7 +664,7 @@ private manageSchedules() {
659
664
// Generate a random offset (1-60):
660
665
Random rand = new Random (now())
661
666
def randomOffset = 0
662
-
667
+
663
668
// softPoll:
664
669
try {
665
670
unschedule(softPoll)
@@ -673,26 +678,26 @@ private manageSchedules() {
673
678
logger(" manageSchedules(): Scheduling softpoll to run every ${ state.softPollingInterval} minutes (offset of ${ randomOffset} seconds)." ," trace" )
674
679
schedule(" ${ randomOffset} 0/${ state.softPollingInterval} * * * ?" , " softPoll" )
675
680
}
676
-
681
+
677
682
}
678
683
679
684
/**
680
685
* manageSubscriptions()
681
- *
686
+ *
682
687
* Configures subscriptions.
683
688
**/
684
689
private manageSubscriptions () {
685
690
logger(" manageSubscriptions()" ," trace" )
686
691
687
692
// Unsubscribe:
688
693
unsubscribe()
689
-
694
+
690
695
// Subscribe to App Touch events:
691
696
subscribe(app,handleAppTouch)
692
-
697
+
693
698
// Subscribe to mode events:
694
699
if (prefLogModeEvents) subscribe(location, " mode" , handleModeEvent)
695
-
700
+
696
701
// Subscribe to device attributes (iterate over each attribute for each device collection in state.deviceAttributes):
697
702
def devs // dynamic variable holding device collection.
698
703
state. deviceAttributes. each { da ->
@@ -754,9 +759,9 @@ private encodeCredentialsBasic(username, password) {
754
759
* escapeStringForInfluxDB()
755
760
*
756
761
* 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.
760
765
* Commas and spaces will also need to be escaped for measurements, though equals signs = do not.
761
766
*
762
767
* Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/
@@ -770,7 +775,7 @@ private escapeStringForInfluxDB(str) {
770
775
// str = str.replaceAll("'", "_") // Replace apostrophes with underscores.
771
776
}
772
777
else {
773
- str = ' null '
778
+ str = ' 0 '
774
779
}
775
780
return str
776
781
}
@@ -779,10 +784,10 @@ private escapeStringForInfluxDB(str) {
779
784
* getGroupName()
780
785
*
781
786
* Get the name of a 'Group' (i.e. Room) from its ID.
782
- *
787
+ *
783
788
* This is done manually as there does not appear to be a way to enumerate
784
789
* groups from a SmartApp currently.
785
- *
790
+ *
786
791
* GroupIds can be obtained from the SmartThings IDE under 'My Locations'.
787
792
*
788
793
* See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830
@@ -793,5 +798,5 @@ private getGroupName(id) {
793
798
else if (id == ' XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' ) {return ' Kitchen' }
794
799
else if (id == ' XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' ) {return ' Lounge' }
795
800
else if (id == ' XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' ) {return ' Office' }
796
- else {return ' Unknown' }
797
- }
801
+ else {return ' Unknown' }
802
+ }
0 commit comments