homebridge-nest-accfactory 0.0.4-a

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +176 -0
  3. package/README.md +121 -0
  4. package/config.schema.json +107 -0
  5. package/dist/HomeKitDevice.js +441 -0
  6. package/dist/HomeKitHistory.js +2835 -0
  7. package/dist/camera.js +1276 -0
  8. package/dist/doorbell.js +122 -0
  9. package/dist/index.js +35 -0
  10. package/dist/nexustalk.js +741 -0
  11. package/dist/protect.js +240 -0
  12. package/dist/protobuf/google/rpc/status.proto +91 -0
  13. package/dist/protobuf/google/rpc/stream_body.proto +26 -0
  14. package/dist/protobuf/google/trait/product/camera.proto +53 -0
  15. package/dist/protobuf/googlehome/foyer.proto +208 -0
  16. package/dist/protobuf/nest/messages.proto +8 -0
  17. package/dist/protobuf/nest/services/apigateway.proto +107 -0
  18. package/dist/protobuf/nest/trait/audio.proto +7 -0
  19. package/dist/protobuf/nest/trait/cellular.proto +313 -0
  20. package/dist/protobuf/nest/trait/debug.proto +37 -0
  21. package/dist/protobuf/nest/trait/detector.proto +41 -0
  22. package/dist/protobuf/nest/trait/diagnostics.proto +87 -0
  23. package/dist/protobuf/nest/trait/firmware.proto +221 -0
  24. package/dist/protobuf/nest/trait/guest.proto +105 -0
  25. package/dist/protobuf/nest/trait/history.proto +345 -0
  26. package/dist/protobuf/nest/trait/humanlibrary.proto +19 -0
  27. package/dist/protobuf/nest/trait/hvac.proto +1353 -0
  28. package/dist/protobuf/nest/trait/input.proto +29 -0
  29. package/dist/protobuf/nest/trait/lighting.proto +61 -0
  30. package/dist/protobuf/nest/trait/located.proto +193 -0
  31. package/dist/protobuf/nest/trait/media.proto +68 -0
  32. package/dist/protobuf/nest/trait/network.proto +352 -0
  33. package/dist/protobuf/nest/trait/occupancy.proto +373 -0
  34. package/dist/protobuf/nest/trait/olive.proto +15 -0
  35. package/dist/protobuf/nest/trait/pairing.proto +85 -0
  36. package/dist/protobuf/nest/trait/product/camera.proto +283 -0
  37. package/dist/protobuf/nest/trait/product/detect.proto +67 -0
  38. package/dist/protobuf/nest/trait/product/doorbell.proto +18 -0
  39. package/dist/protobuf/nest/trait/product/guard.proto +59 -0
  40. package/dist/protobuf/nest/trait/product/protect.proto +344 -0
  41. package/dist/protobuf/nest/trait/promonitoring.proto +14 -0
  42. package/dist/protobuf/nest/trait/resourcedirectory.proto +32 -0
  43. package/dist/protobuf/nest/trait/safety.proto +119 -0
  44. package/dist/protobuf/nest/trait/security.proto +516 -0
  45. package/dist/protobuf/nest/trait/selftest.proto +78 -0
  46. package/dist/protobuf/nest/trait/sensor.proto +291 -0
  47. package/dist/protobuf/nest/trait/service.proto +46 -0
  48. package/dist/protobuf/nest/trait/structure.proto +85 -0
  49. package/dist/protobuf/nest/trait/system.proto +51 -0
  50. package/dist/protobuf/nest/trait/test.proto +15 -0
  51. package/dist/protobuf/nest/trait/ui.proto +65 -0
  52. package/dist/protobuf/nest/trait/user.proto +98 -0
  53. package/dist/protobuf/nest/trait/voiceassistant.proto +30 -0
  54. package/dist/protobuf/nestlabs/eventingapi/v1.proto +83 -0
  55. package/dist/protobuf/nestlabs/gateway/v1.proto +273 -0
  56. package/dist/protobuf/nestlabs/gateway/v2.proto +96 -0
  57. package/dist/protobuf/nestlabs/history/v1.proto +73 -0
  58. package/dist/protobuf/root.proto +64 -0
  59. package/dist/protobuf/wdl-event-importance.proto +11 -0
  60. package/dist/protobuf/wdl.proto +450 -0
  61. package/dist/protobuf/weave/common.proto +144 -0
  62. package/dist/protobuf/weave/trait/audio.proto +12 -0
  63. package/dist/protobuf/weave/trait/auth.proto +22 -0
  64. package/dist/protobuf/weave/trait/description.proto +32 -0
  65. package/dist/protobuf/weave/trait/heartbeat.proto +38 -0
  66. package/dist/protobuf/weave/trait/locale.proto +20 -0
  67. package/dist/protobuf/weave/trait/network.proto +24 -0
  68. package/dist/protobuf/weave/trait/pairing.proto +8 -0
  69. package/dist/protobuf/weave/trait/peerdevices.proto +18 -0
  70. package/dist/protobuf/weave/trait/power.proto +86 -0
  71. package/dist/protobuf/weave/trait/schedule.proto +76 -0
  72. package/dist/protobuf/weave/trait/security.proto +343 -0
  73. package/dist/protobuf/weave/trait/telemetry/tunnel.proto +37 -0
  74. package/dist/protobuf/weave/trait/time.proto +16 -0
  75. package/dist/res/Nest_camera_connecting.h264 +0 -0
  76. package/dist/res/Nest_camera_connecting.jpg +0 -0
  77. package/dist/res/Nest_camera_off.h264 +0 -0
  78. package/dist/res/Nest_camera_off.jpg +0 -0
  79. package/dist/res/Nest_camera_offline.h264 +0 -0
  80. package/dist/res/Nest_camera_offline.jpg +0 -0
  81. package/dist/res/Nest_camera_transfer.jpg +0 -0
  82. package/dist/streamer.js +344 -0
  83. package/dist/system.js +3112 -0
  84. package/dist/tempsensor.js +99 -0
  85. package/dist/thermostat.js +1026 -0
  86. package/dist/weather.js +205 -0
  87. package/dist/webrtc.js +55 -0
  88. package/package.json +66 -0
