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.
- package/CHANGELOG.md +27 -0
- package/LICENSE +176 -0
- package/README.md +121 -0
- package/config.schema.json +107 -0
- package/dist/HomeKitDevice.js +441 -0
- package/dist/HomeKitHistory.js +2835 -0
- package/dist/camera.js +1276 -0
- package/dist/doorbell.js +122 -0
- package/dist/index.js +35 -0
- package/dist/nexustalk.js +741 -0
- package/dist/protect.js +240 -0
- package/dist/protobuf/google/rpc/status.proto +91 -0
- package/dist/protobuf/google/rpc/stream_body.proto +26 -0
- package/dist/protobuf/google/trait/product/camera.proto +53 -0
- package/dist/protobuf/googlehome/foyer.proto +208 -0
- package/dist/protobuf/nest/messages.proto +8 -0
- package/dist/protobuf/nest/services/apigateway.proto +107 -0
- package/dist/protobuf/nest/trait/audio.proto +7 -0
- package/dist/protobuf/nest/trait/cellular.proto +313 -0
- package/dist/protobuf/nest/trait/debug.proto +37 -0
- package/dist/protobuf/nest/trait/detector.proto +41 -0
- package/dist/protobuf/nest/trait/diagnostics.proto +87 -0
- package/dist/protobuf/nest/trait/firmware.proto +221 -0
- package/dist/protobuf/nest/trait/guest.proto +105 -0
- package/dist/protobuf/nest/trait/history.proto +345 -0
- package/dist/protobuf/nest/trait/humanlibrary.proto +19 -0
- package/dist/protobuf/nest/trait/hvac.proto +1353 -0
- package/dist/protobuf/nest/trait/input.proto +29 -0
- package/dist/protobuf/nest/trait/lighting.proto +61 -0
- package/dist/protobuf/nest/trait/located.proto +193 -0
- package/dist/protobuf/nest/trait/media.proto +68 -0
- package/dist/protobuf/nest/trait/network.proto +352 -0
- package/dist/protobuf/nest/trait/occupancy.proto +373 -0
- package/dist/protobuf/nest/trait/olive.proto +15 -0
- package/dist/protobuf/nest/trait/pairing.proto +85 -0
- package/dist/protobuf/nest/trait/product/camera.proto +283 -0
- package/dist/protobuf/nest/trait/product/detect.proto +67 -0
- package/dist/protobuf/nest/trait/product/doorbell.proto +18 -0
- package/dist/protobuf/nest/trait/product/guard.proto +59 -0
- package/dist/protobuf/nest/trait/product/protect.proto +344 -0
- package/dist/protobuf/nest/trait/promonitoring.proto +14 -0
- package/dist/protobuf/nest/trait/resourcedirectory.proto +32 -0
- package/dist/protobuf/nest/trait/safety.proto +119 -0
- package/dist/protobuf/nest/trait/security.proto +516 -0
- package/dist/protobuf/nest/trait/selftest.proto +78 -0
- package/dist/protobuf/nest/trait/sensor.proto +291 -0
- package/dist/protobuf/nest/trait/service.proto +46 -0
- package/dist/protobuf/nest/trait/structure.proto +85 -0
- package/dist/protobuf/nest/trait/system.proto +51 -0
- package/dist/protobuf/nest/trait/test.proto +15 -0
- package/dist/protobuf/nest/trait/ui.proto +65 -0
- package/dist/protobuf/nest/trait/user.proto +98 -0
- package/dist/protobuf/nest/trait/voiceassistant.proto +30 -0
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +83 -0
- package/dist/protobuf/nestlabs/gateway/v1.proto +273 -0
- package/dist/protobuf/nestlabs/gateway/v2.proto +96 -0
- package/dist/protobuf/nestlabs/history/v1.proto +73 -0
- package/dist/protobuf/root.proto +64 -0
- package/dist/protobuf/wdl-event-importance.proto +11 -0
- package/dist/protobuf/wdl.proto +450 -0
- package/dist/protobuf/weave/common.proto +144 -0
- package/dist/protobuf/weave/trait/audio.proto +12 -0
- package/dist/protobuf/weave/trait/auth.proto +22 -0
- package/dist/protobuf/weave/trait/description.proto +32 -0
- package/dist/protobuf/weave/trait/heartbeat.proto +38 -0
- package/dist/protobuf/weave/trait/locale.proto +20 -0
- package/dist/protobuf/weave/trait/network.proto +24 -0
- package/dist/protobuf/weave/trait/pairing.proto +8 -0
- package/dist/protobuf/weave/trait/peerdevices.proto +18 -0
- package/dist/protobuf/weave/trait/power.proto +86 -0
- package/dist/protobuf/weave/trait/schedule.proto +76 -0
- package/dist/protobuf/weave/trait/security.proto +343 -0
- package/dist/protobuf/weave/trait/telemetry/tunnel.proto +37 -0
- package/dist/protobuf/weave/trait/time.proto +16 -0
- package/dist/res/Nest_camera_connecting.h264 +0 -0
- package/dist/res/Nest_camera_connecting.jpg +0 -0
- package/dist/res/Nest_camera_off.h264 +0 -0
- package/dist/res/Nest_camera_off.jpg +0 -0
- package/dist/res/Nest_camera_offline.h264 +0 -0
- package/dist/res/Nest_camera_offline.jpg +0 -0
- package/dist/res/Nest_camera_transfer.jpg +0 -0
- package/dist/streamer.js +344 -0
- package/dist/system.js +3112 -0
- package/dist/tempsensor.js +99 -0
- package/dist/thermostat.js +1026 -0
- package/dist/weather.js +205 -0
- package/dist/webrtc.js +55 -0
- 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
|
+
}
|