homebridge-lib 6.2.1 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ServiceDelegate/History.js +679 -0
- package/package.json +1 -1
- package/lib/ServiceDelegate/History/Consumption.js +0 -130
- package/lib/ServiceDelegate/History/Light.js +0 -74
- package/lib/ServiceDelegate/History/On.js +0 -98
- package/lib/ServiceDelegate/History/Power.js +0 -147
- package/lib/ServiceDelegate/History/Sensor.js +0 -215
- package/lib/ServiceDelegate/History/Thermo.js +0 -88
- package/lib/ServiceDelegate/History/index.js +0 -310
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
// homebridge-lib/lib/ServiceDelegate/History.js
|
|
2
|
+
//
|
|
3
|
+
// Library for Homebridge plugins.
|
|
4
|
+
// Copyright © 2017-2023 Erik Baauw. All rights reserved.
|
|
5
|
+
//
|
|
6
|
+
// The logic for handling Eve history was inspired by Simone Tisa's
|
|
7
|
+
// fakagato-history repository, copyright © 2017 simont77.
|
|
8
|
+
// See https://github.com/simont77/fakegato-history.
|
|
9
|
+
|
|
10
|
+
'use strict'
|
|
11
|
+
|
|
12
|
+
const { CharacteristicDelegate } = require('../../index')
|
|
13
|
+
const homebridgeLib = require('../../index')
|
|
14
|
+
|
|
15
|
+
const { ServiceDelegate } = homebridgeLib
|
|
16
|
+
|
|
17
|
+
/** Eve history keeps time as # seconds since epoch of 2001/01/01.
|
|
18
|
+
* @type {integer}
|
|
19
|
+
*/
|
|
20
|
+
const epoch = Math.round(new Date('2001-01-01T00:00:00Z').valueOf() / 1000)
|
|
21
|
+
|
|
22
|
+
const defaultMemorySize = 6 * 24 * 7 * 4 // 4 weeks of 1 entry per 10 minutes
|
|
23
|
+
|
|
24
|
+
/** Convert date (in # seconds since NodeJS epoch) to string.
|
|
25
|
+
* @param {integer} d - Seconds since NodeJS epoch.
|
|
26
|
+
* @returns {string} Human readable date string.
|
|
27
|
+
*/
|
|
28
|
+
function dateToString (d) {
|
|
29
|
+
return new Date(1000 * d).toString().slice(0, 24)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Abstract superclass for type of data point in Eve History.
|
|
33
|
+
*
|
|
34
|
+
* An Eve history service supports up to seven data points, corresponding to
|
|
35
|
+
* an associated characteristic value. These data points are defined by tag and
|
|
36
|
+
* length in the fingerprint reported through `eve.HistoryStatus`.
|
|
37
|
+
* Different history entries can contain a different combination of data points.
|
|
38
|
+
* @abstract
|
|
39
|
+
* @memberof ServiceDelegate.History
|
|
40
|
+
*/
|
|
41
|
+
class HistoryValue {
|
|
42
|
+
/** Create a new data point type.
|
|
43
|
+
* @param {ServiceDelegate.History} parent - The delegate for the `eve.History`
|
|
44
|
+
* service.
|
|
45
|
+
* @param {CharactertisticDelegate} delegate - The delefate for the associated
|
|
46
|
+
* characteristic.
|
|
47
|
+
*/
|
|
48
|
+
constructor (parent, delegate) {
|
|
49
|
+
if (!(parent instanceof History)) {
|
|
50
|
+
throw new TypeError('parent: not a ServiceDelegate.History')
|
|
51
|
+
}
|
|
52
|
+
if (!(delegate instanceof CharacteristicDelegate)) {
|
|
53
|
+
throw new TypeError('delegate: not a CharacteristicDelegate')
|
|
54
|
+
}
|
|
55
|
+
this._parent = parent
|
|
56
|
+
this._delegate = delegate
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The 1-byte tag, identifying the data point to Eve.
|
|
60
|
+
* @type {integer}
|
|
61
|
+
*/
|
|
62
|
+
get tag () { }
|
|
63
|
+
|
|
64
|
+
/** The 1-byte length of a data point value.
|
|
65
|
+
* @type {integer}
|
|
66
|
+
*/
|
|
67
|
+
get length () { return 2 }
|
|
68
|
+
|
|
69
|
+
/** The 1-letter id, identifying a data point in the Eve history service delegate.
|
|
70
|
+
* @type {string}
|
|
71
|
+
*/
|
|
72
|
+
get id () { }
|
|
73
|
+
|
|
74
|
+
/** Add the data point to an entry
|
|
75
|
+
* @params {Object} entry - The history entry.
|
|
76
|
+
*/
|
|
77
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value }
|
|
78
|
+
|
|
79
|
+
/** Write the data point from an entry to a buffer.
|
|
80
|
+
* @params {Object} entry - The history entry.
|
|
81
|
+
* @params {Buffer} buffer - The buffer to write to.
|
|
82
|
+
* @params {integer} offset - The offset within the buffer.
|
|
83
|
+
*/
|
|
84
|
+
writeEntry (entry, buffer, offset) { buffer.writeInt16LE(entry[this.id], offset) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class TemperatureValue extends HistoryValue {
|
|
88
|
+
get tag () { return 0x01 }
|
|
89
|
+
get id () { return 't' }
|
|
90
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value * 100 }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class HumidityValue extends HistoryValue {
|
|
94
|
+
get tag () { return 0x02 }
|
|
95
|
+
get id () { return 'h' }
|
|
96
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value * 100 }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class AirPressureValue extends HistoryValue {
|
|
100
|
+
get tag () { return 0x03 }
|
|
101
|
+
get id () { return 'a' }
|
|
102
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value * 10 }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class ContactValue extends HistoryValue {
|
|
106
|
+
get tag () { return 0x06 }
|
|
107
|
+
get length () { return 1 }
|
|
108
|
+
get id () { return 'c' }
|
|
109
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value ? 1 : 0 }
|
|
110
|
+
writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) }
|
|
111
|
+
|
|
112
|
+
constructor (parent, delegate) {
|
|
113
|
+
super(parent, delegate)
|
|
114
|
+
delegate.on('didSet', (value) => {
|
|
115
|
+
const now = History.now()
|
|
116
|
+
const entry = { time: now }
|
|
117
|
+
entry[this.id] = this._delegate.value ? 1 : 0
|
|
118
|
+
parent.addEntry(entry)
|
|
119
|
+
if (parent.lastContactDelegate != null) {
|
|
120
|
+
parent.lastContactDelegate = parent.lastActivationValue(now)
|
|
121
|
+
}
|
|
122
|
+
if (parent.timesOpenedDelegate != null) {
|
|
123
|
+
parent.timesOpenedDelegate.value += value
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
parent.addCharacteristicDelegate({
|
|
127
|
+
key: 'resetTotal',
|
|
128
|
+
Characteristic: this.Characteristics.eve.ResetTotal
|
|
129
|
+
}).on('didSet', (value) => {
|
|
130
|
+
parent.timesOpenedDelegate.value = 0
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class ConsumptionValue extends HistoryValue {
|
|
136
|
+
get tag () { return 0x07 }
|
|
137
|
+
get id () { return 'p' }
|
|
138
|
+
|
|
139
|
+
constructor (parent, delegate) {
|
|
140
|
+
super(parent, delegate)
|
|
141
|
+
this._consumption = delegate.value
|
|
142
|
+
this._time = History.now()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
prepareEntry (entry) {
|
|
146
|
+
const delta = this._delegate.value - this._consumption // kWh
|
|
147
|
+
const period = entry.time - this._time // s
|
|
148
|
+
const power = 1000 * 3600 * delta / period // W
|
|
149
|
+
if (this._parent.computedPowerDelegate != null) {
|
|
150
|
+
this._parent.computedPowerDelegate.value = Math.round(power) // W
|
|
151
|
+
}
|
|
152
|
+
entry[this.id] = Math.round(power * 10) // 0.1 W * 10 min
|
|
153
|
+
this._consumption = this._delegate.value
|
|
154
|
+
this._time = entry.time
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class PowerValue extends HistoryValue {
|
|
159
|
+
get tag () { return 0x07 }
|
|
160
|
+
get id () { return 'p' }
|
|
161
|
+
|
|
162
|
+
constructor (parent, delegate) {
|
|
163
|
+
super(parent, delegate)
|
|
164
|
+
this._runningConsumption = 0 // 10-min-interval running value
|
|
165
|
+
this._power = delegate.value // current power
|
|
166
|
+
this._time = History.now() // start time of current power
|
|
167
|
+
delegate.on('didSet', (value) => {
|
|
168
|
+
const now = History.now()
|
|
169
|
+
const delta = this._power * (now - this._time) // Ws
|
|
170
|
+
this._runningConsumption += Math.round(delta / 60.0) // 0.1W * 10 min
|
|
171
|
+
this._totalConsumption += delta / 36000.0 // 0.01 kWh
|
|
172
|
+
this._power = value
|
|
173
|
+
this._time = now
|
|
174
|
+
})
|
|
175
|
+
parent.addCharacteristicDelegate({
|
|
176
|
+
key: 'resetTotal',
|
|
177
|
+
Characteristic: this.Characteristics.eve.ResetTotal
|
|
178
|
+
}).on('didSet', (value) => {
|
|
179
|
+
this._runningConsumption = 0
|
|
180
|
+
this._totalConsumption = 0
|
|
181
|
+
parent.computedConsumptionDelegate.value = this._totalConsumption
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
prepareEntry (entry) {
|
|
186
|
+
const delta = this._power * (entry.time - this._time) // Ws
|
|
187
|
+
this._runningConsumption += delta / 60.0 // 0.1 W * 10 min
|
|
188
|
+
this._totalConsumption += delta / 36000.0 // 0.01 kWh
|
|
189
|
+
this._parent.computedConsumptionDelegate.value = Math.round(this._totalConsumption) / 100.0 // kWh
|
|
190
|
+
entry[this.id] = Math.round(this._runningConsumption) // 0.1 W * 10 min
|
|
191
|
+
this._power = this._delegate.value
|
|
192
|
+
this._time = entry.time
|
|
193
|
+
this._runningConsumption = 0
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class ValvePositionValue extends HistoryValue {
|
|
198
|
+
get tag () { return 0x10 }
|
|
199
|
+
get length () { return 1 }
|
|
200
|
+
get id () { return 'v' }
|
|
201
|
+
writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
class TargetTemperatureValue extends HistoryValue {
|
|
205
|
+
get tag () { return 0x11 }
|
|
206
|
+
get id () { return 's' }
|
|
207
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value * 100 }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class MotionValue extends HistoryValue {
|
|
211
|
+
get tag () { return 0x1C }
|
|
212
|
+
get length () { return 1 }
|
|
213
|
+
get id () { return 'm' }
|
|
214
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value ? 1 : 0 }
|
|
215
|
+
writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) }
|
|
216
|
+
|
|
217
|
+
constructor (parent, delegate) {
|
|
218
|
+
super(parent, delegate)
|
|
219
|
+
delegate.on('didSet', (value) => {
|
|
220
|
+
const now = History.now()
|
|
221
|
+
const entry = { time: now }
|
|
222
|
+
entry[this.id] = this._delegate.value ? 1 : 0
|
|
223
|
+
parent.addEntry(entry)
|
|
224
|
+
parent.lastMotionDelegate.value = parent.lastActivationValue(now)
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class OnValue extends HistoryValue {
|
|
230
|
+
get tag () { return 0x1E }
|
|
231
|
+
get length () { return 1 }
|
|
232
|
+
get id () { return 'o' }
|
|
233
|
+
prepareEntry (entry) { entry[this.id] = this._delegate.value ? 1 : 0 }
|
|
234
|
+
writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) }
|
|
235
|
+
|
|
236
|
+
constructor (parent, delegate) {
|
|
237
|
+
super(parent, delegate)
|
|
238
|
+
delegate.on('didSet', (value) => {
|
|
239
|
+
const now = History.now()
|
|
240
|
+
const entry = { time: now }
|
|
241
|
+
entry[this.id] = this._delegate.value ? 1 : 0
|
|
242
|
+
this._parent.addEntry(entry)
|
|
243
|
+
parent.lastOnDelegate.value = parent.lastActivationValue(now)
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
class VocDensityValue extends HistoryValue {
|
|
249
|
+
get tag () { return 0x22 }
|
|
250
|
+
get id () { return 'q' }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
class LightLevelValue extends HistoryValue {
|
|
254
|
+
get tag () { return 0x30 }
|
|
255
|
+
get id () { return 'l' }
|
|
256
|
+
// writeEntry (entry, buffer, offset) { buffer.writeUInt16LE(entry[this.id], offset) }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Types of delegates for characteristics for history data points.
|
|
260
|
+
const historyValueTypes = {
|
|
261
|
+
temperature: TemperatureValue,
|
|
262
|
+
humidity: HumidityValue,
|
|
263
|
+
airPressure: AirPressureValue,
|
|
264
|
+
contact: ContactValue,
|
|
265
|
+
power: PowerValue,
|
|
266
|
+
consumption: ConsumptionValue,
|
|
267
|
+
valvePosition: ValvePositionValue,
|
|
268
|
+
targetTemperature: TargetTemperatureValue,
|
|
269
|
+
motion: MotionValue,
|
|
270
|
+
on: OnValue,
|
|
271
|
+
vocDensity: VocDensityValue,
|
|
272
|
+
lightLevel: LightLevelValue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Types of delegates for characteristic maintained by Eve history.
|
|
276
|
+
const historyDerivedTypes = {
|
|
277
|
+
lastContact: 'contact',
|
|
278
|
+
timesOpened: 'contact',
|
|
279
|
+
lastMotion: 'motion',
|
|
280
|
+
lastOn: 'on',
|
|
281
|
+
lastLightOn: 'lightOn',
|
|
282
|
+
computedConsumption: 'power',
|
|
283
|
+
computedPower: 'consumption'
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Class for an Eve _History_ service delegate.
|
|
287
|
+
*
|
|
288
|
+
* This delegate sets up a `Services.eve.History` HomeKit service
|
|
289
|
+
* with keys for the following HomeKit characteristics:
|
|
290
|
+
*
|
|
291
|
+
* key | Characteristic
|
|
292
|
+
* ---------------- | ----------------------------------
|
|
293
|
+
* `name` | `Characteristics.hap.Name`
|
|
294
|
+
* `historyRequest` | `Characteristics.eve.HistoryRequest`
|
|
295
|
+
* `setTime` | `Characteristics.eve.SetTime`
|
|
296
|
+
* `historyStatus` | `Characteristics.eve.HistoryStatus`
|
|
297
|
+
* `historyEntries` | `Characteristics.eve.HistoryEntries`
|
|
298
|
+
* @abstract
|
|
299
|
+
* @extends ServiceDelegate
|
|
300
|
+
* @memberof ServiceDelegate
|
|
301
|
+
*/
|
|
302
|
+
class History extends ServiceDelegate {
|
|
303
|
+
/** Create a new instance of an Eve _History_ service delegate.
|
|
304
|
+
* @param {!AccessoryDelegate} accessoryDelegate - The delegate of the
|
|
305
|
+
* corresponding HomeKit accessory.
|
|
306
|
+
* @param {!object} params - The parameters for the _History_ HomeKit service.
|
|
307
|
+
* @param {?CharacteristicDelegate} params.contactDelegate - The
|
|
308
|
+
* `hap.ContactSensorState` characteristic delegate
|
|
309
|
+
* for a `hap.ContactSensor` service.
|
|
310
|
+
* @param {?CharacteristicDelegate} params.lastContactDelegate - The
|
|
311
|
+
* `eve.LastActivation` characteristic delegate
|
|
312
|
+
* for a `hap.ContactSensor` service.
|
|
313
|
+
* @param {?CharacteristicDelegate} params.timesOpenedDelegate - The
|
|
314
|
+
* `eve.TimesOpened` characteristic delegate
|
|
315
|
+
* for a `hap.ContactSensor` service.
|
|
316
|
+
* @param {?CharacteristicDelegate} params.motionDelegate - The
|
|
317
|
+
* `.hap.MotionDetected` characteristic delegate
|
|
318
|
+
* for a `hap.MotionSensor` service.
|
|
319
|
+
* @param {?CharacteristicDelegate} params.lastMotionDelegate - The
|
|
320
|
+
* `.eve.LastActivation` characteristic delegate
|
|
321
|
+
* for a `hap.MotionSensor` service.
|
|
322
|
+
* @param {?CharacteristicDelegate} params.lightLevelDelegate - The
|
|
323
|
+
* `.hap.CurrentAmbientLightLevel` characteristic delegate
|
|
324
|
+
* for a `hap.LightLevelSensor` service.
|
|
325
|
+
* @param {?CharacteristicDelegate} params.temperatureDelegate - The
|
|
326
|
+
* `.hap.CurrentTemperature` characteristic delegate
|
|
327
|
+
* for a `hap.TemperatureSensor`, `eve.Weather`, or `hap.Thermostat` service.
|
|
328
|
+
* @param {?CharacteristicDelegate} params.humidityDelegate - The
|
|
329
|
+
* `hap.CurrentRelativeHumidity` characteristic delegate
|
|
330
|
+
* for a `hap.HumiditySensor` or `eve.Weather` service.
|
|
331
|
+
* @param {?CharacteristicDelegate} params.airPressureDelegate - The
|
|
332
|
+
* `.eve.AirPressure` characteristic delegate
|
|
333
|
+
* for an `eve.AirPressureSensor` or `eve.Weather` service.
|
|
334
|
+
* @param {?CharacteristicDelegate} params.vocDensityDelegate - The
|
|
335
|
+
* `hap.VOCDensity` characteristic delegate
|
|
336
|
+
* for a `hap.AirQualilitySensor` service.
|
|
337
|
+
* @param {?CharacteristicDelegate} params.targetTemperatureDelegate - The
|
|
338
|
+
* `hap.TargetTemperature` characteristic delegate
|
|
339
|
+
* for a `hap.Thermostat` service.
|
|
340
|
+
* @param {?CharacteristicDelegate} params.valvePositionDelegate - The
|
|
341
|
+
* `.eve.ValvePosition` characteristic delegate
|
|
342
|
+
* for a `hap.Thermostat` service.
|
|
343
|
+
* @param {?CharacteristicDelegate} params.onDelegate - The
|
|
344
|
+
* `Characteristics.hap.On` characteristic delegate
|
|
345
|
+
* for a `hap.Outlet` service.
|
|
346
|
+
* @param {?CharacteristicDelegate} params.lastOnDelegate - The
|
|
347
|
+
* `.eve.LastActivation` characteristic delegate
|
|
348
|
+
* for a `hap.Outlet` service.
|
|
349
|
+
* @param {?CharacteristicDelegate} params.lightOnDelegate - The
|
|
350
|
+
* `Characteristics.hap.On` characteristic delegate
|
|
351
|
+
* for a `hap.Lightbulb` service.
|
|
352
|
+
* @param {?CharacteristicDelegate} params.lastLightOnDelegate - A
|
|
353
|
+
* `.eve.LastActivation` characteristic delegate
|
|
354
|
+
* for a `hap.Lightbulb` service.
|
|
355
|
+
* @param {?CharacteristicDelegate} params.powerDelegate - The
|
|
356
|
+
* `.eve.CurrentConsumption` characteristic delegate
|
|
357
|
+
* for a `hap.Outlet` or `eve.Consumption` service
|
|
358
|
+
* for a device that reports current consumption.
|
|
359
|
+
* @param {?CharacteristicDelegate} params.computedConsumptionDelegate - The
|
|
360
|
+
* `eve.TotalConsumption` characteristic delegate
|
|
361
|
+
* for a `hap.Outlet` or `eve.Consumption` service
|
|
362
|
+
* for a device that reports current consumption, but not total consumption.
|
|
363
|
+
* @param {?CharacteristicDelegate} params.consumptionDelegate - The
|
|
364
|
+
* `.eve.TotalConsumption` characteristic delegate
|
|
365
|
+
* for a `hap.Outlet` or `eve.Consumption` service
|
|
366
|
+
* for a device that reports total consumption.
|
|
367
|
+
* @param {?CharacteristicDelegate} params.computedPowerDelegate - The
|
|
368
|
+
* `.eve.CurrentConsumption` characteristic delegate
|
|
369
|
+
* for a `hap.Outlet` or `eve.Consumption` service
|
|
370
|
+
* for a device that reports total consumption but not current consumption.
|
|
371
|
+
* @param {integer} [params.memorySize=4032] - The memory size, in number of
|
|
372
|
+
* history entries. The default is 4 weeks of 1 entry per 10 minutes.
|
|
373
|
+
* @param {?boolean} params.config - Expose config.
|
|
374
|
+
*/
|
|
375
|
+
constructor (accessoryDelegate, params = {}) {
|
|
376
|
+
params.name = accessoryDelegate.name + ' History'
|
|
377
|
+
params.Service = accessoryDelegate.Services.eve.History
|
|
378
|
+
params.hidden = true
|
|
379
|
+
super(accessoryDelegate, params)
|
|
380
|
+
|
|
381
|
+
const delegates = []
|
|
382
|
+
let i = 0
|
|
383
|
+
for (const param of Object.keys(params)) {
|
|
384
|
+
if (param.endsWith('Delegate')) {
|
|
385
|
+
if (!(params[param] instanceof CharacteristicDelegate)) {
|
|
386
|
+
throw new TypeError(`params.${param}: not a CharacteristicDelegate`)
|
|
387
|
+
}
|
|
388
|
+
const key = param.slice(0, -8)
|
|
389
|
+
if (historyDerivedTypes[key] != null) {
|
|
390
|
+
if (params[historyDerivedTypes[key] + 'Delegate'] == null) {
|
|
391
|
+
throw new SyntaxError(`params.${param}: missing params.${key}Delegate`)
|
|
392
|
+
}
|
|
393
|
+
this[param] = params[param]
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
if (key === 'lightOn') {
|
|
397
|
+
if (!(params.lastLightOnDelegate instanceof CharacteristicDelegate)) {
|
|
398
|
+
throw new SyntaxError(`params.${param}: missing params.${key}Delegate`)
|
|
399
|
+
}
|
|
400
|
+
params[param].on('didSet', (value) => {
|
|
401
|
+
const now = History.now()
|
|
402
|
+
params.lastLightOnDelegate.value = this.lastActivationValue(now)
|
|
403
|
+
this.addEntry({ time: now })
|
|
404
|
+
})
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
if (historyValueTypes[key] == null) {
|
|
408
|
+
throw new SyntaxError(`params.${param}: invalid parameter`)
|
|
409
|
+
}
|
|
410
|
+
if (++i > 7) {
|
|
411
|
+
throw new SyntaxError(`params.${param}: more than 7 history values`)
|
|
412
|
+
}
|
|
413
|
+
delegates.push({ key, param })
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const fingerPrint = Buffer.alloc(15)
|
|
417
|
+
let offset = 0
|
|
418
|
+
fingerPrint.writeUInt8(i, offset); offset++
|
|
419
|
+
this._valueTypes = []
|
|
420
|
+
for (const { key, param } of delegates) {
|
|
421
|
+
const valueType = new historyValueTypes[key](this, params[param])
|
|
422
|
+
fingerPrint.writeUInt8(valueType.tag, offset); offset++
|
|
423
|
+
fingerPrint.writeUInt8(valueType.length, offset); offset++
|
|
424
|
+
this._valueTypes.push(new historyValueTypes[key](this, params[param]))
|
|
425
|
+
}
|
|
426
|
+
this.fingerPrint = fingerPrint.slice(0, offset)
|
|
427
|
+
this.debug('fingerPrint: 0x%s', this.fingerPrint.toString('hex').toUpperCase())
|
|
428
|
+
const memorySize = i === 0
|
|
429
|
+
? 0
|
|
430
|
+
: params.memorySize == null ? defaultMemorySize : params.memorySize
|
|
431
|
+
|
|
432
|
+
this._transfer = false
|
|
433
|
+
|
|
434
|
+
this.addCharacteristicDelegate({
|
|
435
|
+
key: 'history',
|
|
436
|
+
silent: true
|
|
437
|
+
})
|
|
438
|
+
this._h = this.values.history
|
|
439
|
+
if (this._h == null) {
|
|
440
|
+
this.values.history = {
|
|
441
|
+
memorySize,
|
|
442
|
+
firstEntry: 1,
|
|
443
|
+
lastEntry: 1,
|
|
444
|
+
entryOffset: 0,
|
|
445
|
+
entries: [null, null],
|
|
446
|
+
initialTime: History.now()
|
|
447
|
+
}
|
|
448
|
+
this._h = this.values.history
|
|
449
|
+
} else if (this._h.memorySize !== memorySize) {
|
|
450
|
+
this.values.history = {
|
|
451
|
+
memorySize,
|
|
452
|
+
firstEntry: this._h.lastEntry,
|
|
453
|
+
lastEntry: this._h.lastEntry,
|
|
454
|
+
entryOffset: this._h.lastEntry - 1,
|
|
455
|
+
entries: [null, null],
|
|
456
|
+
initialTime: this._h.initialTime
|
|
457
|
+
}
|
|
458
|
+
this._h = this.values.history
|
|
459
|
+
} else {
|
|
460
|
+
this.debug(
|
|
461
|
+
'restored %d history entries (%d to %d)', this._h.entries.length,
|
|
462
|
+
this._h.firstEntry, this._h.lastEntry
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.addCharacteristicDelegate({
|
|
467
|
+
key: 'historyRequest',
|
|
468
|
+
Characteristic: this.Characteristics.eve.HistoryRequest,
|
|
469
|
+
value: params.historyRequest,
|
|
470
|
+
setter: this._onSetHistoryRequest.bind(this),
|
|
471
|
+
silent: true
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
this.addCharacteristicDelegate({
|
|
475
|
+
key: 'setTime',
|
|
476
|
+
Characteristic: this.Characteristics.eve.SetTime,
|
|
477
|
+
value: params.setTime,
|
|
478
|
+
silent: true
|
|
479
|
+
}).on('didSet', (value) => {
|
|
480
|
+
const buffer = Buffer.from(value, 'base64')
|
|
481
|
+
this.vdebug('SetTime changed to %j', buffer.toString('hex'))
|
|
482
|
+
const date = dateToString(buffer.readUInt32LE() + epoch)
|
|
483
|
+
this.debug('SetTime changed to %s', date)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
this.addCharacteristicDelegate({
|
|
487
|
+
key: 'historyStatus',
|
|
488
|
+
Characteristic: this.Characteristics.eve.HistoryStatus,
|
|
489
|
+
value: params.historyStatus,
|
|
490
|
+
silent: true
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
this.addCharacteristicDelegate({
|
|
494
|
+
key: 'historyEntries',
|
|
495
|
+
Characteristic: this.Characteristics.eve.HistoryEntries,
|
|
496
|
+
value: params.historyEntries,
|
|
497
|
+
getter: this._onGetEntries.bind(this),
|
|
498
|
+
silent: true
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
if (params.config) {
|
|
502
|
+
this.addCharacteristicDelegate({
|
|
503
|
+
key: 'configCommand',
|
|
504
|
+
Characteristic: this.Characteristics.eve.ConfigCommand,
|
|
505
|
+
setter: this._onSetConfig.bind(this)
|
|
506
|
+
// silent: true
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
this.addCharacteristicDelegate({
|
|
510
|
+
key: 'configData',
|
|
511
|
+
Characteristic: this.Characteristics.eve.ConfigData,
|
|
512
|
+
getter: this._onGetConfig.bind(this)
|
|
513
|
+
// silent: true
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
accessoryDelegate.propertyDelegate('name')
|
|
518
|
+
.on('didSet', (value) => {
|
|
519
|
+
this.values.configuredName = value + ' History'
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
this._accessoryDelegate.heartbeatEnabled = true
|
|
523
|
+
this._accessoryDelegate
|
|
524
|
+
.once('heartbeat', (beat) => {
|
|
525
|
+
this._historyBeat = (beat % 600) + 5
|
|
526
|
+
})
|
|
527
|
+
.on('heartbeat', (beat) => {
|
|
528
|
+
if (beat % 600 === this._historyBeat) {
|
|
529
|
+
const entry = { time: History.now() }
|
|
530
|
+
for (const valueType of this._valueTypes) {
|
|
531
|
+
valueType.prepareEntry(entry)
|
|
532
|
+
}
|
|
533
|
+
this.addEntry(entry)
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
.on('shutdown', () => {
|
|
537
|
+
this.debug(
|
|
538
|
+
'saved %d history entries (%d to %d)', this._h.entries.length,
|
|
539
|
+
this._h.firstEntry, this._h.lastEntry
|
|
540
|
+
)
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Return current time as # seconds since NodeJS epoch.
|
|
545
|
+
* @returns {integer} # seconds since NodeJS epoch.
|
|
546
|
+
*/
|
|
547
|
+
static now () { return Math.round(new Date().valueOf() / 1000) }
|
|
548
|
+
|
|
549
|
+
/** Convert date intp `Characteristics.eve.LastActivation` characteristic value.
|
|
550
|
+
* @param {integer} date - Seconds since NodeJS epoch.
|
|
551
|
+
* @returns {integer} Value for last activation.
|
|
552
|
+
*/
|
|
553
|
+
lastActivationValue (date = History.now()) { return date - this._h.initialTime }
|
|
554
|
+
|
|
555
|
+
/** Convert a history entry to a buffer.
|
|
556
|
+
* @abstract
|
|
557
|
+
* @param {object} entry - The entry.
|
|
558
|
+
* @returns {Buffer} A Buffer with the values from the entry.
|
|
559
|
+
*/
|
|
560
|
+
entryToBuffer (entry) {
|
|
561
|
+
const buffer = Buffer.alloc(16)
|
|
562
|
+
let bitmap = 0
|
|
563
|
+
let offset = 1
|
|
564
|
+
for (const i in this._valueTypes) {
|
|
565
|
+
const valueType = this._valueTypes[i]
|
|
566
|
+
if (entry[valueType.id] != null) {
|
|
567
|
+
bitmap |= 0x01 << i
|
|
568
|
+
valueType.writeEntry(entry, buffer, offset)
|
|
569
|
+
offset += valueType.length
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
buffer.writeUInt8(bitmap, 0)
|
|
573
|
+
return buffer.slice(0, offset)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Add an entry to the history.
|
|
577
|
+
* @param {object} entry - The entry.
|
|
578
|
+
*/
|
|
579
|
+
addEntry (entry) {
|
|
580
|
+
if (this._h.memorySize > 0) {
|
|
581
|
+
if (this._h.lastEntry - this._h.entryOffset >= this._h.memorySize) {
|
|
582
|
+
this._h.firstEntry++
|
|
583
|
+
}
|
|
584
|
+
this._h.lastEntry++
|
|
585
|
+
const index = (this._h.lastEntry - this._h.entryOffset) % this._h.memorySize
|
|
586
|
+
this.debug(
|
|
587
|
+
'History Entries: set entry %d (index %d) to %j',
|
|
588
|
+
this._h.lastEntry, index, entry
|
|
589
|
+
)
|
|
590
|
+
this._h.entries[index] = entry
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this.debug('set History Status to %d .. %d', this._h.firstEntry, this._h.lastEntry)
|
|
594
|
+
const buffer = Buffer.alloc(1024)
|
|
595
|
+
let offset = 0
|
|
596
|
+
buffer.writeUInt32LE(entry.time - this._h.initialTime, offset); offset += 4
|
|
597
|
+
buffer.writeUInt32LE(0, offset); offset += 4
|
|
598
|
+
buffer.writeUInt32LE(this._h.initialTime - epoch, offset); offset += 4
|
|
599
|
+
this.fingerPrint.copy(buffer, offset); offset += this.fingerPrint.length
|
|
600
|
+
buffer.writeUInt16LE(this._h.lastEntry - this._h.firstEntry + 1, offset); offset += 2
|
|
601
|
+
buffer.writeUInt16LE(this._h.memorySize, offset); offset += 2
|
|
602
|
+
buffer.writeUInt32LE(this._h.firstEntry, offset); offset += 4
|
|
603
|
+
buffer.writeUInt32LE(0, offset); offset += 4
|
|
604
|
+
buffer.writeUInt8(1, offset); offset += 1
|
|
605
|
+
buffer.writeUInt8(1, offset); offset += 1
|
|
606
|
+
const value = buffer.slice(0, offset)
|
|
607
|
+
this.values.historyStatus = value.toString('base64')
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async _onSetHistoryRequest (value) {
|
|
611
|
+
const buffer = Buffer.from(value, 'base64')
|
|
612
|
+
this.vdebug('History Request changed to %j', buffer.toString('hex'))
|
|
613
|
+
const entry = buffer.readUInt32LE(2)
|
|
614
|
+
this.debug(
|
|
615
|
+
'History Request changed to %d (%d to %d)', entry,
|
|
616
|
+
this._h.firstEntry, this._h.lastEntry
|
|
617
|
+
)
|
|
618
|
+
this._currentEntry = Math.max(this._h.firstEntry, entry)
|
|
619
|
+
this._transfer = true
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async _onGetEntries () {
|
|
623
|
+
if (this._currentEntry > this._h.lastEntry || !this._transfer) {
|
|
624
|
+
this.debug('History Entries: no entry')
|
|
625
|
+
this.vdebug('History Entries: send data: 00')
|
|
626
|
+
this._transfer = false
|
|
627
|
+
return Buffer.from('00', 'hex').toString('base64')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const buffer = Buffer.alloc(1024)
|
|
631
|
+
let offset = 0
|
|
632
|
+
for (let i = 0; i < 11; i++) {
|
|
633
|
+
const index = this._h.memorySize === 0
|
|
634
|
+
? 1
|
|
635
|
+
: (this._currentEntry - this._h.entryOffset) % this._h.memorySize
|
|
636
|
+
if (this._currentEntry === this._h.firstEntry) {
|
|
637
|
+
this.debug(
|
|
638
|
+
'History Entries (%d/11): entry %d (index: %d): %j (%s)',
|
|
639
|
+
i, this._currentEntry, index, { initialTime: this._h.initialTime - epoch },
|
|
640
|
+
dateToString(this._h.initialTime)
|
|
641
|
+
)
|
|
642
|
+
buffer.writeUInt8(21, offset); offset += 1
|
|
643
|
+
buffer.writeUInt32LE(this._currentEntry, offset); offset += 4
|
|
644
|
+
buffer.write('0100000081', offset, 'hex'); offset += 5
|
|
645
|
+
buffer.writeUInt32LE(this._h.initialTime - epoch, offset); offset += 4
|
|
646
|
+
buffer.write('00000000000000', offset, 'hex'); offset += 7
|
|
647
|
+
} else {
|
|
648
|
+
const entry = this._h.entries[index]
|
|
649
|
+
this.debug(
|
|
650
|
+
'History Entries (%d/11): entry %d (index: %d): %j (%s)',
|
|
651
|
+
i, this._currentEntry, index, entry, dateToString(entry.time)
|
|
652
|
+
)
|
|
653
|
+
const b = this.entryToBuffer(entry)
|
|
654
|
+
buffer.writeUInt8(b.length + 9, offset); offset += 1
|
|
655
|
+
buffer.writeUInt32LE(this._currentEntry, offset); offset += 4
|
|
656
|
+
buffer.writeUInt32LE(entry.time - this._h.initialTime, offset); offset += 4
|
|
657
|
+
b.copy(buffer, offset); offset += b.length
|
|
658
|
+
}
|
|
659
|
+
this._currentEntry++
|
|
660
|
+
if (this._currentEntry > this._h.lastEntry) {
|
|
661
|
+
break
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const value = buffer.slice(0, offset)
|
|
665
|
+
this.vdebug('History Entries: send data: %s', value.toString('hex'))
|
|
666
|
+
return value.toString('base64')
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async _onSetConfig (value) {
|
|
670
|
+
const buffer = Buffer.from(value, 'base64')
|
|
671
|
+
this.vdebug('Config Request changed to %j', buffer.toString('hex'))
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async _onGetConfig () {
|
|
675
|
+
return Buffer.from('D200', 'hex').toString('base64')
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
module.exports = History
|