@@ -0,0 +1,2835 @@
1
+ // HomeKit history service
2
+ // Simple history service for HomeKit developed accessories with HAP-NodeJS
3
+ //
4
+ // todo (EveHome integration)
5
+ // -- get history to show for motion when attached to a smoke sensor
6
+ // -- get history to show for smoke when attached to a smoke sensor
7
+ // -- thermo valve protection
8
+ // -- Eve Degree/Weather2 history
9
+ // -- Eve Water guard history
10
+ //
11
+ // Version 29/8/2024
12
+ // Mark Hulskamp
13
+
14
+ // Define nodejs module requirements
15
+ import { setTimeout } from 'node:timers';
16
+ import { Buffer } from 'node:buffer';
17
+ import util from 'util';
18
+ import fs from 'fs';
19
+
20
+ // Define constants
21
+ const MAX_HISTORY_SIZE = 16384; // 16k entries
22
+ const EPOCH_OFFSET = 978307200; // Seconds since 1/1/1970 to 1/1/2001
23
+ const EVEHOME_MAX_STREAM = 11; // Maximum number of history events we can stream to EveHome
24
+ const DAYSOFWEEK = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
25
+
26
+ // Create the history object
27
+ export default class HomeKitHistory {
28
+ accessory = undefined; // Accessory service for this history
29
+ hap = undefined; // HomeKit Accessory Protocol API stub
30
+ log = undefined; // Logging function object
31
+ maxEntries = MAX_HISTORY_SIZE; // used for rolling history. if 0, means no rollover
32
+ EveHome = undefined;
33
+
34
+ constructor(accessory, log, api, options) {
35
+ // Validate the passed in logging object. We are expecting certain functions to be present
36
+ if (
37
+ typeof log?.info === 'function' &&
38
+ typeof log?.success === 'function' &&
39
+ typeof log?.warn === 'function' &&
40
+ typeof log?.error === 'function' &&
41
+ typeof log?.debug === 'function'
42
+ ) {
43
+ this.log = log;
44
+ }
45
+
46
+ if (typeof accessory !== 'undefined' && typeof accessory === 'object') {
47
+ this.accessory = accessory;
48
+ }
49
+
50
+ if (typeof options === 'object') {
51
+ if (typeof options?.maxEntries === 'number') {
52
+ this.maxEntries = options.maxEntries;
53
+ }
54
+ }
55
+
56
+ // Workout if we're running under HomeBridge or HAP-NodeJS library
57
+ if (typeof api?.version === 'number' && typeof api?.hap === 'object' && typeof api?.HAPLibraryVersion === 'undefined') {
58
+ // We have the HomeBridge version number and hap API object
59
+ this.hap = api.hap;
60
+ }
61
+
62
+ if (typeof api?.HAPLibraryVersion === 'function' && typeof api?.version === 'undefined' && typeof api?.hap === 'undefined') {
63
+ // As we're missing the HomeBridge entry points but have the HAP library version
64
+ this.hap = api;
65
+ }
66
+
67
+ // Dynamically create the additional services and characteristics
68
+ this.#createHomeKitServicesAndCharacteristics();
69
+
70
+ // Setup HomeKitHistory using HAP-NodeJS library
71
+ if (typeof accessory?.username !== 'undefined') {
72
+ // Since we have a username for the accessory, we'll assume this is not running under Homebridge
73
+ // We'll use it's persist folder for storing history files
74
+ this.storageKey = util.format('History.%s.json', accessory.username.replace(/:/g, '').toUpperCase());
75
+ }
76
+
77
+ // Setup HomeKitHistory under Homebridge
78
+ if (typeof accessory?.username === 'undefined') {
79
+ this.storageKey = util.format('History.%s.json', accessory.UUID);
80
+ }
81
+
82
+ this.storage = this.hap.HAPStorage.storage();
83
+
84
+ this.historyData = this.storage.getItem(this.storageKey);
85
+ if (typeof this.historyData !== 'object') {
86
+ // Getting storage key didnt return an object, we'll assume no history present, so start new history for this accessory
87
+ this.resetHistory(); // Start with blank history
88
+ }
89
+
90
+ this.restart = Math.floor(Date.now() / 1000); // time we restarted
91
+
92
+ // perform rollover if needed when starting service
93
+ if (this.maxEntries !== 0 && this.historyData.next >= this.maxEntries) {
94
+ this.rolloverHistory();
95
+ }
96
+ }
97
+
98
+ // Class functions
99
+ addHistory(service, entry, timegap) {
100
+ // we'll use the service or characteristic UUID to determine the history entry time and data we'll add
101
+ // reformat the entry object to order the fields consistantly in the output
102
+ // Add new history types in the switch statement
103
+ let historyEntry = {};
104
+ if (this.restart !== null && typeof entry.restart === 'undefined') {
105
+ // Object recently created, so log the time restarted our history service
106
+ entry.restart = this.restart;
107
+ this.restart = null;
108
+ }
109
+ if (typeof entry.time === 'undefined') {
110
+ // No logging time was passed in, so set
111
+ entry.time = Math.floor(Date.now() / 1000);
112
+ }
113
+ if (typeof service.subtype === 'undefined') {
114
+ service.subtype = 0;
115
+ }
116
+ if (typeof timegap === 'undefined') {
117
+ timegap = 0; // Zero minimum time gap between entries
118
+ }
119
+ switch (service.UUID) {
120
+ case this.hap.Service.GarageDoorOpener.UUID: {
121
+ // Garage door history
122
+ // entry.time => unix time in seconds
123
+ // entry.status => 0 = closed, 1 = open
124
+ historyEntry.status = entry.status;
125
+ if (typeof entry.restart !== 'undefined') {
126
+ historyEntry.restart = entry.restart;
127
+ }
128
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
129
+ break;
130
+ }
131
+
132
+ case this.hap.Service.MotionSensor.UUID: {
133
+ // Motion sensor history
134
+ // entry.time => unix time in seconds
135
+ // entry.status => 0 = motion cleared, 1 = motion detected
136
+ historyEntry.status = entry.status;
137
+ if (typeof entry.restart !== 'undefined') {
138
+ historyEntry.restart = entry.restart;
139
+ }
140
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
141
+ break;
142
+ }
143
+
144
+ case this.hap.Service.Window.UUID:
145
+ case this.hap.Service.WindowCovering.UUID: {
146
+ // Window and Window Covering history
147
+ // entry.time => unix time in seconds
148
+ // entry.status => 0 = closed, 1 = open
149
+ // entry.position => position in % 0% = closed 100% fully open
150
+ historyEntry.status = entry.status;
151
+ historyEntry.position = entry.position;
152
+ if (typeof entry.restart !== 'undefined') {
153
+ historyEntry.restart = entry.restart;
154
+ }
155
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
156
+ break;
157
+ }
158
+
159
+ case this.hap.Service.HeaterCooler.UUID:
160
+ case this.hap.Service.Thermostat.UUID: {
161
+ // Thermostat and Heater/Cooler history
162
+ // entry.time => unix time in seconds
163
+ // entry.status => 0 = off, 1 = fan, 2 = heating, 3 = cooling, 4 = dehumidifying
164
+ // entry.temperature => current temperature in degress C
165
+ // entry.target => {low, high} = cooling limit, heating limit
166
+ // entry.humidity => current humidity
167
+ historyEntry.status = entry.status;
168
+ historyEntry.temperature = entry.temperature;
169
+ historyEntry.target = entry.target;
170
+ historyEntry.humidity = entry.humidity;
171
+ if (typeof entry.restart !== 'undefined') {
172
+ historyEntry.restart = entry.restart;
173
+ }
174
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
175
+ break;
176
+ }
177
+
178
+ case this.hap.Service.EveAirPressureSensor.UUID:
179
+ case this.hap.Service.AirQualitySensor.UUID:
180
+ case this.hap.Service.TemperatureSensor.UUID: {
181
+ // Temperature sensor history
182
+ // entry.time => unix time in seconds
183
+ // entry.temperature => current temperature in degress C
184
+ // entry.humidity => current humidity
185
+ // optional (entry.ppm)
186
+ // optional (entry.voc => current VOC measurement in ppb)\
187
+ // optional (entry.pressure -> in hpa)
188
+ historyEntry.temperature = entry.temperature;
189
+ if (typeof entry.humidity === 'undefined') {
190
+ // fill out humidity if missing
191
+ entry.humidity = 0;
192
+ }
193
+ if (typeof entry.ppm === 'undefined') {
194
+ // fill out ppm if missing
195
+ entry.ppm = 0;
196
+ }
197
+ if (typeof entry.voc === 'undefined') {
198
+ // fill out voc if missing
199
+ entry.voc = 0;
200
+ }
201
+ if (typeof entry.pressure === 'undefined') {
202
+ // fill out pressure if missing
203
+ entry.pressure = 0;
204
+ }
205
+ historyEntry.temperature = entry.temperature;
206
+ historyEntry.humidity = entry.humidity;
207
+ historyEntry.ppm = entry.ppm;
208
+ historyEntry.voc = entry.voc;
209
+ historyEntry.pressure = entry.pressure;
210
+ if (typeof entry.restart !== 'undefined') {
211
+ historyEntry.restart = entry.restart;
212
+ }
213
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
214
+ break;
215
+ }
216
+
217
+ case this.hap.Service.Valve.UUID: {
218
+ // Water valve history
219
+ // entry.time => unix time in seconds
220
+ // entry.status => 0 = valve closed, 1 = valve opened
221
+ // entry.water => amount of water in L's
222
+ // entry.duration => time for water amount
223
+ historyEntry.status = entry.status;
224
+ historyEntry.water = entry.water;
225
+ historyEntry.duration = entry.duration;
226
+ if (typeof entry.restart !== 'undefined') {
227
+ historyEntry.restart = entry.restart;
228
+ }
229
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
230
+ break;
231
+ }
232
+
233
+ case this.hap.Characteristic.WaterLevel.UUID: {
234
+ // Water level history
235
+ // entry.time => unix time in seconds
236
+ // entry.level => water level as percentage
237
+ historyEntry.level = entry.level;
238
+ if (typeof entry.restart !== 'undefined') {
239
+ historyEntry.restart = entry.restart;
240
+ }
241
+ this.#addEntry(service.UUID, 0, entry.time, timegap, historyEntry); // Characteristics don't have sub type, so we'll use 0 for it
242
+ break;
243
+ }
244
+
245
+ case this.hap.Service.LeakSensor.UUID: {
246
+ // Leak sensor history
247
+ // entry.time => unix time in seconds
248
+ // entry.status => 0 = no leak, 1 = leak
249
+ historyEntry.status = entry.status;
250
+ if (typeof entry.restart !== 'undefined') {
251
+ historyEntry.restart = entry.restart;
252
+ }
253
+ this.#addEntry(service.UUID, 0, entry.time, timegap, historyEntry); // Characteristics don't have sub type, so we'll use 0 for it
254
+ break;
255
+ }
256
+
257
+ case this.hap.Service.Outlet.UUID: {
258
+ // Power outlet history
259
+ // entry.time => unix time in seconds
260
+ // entry.status => 0 = off, 1 = on
261
+ // entry.volts => voltage in Vs
262
+ // entry.watts => watts in W's
263
+ // entry.amps => current in A's
264
+ historyEntry.status = entry.status;
265
+ historyEntry.volts = entry.volts;
266
+ historyEntry.watts = entry.watts;
267
+ historyEntry.amps = entry.amps;
268
+ if (typeof entry.restart !== 'undefined') {
269
+ historyEntry.restart = entry.restart;
270
+ }
271
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
272
+ break;
273
+ }
274
+
275
+ case this.hap.Service.Doorbell.UUID: {
276
+ // Doorbell press history
277
+ // entry.time => unix time in seconds
278
+ // entry.status => 0 = not pressed, 1 = doorbell pressed
279
+ historyEntry.status = entry.status;
280
+ if (typeof entry.restart !== 'undefined') {
281
+ historyEntry.restart = entry.restart;
282
+ }
283
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
284
+ break;
285
+ }
286
+
287
+ case this.hap.Service.SmokeSensor.UUID: {
288
+ // Smoke sensor history
289
+ // entry.time => unix time in seconds
290
+ // entry.status => 0 = smoke cleared, 1 = smoke detected
291
+ historyEntry.status = entry.status;
292
+ if (typeof historyEntry.restart !== 'undefined') {
293
+ historyEntry.restart = entry.restart;
294
+ }
295
+ this.#addEntry(service.UUID, service.subtype, entry.time, timegap, historyEntry);
296
+ break;
297
+ }
298
+ }
299
+ }
300
+
301
+ resetHistory() {
302
+ // Reset history to nothing
303
+ this.historyData = {};
304
+ this.historyData.reset = Math.floor(Date.now() / 1000); // time history was reset
305
+ this.historyData.rollover = 0; // no last rollover time
306
+ this.historyData.next = 0; // next entry for history is at start
307
+ this.historyData.types = []; // no service types in history
308
+ this.historyData.data = []; // no history data
309
+ this.storage.setItem(this.storageKey, this.historyData);
310
+ }
311
+
312
+ rolloverHistory() {
313
+ // Roll history over and start from zero.
314
+ // We'll include an entry as to when the rollover took place
315
+ // remove all history data after the rollover entry
316
+ this.historyData.data.splice(this.maxEntries, this.historyData.data.length);
317
+ this.historyData.rollover = Math.floor(Date.now() / 1000);
318
+ this.historyData.next = 0;
319
+ this.#updateHistoryTypes();
320
+ this.storage.setItem(this.storageKey, this.historyData);
321
+ }
322
+
323
+ #addEntry(type, sub, time, timegap, entry) {
324
+ let historyEntry = {};
325
+ let recordEntry = true; // always record entry unless we don't need to
326
+ historyEntry.time = time;
327
+ historyEntry.type = type;
328
+ historyEntry.sub = sub;
329
+ Object.entries(entry).forEach(([key, value]) => {
330
+ if (key !== 'time' || key !== 'type' || key !== 'sub') {
331
+ // Filer out events we want to control
332
+ historyEntry[key] = value;
333
+ }
334
+ });
335
+
336
+ // If we have a minimum time gap specified, find the last time entry for this type and if less than min gap, ignore
337
+ if (timegap !== 0) {
338
+ let typeIndex = this.historyData.types.findIndex((type) => type.type === historyEntry.type && type.sub === historyEntry.sub);
339
+ if (
340
+ typeIndex >= 0 &&
341
+ time - this.historyData.data[this.historyData.types[typeIndex].lastEntry].time < timegap &&
342
+ typeof historyEntry.restart === 'undefined'
343
+ ) {
344
+ // time between last recorded entry and new entry is less than minimum gap and its not a 'restart' entry
345
+ // so don't log it
346
+ recordEntry = false;
347
+ }
348
+ }
349
+
350
+ if (recordEntry === true) {
351
+ // Work out where this goes in the history data array
352
+ if (this.maxEntries !== 0 && this.historyData.next >= this.maxEntries) {
353
+ // roll over history data as we've reached the defined max entry size
354
+ this.rolloverHistory();
355
+ }
356
+ this.historyData.data[this.historyData.next] = historyEntry;
357
+ this.historyData.next++;
358
+
359
+ // Update types we have in history. This will just be the main type and its latest location in history
360
+ let typeIndex = this.historyData.types.findIndex((type) => type.type === historyEntry.type && type.sub === historyEntry.sub);
361
+ if (typeIndex === -1) {
362
+ this.historyData.types.push({ type: historyEntry.type, sub: historyEntry.sub, lastEntry: this.historyData.next - 1 });
363
+ } else {
364
+ this.historyData.types[typeIndex].lastEntry = this.historyData.next - 1;
365
+ }
366
+
367
+ // Validate types last entries. Helps with rolled over data etc. If we cannot find the type anymore, remove from known types
368
+ this.historyData.types.forEach((typeEntry, index) => {
369
+ if (this.historyData.data[typeEntry.lastEntry].type !== typeEntry.type) {
370
+ // not found, so remove from known types
371
+ this.historyData.types.splice(index, 1);
372
+ }
373
+ });
374
+
375
+ this.storage.setItem(this.storageKey, this.historyData); // Save to persistent storage
376
+ }
377
+ }
378
+
379
+ getHistory(service, subtype, specifickey) {
380
+ // returns a JSON object of all history for this service and subtype
381
+ // handles if we've rolled over history also
382
+ let tempHistory = [];
383
+ let findUUID = null;
384
+ let findSub = null;
385
+ if (typeof subtype !== 'undefined' && subtype !== null) {
386
+ findSub = subtype;
387
+ }
388
+ if (typeof service !== 'object') {
389
+ // passed in UUID byself, rather than service object
390
+ findUUID = service;
391
+ }
392
+ if (typeof service?.UUID === 'string') {
393
+ findUUID = service.UUID;
394
+ }
395
+ if (typeof service.subtype === 'undefined' && typeof subtype === 'undefined') {
396
+ findSub = 0;
397
+ }
398
+ tempHistory = tempHistory.concat(
399
+ this.historyData.data.slice(this.historyData.next, this.historyData.data.length),
400
+ this.historyData.data.slice(0, this.historyData.next),
401
+ );
402
+ tempHistory = tempHistory.filter((historyEntry) => {
403
+ if (typeof specifickey === 'object' && Object.keys(specifickey).length === 1) {
404
+ // limit entry to a specifc key type value if specified
405
+ if (
406
+ (findSub === null && historyEntry.type === findUUID && historyEntry[Object.keys(specifickey)] === Object.values(specifickey)) ||
407
+ (findSub !== null &&
408
+ historyEntry.type === findUUID &&
409
+ historyEntry.sub === findSub &&
410
+ historyEntry[Object.keys(specifickey)] === Object.values(specifickey))
411
+ ) {
412
+ return historyEntry;
413
+ }
414
+ } else if (
415
+ (findSub === null && historyEntry.type === findUUID) ||
416
+ (findSub !== null && historyEntry.type === findUUID && historyEntry.sub === findSub)
417
+ ) {
418
+ return historyEntry;
419
+ }
420
+ });
421
+ return tempHistory;
422
+ }
423
+
424
+ generateCSV(service, csvfile) {
425
+ // Generates a CSV file for use in applications such as Numbers/Excel for graphing
426
+ // we get all the data for the service, ignoring the specific subtypes
427
+ let tempHistory = this.getHistory(service, null); // all history
428
+ if (tempHistory.length !== 0) {
429
+ let writer = fs.createWriteStream(csvfile, { flags: 'w', autoClose: 'true' });
430
+ if (writer !== null) {
431
+ // write header, we'll use the first record keys for the header keys
432
+ let header = 'time,subtype';
433
+ Object.keys(tempHistory[0]).forEach((key) => {
434
+ if (key !== 'time' && key !== 'type' && key !== 'sub' && key !== 'restart') {
435
+ header = header + ',' + key;
436
+ }
437
+ });
438
+ writer.write(header + '\n');
439
+
440
+ // write data
441
+ // Date/Time converted into local timezone
442
+ tempHistory.forEach((historyEntry) => {
443
+ let csvline = new Date(historyEntry.time * 1000).toLocaleString().replace(',', '') + ',' + historyEntry.sub;
444
+ Object.entries(historyEntry).forEach(([key, value]) => {
445
+ if (key !== 'time' && key !== 'type' && key !== 'sub' && key !== 'restart') {
446
+ csvline = csvline + ',' + value;
447
+ }
448
+ });
449
+ writer.write(csvline + '\n');
450
+ });
451
+ writer.end();
452
+ }
453
+ }
454
+ }
455
+
456
+ lastHistory(service, subtype) {
457
+ // returns the last history event for this service type and subtype
458
+ let findUUID = null;
459
+ let findSub = null;
460
+ if (typeof subtype !== 'undefined') {
461
+ findSub = subtype;
462
+ }
463
+ if (typeof service !== 'object') {
464
+ // passed in UUID byself, rather than service object
465
+ findUUID = service;
466
+ }
467
+ if (typeof service?.UUID === 'string') {
468
+ findUUID = service.UUID;
469
+ }
470
+ if (typeof service.subtype === 'undefined' && typeof subtype === 'undefined') {
471
+ findSub = 0;
472
+ }
473
+
474
+ // If subtype is 'null' find newest event based on time
475
+ let typeIndex = this.historyData.types.findIndex(
476
+ (type) => (type.type === findUUID && type.sub === findSub && subtype !== null) || (type.type === findUUID && subtype === null),
477
+ );
478
+ return typeIndex !== -1 ? this.historyData.data[this.historyData.types[typeIndex].lastEntry] : null;
479
+ }
480
+
481
+ entryCount(service, subtype, specifickey) {
482
+ // returns the number of history entries for this service type and subtype
483
+ // can can also be limited to a specific key value
484
+ let tempHistory = this.getHistory(service, subtype, specifickey);
485
+ return tempHistory.length;
486
+ }
487
+
488
+ #updateHistoryTypes() {
489
+ // Builds the known history types and last entry in current history data
490
+ // Might be time consuming.....
491
+ this.historyData.types = [];
492
+ for (let index = this.historyData.data.length - 1; index > 0; index--) {
493
+ if (
494
+ this.historyData.types.findIndex(
495
+ (type) =>
496
+ (typeof type.sub !== 'undefined' &&
497
+ type.type === this.historyData.data[index].type &&
498
+ type.sub === this.historyData.data[index].sub) ||
499
+ (typeof type.sub === 'undefined' && type.type === this.historyData.data[index].type),
500
+ ) === -1
501
+ ) {
502
+ this.historyData.types.push({ type: this.historyData.data[index].type, sub: this.historyData.data[index].sub, lastEntry: index });
503
+ }
504
+ }
505
+ }
506
+
507
+ // Overlay EveHome service, characteristics and functions
508
+ // Alot of code taken from fakegato https://github.com/simont77/fakegato-history
509
+ // references from https://github.com/ebaauw/homebridge-lib/blob/master/lib/EveHomeKitTypes.js
510
+ //
511
+
512
+ // Overlay our history into EveHome. Can only have one service history exposed to EveHome (ATM... see if can work around)
513
+ // Returns object created for our EveHome accessory if successfull
514
+ linkToEveHome(service, options) {
515
+ if (typeof service !== 'object' || typeof this?.EveHome?.service !== 'undefined') {
516
+ return;
517
+ }
518
+
519
+ if (typeof options !== 'object') {
520
+ options = {};
521
+ }
522
+
523
+ switch (service.UUID) {
524
+ case this.hap.Service.ContactSensor.UUID:
525
+ case this.hap.Service.Door.UUID:
526
+ case this.hap.Service.Window.UUID:
527
+ case this.hap.Service.GarageDoorOpener.UUID: {
528
+ // treat these as EveHome Door
529
+ // Inverse status used for all UUID types except this.hap.Service.ContactSensor.UUID
530
+
531
+ // Setup the history service and the required characteristics for this service UUID type
532
+ // Callbacks setup below after this is created
533
+ let historyService = this.#createHistoryService(service, [
534
+ this.hap.Characteristic.EveLastActivation,
535
+ this.hap.Characteristic.EveOpenDuration,
536
+ this.hap.Characteristic.EveTimesOpened,
537
+ ]);
538
+
539
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
540
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
541
+ if (tempHistory.length !== 0) {
542
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
543
+ }
544
+
545
+ this.EveHome = {
546
+ service: historyService,
547
+ linkedservice: service,
548
+ type: service.UUID,
549
+ sub: service.subtype,
550
+ evetype: service.UUID === this.hap.Service.ContactSensor.UUID ? 'contact' : 'door',
551
+ fields: '0601',
552
+ entry: 0,
553
+ count: tempHistory.length,
554
+ reftime: historyreftime,
555
+ send: 0,
556
+ };
557
+
558
+ // Setup initial values and callbacks for charateristics we are using
559
+ service.updateCharacteristic(
560
+ this.hap.Characteristic.EveTimesOpened,
561
+ this.entryCount(this.EveHome.type, this.EveHome.sub, { status: 1 }),
562
+ ); // Count of entries based upon status = 1, opened
563
+ service.updateCharacteristic(this.hap.Characteristic.EveLastActivation, this.#EveLastEventTime());
564
+
565
+ // Setup callbacks for characteristics
566
+ service.getCharacteristic(this.hap.Characteristic.EveTimesOpened).onGet(() => {
567
+ return this.entryCount(this.EveHome.type, this.EveHome.sub, { status: 1 }); // Count of entries based upon status = 1, opened
568
+ });
569
+
570
+ service.getCharacteristic(this.hap.Characteristic.EveLastActivation).onGet(() => {
571
+ return this.#EveLastEventTime(); // time of last event in seconds since first event
572
+ });
573
+ break;
574
+ }
575
+
576
+ case this.hap.Service.WindowCovering.UUID: {
577
+ // Treat as Eve MotionBlinds
578
+
579
+ // Setup the history service and the required characteristics for this service UUID type
580
+ // Callbacks setup below after this is created
581
+ let historyService = this.#createHistoryService(service, [
582
+ this.hap.Characteristic.EveGetConfiguration,
583
+ this.hap.Characteristic.EveSetConfiguration,
584
+ ]);
585
+
586
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
587
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
588
+ if (tempHistory.length !== 0) {
589
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
590
+ }
591
+
592
+ this.EveHome = {
593
+ service: historyService,
594
+ linkedservice: service,
595
+ type: service.UUID,
596
+ sub: service.subtype,
597
+ evetype: 'blind',
598
+ fields: '1702 1802 1901',
599
+ entry: 0,
600
+ count: tempHistory.length,
601
+ reftime: historyreftime,
602
+ send: 0,
603
+ };
604
+
605
+ //17 CurrentPosition
606
+ //18 TargetPosition
607
+ //19 PositionState
608
+
609
+ service.getCharacteristic(this.hap.Characteristic.EveGetConfiguration).onGet(() => {
610
+ let value = util.format(
611
+ '0002 5500 0302 %s 9b04 %s 1e02 5500 0c',
612
+ numberToEveHexString(2979, 4), // firmware version (build xxxx)
613
+ numberToEveHexString(Math.floor(Date.now() / 1000), 8),
614
+ ); // 'now' time
615
+
616
+ return encodeEveData(value);
617
+ });
618
+
619
+ service.getCharacteristic(this.hap.Characteristic.EveSetConfiguration).onSet((value) => {
620
+ //let processedData = {};
621
+ let valHex = decodeEveData(value);
622
+ let index = 0;
623
+
624
+ //console.log('EveSetConfiguration', valHex);
625
+
626
+ while (index < valHex.length) {
627
+ // first byte is command in this data stream
628
+ // second byte is size of data for command
629
+ let command = valHex.substr(index, 2);
630
+ let size = parseInt(valHex.substr(index + 2, 2), 16) * 2;
631
+ let data = valHex.substr(index + 4, parseInt(valHex.substr(index + 2, 2), 16) * 2);
632
+ switch (command) {
633
+ case '00': {
634
+ // end of command?
635
+ break;
636
+ }
637
+
638
+ case 'f0': {
639
+ // set limits
640
+ // data
641
+ // 02 bottom position set
642
+ // 01 top position set
643
+ // 04 favourite position set
644
+ break;
645
+ }
646
+
647
+ case 'f1': {
648
+ // orientation set??
649
+ break;
650
+ }
651
+
652
+ case 'f3': {
653
+ // move window covering to set limits
654
+ // xxyyyy - xx = move command (01 = up, 02 = down, 03 = stop), yyyy - distance/time/ticks/increment to move??
655
+ //let moveCommand = data.substring(0, 2);
656
+ //let moveAmount = EveHexStringToNumber(data.substring(2));
657
+
658
+ //console.log('move', moveCommand, moveAmount);
659
+
660
+ let currentPosition = service.getCharacteristic(this.hap.Characteristic.CurrentPosition).value;
661
+ if (data === '015802') {
662
+ currentPosition = currentPosition + 1;
663
+ }
664
+ if (data === '025802') {
665
+ currentPosition = currentPosition - 1;
666
+ }
667
+ //console.log('move', currentPosition, data);
668
+ service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, currentPosition);
669
+ service.updateCharacteristic(this.hap.Characteristic.TargetPosition, currentPosition);
670
+ break;
671
+ }
672
+
673
+ default: {
674
+ this?.log?.debug && this.log.debug('Unknown Eve MotionBlinds command "%s" with data "%s"', command, data);
675
+ break;
676
+ }
677
+ }
678
+ index += 4 + size; // Move to next command accounting for header size of 4 bytes
679
+ }
680
+ });
681
+ break;
682
+ }
683
+
684
+ case this.hap.Service.HeaterCooler.UUID:
685
+ case this.hap.Service.Thermostat.UUID: {
686
+ // treat these as EveHome Thermo
687
+
688
+ // Setup the history service and the required characteristics for this service UUID type
689
+ // Callbacks setup below after this is created
690
+ let historyService = this.#createHistoryService(service, [
691
+ this.hap.Characteristic.EveValvePosition,
692
+ this.hap.Characteristic.EveFirmware,
693
+ this.hap.Characteristic.EveProgramData,
694
+ this.hap.Characteristic.EveProgramCommand,
695
+ this.hap.Characteristic.StatusActive,
696
+ this.hap.Characteristic.CurrentTemperature,
697
+ this.hap.Characteristic.TemperatureDisplayUnits,
698
+ this.hap.Characteristic.LockPhysicalControls,
699
+ ]);
700
+
701
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
702
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
703
+ if (tempHistory.length !== 0) {
704
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
705
+ }
706
+
707
+ this.EveHome = {
708
+ service: historyService,
709
+ linkedservice: service,
710
+ type: service.UUID,
711
+ sub: service.subtype,
712
+ evetype: 'thermo',
713
+ fields: '0102 0202 1102 1001 1201 1d01',
714
+ entry: 0,
715
+ count: tempHistory.length,
716
+ reftime: historyreftime,
717
+ send: 0,
718
+ };
719
+
720
+ // Need some internal storage to track Eve Thermo configuration from EveHome app
721
+ this.EveThermoPersist = {
722
+ firmware: typeof options?.EveThermo_firmware === 'number' ? options.EveThermo_firmware : 1251, // 1251 (2015), 2834 (2020) thermo
723
+ attached: options?.EveThermo_attached === true, // attached to base?
724
+ tempoffset: typeof options?.EveThermo_tempoffset === 'number' ? options.EveThermo_tempoffset : -2.5, // Temperature offset
725
+ enableschedule: options?.EveThermo_enableschedule === true, // Schedules on/off
726
+ pause: options?.EveThermo_pause === true, // Paused on/off
727
+ vacation: options?.EveThermo_vacation === true, // Vacation status - disabled ie: Home
728
+ vacationtemp: typeof options?.EveThermo_vacationtemp === 'number' ? options.EveThermo_vactiontemp : null, // Vacation temp
729
+ programs: typeof options?.EveThermo_programs === 'object' ? options.EveThermo_programs : [],
730
+ };
731
+
732
+ // Setup initial values and callbacks for charateristics we are using
733
+ service.updateCharacteristic(
734
+ this.hap.Characteristic.EveFirmware,
735
+ encodeEveData(util.format('2c %s be', numberToEveHexString(this.EveThermoPersist.firmware, 4))),
736
+ ); // firmware version (build xxxx)));
737
+
738
+ service.updateCharacteristic(this.hap.Characteristic.EveProgramData, this.#EveThermoGetDetails(options.getcommand));
739
+ service.getCharacteristic(this.hap.Characteristic.EveProgramData).onGet(() => {
740
+ return this.#EveThermoGetDetails(options.getcommand);
741
+ });
742
+
743
+ service.getCharacteristic(this.hap.Characteristic.EveProgramCommand).onSet((value) => {
744
+ let programs = [];
745
+ let processedData = {};
746
+ let valHex = decodeEveData(value);
747
+ let index = 0;
748
+ while (index < valHex.length) {
749
+ let command = valHex.substr(index, 2);
750
+ index += 2; // skip over command value, and this is where data starts.
751
+ switch (command) {
752
+ case '00': {
753
+ // start of command string ??
754
+ break;
755
+ }
756
+
757
+ case '06': {
758
+ // end of command string ??
759
+ break;
760
+ }
761
+
762
+ case '7f': {
763
+ // end of command string ??
764
+ break;
765
+ }
766
+
767
+ case '11': {
768
+ // valve calibration/protection??
769
+ //0011ff00f22076
770
+ // 00f22076 - 111100100010000001110110
771
+ // 15868022
772
+ // 7620f2 - 011101100010000011110010
773
+ // 7741682
774
+ //console.log(Math.floor(Date.now() / 1000));
775
+ index += 10;
776
+ break;
777
+ }
778
+
779
+ case '10': {
780
+ // OK to remove
781
+ break;
782
+ }
783
+
784
+ case '12': {
785
+ // temperature offset
786
+ // 8bit signed value. Divide by 10 to get float value
787
+ this.EveThermoPersist.tempoffset = EveHexStringToNumber(valHex.substr(index, 2)) / 10;
788
+ processedData.tempoffset = this.EveThermoPersist.tempoffset;
789
+ index += 2;
790
+ break;
791
+ }
792
+
793
+ case '13': {
794
+ // schedules enabled/disable
795
+ this.EveThermoPersist.enableschedule = valHex.substr(index, 2) === '01' ? true : false;
796
+ processedData.enableschedule = this.EveThermoPersist.enableschedule;
797
+ index += 2;
798
+ break;
799
+ }
800
+
801
+ case '14': {
802
+ // Installed status
803
+ index += 2;
804
+ break;
805
+ }
806
+
807
+ case '18': {
808
+ // Pause/resume via HomeKit automation/scene
809
+ // 20 - pause thermostat operation
810
+ // 10 - resume thermostat operation
811
+ this.EveThermoPersist.pause = valHex.substr(index, 2) === '20' ? true : false;
812
+ processedData.pause = this.EveThermoPersist.pause;
813
+ index += 2;
814
+ break;
815
+ }
816
+
817
+ case '19': {
818
+ // Vacation on/off, vacation temperature via HomeKit automation/scene
819
+ this.EveThermoPersist.vacation = valHex.substr(index, 2) === '01' ? true : false;
820
+ this.EveThermoPersist.vacationtemp =
821
+ valHex.substr(index, 2) === '01' ? parseInt(valHex.substr(index + 2, 2), 16) * 0.5 : null;
822
+ processedData.vacation = {
823
+ status: this.EveThermoPersist.vacation,
824
+ temp: this.EveThermoPersist.vacationtemp,
825
+ };
826
+ index += 4;
827
+ break;
828
+ }
829
+
830
+ case 'f4': {
831
+ // Temperature Levels for schedule
832
+ //let nowTemp = valHex.substr(index, 2) === '80' ? null : parseInt(valHex.substr(index, 2), 16) * 0.5;
833
+ let ecoTemp = valHex.substr(index + 2, 2) === '80' ? null : parseInt(valHex.substr(index + 2, 2), 16) * 0.5;
834
+ let comfortTemp = valHex.substr(index + 4, 2) === '80' ? null : parseInt(valHex.substr(index + 4, 2), 16) * 0.5;
835
+ processedData.scheduleTemps = { eco: ecoTemp, comfort: comfortTemp };
836
+ index += 6;
837
+ break;
838
+ }
839
+
840
+ case 'fc': {
841
+ // Date/Time mmhhDDMMYY
842
+ index += 10;
843
+ break;
844
+ }
845
+
846
+ case 'fa': {
847
+ // Programs (week - mon, tue, wed, thu, fri, sat, sun)
848
+ // index += 112;
849
+ for (let index2 = 0; index2 < 7; index2++) {
850
+ let times = [];
851
+ for (let index3 = 0; index3 < 4; index3++) {
852
+ // decode start time
853
+ let start = parseInt(valHex.substr(index, 2), 16);
854
+ //let start_min = null;
855
+ //let start_hr = null;
856
+ let start_offset = null;
857
+ if (start !== 0xff) {
858
+ //start_min = (start * 10) % 60; // Start minute
859
+ //start_hr = ((start * 10) - start_min) / 60; // Start hour
860
+ start_offset = start * 10 * 60; // Seconds since 00:00
861
+ }
862
+
863
+ // decode end time
864
+ let end = parseInt(valHex.substr(index + 2, 2), 16);
865
+ //let end_min = null;
866
+ //let end_hr = null;
867
+ let end_offset = null;
868
+ if (end !== 0xff) {
869
+ //end_min = (end * 10) % 60; // End minute
870
+ //end_hr = ((end * 10) - end_min) / 60; // End hour
871
+ end_offset = end * 10 * 60; // Seconds since 00:00
872
+ }
873
+
874
+ if (start_offset !== null && end_offset !== null) {
875
+ times.push({
876
+ start: start_offset,
877
+ duration: end_offset - start_offset,
878
+ ecotemp: processedData.scheduleTemps.eco,
879
+ comforttemp: processedData.scheduleTemps.comfort,
880
+ });
881
+ }
882
+ index += 4;
883
+ }
884
+ programs.push({ id: programs.length + 1, days: DAYSOFWEEK[index2], schedule: times });
885
+ }
886
+
887
+ this.EveThermoPersist.programs = programs;
888
+ processedData.programs = this.EveThermoPersist.programs;
889
+ break;
890
+ }
891
+
892
+ case '1a': {
893
+ // Program (day)
894
+ index += 16;
895
+ break;
896
+ }
897
+
898
+ case 'f2': {
899
+ // ??
900
+ index += 2;
901
+ break;
902
+ }
903
+
904
+ case 'f6': {
905
+ //??
906
+ index += 6;
907
+ break;
908
+ }
909
+
910
+ case 'ff': {
911
+ // ??
912
+ index += 4;
913
+ break;
914
+ }
915
+
916
+ default: {
917
+ this?.log?.debug && this.log.debug('Unknown Eve Thermo command "%s"', command);
918
+ break;
919
+ }
920
+ }
921
+ }
922
+
923
+ // Send complete processed command data if configured to our callback
924
+ if (typeof options?.setcommand === 'function' && Object.keys(processedData).length !== 0) {
925
+ options.setcommand(processedData);
926
+ }
927
+ });
928
+ break;
929
+ }
930
+
931
+ case this.hap.Service.EveAirPressureSensor.UUID: {
932
+ // treat these as EveHome Weather (2015)
933
+
934
+ // Setup the history service and the required characteristics for this service UUID type
935
+ // Callbacks setup below after this is created
936
+ let historyService = this.#createHistoryService(service, [this.hap.Characteristic.EveFirmware]);
937
+
938
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
939
+ let historyreftime = tempHistory.length === 0 ? this.historyData.reset - EPOCH_OFFSET : tempHistory[0].time - EPOCH_OFFSET;
940
+ this.EveHome = {
941
+ service: historyService,
942
+ linkedservice: service,
943
+ type: service.UUID,
944
+ sub: service.subtype,
945
+ evetype: 'weather',
946
+ fields: '0102 0202 0302',
947
+ entry: 0,
948
+ count: tempHistory.length,
949
+ reftime: historyreftime,
950
+ send: 0,
951
+ };
952
+
953
+ service.updateCharacteristic(
954
+ this.hap.Characteristic.EveFirmware,
955
+ encodeEveData(util.format('01 %s be', numberToEveHexString(809, 4))),
956
+ );
957
+ break;
958
+ }
959
+
960
+ case this.hap.Service.AirQualitySensor.UUID:
961
+ case this.hap.Service.TemperatureSensor.UUID: {
962
+ // treat these as EveHome Room(s)
963
+
964
+ // Setup the history service and the required characteristics for this service UUID type
965
+ // Callbacks setup below after this is created
966
+ let historyService = this.#createHistoryService(service, [
967
+ this.hap.Characteristic.EveFirmware,
968
+ service.UUID === this.hap.Service.AirQualitySensor.UUID
969
+ ? this.hap.Characteristic.VOCDensity
970
+ : this.hap.Characteristic.TemperatureDisplayUnits,
971
+ ]);
972
+
973
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
974
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
975
+ if (tempHistory.length !== 0) {
976
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
977
+ }
978
+
979
+ if (service.UUID === this.hap.Service.AirQualitySensor.UUID) {
980
+ // Eve Room 2 (2018)
981
+ this.EveHome = {
982
+ service: historyService,
983
+ linkedservice: service,
984
+ type: service.UUID,
985
+ sub: service.subtype,
986
+ evetype: 'room2',
987
+ fields: '0102 0202 2202 2901 2501 2302 2801',
988
+ entry: 0,
989
+ count: tempHistory.length,
990
+ reftime: historyreftime,
991
+ send: 0,
992
+ };
993
+
994
+ service.updateCharacteristic(
995
+ this.hap.Characteristic.EveFirmware,
996
+ encodeEveData(util.format('27 %s be', numberToEveHexString(1416, 4))),
997
+ ); // firmware version (build xxxx)));
998
+
999
+ // Need to ensure HomeKit accessory which has Air Quality service also has temperature & humidity services.
1000
+ // Temperature service needs characteristic this.hap.Characteristic.TemperatureDisplayUnits set to CELSIUS
1001
+ }
1002
+
1003
+ if (service.UUID === this.hap.Service.TemperatureSensor.UUID) {
1004
+ // Eve Room (2015)
1005
+ this.EveHome = {
1006
+ service: historyService,
1007
+ linkedservice: service,
1008
+ type: service.UUID,
1009
+ sub: service.subtype,
1010
+ evetype: 'room',
1011
+ fields: '0102 0202 0402 0f03',
1012
+ entry: 0,
1013
+ count: tempHistory.length,
1014
+ reftime: historyreftime,
1015
+ send: 0,
1016
+ };
1017
+
1018
+ service.updateCharacteristic(
1019
+ this.hap.Characteristic.EveFirmware,
1020
+ encodeEveData(util.format('02 %s be', numberToEveHexString(1151, 4))),
1021
+ ); // firmware version (build xxxx)));
1022
+
1023
+ // Temperature needs to be in Celsius
1024
+ service.updateCharacteristic(
1025
+ this.hap.Characteristic.TemperatureDisplayUnits,
1026
+ this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS,
1027
+ );
1028
+ }
1029
+ break;
1030
+ }
1031
+
1032
+ case this.hap.Service.MotionSensor.UUID: {
1033
+ // treat these as EveHome Motion
1034
+
1035
+ // Setup the history service and the required characteristics for this service UUID type
1036
+ // Callbacks setup below after this is created
1037
+ let historyService = this.#createHistoryService(service, [
1038
+ this.hap.Characteristic.EveMotionSensitivity,
1039
+ this.hap.Characteristic.EveMotionDuration,
1040
+ this.hap.Characteristic.EveLastActivation,
1041
+ // this.hap.Characteristic.EveGetConfiguration,
1042
+ // this.hap.Characteristic.EveSetConfiguration,
1043
+ ]);
1044
+
1045
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
1046
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
1047
+ if (tempHistory.length !== 0) {
1048
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
1049
+ }
1050
+
1051
+ this.EveHome = {
1052
+ service: historyService,
1053
+ linkedservice: service,
1054
+ type: service.UUID,
1055
+ sub: service.subtype,
1056
+ evetype: 'motion',
1057
+ fields: '1301 1c01',
1058
+ entry: 0,
1059
+ count: tempHistory.length,
1060
+ reftime: historyreftime,
1061
+ send: 0,
1062
+ };
1063
+
1064
+ // Need some internal storage to track Eve Motion configuration from EveHome app
1065
+ this.EveMotionPersist = {
1066
+ duration: typeof options?.EveMotion_duration === 'number' ? options.EveMotion_duration : 5, // default 5 seconds
1067
+ sensitivity:
1068
+ typeof options?.EveMotion_sensitivity === 'number'
1069
+ ? options.EveMotion_sensivity
1070
+ : this.hap.Characteristic.EveMotionSensitivity.HIGH, // default sensitivity
1071
+ ledmotion: options?.EveMotion_ledmotion === true, // off
1072
+ };
1073
+
1074
+ // Setup initial values and callbacks for charateristics we are using
1075
+ service.updateCharacteristic(this.hap.Characteristic.EveLastActivation, this.#EveLastEventTime());
1076
+
1077
+ service.getCharacteristic(this.hap.Characteristic.EveLastActivation).onGet(() => {
1078
+ return this.#EveLastEventTime(); // time of last event in seconds since first event
1079
+ });
1080
+
1081
+ service.updateCharacteristic(this.hap.Characteristic.EveMotionSensitivity, this.EveMotionPersist.sensitivity);
1082
+ service.getCharacteristic(this.hap.Characteristic.EveMotionSensitivity).onGet(() => {
1083
+ return this.EveMotionPersist.sensitivity;
1084
+ });
1085
+ service.getCharacteristic(this.hap.Characteristic.EveMotionSensitivity).onSet((value) => {
1086
+ this.EveMotionPersist.sensitivity = value;
1087
+ });
1088
+
1089
+ service.updateCharacteristic(this.hap.Characteristic.EveMotionDuration, this.EveMotionPersist.duration);
1090
+ service.getCharacteristic(this.hap.Characteristic.EveMotionDuration).onGet(() => {
1091
+ return this.EveMotionPersist.duration;
1092
+ });
1093
+ service.getCharacteristic(this.hap.Characteristic.EveMotionDuration).onSet((value) => {
1094
+ this.EveMotionPersist.duration = value;
1095
+ });
1096
+
1097
+ /*service.updateCharacteristic(this.hap.Characteristic.EveGetConfiguration, encodeEveData('300100'));
1098
+ service.getCharacteristic(this.hap.Characteristic.EveGetConfiguration).onGet(() => {
1099
+ let value = util.format(
1100
+ '0002 2500 0302 %s 9b04 %s 8002 ffff 1e02 2500 0c',
1101
+ numberToEveHexString(1144, 4), // firmware version (build xxxx)
1102
+ numberToEveHexString(Math.floor(Date.now() / 1000), 8), // 'now' time
1103
+ ); // Not sure why 64bit value???
1104
+
1105
+ console.log('Motion set', value)
1106
+
1107
+ return encodeEveData(value));
1108
+ });
1109
+ service.getCharacteristic(this.hap.Characteristic.EveSetConfiguration).onSet((value) => {
1110
+ let valHex = decodeEveData(value);
1111
+ let index = 0;
1112
+ while (index < valHex.length) {
1113
+ // first byte is command in this data stream
1114
+ // second byte is size of data for command
1115
+ let command = valHex.substr(index, 2);
1116
+ let size = parseInt(valHex.substr(index + 2, 2), 16) * 2;
1117
+ let data = valHex.substr(index + 4, parseInt(valHex.substr(index + 2, 2), 16) * 2);
1118
+ switch(command) {
1119
+ case '30' : {
1120
+ this.EveMotionPersist.ledmotion = (data === '01' ? true : false);
1121
+ break;
1122
+ }
1123
+
1124
+ case '80' : {
1125
+ //0000 0400 (mostly) and sometimes 300103 and 80040000 ffff
1126
+ break;
1127
+ }
1128
+
1129
+ default : {
1130
+ this?.log?.debug && this.log.debug('Unknown Eve Motion command "%s" with data "%s"', command, data);
1131
+ break;
1132
+ }
1133
+ }
1134
+ index += (4 + size); // Move to next command accounting for header size of 4 bytes
1135
+ }
1136
+ }); */
1137
+ break;
1138
+ }
1139
+
1140
+ case this.hap.Service.SmokeSensor.UUID: {
1141
+ // treat these as EveHome Smoke
1142
+
1143
+ // Setup the history service and the required characteristics for this service UUID type
1144
+ // Callbacks setup below after this is created
1145
+ let historyService = this.#createHistoryService(service, [
1146
+ this.hap.Characteristic.EveGetConfiguration,
1147
+ this.hap.Characteristic.EveSetConfiguration,
1148
+ this.hap.Characteristic.EveDeviceStatus,
1149
+ ]);
1150
+
1151
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
1152
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
1153
+ if (tempHistory.length !== 0) {
1154
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
1155
+ }
1156
+
1157
+ this.EveHome = {
1158
+ service: historyService,
1159
+ linkedservice: service,
1160
+ type: service.UUID,
1161
+ sub: service.subtype,
1162
+ evetype: 'smoke',
1163
+ fields: '1601 1b02 0f03 2302',
1164
+ entry: 0,
1165
+ count: tempHistory.length,
1166
+ reftime: historyreftime,
1167
+ send: 0,
1168
+ };
1169
+
1170
+ // TODO = work out what the 'signatures' need to be for an Eve Smoke
1171
+ // Also, how to make alarm test button active in Eve app and not say 'Eve Smoke is not mounted correctly'
1172
+
1173
+ // Need some internal storage to track Eve Smoke configuration from EveHome app
1174
+ this.EveSmokePersist = {
1175
+ firmware: typeof options?.EveSmoke_firmware === 'number' ? options.EveSmoke_firmware : 1208, // Firmware version
1176
+ lastalarmtest: typeof options?.EveSmoke_lastalarmtest === 'number' ? options.EveSmoke_lastalarmtest : 0, // Seconds of alarm test
1177
+ alarmtest: options?.EveSmoke_alarmtest === true, // Is alarmtest running
1178
+ heatstatus: typeof options?.EveSmoke_heatstatus === 'number' ? options.EveSmoke_heatstatus : 0, // Heat sensor status
1179
+ statusled: options?.EveSmoke_statusled === false, // Status LED flash/enabled
1180
+ smoketestpassed: options?.EveSmoke_smoketestpassed === false, // Passed smoke test?
1181
+ heattestpassed: options?.EveSmoke_heattestpassed === false, // Passed smoke test?
1182
+ hushedstate: options.EveSmoke_hushedstate === true, // Alarms muted
1183
+ };
1184
+
1185
+ // Setup initial values and callbacks for charateristics we are using
1186
+ service.updateCharacteristic(
1187
+ this.hap.Characteristic.EveDeviceStatus,
1188
+ this.#EveSmokeGetDetails(options.getcommand, this.hap.Characteristic.EveDeviceStatus),
1189
+ );
1190
+ service.getCharacteristic(this.hap.Characteristic.EveDeviceStatus).onGet(() => {
1191
+ return this.#EveSmokeGetDetails(options.getcommand, this.hap.Characteristic.EveDeviceStatus);
1192
+ });
1193
+
1194
+ service.updateCharacteristic(
1195
+ this.hap.Characteristic.EveGetConfiguration,
1196
+ this.#EveSmokeGetDetails(options.getcommand, this.hap.Characteristic.EveGetConfiguration),
1197
+ );
1198
+ service.getCharacteristic(this.hap.Characteristic.EveGetConfiguration).onGet(() => {
1199
+ return this.#EveSmokeGetDetails(options.getcommand, this.hap.Characteristic.EveGetConfiguration);
1200
+ });
1201
+
1202
+ service.getCharacteristic(this.hap.Characteristic.EveSetConfiguration).onSet((value) => {
1203
+ // Loop through set commands passed to us
1204
+ let processedData = {};
1205
+ let valHex = decodeEveData(value);
1206
+ let index = 0;
1207
+ while (index < valHex.length) {
1208
+ // first byte is command in this data stream
1209
+ // second byte is size of data for command
1210
+ let command = valHex.substr(index, 2);
1211
+ let size = parseInt(valHex.substr(index + 2, 2), 16) * 2;
1212
+ let data = valHex.substr(index + 4, parseInt(valHex.substr(index + 2, 2), 16) * 2);
1213
+ switch (command) {
1214
+ case '40': {
1215
+ let subCommand = EveHexStringToNumber(data.substr(0, 2));
1216
+ if (subCommand === 0x02) {
1217
+ // Alarm test start/stop
1218
+ this.EveSmokePersist.alarmtest = data === '0201' ? true : false;
1219
+ processedData.alarmtest = this.EveSmokePersist.alarmtest;
1220
+ }
1221
+ if (subCommand === 0x05) {
1222
+ // Flash status Led on/off
1223
+ this.EveSmokePersist.statusled = data === '0501' ? true : false;
1224
+ processedData.statusled = this.EveSmokePersist.statusled;
1225
+ }
1226
+ if (subCommand !== 0x02 && subCommand !== 0x05) {
1227
+ this?.log?.debug && this.log.debug('Unknown Eve Smoke command "%s" with data "%s"', command, data);
1228
+ }
1229
+ break;
1230
+ }
1231
+
1232
+ default: {
1233
+ this?.log?.debug && this.log.debug('Unknown Eve Smoke command "%s" with data "%s"', command, data);
1234
+ break;
1235
+ }
1236
+ }
1237
+ index += 4 + size; // Move to next command accounting for header size of 4 bytes
1238
+ }
1239
+
1240
+ // Send complete processed command data if configured to our callback
1241
+ if (typeof options?.setcommand === 'function' && Object.keys(processedData).length !== 0) {
1242
+ options.setcommand(processedData);
1243
+ }
1244
+ });
1245
+ break;
1246
+ }
1247
+
1248
+ case this.hap.Service.Valve.UUID:
1249
+ case this.hap.Service.IrrigationSystem.UUID: {
1250
+ // treat an irrigation system as EveHome Aqua
1251
+ // Under this, any valve history will be presented under this. We don't log our History under irrigation service ID at all
1252
+
1253
+ // TODO - see if we can add history per valve service under the irrigation system????. History service per valve???
1254
+
1255
+ // Setup the history service and the required characteristics for this service UUID type
1256
+ // Callbacks setup below after this is created
1257
+ let historyService = this.#createHistoryService(service, [
1258
+ this.hap.Characteristic.EveGetConfiguration,
1259
+ this.hap.Characteristic.EveSetConfiguration,
1260
+ this.hap.Characteristic.LockPhysicalControls,
1261
+ ]);
1262
+
1263
+ let tempHistory = this.getHistory(
1264
+ this.hap.Service.Valve.UUID,
1265
+ service.UUID === this.hap.Service.IrrigationSystem.UUID ? null : service.subtype,
1266
+ );
1267
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
1268
+ if (tempHistory.length !== 0) {
1269
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
1270
+ }
1271
+
1272
+ this.EveHome = {
1273
+ service: historyService,
1274
+ linkedservice: service,
1275
+ type: this.hap.Service.Valve.UUID,
1276
+ sub: service.UUID === this.hap.Service.IrrigationSystem.UUID ? null : service.subtype,
1277
+ evetype: 'aqua',
1278
+ fields: '1f01 2a08 2302',
1279
+ entry: 0,
1280
+ count: tempHistory.length,
1281
+ reftime: historyreftime,
1282
+ send: 0,
1283
+ };
1284
+
1285
+ // Need some internal storage to track Eve Aqua configuration from EveHome app
1286
+ this.EveAquaPersist = {
1287
+ firmware: typeof options?.EveAqua_firmware === 'number' ? options.EveAqua_firmware : 1208, // Firmware version
1288
+ flowrate: typeof options?.EveAqua_flowrate === 'number' ? options.EveAqua_flowrate : 18, // 18 L/Min default
1289
+ latitude: typeof options?.EveAqua_latitude === 'number' ? options.EveAqua_latitude : 0.0, // Latitude
1290
+ longitude: typeof options?.EveAqua_longitude === 'number' ? options.EveAqua_longitude : 0.0, // Longitude
1291
+ utcoffset: typeof options?.EveAqua_utcoffset === 'number' ? options.EveAqua_utcoffset : new Date().getTimezoneOffset() * -60, // UTC offset in seconds
1292
+ enableschedule: options.EveAqua_enableschedule === true, // Schedules on/off
1293
+ pause: typeof options?.EveAqua_pause === 'number' ? options.EveAqua_pause : 0, // Day pause
1294
+ programs: typeof options?.EveAqua_programs === 'object' ? options.EveAqua_programs : [], // Schedules
1295
+ };
1296
+
1297
+ // Setup initial values and callbacks for charateristics we are using
1298
+ service.updateCharacteristic(this.hap.Characteristic.EveGetConfiguration, this.#EveAquaGetDetails(options.getcommand));
1299
+ service.getCharacteristic(this.hap.Characteristic.EveGetConfiguration).onGet(() => {
1300
+ return this.#EveAquaGetDetails(options.getcommand);
1301
+ });
1302
+
1303
+ service.getCharacteristic(this.hap.Characteristic.EveSetConfiguration).onSet((value) => {
1304
+ // Loop through set commands passed to us
1305
+ let programs = [];
1306
+ let processedData = {};
1307
+ let valHex = decodeEveData(value);
1308
+ let index = 0;
1309
+ while (index < valHex.length) {
1310
+ // first byte is command in this data stream
1311
+ // second byte is size of data for command
1312
+ let command = valHex.substr(index, 2);
1313
+ let size = parseInt(valHex.substr(index + 2, 2), 16) * 2;
1314
+ let data = valHex.substr(index + 4, parseInt(valHex.substr(index + 2, 2), 16) * 2);
1315
+ switch (command) {
1316
+ case '2e': {
1317
+ // flow rate in L/Minute
1318
+ this.EveAquaPersist.flowrate = Number(((EveHexStringToNumber(data) * 60) / 1000).toFixed(1));
1319
+ processedData.flowrate = this.EveAquaPersist.flowrate;
1320
+ break;
1321
+ }
1322
+
1323
+ case '2f': {
1324
+ // reset timestamp in seconds since EPOCH
1325
+ this.EveAquaPersist.timestamp = EPOCH_OFFSET + EveHexStringToNumber(data);
1326
+ processedData.timestamp = this.EveAquaPersist.timestamp;
1327
+ break;
1328
+ }
1329
+
1330
+ case '44': {
1331
+ // Schedules on/off and Timezone/location information
1332
+ let subCommand = EveHexStringToNumber(data.substr(2, 4));
1333
+ this.EveAquaPersist.enableschedule = (subCommand & 0x01) === 0x01; // Bit 1 is schedule status on/off
1334
+ if ((subCommand & 0x10) === 0x10) {
1335
+ this.EveAquaPersist.utcoffset = EveHexStringToNumber(data.substr(10, 8)) * 60; // Bit 5 is UTC offset in seconds
1336
+ }
1337
+ if ((subCommand & 0x04) === 0x04) {
1338
+ this.EveAquaPersist.latitude = EveHexStringToNumber(data.substr(18, 8), 5); // Bit 4 is lat/long information
1339
+ }
1340
+ if ((subCommand & 0x04) === 0x04) {
1341
+ this.EveAquaPersist.longitude = EveHexStringToNumber(data.substr(26, 8), 5); // Bit 4 is lat/long information
1342
+ }
1343
+ if ((subCommand & 0x02) === 0x02) {
1344
+ // If bit 2 is set, indicates just a schedule on/off command
1345
+ processedData.enabled = this.EveAquaPersist.enableschedule;
1346
+ }
1347
+ if ((subCommand & 0x02) !== 0x02) {
1348
+ // If bit 2 is not set, this command includes Timezone/location information
1349
+ processedData.utcoffset = this.EveAquaPersist.utcoffset;
1350
+ processedData.latitude = this.EveAquaPersist.latitude;
1351
+ processedData.longitude = this.EveAquaPersist.longitude;
1352
+ }
1353
+ break;
1354
+ }
1355
+
1356
+ case '45': {
1357
+ // Eve App Scheduling Programs
1358
+ //let programcount = EveHexStringToNumber(data.substr(2, 2)); // Number of defined programs
1359
+ //let unknown = EveHexStringToNumber(data.substr(4, 6)); // Unknown data for 6 bytes
1360
+
1361
+ let index2 = 14; // Program schedules start at offset 14 in data
1362
+ let programs = [];
1363
+ while (index2 < data.length) {
1364
+ let scheduleSize = parseInt(data.substr(index2 + 2, 2), 16) * 8;
1365
+ let schedule = data.substring(index2 + 4, index2 + 4 + scheduleSize);
1366
+
1367
+ if (schedule !== '') {
1368
+ let times = [];
1369
+ for (let index3 = 0; index3 < schedule.length / 8; index3++) {
1370
+ // schedules appear to be a 32bit word
1371
+ // after swapping 16bit words
1372
+ // 1st 16bits = end time
1373
+ // 2nd 16bits = start time
1374
+ // starttime decode
1375
+ // bit 1-5 specific time or sunrise/sunset 05 = time, 07 = sunrise/sunset
1376
+ // if sunrise/sunset
1377
+ // bit 6, sunrise = 1, sunset = 0
1378
+ // bit 7, before = 1, after = 0
1379
+ // bit 8 - 16 - minutes for sunrise/sunset
1380
+ // if time
1381
+ // bit 6 - 16 - minutes from 00:00
1382
+ //
1383
+ // endtime decode
1384
+ // bit 1-5 specific time or sunrise/sunset 01 = time, 03 = sunrise/sunset
1385
+ // if sunrise/sunset
1386
+ // bit 6, sunrise = 1, sunset = 0
1387
+ // bit 7, before = 1, after = 0
1388
+ // bit 8 - 16 - minutes for sunrise/sunset
1389
+ // if time
1390
+ // bit 6 - 16 - minutes from 00:00
1391
+ // decode start time
1392
+ let start = parseInt(
1393
+ schedule
1394
+ .substring(index3 * 8, index3 * 8 + 4)
1395
+ .match(/[a-fA-F0-9]{2}/g)
1396
+ .reverse()
1397
+ .join(''),
1398
+ 16,
1399
+ );
1400
+ // let start_min = null;
1401
+ //let start_hr = null;
1402
+ let start_offset = null;
1403
+ let start_sunrise = null;
1404
+ if ((start & 0x1f) === 5) {
1405
+ // specific time
1406
+ //start_min = (start >>> 5) % 60; // Start minute
1407
+ //start_hr = ((start >>> 5) - start_min) / 60; // Start hour
1408
+ start_offset = (start >>> 5) * 60; // Seconds since 00:00
1409
+ } else if ((start & 0x1f) === 7) {
1410
+ // sunrise/sunset
1411
+ start_sunrise = (start >>> 5) & 0x01; // 1 = sunrise, 0 = sunset
1412
+ start_offset = (start >>> 6) & 0x01 ? ~((start >>> 7) * 60) + 1 : (start >>> 7) * 60; // offset from sunrise/sunset (plus/minus value)
1413
+ }
1414
+
1415
+ // decode end time
1416
+ let end = parseInt(
1417
+ schedule
1418
+ .substring(index3 * 8 + 4, index3 * 8 + 8)
1419
+ .match(/[a-fA-F0-9]{2}/g)
1420
+ .reverse()
1421
+ .join(''),
1422
+ 16,
1423
+ );
1424
+ //let end_min = null;
1425
+ //let end_hr = null;
1426
+ let end_offset = null;
1427
+ //let end_sunrise = null;
1428
+ if ((end & 0x1f) === 1) {
1429
+ // specific time
1430
+ //end_min = (end >>> 5) % 60; // End minute
1431
+ //end_hr = ((end >>> 5) - end_min) / 60; // End hour
1432
+ end_offset = (end >>> 5) * 60; // Seconds since 00:00
1433
+ } else if ((end & 0x1f) === 3) {
1434
+ //end_sunrise = ((end >>> 5) & 0x01); // 1 = sunrise, 0 = sunset
1435
+ end_offset = (end >>> 6) & 0x01 ? ~((end >>> 7) * 60) + 1 : (end >>> 7) * 60; // offset sunrise/sunset (+/- value)
1436
+ }
1437
+ times.push({
1438
+ start: start_sunrise === null ? start_offset : start_sunrise ? 'sunrise' : 'sunset',
1439
+ duration: end_offset - start_offset,
1440
+ offset: start_offset,
1441
+ });
1442
+ }
1443
+ programs.push({ id: programs.length + 1, days: [], schedule: times });
1444
+ }
1445
+ index2 = index2 + 4 + scheduleSize; // Move to next program
1446
+ }
1447
+ break;
1448
+ }
1449
+
1450
+ case '46': {
1451
+ // Eve App active days across programs
1452
+ //let daynumber = (EveHexStringToNumber(data.substr(8, 6)) >>> 4);
1453
+
1454
+ // bit masks for active days mapped to programm id
1455
+ /* let mon = (daynumber & 0x7);
1456
+ let tue = ((daynumber >>> 3) & 0x7)
1457
+ let wed = ((daynumber >>> 6) & 0x7)
1458
+ let thu = ((daynumber >>> 9) & 0x7)
1459
+ let fri = ((daynumber >>> 12) & 0x7)
1460
+ let sat = ((daynumber >>> 15) & 0x7)
1461
+ let sun = ((daynumber >>> 18) & 0x7) */
1462
+ //let unknown = EveHexStringToNumber(data.substr(0, 6)); // Unknown data for first 6 bytes
1463
+ let daysbitmask = EveHexStringToNumber(data.substr(8, 6)) >>> 4;
1464
+ programs.forEach((program) => {
1465
+ for (let index2 = 0; index2 < DAYSOFWEEK.length; index2++) {
1466
+ if (((daysbitmask >>> (index2 * 3)) & 0x7) === program.id) {
1467
+ program.days.push(DAYSOFWEEK[index2]);
1468
+ }
1469
+ }
1470
+ });
1471
+
1472
+ processedData.programs = programs;
1473
+ break;
1474
+ }
1475
+
1476
+ case '47': {
1477
+ // Eve App DST information
1478
+ this.EveAquaPersist.command47 = command + valHex.substr(index + 2, 2) + data;
1479
+ break;
1480
+ }
1481
+
1482
+ case '4b': {
1483
+ // Eve App suspension scene triggered from HomeKit
1484
+ // 1440 mins in a day. Zero based day, so we add one
1485
+ this.EveAquaPersist.pause = EveHexStringToNumber(data.substr(0, 8)) / 1440 + 1;
1486
+ processedData.pause = this.EveAquaPersist.pause;
1487
+ break;
1488
+ }
1489
+
1490
+ case 'b1': {
1491
+ // Child lock on/off. Seems data packet is always same (0100)
1492
+ // inspect 'this.hap.Characteristic.LockPhysicalControls)' for actual status
1493
+ this.EveAquaPersist.childlock =
1494
+ service.getCharacteristic(this.hap.Characteristic.LockPhysicalControls).value ===
1495
+ this.hap.Characteristic.CONTROL_LOCK_ENABLED
1496
+ ? true
1497
+ : false;
1498
+ processedData.childlock = this.EveAquaPersist.childlock;
1499
+ break;
1500
+ }
1501
+
1502
+ default: {
1503
+ this?.log?.debug && this.log.debug('Unknown Eve Aqua command "%s" with data "%s"', command, data);
1504
+ break;
1505
+ }
1506
+ }
1507
+ index += 4 + size; // Move to next command accounting for header size of 4 bytes
1508
+ }
1509
+
1510
+ // Send complete processed command data if configured to our callback
1511
+ if (typeof options?.setcommand === 'function' && Object.keys(processedData).length !== 0) {
1512
+ options.setcommand(processedData);
1513
+ }
1514
+ });
1515
+ break;
1516
+ }
1517
+
1518
+ case this.hap.Service.Outlet.UUID: {
1519
+ // treat these as EveHome energy
1520
+ // TODO - schedules
1521
+
1522
+ // Setup the history service and the required characteristics for this service UUID type
1523
+ // Callbacks setup below after this is created
1524
+ let historyService = this.#createHistoryService(service, [
1525
+ this.hap.Characteristic.EveFirmware,
1526
+ this.hap.Characteristic.EveElectricalVoltage,
1527
+ this.hap.Characteristic.EveElectricalCurrent,
1528
+ this.hap.Characteristic.EveElectricalWattage,
1529
+ this.hap.Characteristic.EveTotalConsumption,
1530
+ ]);
1531
+
1532
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
1533
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
1534
+ if (tempHistory.length !== 0) {
1535
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
1536
+ }
1537
+
1538
+ this.EveHome = {
1539
+ service: historyService,
1540
+ linkedservice: service,
1541
+ type: service.UUID,
1542
+ sub: service.subtype,
1543
+ evetype: 'energy',
1544
+ fields: '0702 0e01',
1545
+ entry: 0,
1546
+ count: tempHistory.length,
1547
+ reftime: historyreftime,
1548
+ send: 0,
1549
+ };
1550
+
1551
+ // Setup initial values and callbacks for charateristics we are using
1552
+ service.updateCharacteristic(
1553
+ this.hap.Characteristic.EveFirmware,
1554
+ encodeEveData(util.format('29 %s be', numberToEveHexString(807, 4))),
1555
+ );
1556
+
1557
+ service.updateCharacteristic(
1558
+ this.hap.Characteristic.EveElectricalCurrent,
1559
+ this.#EveEnergyGetDetails(options.getcommand, this.hap.Characteristic.EveElectricalCurrent),
1560
+ );
1561
+ service.getCharacteristic(this.hap.Characteristic.EveElectricalCurrent).onGet(() => {
1562
+ return this.#EveEnergyGetDetails(options.getcommand, this.hap.Characteristic.EveElectricalCurrent);
1563
+ });
1564
+
1565
+ service.updateCharacteristic(
1566
+ this.hap.Characteristic.EveElectricalVoltage,
1567
+ this.#EveEnergyGetDetails(options.getcommand, this.hap.Characteristic.EveElectricalVoltage),
1568
+ );
1569
+ service.getCharacteristic(this.hap.Characteristic.EveElectricalVoltage).onGet(() => {
1570
+ return this.#EveEnergyGetDetails(options.getcommand, this.hap.Characteristic.EveElectricalVoltage);
1571
+ });
1572
+
1573
+ service.updateCharacteristic(
1574
+ this.hap.Characteristic.EveElectricalWattage,
1575
+ this.#EveEnergyGetDetails(options.getcommand, this.hap.Characteristic.EveElectricalWattage),
1576
+ );
1577
+ service.getCharacteristic(this.hap.Characteristic.EveElectricalWattage).onGet(() => {
1578
+ return this.#EveEnergyGetDetails(options.getcommand, this.hap.Characteristic.EveElectricalWattage);
1579
+ });
1580
+ break;
1581
+ }
1582
+
1583
+ case this.hap.Service.LeakSensor.UUID: {
1584
+ // treat these as EveHome Water Guard
1585
+
1586
+ // Setup the history service and the required characteristics for this service UUID type
1587
+ // Callbacks setup below after this is created
1588
+ let historyService = this.#createHistoryService(service, [
1589
+ this.hap.Characteristic.EveGetConfiguration,
1590
+ this.hap.Characteristic.EveSetConfiguration,
1591
+ this.hap.Characteristic.StatusFault,
1592
+ ]);
1593
+
1594
+ let tempHistory = this.getHistory(service.UUID, service.subtype);
1595
+ let historyreftime = this.historyData.reset - EPOCH_OFFSET;
1596
+ if (tempHistory.length !== 0) {
1597
+ historyreftime = tempHistory[0].time - EPOCH_OFFSET;
1598
+ }
1599
+
1600
+ // <---- Still need to determine signature fields
1601
+ this.EveHome = {
1602
+ service: historyService,
1603
+ linkedservice: service,
1604
+ type: service.UUID,
1605
+ sub: service.subtype,
1606
+ evetype: 'waterguard',
1607
+ fields: 'xxxx',
1608
+ entry: 0,
1609
+ count: tempHistory.length,
1610
+ reftime: historyreftime,
1611
+ send: 0,
1612
+ };
1613
+
1614
+ // Need some internal storage to track Eve Water Guard configuration from EveHome app
1615
+ this.EveWaterGuardPersist = {
1616
+ firmware: typeof options?.EveWaterGuard_firmware === 'number' ? options.EveWaterGuard_firmware : 2866, // Firmware version
1617
+ lastalarmtest: typeof options?.EveWaterGuard_lastalarmtest === 'number' ? options.EveWaterGuard_lastalarmtest : 0, // In seconds
1618
+ muted: options?.EveWaterGuard_muted === true, // Leak alarms are not muted
1619
+ };
1620
+
1621
+ // Setup initial values and callbacks for charateristics we are using
1622
+ service.updateCharacteristic(this.hap.Characteristic.EveGetConfiguration, this.#EveWaterGuardGetDetails(options.getcommand));
1623
+ service.getCharacteristic(this.hap.Characteristic.EveGetConfiguration).onGet(() => {
1624
+ return this.#EveWaterGuardGetDetails(options.getcommand);
1625
+ });
1626
+
1627
+ service.getCharacteristic(this.hap.Characteristic.EveSetConfiguration).onSet((value) => {
1628
+ let valHex = decodeEveData(value);
1629
+ let index = 0;
1630
+ while (index < valHex.length) {
1631
+ // first byte is command in this data stream
1632
+ // second byte is size of data for command
1633
+ let command = valHex.substr(index, 2);
1634
+ let size = parseInt(valHex.substr(index + 2, 2), 16) * 2;
1635
+ let data = valHex.substr(index + 4, parseInt(valHex.substr(index + 2, 2), 16) * 2);
1636
+
1637
+ //console.log(command, data);
1638
+ switch (command) {
1639
+ case '4d': {
1640
+ // Alarm test
1641
+ // b4 - start
1642
+ // 00 - finished
1643
+ break;
1644
+ }
1645
+
1646
+ case '4e': {
1647
+ // Mute alarm
1648
+ // 00 - unmute alarm
1649
+ // 01 - mute alarm
1650
+ // 03 - alarm test
1651
+ if (data === '03') {
1652
+ // Simulate a leak test
1653
+ service.updateCharacteristic(this.hap.Characteristic.LeakDetected, this.hap.Characteristic.LeakDetected.LEAK_DETECTED);
1654
+ this.EveWaterGuardPersist.lastalarmtest = Math.floor(Date.now() / 1000); // Now time for last test
1655
+
1656
+ setTimeout(() => {
1657
+ // Clear our simulated leak test after 5 seconds
1658
+ service.updateCharacteristic(
1659
+ this.hap.Characteristic.LeakDetected,
1660
+ this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED,
1661
+ );
1662
+ }, 5000);
1663
+ }
1664
+ if (data === '00' || data === '01') {
1665
+ this.EveWaterGuardPersist.muted = data === '01' ? true : false;
1666
+ }
1667
+ break;
1668
+ }
1669
+
1670
+ default: {
1671
+ this?.log?.debug && this.log.debug('Unknown Eve Water Guard command "%s" with data "%s"', command, data);
1672
+ break;
1673
+ }
1674
+ }
1675
+ index += 4 + size; // Move to next command accounting for header size of 4 bytes
1676
+ }
1677
+ });
1678
+ break;
1679
+ }
1680
+ }
1681
+
1682
+ // Setup callbacks if our service successfully created
1683
+ if (typeof this?.EveHome?.service === 'object') {
1684
+ this.EveHome.service.getCharacteristic(this.hap.Characteristic.EveResetTotal).onGet(() => {
1685
+ // time since history reset
1686
+ return this.historyData.reset - EPOCH_OFFSET;
1687
+ });
1688
+ this.EveHome.service.getCharacteristic(this.hap.Characteristic.EveHistoryStatus).onGet(() => {
1689
+ return this.#EveHistoryStatus();
1690
+ });
1691
+ this.EveHome.service.getCharacteristic(this.hap.Characteristic.EveHistoryEntries).onGet(() => {
1692
+ return this.#EveHistoryEntries();
1693
+ });
1694
+ this.EveHome.service.getCharacteristic(this.hap.Characteristic.EveHistoryRequest).onSet((value) => {
1695
+ this.#EveHistoryRequest(value);
1696
+ });
1697
+ this.EveHome.service.getCharacteristic(this.hap.Characteristic.EveSetTime).onSet((value) => {
1698
+ this.#EveSetTime(value);
1699
+ });
1700
+
1701
+ return this.EveHome.service; // Return service handle for our EveHome accessory service
1702
+ }
1703
+ }
1704
+
1705
+ updateEveHome(service, getcommand) {
1706
+ if (typeof this?.EveHome?.service !== 'object' || typeof getcommand !== 'function') {
1707
+ return;
1708
+ }
1709
+
1710
+ switch (service.UUID) {
1711
+ case this.hap.Service.SmokeSensor.UUID: {
1712
+ service.updateCharacteristic(
1713
+ this.hap.Characteristic.EveDeviceStatus,
1714
+ this.#EveSmokeGetDetails(getcommand, this.hap.Characteristic.EveDeviceStatus),
1715
+ );
1716
+ service.updateCharacteristic(
1717
+ this.hap.Characteristic.EveGetConfiguration,
1718
+ this.#EveSmokeGetDetails(getcommand, this.hap.Characteristic.EveGetConfiguration),
1719
+ );
1720
+ break;
1721
+ }
1722
+
1723
+ case this.hap.Service.HeaterCooler.UUID:
1724
+ case this.hap.Service.Thermostat.UUID: {
1725
+ service.updateCharacteristic(this.hap.Characteristic.EveProgramCommand, this.#EveThermoGetDetails(getcommand));
1726
+ break;
1727
+ }
1728
+
1729
+ case this.hap.Service.Valve.UUID:
1730
+ case this.hap.Service.IrrigationSystem.UUID: {
1731
+ service.updateCharacteristic(this.hap.Characteristic.EveGetConfiguration, this.#EveAquaGetDetails(getcommand));
1732
+ break;
1733
+ }
1734
+
1735
+ case this.hap.Service.Outlet.UUID: {
1736
+ service.updateCharacteristic(
1737
+ this.hap.Characteristic.EveElectricalWattage,
1738
+ this.#EveEnergyGetDetails(getcommand, this.hap.Characteristic.EveElectricalWattage),
1739
+ );
1740
+ service.updateCharacteristic(
1741
+ this.hap.Characteristic.EveElectricalVoltage,
1742
+ this.#EveEnergyGetDetails(getcommand, this.hap.Characteristic.EveElectricalVoltage),
1743
+ );
1744
+ service.updateCharacteristic(
1745
+ this.hap.Characteristic.EveElectricalCurrent,
1746
+ this.#EveEnergyGetDetails(getcommand, this.hap.Characteristic.EveElectricalCurrent),
1747
+ );
1748
+ break;
1749
+ }
1750
+ }
1751
+ }
1752
+
1753
+ #EveLastEventTime() {
1754
+ // calculate time in seconds since first event to last event. If no history we'll use the current time as the last event time
1755
+ let historyEntry = this.lastHistory(this.EveHome.type, this.EveHome.sub);
1756
+ let lastTime = Math.floor(Date.now() / 1000) - (this.EveHome.reftime + EPOCH_OFFSET);
1757
+ if (historyEntry && Object.keys(historyEntry).length !== 0) {
1758
+ lastTime -= Math.floor(Date.now() / 1000) - historyEntry.time;
1759
+ }
1760
+ return lastTime;
1761
+ }
1762
+
1763
+ #EveThermoGetDetails(getOptions) {
1764
+ // returns an encoded value formatted for an Eve Thermo device
1765
+ //
1766
+ // TODO - before enabling below need to workout:
1767
+ // - mode graph to show
1768
+ // - temperature unit setting
1769
+ // - thermo 2020??
1770
+ //
1771
+ // commands
1772
+ // 11 - valve protection on/off - TODO
1773
+ // 12 - temp offset
1774
+ // 13 - schedules enabled/disabled
1775
+ // 16 - Window/Door open status
1776
+ // 100000 - open
1777
+ // 000000 - close
1778
+ // 14 - installation status
1779
+ // c0,c8 = ok
1780
+ // c1,c6,c9 = in-progress
1781
+ // c2,c3,c4,c5 = error on removal
1782
+ // c7 = not attached
1783
+ // 19 - vacation mode
1784
+ // 00ff - off
1785
+ // 01 + 'away temp' - enabled with vacation temp
1786
+ // f4 - temperatures
1787
+ // fa - programs for week
1788
+ // fc - date/time (mmhhDDMMYY)
1789
+ // 1a - default day program??
1790
+
1791
+ if (typeof getOptions === 'function') {
1792
+ // Fill in details we might want to be dynamic
1793
+ this.EveThermoPersist = getOptions(this.EveThermoPersist);
1794
+ }
1795
+
1796
+ // Encode current date/time
1797
+ //let tempDateTime = numberToEveHexString(new Date(Date.now()).getMinutes(), 2) +
1798
+ // numberToEveHexString(new Date(Date.now()).getHours(), 2) +
1799
+ // numberToEveHexString(new Date(Date.now()).getDate(), 2) +
1800
+ // numberToEveHexString(new Date(Date.now()).getMonth() + 1, 2) +
1801
+ // numberToEveHexString(parseInt(new Date(Date.now()).getFullYear().toString().substr(-2)), 2);
1802
+
1803
+ // Encode program schedule and temperatures
1804
+ // f4 = temps
1805
+ // fa = schedule
1806
+ const EMPTYSCHEDULE = 'ffffffffffffffff';
1807
+ let encodedSchedule = [EMPTYSCHEDULE, EMPTYSCHEDULE, EMPTYSCHEDULE, EMPTYSCHEDULE, EMPTYSCHEDULE, EMPTYSCHEDULE, EMPTYSCHEDULE];
1808
+ let encodedTemperatures = '0000';
1809
+ if (typeof this.EveThermoPersist.programs === 'object') {
1810
+ let tempTemperatures = [];
1811
+ Object.values(this.EveThermoPersist.programs).forEach((days) => {
1812
+ let temp = '';
1813
+ days.schedule.forEach((time) => {
1814
+ temp =
1815
+ temp +
1816
+ numberToEveHexString(Math.round(time.start / 600), 2) +
1817
+ numberToEveHexString(Math.round((time.start + time.duration) / 600), 2);
1818
+ tempTemperatures.push(time.ecotemp, time.comforttemp);
1819
+ });
1820
+ encodedSchedule[DAYSOFWEEK.indexOf(days.days.toLowerCase())] =
1821
+ temp.substring(0, EMPTYSCHEDULE.length) + EMPTYSCHEDULE.substring(temp.length, EMPTYSCHEDULE.length);
1822
+ });
1823
+ let ecoTemp = tempTemperatures.length === 0 ? 0 : Math.min(...tempTemperatures);
1824
+ let comfortTemp = tempTemperatures.length === 0 ? 0 : Math.max(...tempTemperatures);
1825
+ encodedTemperatures = numberToEveHexString(Math.round(ecoTemp * 2), 2) + numberToEveHexString(Math.round(comfortTemp * 2), 2);
1826
+ }
1827
+
1828
+ let value = util.format(
1829
+ '12%s 13%s 14%s 19%s f40000%s fa%s',
1830
+ numberToEveHexString(this.EveThermoPersist.tempoffset * 10, 2),
1831
+ this.EveThermoPersist.enableschedule === true ? '01' : '00',
1832
+ this.EveThermoPersist.attached === true ? 'c0' : 'c7',
1833
+ this.EveThermoPersist.vacation === true ? '01' + numberToEveHexString(this.EveThermoPersist.vacationtemp * 2, 2) : '00ff', // away status and temp
1834
+ encodedTemperatures,
1835
+ encodedSchedule[0] +
1836
+ encodedSchedule[1] +
1837
+ encodedSchedule[2] +
1838
+ encodedSchedule[3] +
1839
+ encodedSchedule[4] +
1840
+ encodedSchedule[5] +
1841
+ encodedSchedule[6],
1842
+ );
1843
+
1844
+ return encodeEveData(value);
1845
+ }
1846
+
1847
+ #EveAquaGetDetails(getOptions) {
1848
+ // returns an encoded value formatted for an Eve Aqua device for water usage and last water time
1849
+ if (typeof getOptions === 'function') {
1850
+ // Fill in details we might want to be dynamic
1851
+ this.EveAquaPersist = getOptions(this.EveAquaPersist);
1852
+ }
1853
+
1854
+ if (Array.isArray(this.EveAquaPersist.programs) === false) {
1855
+ // Ensure any program information is an array
1856
+ this.EveAquaPersist.programs = [];
1857
+ }
1858
+
1859
+ let tempHistory = this.getHistory(this.EveHome.type, this.EveHome.sub); // get flattened history array for easier processing
1860
+
1861
+ // Calculate total water usage over history period
1862
+ let totalWater = 0;
1863
+ tempHistory.forEach((historyEntry) => {
1864
+ if (historyEntry.status === 0) {
1865
+ // add to total water usage if we have a valve closed event
1866
+ totalWater += parseFloat(historyEntry.water);
1867
+ }
1868
+ });
1869
+
1870
+ // Encode program schedule
1871
+ // 45 = schedules
1872
+ // 46 = days of weeks for schedule;
1873
+ const EMPTYSCHEDULE = '0800';
1874
+ let encodedSchedule = '';
1875
+ let daysbitmask = 0;
1876
+ let temp45Command = '';
1877
+ let temp46Command = '';
1878
+
1879
+ this.EveAquaPersist.programs.forEach((program) => {
1880
+ let tempEncodedSchedule = '';
1881
+ program.schedule.forEach((schedule) => {
1882
+ // Encode absolute time (ie: not sunrise/sunset one)
1883
+ if (typeof schedule.start === 'number') {
1884
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString(((schedule.start / 60) << 5) + 0x05, 4);
1885
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString((((schedule.start + schedule.duration) / 60) << 5) + 0x01, 4);
1886
+ }
1887
+ if (typeof schedule.start === 'string' && schedule.start === 'sunrise') {
1888
+ if (schedule.offset < 0) {
1889
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString(((Math.abs(schedule.offset) / 60) << 7) + 0x67, 4);
1890
+ tempEncodedSchedule =
1891
+ tempEncodedSchedule + numberToEveHexString((((Math.abs(schedule.offset) + schedule.duration) / 60) << 7) + 0x63, 4);
1892
+ }
1893
+ if (schedule.offset >= 0) {
1894
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString(((schedule.offset / 60) << 7) + 0x27, 4);
1895
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString((((schedule.offset + schedule.duration) / 60) << 7) + 0x23, 4);
1896
+ }
1897
+ }
1898
+ if (typeof schedule.start === 'string' && schedule.start === 'sunset') {
1899
+ if (schedule.offset < 0) {
1900
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString(((Math.abs(schedule.offset) / 60) << 7) + 0x47, 4);
1901
+ tempEncodedSchedule =
1902
+ tempEncodedSchedule + numberToEveHexString((((Math.abs(schedule.offset) + schedule.duration) / 60) << 7) + 0x43, 4);
1903
+ }
1904
+ if (schedule.offset >= 0) {
1905
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString(((schedule.offset / 60) << 7) + 0x07, 4);
1906
+ tempEncodedSchedule = tempEncodedSchedule + numberToEveHexString((((schedule.offset + schedule.duration) / 60) << 7) + 0x03, 4);
1907
+ }
1908
+ }
1909
+ });
1910
+ encodedSchedule =
1911
+ encodedSchedule +
1912
+ numberToEveHexString(tempEncodedSchedule.length / 8 < 2 ? 10 : 11, 2) +
1913
+ numberToEveHexString(tempEncodedSchedule.length / 8, 2) +
1914
+ tempEncodedSchedule;
1915
+
1916
+ // Encode days for this program
1917
+ // Program ID is set in 3bit repeating sections
1918
+ // sunsatfrithuwedtuemon
1919
+ program.days.forEach((day) => {
1920
+ daysbitmask = daysbitmask + (program.id << (DAYSOFWEEK.indexOf(day) * 3));
1921
+ });
1922
+ });
1923
+
1924
+ // Build the encoded schedules command to send back to Eve
1925
+ temp45Command = '05' + numberToEveHexString(this.EveAquaPersist.programs.length + 1, 2) + '000000' + EMPTYSCHEDULE + encodedSchedule;
1926
+ temp45Command = '45' + numberToEveHexString(temp45Command.length / 2, 2) + temp45Command;
1927
+
1928
+ // Build the encoded days command to send back to Eve
1929
+ // 00000 appears to always be 1b202c??
1930
+ temp46Command = '05' + '000000' + numberToEveHexString((daysbitmask << 4) + 0x0f, 6);
1931
+ temp46Command = temp46Command.padEnd(daysbitmask === 0 ? 18 : 168, '0'); // Pad the command out to Eve's lengths
1932
+ temp46Command = '46' + numberToEveHexString(temp46Command.length / 2, 2) + temp46Command;
1933
+
1934
+ let value = util.format(
1935
+ '0002 2300 0302 %s d004 %s 9b04 %s 2f0e %s 2e02 %s 441105 %s%s%s%s %s %s %s 0000000000000000 1e02 2300 0c',
1936
+ numberToEveHexString(this.EveAquaPersist.firmware, 4), // firmware version (build xxxx)
1937
+ numberToEveHexString(tempHistory.length !== 0 ? tempHistory[tempHistory.length - 1].time : 0, 8), // time of last event, 0 if never
1938
+ numberToEveHexString(Math.floor(Date.now() / 1000), 8), // 'now' time
1939
+ numberToEveHexString(Math.floor(totalWater * 1000), 20), // total water usage in ml (64bit value)
1940
+ numberToEveHexString(Math.floor((this.EveAquaPersist.flowrate * 1000) / 60), 4), // water flow rate (16bit value)
1941
+ numberToEveHexString(this.EveAquaPersist.enableschedule === true ? parseInt('10111', 2) : parseInt('10110', 2), 8),
1942
+ numberToEveHexString(Math.floor(this.EveAquaPersist.utcoffset / 60), 8),
1943
+ numberToEveHexString(this.EveAquaPersist.latitude, 8, 5), // For lat/long, we need 5 digits of precession
1944
+ numberToEveHexString(this.EveAquaPersist.longitude, 8, 5), // For lat/long, we need 5 digits of precession
1945
+ this.EveAquaPersist.pause !== 0 ? '4b04' + numberToEveHexString((this.EveAquaPersist.pause - 1) * 1440, 8) : '',
1946
+ temp45Command,
1947
+ temp46Command,
1948
+ );
1949
+
1950
+ return encodeEveData(value);
1951
+ }
1952
+
1953
+ #EveEnergyGetDetails(getOptions, returnForCharacteristic) {
1954
+ let energyDetails = {};
1955
+ let returnValue = null;
1956
+
1957
+ if (typeof getOptions === 'function') {
1958
+ // Fill in details we might want to be dynamic
1959
+ energyDetails = getOptions(energyDetails);
1960
+ }
1961
+
1962
+ if (returnForCharacteristic.UUID === this.hap.Characteristic.EveElectricalWattage.UUID && typeof energyDetails?.watts === 'number') {
1963
+ returnValue = energyDetails.watts;
1964
+ }
1965
+ if (returnForCharacteristic.UUID === this.hap.Characteristic.EveElectricalVoltage.UUID && typeof energyDetails?.volts === 'number') {
1966
+ returnValue = energyDetails.volts;
1967
+ }
1968
+ if (returnForCharacteristic.UUID === this.hap.Characteristic.EveElectricalCurrent.UUID && typeof energyDetails?.amps === 'number') {
1969
+ returnValue = energyDetails.amps;
1970
+ }
1971
+
1972
+ return returnValue;
1973
+ }
1974
+
1975
+ #EveSmokeGetDetails(getOptions, returnForCharacteristic) {
1976
+ // returns an encoded value formatted for an Eve Smoke device
1977
+ let returnValue = null;
1978
+
1979
+ if (typeof getOptions === 'function') {
1980
+ // Fill in details we might want to be dynamic
1981
+ this.EveSmokePersist = getOptions(this.EveSmokePersist);
1982
+ }
1983
+
1984
+ if (returnForCharacteristic.UUID === this.hap.Characteristic.EveGetConfiguration.UUID) {
1985
+ let value = util.format(
1986
+ '0002 1800 0302 %s 9b04 %s 8608 %s 1e02 1800 0c',
1987
+ numberToEveHexString(this.EveSmokePersist.firmware, 4), // firmware version (build xxxx)
1988
+ numberToEveHexString(Math.floor(Date.now() / 1000), 8), // 'now' time
1989
+ numberToEveHexString(this.EveSmokePersist.lastalarmtest, 8),
1990
+ ); // Not sure why 64bit value???
1991
+ returnValue = encodeEveData(value);
1992
+ }
1993
+
1994
+ if (returnForCharacteristic.UUID === this.hap.Characteristic.EveDeviceStatus.UUID) {
1995
+ // Status bits
1996
+ // 0 = Smoked Detected
1997
+ // 1 = Heat Detected
1998
+ // 2 = Alarm test active
1999
+ // 5 = Smoke sensor error
2000
+ // 6 = Heat sensor error
2001
+ // 7 = Sensor error??
2002
+ // 9 = Smoke chamber error
2003
+ // 14 = Smoke sensor deactivated
2004
+ // 15 = flash status led (on)
2005
+ // 24 & 25 = alarms paused
2006
+ // 25 = alarm muted
2007
+ let value = 0x00000000;
2008
+ if (
2009
+ this.EveHome.linkedservice.getCharacteristic(this.hap.Characteristic.SmokeDetected).value ===
2010
+ this.hap.Characteristic.SmokeDetected.SMOKE_DETECTED
2011
+ ) {
2012
+ value |= 1 << 0; // 1st bit, smoke detected
2013
+ }
2014
+ if (this.EveSmokePersist.heatstatus !== 0) {
2015
+ value |= 1 << 1; // 2th bit - heat detected
2016
+ }
2017
+ if (this.EveSmokePersist.alarmtest === true) {
2018
+ value |= 1 << 2; // 4th bit - alarm test running
2019
+ }
2020
+ if (this.EveSmokePersist.smoketestpassed === false) {
2021
+ value |= 1 << 5; // 5th bit - smoke test OK
2022
+ }
2023
+ if (this.EveSmokePersist.heattestpassed === false) {
2024
+ value |= 1 << 6; // 6th bit - heat test OK
2025
+ }
2026
+ if (this.EveSmokePersist.smoketestpassed === false) {
2027
+ value |= 1 << 9; // 9th bit - smoke test OK
2028
+ }
2029
+ if (this.EveSmokePersist.statusled === true) {
2030
+ value |= 1 << 15; // 15th bit - flash status led
2031
+ }
2032
+ if (this.EveSmokePersist.hushedstate === true) {
2033
+ value |= 1 << 25; // 25th bit, alarms muted
2034
+ }
2035
+
2036
+ returnValue = value >>> 0; // Ensure UINT32
2037
+ }
2038
+ return returnValue;
2039
+ }
2040
+
2041
+ #EveWaterGuardGetDetails(getOptions) {
2042
+ // returns an encoded value formatted for an Eve Water Guard
2043
+ if (typeof getOptions === 'function') {
2044
+ // Fill in details we might want to be dynamic
2045
+ this.EveWaterGuardPersist = getOptions(this.EveWaterGuardPersist);
2046
+ }
2047
+
2048
+ let value = util.format(
2049
+ '0002 5b00 0302 %s 9b04 %s 8608 %s 4e01 %s %s 1e02 5b00 0c',
2050
+ numberToEveHexString(this.EveWaterGuardPersist.firmware, 4), // firmware version (build xxxx)
2051
+ numberToEveHexString(Math.floor(Date.now() / 1000), 8), // 'now' time
2052
+ numberToEveHexString(this.EveWaterGuardPersist.lastalarmtest, 8), // Not sure why 64bit value???
2053
+ numberToEveHexString(this.EveWaterGuardPersist.muted === true ? 1 : 0, 2),
2054
+ ); // Alarm mute status
2055
+
2056
+ return encodeEveData(value);
2057
+ }
2058
+
2059
+ #EveHistoryStatus() {
2060
+ let tempHistory = this.getHistory(this.EveHome.type, this.EveHome.sub); // get flattened history array for easier processing
2061
+ let historyTime = tempHistory.length === 0 ? Math.floor(Date.now() / 1000) : tempHistory[tempHistory.length - 1].time;
2062
+ this.EveHome.reftime = tempHistory.length === 0 ? this.historyData.reset - EPOCH_OFFSET : tempHistory[0].time - EPOCH_OFFSET;
2063
+ this.EveHome.count = tempHistory.length; // Number of history entries for this type
2064
+
2065
+ let value = util.format(
2066
+ '%s 00000000 %s %s %s %s %s %s 000000000101',
2067
+ numberToEveHexString(historyTime - this.EveHome.reftime - EPOCH_OFFSET, 8),
2068
+ numberToEveHexString(this.EveHome.reftime, 8), // reference time (time of first history??)
2069
+ numberToEveHexString(this.EveHome.fields.trim().match(/\S*[0-9]\S*/g).length, 2), // Calclate number of fields we have
2070
+ this.EveHome.fields.trim(), // Fields listed in string. Each field is seperated by spaces
2071
+ numberToEveHexString(this.EveHome.count, 4), // count of entries
2072
+ numberToEveHexString(this.maxEntries === 0 ? MAX_HISTORY_SIZE : this.maxEntries, 4), // history max size
2073
+ numberToEveHexString(1, 8),
2074
+ ); // first entry
2075
+
2076
+ if (this?.log?.debug) {
2077
+ this.log.debug(
2078
+ '#EveHistoryStatus: history for "%s:%s" (%s) - Entries %s',
2079
+ this.EveHome.type,
2080
+ this.EveHome.sub,
2081
+ this.EveHome.evetype,
2082
+ this.EveHome.count,
2083
+ );
2084
+ }
2085
+ return encodeEveData(value);
2086
+ }
2087
+
2088
+ #EveHistoryEntries() {
2089
+ // Streams our history data back to EveHome when requested
2090
+ let dataStream = '';
2091
+ if (this.EveHome.entry <= this.EveHome.count && this.EveHome.send !== 0) {
2092
+ let tempHistory = this.getHistory(this.EveHome.type, this.EveHome.sub); // get flattened history array for easier processing
2093
+
2094
+ // Generate eve home history header for data following
2095
+ let data = util.format(
2096
+ '%s 0100 0000 81 %s 0000 0000 00 0000',
2097
+ numberToEveHexString(this.EveHome.entry, 8),
2098
+ numberToEveHexString(this.EveHome.reftime, 8),
2099
+ );
2100
+
2101
+ // Format the data string, including calculating the number of 'bytes' the data fits into
2102
+ data = data.replace(/ /g, '');
2103
+ dataStream += util.format('%s %s', (data.length / 2 + 1).toString(16), data);
2104
+
2105
+ for (let i = 0; i < EVEHOME_MAX_STREAM; i++) {
2106
+ if (tempHistory.length !== 0 && this.EveHome.entry - 1 <= tempHistory.length) {
2107
+ let historyEntry = tempHistory[this.EveHome.entry - 1]; // map EveHome address to our history, as EvenHome addresses start at 1
2108
+ let data = util.format(
2109
+ '%s %s',
2110
+ numberToEveHexString(this.EveHome.entry, 8),
2111
+ numberToEveHexString(historyEntry.time - this.EveHome.reftime - EPOCH_OFFSET, 8),
2112
+ ); // Create the common header data for eve entry
2113
+
2114
+ switch (this.EveHome.evetype) {
2115
+ case 'aqua': {
2116
+ // 1f01 2a08 2302
2117
+ // 1f - InUse
2118
+ // 2a - Water Usage (ml)
2119
+ // 23 - Battery millivolts
2120
+ data += util.format(
2121
+ '%s %s %s %s',
2122
+ numberToEveHexString(historyEntry.status === 0 ? parseInt('111', 2) : parseInt('101', 2), 2), // Field mask, 111 is for sending water usage when a valve is recorded as closed, 101 is for when valve is recorded as opened, no water usage is sent
2123
+ numberToEveHexString(historyEntry.status, 2),
2124
+ historyEntry.status === 0 ? numberToEveHexString(Math.floor(parseFloat(historyEntry.water) * 1000), 16) : '', // water used in millilitres if valve closed entry (64bit value)
2125
+ numberToEveHexString(3120, 4), // battery millivolts - 3120mv which think should be 100% for an eve aqua running on 2 x AAs?
2126
+ );
2127
+ break;
2128
+ }
2129
+
2130
+ case 'room': {
2131
+ // 0102 0202 0402 0f03
2132
+ // 01 - Temperature
2133
+ // 02 - Humidity
2134
+ // 04 - Air Quality (ppm)
2135
+ // 0f - VOC Heat Sense??
2136
+ data += util.format(
2137
+ '%s %s %s %s %s',
2138
+ numberToEveHexString(parseInt('1111', 2), 2), // Field include/exclude mask
2139
+ numberToEveHexString(historyEntry.temperature * 100, 4), // temperature
2140
+ numberToEveHexString(historyEntry.humidity * 100, 4), // Humidity
2141
+ numberToEveHexString(typeof historyEntry?.ppm === 'number' ? historyEntry.ppm * 10 : 10, 4), // PPM - air quality
2142
+ numberToEveHexString(0, 6),
2143
+ ); // VOC??
2144
+ break;
2145
+ }
2146
+
2147
+ case 'room2': {
2148
+ // 0102 0202 2202 2901 2501 2302 2801
2149
+ // 01 - Temperature
2150
+ // 02 - Humidity
2151
+ // 22 - VOC Density (ppb)
2152
+ // 29 - ??
2153
+ // 25 - Battery level %
2154
+ // 23 - Battery millivolts
2155
+ // 28 - ??
2156
+ data += util.format(
2157
+ '%s %s %s %s %s %s %s %s',
2158
+ numberToEveHexString(parseInt('1111111', 2), 2), // Field include/exclude mask
2159
+ numberToEveHexString(historyEntry.temperature * 100, 4), // temperature
2160
+ numberToEveHexString(historyEntry.humidity * 100, 4), // Humidity
2161
+ numberToEveHexString(typeof historyEntry?.voc === 'number' ? historyEntry.voc : 0, 4), // VOC - air quality in ppm
2162
+ numberToEveHexString(0, 2), // ??
2163
+ numberToEveHexString(100, 2), // battery level % - 100%
2164
+ numberToEveHexString(4771, 4), // battery millivolts - 4771mv
2165
+ numberToEveHexString(1, 2),
2166
+ ); // ??
2167
+ break;
2168
+ }
2169
+
2170
+ case 'weather': {
2171
+ // 0102 0202 0302
2172
+ // 01 - Temperature
2173
+ // 02 - Humidity
2174
+ // 03 - Air Pressure
2175
+ data += util.format(
2176
+ '%s %s %s %s',
2177
+ numberToEveHexString(parseInt('111', 2), 2), // Field include/exclude mask
2178
+ numberToEveHexString(historyEntry.temperature * 100, 4), // temperature
2179
+ numberToEveHexString(historyEntry.humidity * 100, 4), // Humidity
2180
+ numberToEveHexString(typeof historyEntry?.pressure === 'number' ? historyEntry.pressure * 10 : 10, 4),
2181
+ ); // Pressure
2182
+ break;
2183
+ }
2184
+
2185
+ case 'motion': {
2186
+ // 1301 1c01
2187
+ // 13 - Motion detected
2188
+ // 1c - Motion currently active??
2189
+ data += util.format(
2190
+ '%s %s',
2191
+ numberToEveHexString(parseInt('10', 2), 2), // Field include/exclude mask
2192
+ numberToEveHexString(historyEntry.status, 2),
2193
+ );
2194
+ break;
2195
+ }
2196
+
2197
+ case 'contact':
2198
+ case 'switch': {
2199
+ // contact, motion and switch sensors treated the same for status
2200
+ // 0601
2201
+ // 06 - Contact status 0 = no contact, 1 = contact
2202
+ data += util.format(
2203
+ '%s %s',
2204
+ numberToEveHexString(parseInt('1', 2), 2), // Field include/exclude mask
2205
+ numberToEveHexString(historyEntry.status, 2),
2206
+ );
2207
+ break;
2208
+ }
2209
+
2210
+ case 'door': {
2211
+ // Invert status as EveHome door is a contact sensor where 1 is contact and 0 is no contact
2212
+ // (opposite of what we expect a door to be)
2213
+ // ie: 0 = closed, 1 = opened
2214
+ // 0601
2215
+ // 06 - Contact status 0 = no contact, 1 = contact
2216
+ data += util.format(
2217
+ '%s %s',
2218
+ numberToEveHexString(parseInt('1', 2), 2), // Field include/exclude mask
2219
+ numberToEveHexString(historyEntry.status === 1 ? 0 : 1, 2),
2220
+ ); // status for EveHome (inverted ie: 1 = closed, 0 = opened) */
2221
+ break;
2222
+ }
2223
+
2224
+ case 'thermo': {
2225
+ // 0102 0202 1102 1001 1201 1d01
2226
+ // 01 - Temperature
2227
+ // 02 - Humidity
2228
+ // 11 - Target Temperature
2229
+ // 10 - Valve percentage
2230
+ // 12 - Thermo target
2231
+ // 1d - Open window
2232
+ let tempTarget = 0;
2233
+ if (typeof historyEntry.target === 'object') {
2234
+ if (historyEntry.target.low === 0 && historyEntry.target.high !== 0) {
2235
+ tempTarget = historyEntry.target.high; // heating limit
2236
+ }
2237
+ if (historyEntry.target.low !== 0 && historyEntry.target.high !== 0) {
2238
+ tempTarget = historyEntry.target.high; // range, so using heating limit
2239
+ }
2240
+ if (historyEntry.target.low !== 0 && historyEntry.target.high === 0) {
2241
+ tempTarget = 0; // cooling limit
2242
+ }
2243
+ if (historyEntry.target.low === 0 && historyEntry.target.high === 0) {
2244
+ tempTarget = 0; // off
2245
+ }
2246
+ }
2247
+
2248
+ data += util.format(
2249
+ '%s %s %s %s %s %s %s',
2250
+ numberToEveHexString(parseInt('111111', 2), 2), // Field include/exclude mask
2251
+ numberToEveHexString(historyEntry.temperature * 100, 4), // temperature
2252
+ numberToEveHexString(historyEntry.humidity * 100, 4), // Humidity
2253
+ numberToEveHexString(tempTarget * 100, 4), // target temperature for heating
2254
+ numberToEveHexString(historyEntry.status === 2 ? 100 : historyEntry.status === 3 ? 50 : 0, 2), // 0% = off, 50% = cooling, 100% = heating
2255
+ numberToEveHexString(0, 2), // Thermo target
2256
+ numberToEveHexString(0, 2), // Window open status 0 = closed, 1 = open
2257
+ );
2258
+ break;
2259
+ }
2260
+
2261
+ case 'energy': {
2262
+ // 0702 0e01
2263
+ // 07 - Power10thWh
2264
+ // 0e - on/off
2265
+ data += util.format(
2266
+ '%s %s %s',
2267
+ numberToEveHexString(parseInt('11', 2), 2), // Field include/exclude mask
2268
+ numberToEveHexString(historyEntry.watts * 10, 4), // Power in watts
2269
+ numberToEveHexString(historyEntry.status, 2),
2270
+ ); // Power status, 1 = on, 0 = off
2271
+ break;
2272
+ }
2273
+
2274
+ case 'smoke': {
2275
+ // TODO - What do we send back??
2276
+ break;
2277
+ }
2278
+
2279
+ case 'blind': {
2280
+ // TODO - What do we send back??
2281
+ break;
2282
+ }
2283
+
2284
+ case 'waterguard': {
2285
+ // TODO - What do we send back??
2286
+ break;
2287
+ }
2288
+ }
2289
+
2290
+ // Format the data string, including calculating the number of 'bytes' the data fits into
2291
+ data = data.replace(/ /g, '');
2292
+ dataStream += util.format('%s%s', numberToEveHexString(data.length / 2 + 1, 2), data);
2293
+
2294
+ this.EveHome.entry++;
2295
+ if (this.EveHome.entry > this.EveHome.count) {
2296
+ break;
2297
+ }
2298
+ }
2299
+ }
2300
+ if (this.EveHome.entry > this.EveHome.count) {
2301
+ // No more history data to send back
2302
+ this?.log?.debug &&
2303
+ this.log.debug(
2304
+ '#EveHistoryEntries: sent "%s" entries to EveHome ("%s") for "%s:%s"',
2305
+ this.EveHome.send,
2306
+ this.EveHome.evetype,
2307
+ this.EveHome.type,
2308
+ this.EveHome.sub,
2309
+ );
2310
+ this.EveHome.send = 0; // no more to send
2311
+ dataStream += '00';
2312
+ }
2313
+ } else {
2314
+ // We're not transferring any data back
2315
+ this?.log?.debug &&
2316
+ this.log.debug(
2317
+ '#EveHistoryEntries: no more entries to send to EveHome ("%s") for "%s:%s',
2318
+ this.EveHome.evetype,
2319
+ this.EveHome.type,
2320
+ this.EveHome.sub,
2321
+ );
2322
+ this.EveHome.send = 0; // no more to send
2323
+ dataStream = '00';
2324
+ }
2325
+ return encodeEveData(dataStream);
2326
+ }
2327
+
2328
+ #EveHistoryRequest(value) {
2329
+ // Requesting history, starting at specific entry
2330
+ this.EveHome.entry = EveHexStringToNumber(decodeEveData(value).substring(4, 12)); // Starting entry
2331
+ if (this.EveHome.entry === 0) {
2332
+ this.EveHome.entry = 1; // requested to restart from beginning of history for sending to EveHome
2333
+ }
2334
+ this.EveHome.send = this.EveHome.count - this.EveHome.entry + 1; // Number of entries we're expected to send
2335
+ this?.log?.debug && this.log.debug('#EveHistoryRequest: requested address', this.EveHome.entry);
2336
+ }
2337
+
2338
+ #EveSetTime(value) {
2339
+ // Time stamp from EveHome
2340
+ let timestamp = EPOCH_OFFSET + EveHexStringToNumber(decodeEveData(value));
2341
+
2342
+ this?.log?.debug && this.log.debug('#EveSetTime: timestamp offset', new Date(timestamp * 1000));
2343
+ }
2344
+
2345
+ #createHistoryService(service, characteristics) {
2346
+ // Setup the history service
2347
+ let historyService = this.accessory.getService(this.hap.Service.EveHomeHistory);
2348
+ if (historyService === undefined) {
2349
+ historyService = this.accessory.addService(this.hap.Service.EveHomeHistory, '', 1);
2350
+ }
2351
+
2352
+ // Add in any specified characteristics
2353
+ characteristics.forEach((characteristic) => {
2354
+ if (service.testCharacteristic(characteristic) === false) {
2355
+ service.addCharacteristic(characteristic);
2356
+ }
2357
+ });
2358
+
2359
+ return historyService;
2360
+ }
2361
+
2362
+ #createHomeKitServicesAndCharacteristics() {
2363
+ const createCustomCharacteristic = (name, uuid, props, values) => {
2364
+ let className = name.replace(/(\s*)/g, '');
2365
+
2366
+ if (this.hap.Characteristic[className] === undefined) {
2367
+ // Create the custom characteristic
2368
+ this.hap.Characteristic[className] = {
2369
+ [className]: class extends this.hap.Characteristic {
2370
+ static UUID = uuid;
2371
+
2372
+ constructor() {
2373
+ super(name, uuid, props);
2374
+ this.value = this.getDefaultValue();
2375
+ }
2376
+ },
2377
+ }[className];
2378
+
2379
+ // Add in any static defines for the object
2380
+ if (typeof values === 'object') {
2381
+ Object.entries(values).forEach(([key, value]) => {
2382
+ this.hap.Characteristic[className][key] = value;
2383
+ });
2384
+ }
2385
+ }
2386
+ };
2387
+
2388
+ const createCustomService = (name, uuid, required, optional) => {
2389
+ let className = name.replace(/(\s*)/g, '');
2390
+ if (this.hap.Service[className] === undefined) {
2391
+ this.hap.Service[className] = {
2392
+ [className]: class extends this.hap.Service {
2393
+ static UUID = uuid;
2394
+
2395
+ constructor(name, subtype) {
2396
+ super(name, uuid, subtype);
2397
+
2398
+ // Add in any required characteristics for the service
2399
+ if (typeof required === 'object') {
2400
+ for (const Characteristic of required) {
2401
+ this.addCharacteristic(Characteristic);
2402
+ }
2403
+ }
2404
+
2405
+ // Add in any optional characteristics for the service
2406
+ if (typeof optional === 'object') {
2407
+ for (const Characteristic of optional) {
2408
+ this.addOptionalCharacteristic(Characteristic);
2409
+ }
2410
+ }
2411
+ }
2412
+ },
2413
+ }[className];
2414
+ }
2415
+ };
2416
+
2417
+ createCustomCharacteristic('Eve Reset Total', 'E863F112-079E-48FF-8F27-9C2605A29F52', {
2418
+ format: this.hap.Formats.UINT32,
2419
+ unit: this.hap.Units.SECONDS, // since 2001/01/01
2420
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY, this.hap.Perms.PAIRED_WRITE],
2421
+ });
2422
+
2423
+ createCustomCharacteristic('Eve History Status', 'E863F116-079E-48FF-8F27-9C2605A29F52', {
2424
+ format: this.hap.Formats.DATA,
2425
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY, this.hap.Perms.HIDDEN],
2426
+ });
2427
+
2428
+ createCustomCharacteristic('Eve History Entries', 'E863F117-079E-48FF-8F27-9C2605A29F52', {
2429
+ format: this.hap.Formats.DATA,
2430
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY, this.hap.Perms.HIDDEN],
2431
+ });
2432
+
2433
+ createCustomCharacteristic('Eve History Request', 'E863F11C-079E-48FF-8F27-9C2605A29F52', {
2434
+ format: this.hap.Formats.DATA,
2435
+ perms: [this.hap.Perms.PAIRED_WRITE, this.hap.Perms.HIDDEN],
2436
+ });
2437
+
2438
+ createCustomCharacteristic('Eve Set Time', 'E863F121-079E-48FF-8F27-9C2605A29F52', {
2439
+ format: this.hap.Formats.DATA,
2440
+ perms: [this.hap.Perms.PAIRED_WRITE, this.hap.Perms.HIDDEN],
2441
+ });
2442
+
2443
+ createCustomCharacteristic('Eve Valve Position', 'E863F12E-079E-48FF-8F27-9C2605A29F52', {
2444
+ format: this.hap.Formats.UINT8,
2445
+ unit: this.hap.Units.PERCENTAGE,
2446
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2447
+ });
2448
+
2449
+ createCustomCharacteristic('Eve Last Activation', 'E863F11A-079E-48FF-8F27-9C2605A29F52', {
2450
+ format: this.hap.Formats.UINT32,
2451
+ unit: this.hap.Units.SECONDS,
2452
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2453
+ });
2454
+
2455
+ createCustomCharacteristic('Eve Times Opened', 'E863F129-079E-48FF-8F27-9C2605A29F52', {
2456
+ format: this.hap.Formats.UINT32,
2457
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2458
+ });
2459
+
2460
+ createCustomCharacteristic('Eve Closed Duration', 'E863F118-079E-48FF-8F27-9C2605A29F52', {
2461
+ format: this.hap.Formats.UINT32,
2462
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2463
+ });
2464
+
2465
+ createCustomCharacteristic('Eve Opened Duration', 'E863F119-079E-48FF-8F27-9C2605A29F52', {
2466
+ format: this.hap.Formats.UINT32,
2467
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2468
+ });
2469
+ createCustomCharacteristic('Eve Program Command', 'E863F12C-079E-48FF-8F27-9C2605A29F52', {
2470
+ format: this.hap.Formats.DATA,
2471
+ perms: [this.hap.Perms.PAIRED_WRITE, this.hap.Perms.HIDDEN],
2472
+ });
2473
+
2474
+ createCustomCharacteristic('Eve Program Data', 'E863F12F-079E-48FF-8F27-9C2605A29F52', {
2475
+ format: this.hap.Formats.DATA,
2476
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2477
+ });
2478
+
2479
+ createCustomCharacteristic('Eve Electrical Voltage', 'E863F10A-079E-48FF-8F27-9C2605A29F52', {
2480
+ format: this.hap.Formats.FLOAT,
2481
+ unit: 'V',
2482
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2483
+ });
2484
+
2485
+ createCustomCharacteristic('Eve Electrical Current', 'E863F126-079E-48FF-8F27-9C2605A29F52', {
2486
+ format: this.hap.Formats.FLOAT,
2487
+ unit: 'A',
2488
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2489
+ });
2490
+
2491
+ createCustomCharacteristic('Eve Total Consumption', 'E863F10C-079E-48FF-8F27-9C2605A29F52', {
2492
+ format: this.hap.Formats.FLOAT,
2493
+ unit: 'kWh',
2494
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2495
+ });
2496
+
2497
+ createCustomCharacteristic('Eve Electrical Wattage', 'E863F10D-079E-48FF-8F27-9C2605A29F52', {
2498
+ format: this.hap.Formats.FLOAT,
2499
+ unit: 'W',
2500
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2501
+ });
2502
+
2503
+ createCustomCharacteristic('Eve Get Configuration', 'E863F131-079E-48FF-8F27-9C2605A29F52', {
2504
+ format: this.hap.Formats.DATA,
2505
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2506
+ });
2507
+
2508
+ createCustomCharacteristic('Eve Set Configuration', 'E863F11D-079E-48FF-8F27-9C2605A29F52', {
2509
+ format: this.hap.Formats.DATA,
2510
+ perms: [this.hap.Perms.PAIRED_WRITE, this.hap.Perms.HIDDEN],
2511
+ });
2512
+
2513
+ createCustomCharacteristic('Eve Firmware', 'E863F11E-079E-48FF-8F27-9C2605A29F52', {
2514
+ format: this.hap.Formats.DATA,
2515
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.PAIRED_WRITE, this.hap.Perms.NOTIFY],
2516
+ });
2517
+
2518
+ createCustomCharacteristic(
2519
+ 'Eve Motion Sensitivity',
2520
+ 'E863F120-079E-48FF-8F27-9C2605A29F52',
2521
+ {
2522
+ format: this.hap.Formats.UINT8,
2523
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.PAIRED_WRITE, this.hap.Perms.NOTIFY],
2524
+ minValue: 0,
2525
+ maxValue: 7,
2526
+ validValues: [0, 4, 7],
2527
+ },
2528
+ { HIGH: 0, MEDIUM: 4, LOW: 7 },
2529
+ );
2530
+
2531
+ createCustomCharacteristic('Eve Motion Duration', 'E863F12D-079E-48FF-8F27-9C2605A29F52', {
2532
+ format: this.hap.Formats.UINT16,
2533
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.PAIRED_WRITE, this.hap.Perms.NOTIFY],
2534
+ minValue: 5,
2535
+ maxValue: 54000,
2536
+ validValues: [5, 10, 20, 30, 60, 120, 300, 600, 1200, 1800, 3600, 7200, 10800, 18000, 36000, 43200, 54000],
2537
+ });
2538
+
2539
+ createCustomCharacteristic(
2540
+ 'Eve Device Status',
2541
+ 'E863F134-079E-48FF-8F27-9C2605A29F52',
2542
+ {
2543
+ format: this.hap.Formats.UINT32,
2544
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2545
+ },
2546
+ {
2547
+ SMOKE_DETECTED: 1 << 0,
2548
+ HEAT_DETECTED: 1 << 1,
2549
+ ALARM_TEST_ACTIVE: 1 << 2,
2550
+ SMOKE_SENSOR_ERROR: 1 << 5,
2551
+ HEAT_SENSOR_ERROR: 1 << 7,
2552
+ SMOKE_CHAMBER_ERROR: 1 << 9,
2553
+ SMOKE_SENSOR_DEACTIVATED: 1 << 14,
2554
+ FLASH_STATUS_LED: 1 << 15,
2555
+ ALARM_PAUSED: 1 << 24,
2556
+ ALARM_MUTED: 1 << 25,
2557
+ },
2558
+ );
2559
+
2560
+ createCustomCharacteristic('Eve Air Pressure', 'E863F10F-079E-48FF-8F27-9C2605A29F52', {
2561
+ format: this.hap.Formats.UINT16,
2562
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2563
+ unit: 'hPa',
2564
+ minValue: 700,
2565
+ maxValue: 1100,
2566
+ });
2567
+
2568
+ createCustomCharacteristic('Eve Elevation', 'E863F130-079E-48FF-8F27-9C2605A29F52', {
2569
+ format: this.hap.Formats.INT,
2570
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.PAIRED_WRITE, this.hap.Perms.NOTIFY],
2571
+ unit: 'm',
2572
+ minValue: -430,
2573
+ maxValue: 8850,
2574
+ minStep: 10,
2575
+ });
2576
+
2577
+ createCustomCharacteristic('Eve VOC Level', 'E863F10B-079E-48FF-8F27-9C2605A29F5', {
2578
+ format: this.hap.Formats.UINT16,
2579
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2580
+ unit: 'ppm',
2581
+ minValue: 5,
2582
+ maxValue: 5000,
2583
+ minStep: 5,
2584
+ });
2585
+
2586
+ createCustomCharacteristic(
2587
+ 'Eve Weather Trend',
2588
+ 'E863F136-079E-48FF-8F27-9C2605A29F52',
2589
+ {
2590
+ format: this.hap.Formats.UINT8,
2591
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2592
+ minValue: 0,
2593
+ maxValue: 15,
2594
+ minStep: 1,
2595
+ },
2596
+ {
2597
+ BLANK: 0,
2598
+ SUN: 1,
2599
+ CLOUDS_SUN: 3,
2600
+ RAIN: 4,
2601
+ RAIN_WIND: 12,
2602
+ },
2603
+ );
2604
+
2605
+ createCustomCharacteristic('Apparent Temperature', 'C1283352-3D12-4777-ACD5-4734760F1AC8', {
2606
+ format: this.hap.Formats.FLOAT,
2607
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2608
+ unit: this.hap.Units.CELSIUS,
2609
+ minValue: -40,
2610
+ maxValue: 100,
2611
+ minStep: 0.1,
2612
+ });
2613
+
2614
+ createCustomCharacteristic('Cloud Cover', '64392FED-1401-4F7A-9ADB-1710DD6E3897', {
2615
+ format: this.hap.Formats.UINT8,
2616
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2617
+ unit: this.hap.Units.PERCENTAGE,
2618
+ minValue: 0,
2619
+ maxValue: 100,
2620
+ });
2621
+
2622
+ createCustomCharacteristic('Condition', 'CD65A9AB-85AD-494A-B2BD-2F380084134D', {
2623
+ format: this.hap.Formats.STRING,
2624
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2625
+ });
2626
+
2627
+ createCustomCharacteristic('Condition Category', 'CD65A9AB-85AD-494A-B2BD-2F380084134C', {
2628
+ format: this.hap.Formats.UINT8,
2629
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2630
+ minValue: 0,
2631
+ maxValue: 9,
2632
+ });
2633
+
2634
+ createCustomCharacteristic('Dew Point', '095C46E2-278E-4E3C-B9E7-364622A0F501', {
2635
+ format: this.hap.Formats.FLOAT,
2636
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2637
+ unit: this.hap.Units.CELSIUS,
2638
+ minValue: -40,
2639
+ maxValue: 100,
2640
+ minStep: 0.1,
2641
+ });
2642
+
2643
+ createCustomCharacteristic('Forecast Day', '57F1D4B2-0E7E-4307-95B5-808750E2C1C7', {
2644
+ format: this.hap.Formats.STRING,
2645
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2646
+ });
2647
+
2648
+ createCustomCharacteristic('Maximum Wind Speed', '6B8861E5-D6F3-425C-83B6-069945FFD1F1', {
2649
+ format: this.hap.Formats.FLOAT,
2650
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2651
+ unit: 'km/h',
2652
+ minValue: 0,
2653
+ maxValue: 150,
2654
+ minStep: 0.1,
2655
+ });
2656
+
2657
+ createCustomCharacteristic('Minimum Temperature', '707B78CA-51AB-4DC9-8630-80A58F07E411', {
2658
+ format: this.hap.Formats.FLOAT,
2659
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2660
+ unit: this.hap.Units.CELSIUS,
2661
+ minValue: -40,
2662
+ maxValue: 100,
2663
+ minStep: 0.1,
2664
+ });
2665
+
2666
+ createCustomCharacteristic('Observation Station', 'D1B2787D-1FC4-4345-A20E-7B5A74D693ED', {
2667
+ format: this.hap.Formats.STRING,
2668
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2669
+ });
2670
+
2671
+ createCustomCharacteristic('Observation Time', '234FD9F1-1D33-4128-B622-D052F0C402AF', {
2672
+ format: this.hap.Formats.STRING,
2673
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2674
+ });
2675
+
2676
+ createCustomCharacteristic('Ozone', 'BBEFFDDD-1BCD-4D75-B7CD-B57A90A04D13', {
2677
+ format: this.hap.Formats.UINT8,
2678
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2679
+ unit: 'DU',
2680
+ minValue: 0,
2681
+ maxValue: 500,
2682
+ });
2683
+
2684
+ createCustomCharacteristic('Rain', 'F14EB1AD-E000-4EF4-A54F-0CF07B2E7BE7', {
2685
+ format: this.hap.Formats.BOOL,
2686
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2687
+ });
2688
+
2689
+ createCustomCharacteristic('Rain Last Hour', '10C88F40-7EC4-478C-8D5A-BD0C3CCE14B7', {
2690
+ format: this.hap.Formats.UINT16,
2691
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2692
+ unit: 'mm',
2693
+ minValue: 0,
2694
+ maxValue: 200,
2695
+ });
2696
+
2697
+ createCustomCharacteristic('Rain Probability', 'FC01B24F-CF7E-4A74-90DB-1B427AF1FFA3', {
2698
+ format: this.hap.Formats.UINT8,
2699
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2700
+ unit: this.hap.Units.PERCENTAGE,
2701
+ minValue: 0,
2702
+ maxValue: 100,
2703
+ });
2704
+
2705
+ createCustomCharacteristic('Total Rain', 'CCC04890-565B-4376-B39A-3113341D9E0F', {
2706
+ format: this.hap.Formats.UINT16,
2707
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2708
+ unit: 'mm',
2709
+ minValue: 0,
2710
+ maxValue: 2000,
2711
+ });
2712
+
2713
+ createCustomCharacteristic('Snow', 'F14EB1AD-E000-4CE6-BD0E-384F9EC4D5DD', {
2714
+ format: this.hap.Formats.BOOL,
2715
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2716
+ });
2717
+
2718
+ createCustomCharacteristic('Solar Radiation', '1819A23E-ECAB-4D39-B29A-7364D299310B', {
2719
+ format: this.hap.Formats.UINT16,
2720
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2721
+ unit: 'W/m²',
2722
+ minValue: 0,
2723
+ maxValue: 2000,
2724
+ });
2725
+
2726
+ createCustomCharacteristic('Sunrise Time', '0D96F60E-3688-487E-8CEE-D75F05BB3008', {
2727
+ format: this.hap.Formats.STRING,
2728
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2729
+ });
2730
+
2731
+ createCustomCharacteristic('Sunset Time', '3DE24EE0-A288-4E15-A5A8-EAD2451B727C', {
2732
+ format: this.hap.Formats.STRING,
2733
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2734
+ });
2735
+
2736
+ createCustomCharacteristic('UV Index', '05BA0FE0-B848-4226-906D-5B64272E05CE', {
2737
+ format: this.hap.Formats.UINT8,
2738
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2739
+ minValue: 0,
2740
+ maxValue: 16,
2741
+ });
2742
+
2743
+ createCustomCharacteristic('Visibility', 'D24ECC1E-6FAD-4FB5-8137-5AF88BD5E857', {
2744
+ format: this.hap.Formats.UINT8,
2745
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2746
+ unit: 'km',
2747
+ minValue: 0,
2748
+ maxValue: 100,
2749
+ });
2750
+
2751
+ createCustomCharacteristic('Wind Direction', '46F1284C-1912-421B-82F5-EB75008B167E', {
2752
+ format: this.hap.Formats.STRING,
2753
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2754
+ });
2755
+
2756
+ createCustomCharacteristic('Wind Speed', '49C8AE5A-A3A5-41AB-BF1F-12D5654F9F41', {
2757
+ format: this.hap.Formats.FLOAT,
2758
+ perms: [this.hap.Perms.PAIRED_READ, this.hap.Perms.NOTIFY],
2759
+ unit: 'km/h',
2760
+ minValue: 0,
2761
+ maxValue: 150,
2762
+ minStep: 0.1,
2763
+ });
2764
+
2765
+ // EveHomeHistory Service
2766
+ createCustomService('Eve Home History', 'E863F007-079E-48FF-8F27-9C2605A29F52', [
2767
+ this.hap.Characteristic.EveResetTotal,
2768
+ this.hap.Characteristic.EveHistoryStatus,
2769
+ this.hap.Characteristic.EveHistoryEntries,
2770
+ this.hap.Characteristic.EveHistoryRequest,
2771
+ this.hap.Characteristic.EveSetTime,
2772
+ ]);
2773
+
2774
+ // Eve custom air pressure service
2775
+ createCustomService('Eve Air Pressure Sensor', 'E863F00A-079E-48FF-8F27-9C2605A29F52', [
2776
+ this.hap.Characteristic.EveAirPressure,
2777
+ this.hap.Characteristic.EveElevation,
2778
+ ]);
2779
+ }
2780
+ }
2781
+
2782
+ // General functions
2783
+ function encodeEveData(data) {
2784
+ if (typeof data !== 'string') {
2785
+ // Since passed in data wasn't as string, return 'undefined'
2786
+ return;
2787
+ }
2788
+ return String(Buffer.from(data.replace(/[^a-fA-F0-9]/gi, ''), 'hex').toString('base64'));
2789
+ }
2790
+
2791
+ function decodeEveData(data) {
2792
+ if (typeof data !== 'string') {
2793
+ // Since passed in data wasn't as string, return 'undefined'
2794
+ return;
2795
+ }
2796
+ return String(Buffer.from(data, 'base64').toString('hex'));
2797
+ }
2798
+
2799
+ // Converts a signed integer number OR float value into a string for EveHome, including formatting to byte width and reverse byte order
2800
+ function numberToEveHexString(number, padtostringlength, precision) {
2801
+ if (typeof number !== 'number' || typeof padtostringlength !== 'number' || padtostringlength % 2 !== 0) {
2802
+ return;
2803
+ }
2804
+
2805
+ let buffer = Buffer.alloc(8); // Max size of buffer needed for 64bit value
2806
+ if (precision === undefined) {
2807
+ // Handle integer value
2808
+ buffer.writeIntLE(number, 0, 6); // Max 48bit value for signed integers
2809
+ }
2810
+ if (precision !== undefined && typeof precision === 'number') {
2811
+ // Handle float value
2812
+ buffer.writeFloatLE(number, 0);
2813
+ }
2814
+ return String(buffer.toString('hex').padEnd(padtostringlength, '0').slice(0, padtostringlength));
2815
+ }
2816
+
2817
+ // Converts Eve encoded hex string to a signed integer value OR float value with number of precission digits
2818
+ function EveHexStringToNumber(string, precision) {
2819
+ if (typeof string !== 'string') {
2820
+ return;
2821
+ }
2822
+
2823
+ let buffer = Buffer.from(string, 'hex');
2824
+ let number = NaN; // Value not defined yet
2825
+ if (precision === undefined) {
2826
+ // Handle integer value
2827
+ number = Number(buffer.readIntLE(0, buffer.length));
2828
+ }
2829
+ if (precision !== undefined && typeof precision === 'number') {
2830
+ // Handle float value
2831
+ let float = buffer.readFloatLE(0);
2832
+ number = Number(typeof precision === 'number' && precision > 0 ? float.toFixed(precision) : float);
2833
+ }
2834
+ return number;
2835
+ }