-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathha7net-parent.groovy
319 lines (250 loc) · 11.3 KB
/
ha7net-parent.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def version() {'v0.2.0'}
import groovy.xml.*
metadata {
definition (name: 'HA7Net 1-Wire - Parent',
namespace: 'ckamps',
author: 'Christopher Kampmeier',
importUrl: 'https://raw.githubusercontent.com/ckamps/hubitat-drivers-ha7net/master/ha7net-parent.groovy') {
capability 'Refresh'
command 'createChildren'
command 'deleteChildren'
command 'deleteUnmatchedChildren'
command 'recreateChildren'
command 'refreshChildren'
}
preferences {
input name: 'address', type: 'text', title: 'HA7Net Address', description: 'FQDN or IP address', required: true
input name: 'logEnable', type: 'bool', title: 'Enable debug logging', defaultValue: false
}
}
def installed() {
initialize()
}
def updated() {
initialize()
}
def initialize() {
state.version = version()
}
def refresh() {
refreshChildren()
}
def createChildren() {
if (logEnable) log.debug("Creating children devices")
getSensorsDs18S20()
getSensorsDs18B20()
getSensorsDs2438()
}
def refreshChildren(){
if (logEnable) log.info "Refreshing children devices"
def children = getChildDevices()
children.each {child->
child.refresh()
}
}
def recreateChildren(){
if (logEnable) log.info "Recreating children devices"
// To Do: Based on a new preference, capture the name and label of each child device and reapply those names and labels
// for all discovered sensors that were previously known.
deleteChildren()
createChildren()
}
def deleteChildren() {
if (logEnable) log.info "Deleting children devices"
def children = getChildDevices()
children.each {child->
deleteChildDevice(child.deviceNetworkId)
}
}
def deleteUnmatchedChildren() {
if (logEnable) log.info "Deleting unmatched children devices"
// To Do: Not yet implemnted.
discoveredSensors = getSensors()
getChildDevices().each { device ->
if (logEnable) log.debug("Found an existing child device")
}
}
private def getSensorsDs18S20() {
if (logEnable) log.info "Getting DS18S20 family sensors"
def sensors = []
sensors = getSensors('10')
sensors.each { sensorId ->
if (getChildDevice(sensorId) == null) {
if (logEnable) log.debug("Child device does not yet exist for DS18S20 sensor: ${sensorId}")
child = addChildDevice("ckamps", "HA7Net 1-Wire - Child - Temperature", sensorId, [name: sensorId, label: "${sensorId} - DS18S20 Temperature", isComponent: false])
child.refresh()
} else {
if (logEnable) log.debug("Child device already exists for DS18S20 sensor: ${sensorId}")
}
}
}
private def getSensorsDs18B20() {
if (logEnable) log.info "Getting DS18B20 family sensors"
def sensors = []
sensors = getSensors('28')
sensors.each { sensorId ->
if (getChildDevice(sensorId) == null) {
if (logEnable) log.debug("Child device does not yet exist for DS18B20 sensor: ${sensorId}")
child = addChildDevice("ckamps", "HA7Net 1-Wire - Child - Temperature", sensorId, [name: sensorId, label: "${sensorId} - DS18B20 Temperature", isComponent: false])
child.refresh()
} else {
if (logEnable) log.debug("Child device already exists for DS18B20 sensor: ${sensorId}")
}
}
}
private def getSensorsDs2438() {
if (logEnable) log.info "Getting DS2438 family sensors"
def sensors = []
sensors = getSensors('26')
sensors.each { sensorId ->
if (getChildDevice(sensorId) == null) {
if (logEnable) log.debug("Child device does not yet exist for DS2438 sensor: ${sensorId}")
if (isDs2438TempOnly(sensorId)) {
if (logEnable) log.debug "Discovered DS2438 temperature sensor: ${sensorId}"
child = addChildDevice("ckamps", "HA7Net 1-Wire - Child - Temperature", sensorId, [name: sensorId, label: "${sensorId} - DS2438 Temperature", isComponent: false])
child.refresh()
} else {
if (logEnable) log.debug "Discovered DS2438 temperature + humidity sensor: ${sensorId}"
child = addChildDevice("ckamps", "HA7Net 1-Wire - Child - Humidity", sensorId, [name: sensorId, label: "${sensorId} - DS2438 Humidity + Temperature" , isComponent: false])
child.refresh()
}
} else {
if (logEnable) log.debug("Child device already exists for DS2438 sensor: ${sensorId}")
}
}
}
private def isDs2438TempOnly(sensorId) {
// The following logic is still a kludge in that the current basis for identifying standalone
// DS2438 sensors is not foolproof. We'll likely need to send lower level commands to the sensor
// and inspect how it's configured.
if (logEnable) log.info "Determining if DS2438 sensor is temperature only: ${sensorId}"
def uri = "http://${address}"
def path = '/1Wire/ReadHumidity.html'
def body = [Address_Array: "${sensorId}"]
response = doHttpPost(uri, path, body)
if (!response) throw new Exception("doHttpPost to get humidity from sensor returned empty response ${sensorId}")
element = response.'**'.find{ it.@name == 'Humidity_0' }
if (!element) throw new Exception("Can't find Humidity_0 element in response from HA7Net for sensor ${sensorId}")
if (!element.@value) throw new Exception("Empty value in Humidity_0 element in response from HA7Net for sensor ${sensorId}")
humidity = [email protected]()
if (logEnable) log.info "Humidity from DS2438 sensor is: ${humidity}"
// We've seen the HA7Net return RH readings far in excess of 100% RH for standalone DS2438 sensors.
if (humidity > 150) {
return true
} else {
return false
}
}
private def getSensors(familyCode) {
if (logEnable) log.info "Getting list of sensors known to HA7Net for family code: ${familyCode}"
lockId = getLock()
def uri = "http://${address}"
def path = '/1Wire/Search.html'
def body = [LockID: lockId, FamilyCode: familyCode]
response = doHttpPost(uri, path, body)
relLock(lockId)
def discoveredSensors = []
def sensorElements = []
// We should be able to modify this findAll statement to construct an array of sensor IDs
// as opposed to depending on the each loop.
//
// Something like the following but I have yet to get the right hand side correct:
//
// discoveredSensors = response.'**'.findAll{ [email protected]().startsWith('Address_') }.*.value.text()
sensorElements = response.'**'.findAll{ [email protected]().startsWith('Address_') }
if (logEnable) log.debug("number of sensor elements found: ${sensorElements.size()}")
sensorElements.each {
def sensorId = [email protected]()
if (logEnable) log.debug("Sensor discovered - value: ${sensorId}")
discoveredSensors.add(sensorId)
}
return(discoveredSensors)
}
def doHttpPost(uri, path, body) {
if (logEnable) log.debug("doHttpPost called: uri: ${uri} path: ${path} body: ${body}")
def response = []
int retries = 0
def cmds = []
cmds << 'delay 1'
// Attempt a max of 3 retries to address cases in which transient read errors can occur when
// interacting with the HA7Net.
while(retries++ < 3) {
try {
httpPost( [uri: uri, path: path, body: body, requestContentType: 'application/x-www-form-urlencoded'] ) { resp ->
if (resp.success) {
response = resp.data
if ((logEnable) && (response.data)) {
serializedDocument = XmlUtil.serialize(response)
log.debug(serializedDocument.replace('\n', '').replace('\r', ''))
}
} else {
throw new Exception("httpPost() not successful for: ${uri} ${path}")
}
}
return(response)
} catch (Exception e) {
log.warn "httpPost() of ${path} to HA7Net failed: ${e.message}"
// When read time out error occurs, retry the operation. Otherwise, throw
// an exception.
if (!e.message.contains('Read timed out')) throw new Exception("httpPost() failed for: ${uri} ${path}")
}
log.warn('Delaying 1 second before next httpPost() retry')
cmds
}
throw new Exception("httpPost() exceeded max retries for: ${uri} ${path}")
}
// To Do: Is there a more direct means for child devices to access parent preferences/settings?
def getHa7netAddress() {
return(address)
}
// HA7Net and Locking: getLock() and relLock()
//
// The following methods are currently used when accessing the /1Wire/Search.html interface to
// avoid conflicts when multiple clients and/or users attempt to access the HA7Net and 1-Wire
// bus at the same time.
//
// Note that these methods are not used when accessing the higher level interfaces of
// the HA7Net such as /1Wire/ReadHumidity.html and /1Wire/ReadTemperature.html. It is believed
// that for these higher level interfaces, the HA7Net performs its own concurrency management
// as it carries out multiple actions on the 1-Wire bus for each invocation of the higher level
// interface. It is believed that when multiple requests for higher level interfaces occur, the
// HA7Net will queue them until either the currently executing higher level action has completed or
// a lock obtained by a client is either released or expires.
//
// See the HA7Net User's Manual for an overview of concurrency management:
//
// https://www.embeddeddatasystems.com/assets/images/supportFiles/manuals/UsersMan-HA7Net.pdf
private def getLock() {
if (logEnable) log.debug("Attempting to obtain 1-Wire network lock")
def response = []
def uri = "http://${address}/1Wire/GetLock.html"
httpGet(uri) { resp ->
if (resp.success) {
response = resp.data
if ((logEnable) && (response.data)) {
serializedDocument = XmlUtil.serialize(response)
log.debug(serializedDocument.replace('\n', '').replace('\r', ''))
}
} else {
throw new Exception("httpGet() not successful for: ${uri} ${path}")
}
}
element = response.'**'.find{ it.@name == 'LockID_0' }
if (!element.@value) throw new Exception("Empty value in LockID_0 element in response from HA7Net")
lockId = [email protected]()
if (logEnable) log.debug("1-Wire network lock obtained successfully: ${lockId}")
return(lockId)
}
private def relLock(lockId) {
if (logEnable) log.debug("Attempting to release 1-Wire network lock: ${lockId}")
def response = []
def uri = "http://${address}"
def path = '/1Wire/ReleaseLock.html'
def body = [LockID: lockId]
response = doHttpPost(uri, path, body)
if (!response) throw new Exception("doHttpPost to release lock returned empty response")
element = response.'**'.find{ it.@name == 'Exception_Code_0' }
if (!element.@value) throw new Exception("Empty value in Exception_Code_0 element in release lock response from HA7Net")
if (element.@value != '0') throw new Exception("Non zero value in Exception_Code_0 element in release lock response from HA7Net: ${element.@value}")
if (logEnable) log.debug("1-Wire network lock released successfully: ${lockId}")
}