@homebridge-plugins/homebridge-meross 10.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +1346 -0
  2. package/LICENSE +21 -0
  3. package/README.md +68 -0
  4. package/config.schema.json +2066 -0
  5. package/eslint.config.js +49 -0
  6. package/lib/connection/http.js +345 -0
  7. package/lib/connection/mqtt.js +174 -0
  8. package/lib/device/baby.js +532 -0
  9. package/lib/device/cooler-single.js +447 -0
  10. package/lib/device/diffuser.js +730 -0
  11. package/lib/device/fan.js +530 -0
  12. package/lib/device/garage-main.js +225 -0
  13. package/lib/device/garage-single.js +495 -0
  14. package/lib/device/garage-sub.js +376 -0
  15. package/lib/device/heater-single.js +445 -0
  16. package/lib/device/hub-contact.js +56 -0
  17. package/lib/device/hub-leak.js +86 -0
  18. package/lib/device/hub-main.js +403 -0
  19. package/lib/device/hub-sensor.js +115 -0
  20. package/lib/device/hub-smoke.js +40 -0
  21. package/lib/device/hub-valve.js +377 -0
  22. package/lib/device/humidifier.js +521 -0
  23. package/lib/device/index.js +63 -0
  24. package/lib/device/light-cct.js +474 -0
  25. package/lib/device/light-dimmer.js +312 -0
  26. package/lib/device/light-rgb.js +528 -0
  27. package/lib/device/outlet-multi.js +383 -0
  28. package/lib/device/outlet-single.js +405 -0
  29. package/lib/device/power-strip.js +282 -0
  30. package/lib/device/purifier-single.js +372 -0
  31. package/lib/device/purifier.js +403 -0
  32. package/lib/device/roller-location.js +317 -0
  33. package/lib/device/roller.js +234 -0
  34. package/lib/device/sensor-presence.js +201 -0
  35. package/lib/device/switch-multi.js +403 -0
  36. package/lib/device/switch-single.js +371 -0
  37. package/lib/device/template.js +177 -0
  38. package/lib/device/thermostat.js +493 -0
  39. package/lib/fakegato/LICENSE +21 -0
  40. package/lib/fakegato/fakegato-history.js +814 -0
  41. package/lib/fakegato/fakegato-storage.js +108 -0
  42. package/lib/fakegato/fakegato-timer.js +125 -0
  43. package/lib/fakegato/uuid.js +27 -0
  44. package/lib/homebridge-ui/public/index.html +316 -0
  45. package/lib/homebridge-ui/server.js +10 -0
  46. package/lib/index.js +8 -0
  47. package/lib/platform.js +1256 -0
  48. package/lib/utils/colour.js +581 -0
  49. package/lib/utils/constants.js +377 -0
  50. package/lib/utils/custom-chars.js +165 -0
  51. package/lib/utils/eve-chars.js +130 -0
  52. package/lib/utils/functions.js +39 -0
  53. package/lib/utils/lang-en.js +114 -0
  54. package/package.json +70 -0
@@ -0,0 +1,814 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { format as Format } from 'node:util'
3
+
4
+ import FakeGatoStorage from './fakegato-storage.js'
5
+ import FakeGatoTimer from './fakegato-timer.js'
6
+ import { toLongFormUUID, toShortFormUUID } from './uuid.js'
7
+
8
+ const _EPOCH = 978307200
9
+ let Characteristic
10
+ let Service
11
+
12
+ export default function (homebridge) {
13
+ Characteristic = homebridge.hap.Characteristic
14
+ Service = homebridge.hap.Service
15
+
16
+ const hexToBase64 = val => Buffer.from((`${val}`).replace(/[^\dA-F]/gi, ''), 'hex').toString('base64')
17
+
18
+ const base64ToHex = (val) => {
19
+ if (!val) {
20
+ return val
21
+ }
22
+ return Buffer.from(val, 'base64').toString('hex')
23
+ }
24
+
25
+ const swap16 = val => ((val & 0xFF) << 8) | ((val >>> 8) & 0xFF)
26
+
27
+ const swap32 = val => (
28
+ ((val & 0xFF) << 24) | ((val & 0xFF00) << 8) | ((val >>> 8) & 0xFF00) | ((val >>> 24) & 0xFF)
29
+ )
30
+
31
+ const numToHex = (val, len) => {
32
+ let s = Number(val >>> 0).toString(16)
33
+ if (s.length % 2 !== 0) {
34
+ s = `0${s}`
35
+ }
36
+ if (len) {
37
+ return (`0000000000000${s}`).slice(-1 * len)
38
+ }
39
+ return s
40
+ }
41
+
42
+ const ucfirst = val => val.charAt(0).toUpperCase() + val.substr(1)
43
+
44
+ const precisionRound = (number, precision) => {
45
+ const factor = 10 ** precision
46
+ return Math.round(number * factor) / factor
47
+ }
48
+
49
+ class S2R1Characteristic extends Characteristic {
50
+ constructor() {
51
+ super('S2R1', S2R1Characteristic.UUID)
52
+ this.setProps({
53
+ format: homebridge.hap.Formats.DATA,
54
+ perms: [homebridge.hap.Perms.READ, homebridge.hap.Perms.NOTIFY, homebridge.hap.Perms.HIDDEN],
55
+ })
56
+ }
57
+ }
58
+
59
+ S2R1Characteristic.UUID = 'E863F116-079E-48FF-8F27-9C2605A29F52'
60
+
61
+ class S2R2Characteristic extends Characteristic {
62
+ constructor() {
63
+ super('S2R2', S2R2Characteristic.UUID)
64
+ this.setProps({
65
+ format: homebridge.hap.Formats.DATA,
66
+ perms: [homebridge.hap.Perms.READ, homebridge.hap.Perms.NOTIFY, homebridge.hap.Perms.HIDDEN],
67
+ })
68
+ }
69
+ }
70
+
71
+ S2R2Characteristic.UUID = 'E863F117-079E-48FF-8F27-9C2605A29F52'
72
+
73
+ class S2W1Characteristic extends Characteristic {
74
+ constructor() {
75
+ super('S2W1', S2W1Characteristic.UUID)
76
+ this.setProps({
77
+ format: homebridge.hap.Formats.DATA,
78
+ perms: [homebridge.hap.Perms.WRITE, homebridge.hap.Perms.HIDDEN],
79
+ })
80
+ }
81
+ }
82
+
83
+ S2W1Characteristic.UUID = 'E863F11C-079E-48FF-8F27-9C2605A29F52'
84
+
85
+ class S2W2Characteristic extends Characteristic {
86
+ constructor() {
87
+ super('S2W2', S2W2Characteristic.UUID)
88
+ this.setProps({
89
+ format: homebridge.hap.Formats.DATA,
90
+ perms: [homebridge.hap.Perms.WRITE, homebridge.hap.Perms.HIDDEN],
91
+ })
92
+ }
93
+ }
94
+
95
+ S2W2Characteristic.UUID = 'E863F121-079E-48FF-8F27-9C2605A29F52'
96
+
97
+ class FakeGatoHistoryService extends Service {
98
+ constructor(displayName, subtype) {
99
+ super(displayName, FakeGatoHistoryService.UUID, subtype)
100
+ this.addCharacteristic(S2R1Characteristic)
101
+ this.addCharacteristic(S2R2Characteristic)
102
+ this.addCharacteristic(S2W1Characteristic)
103
+ this.addCharacteristic(S2W2Characteristic)
104
+ }
105
+ }
106
+
107
+ FakeGatoHistoryService.UUID = 'E863F007-079E-48FF-8F27-9C2605A29F52'
108
+
109
+ let thisAccessory = {}
110
+
111
+ class FakeGatoHistory extends Service {
112
+ constructor(accessoryType, accessory, optionalParams) {
113
+ super(`${accessory.displayName} History`, FakeGatoHistoryService.UUID)
114
+ thisAccessory = accessory
115
+ this.accessoryName = thisAccessory.displayName
116
+ this.signatures = []
117
+ this.path = `${homebridge.user.storagePath()}/persist/`
118
+ this.minutes = optionalParams.minutes || 10
119
+ this.log = optionalParams.log
120
+ if (!this.log) {
121
+ this.log = () => {}
122
+ }
123
+ if (homebridge.globalFakeGatoTimer === undefined) {
124
+ homebridge.globalFakeGatoTimer = new FakeGatoTimer({
125
+ minutes: this.minutes,
126
+ log: this.log,
127
+ })
128
+ }
129
+ this.loaded = false
130
+ if (homebridge.globalFakeGatoStorage === undefined) {
131
+ homebridge.globalFakeGatoStorage = new FakeGatoStorage({
132
+ log: this.log,
133
+ })
134
+ }
135
+ homebridge.globalFakeGatoStorage.addWriter(this, {
136
+ path: this.path,
137
+ onReady: function () {
138
+ this.load(
139
+ (err, loaded) => {
140
+ if (err) {
141
+ this.log('FGH load error: %s.', err)
142
+ } else {
143
+ if (loaded) {
144
+ this.log('FGH loaded from storage.')
145
+ }
146
+ this.loaded = true
147
+ }
148
+ },
149
+ )
150
+ }.bind(this),
151
+ })
152
+ switch (accessoryType) {
153
+ case 'weather':
154
+ this.accessoryType116 = '03 0102 0202 0302'
155
+ this.accessoryType117 = '07'
156
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
157
+ break
158
+ case 'energy':
159
+ this.accessoryType116 = '04 0102 0202 0702 0f03'
160
+ this.accessoryType117 = '1f'
161
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
162
+ break
163
+ case 'room':
164
+ this.accessoryType116 = '04 0102 0202 0402 0f03'
165
+ this.accessoryType117 = '0f'
166
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
167
+ break
168
+ case 'door':
169
+ this.accessoryType116 = '01 0601'
170
+ this.accessoryType117 = '01'
171
+ homebridge.globalFakeGatoTimer.subscribe(this, function (params) {
172
+ const backLog = params.backLog || []
173
+ const { immediate } = params
174
+ const fakegato = this.service
175
+ const actualEntry = {}
176
+ if (backLog.length) {
177
+ if (immediate) {
178
+ actualEntry.time = backLog[0].time
179
+ actualEntry.status = backLog[0].status
180
+ } else {
181
+ actualEntry.time = Math.round(new Date().valueOf() / 1000)
182
+ actualEntry.status = backLog[0].status
183
+ }
184
+ fakegato.log(
185
+ '[%s] FG timer: callbackDoor [%s] immediate [%s].',
186
+ fakegato.accessoryName,
187
+ actualEntry,
188
+ immediate,
189
+ )
190
+ fakegato._addEntry(actualEntry)
191
+ }
192
+ })
193
+ break
194
+ case 'motion':
195
+ this.accessoryType116 = '02 1301 1c01'
196
+ this.accessoryType117 = '02'
197
+ homebridge.globalFakeGatoTimer.subscribe(this, function (params) {
198
+ const backLog = params.backLog || []
199
+ const { immediate } = params
200
+ const fakegato = this.service
201
+ const actualEntry = {}
202
+ if (backLog.length) {
203
+ if (immediate) {
204
+ actualEntry.time = backLog[0].time
205
+ actualEntry.status = backLog[0].status
206
+ } else {
207
+ actualEntry.time = Math.round(new Date().valueOf() / 1000)
208
+ actualEntry.status = backLog[0].status
209
+ }
210
+ fakegato.log(
211
+ '[%s] FG timer: callbackMotion [%s] immediate [%s].',
212
+ fakegato.accessoryName,
213
+ actualEntry,
214
+ immediate,
215
+ )
216
+ fakegato._addEntry(actualEntry)
217
+ }
218
+ })
219
+ break
220
+ case 'switch':
221
+ this.accessoryType116 = '01 0e01'
222
+ this.accessoryType117 = '01'
223
+ homebridge.globalFakeGatoTimer.subscribe(this, function (params) {
224
+ const backLog = params.backLog || []
225
+ const { immediate } = params
226
+ const fakegato = this.service
227
+ const actualEntry = {}
228
+ if (backLog.length) {
229
+ if (immediate) {
230
+ actualEntry.time = backLog[0].time
231
+ actualEntry.status = backLog[0].status
232
+ } else {
233
+ actualEntry.time = Math.round(new Date().valueOf() / 1000)
234
+ actualEntry.status = backLog[0].status
235
+ }
236
+ fakegato.log(
237
+ '[%s] FG timer: callbackSwitch [%s] immediate [%s].',
238
+ fakegato.accessoryName,
239
+ actualEntry,
240
+ immediate,
241
+ )
242
+ fakegato._addEntry(actualEntry)
243
+ }
244
+ })
245
+ break
246
+ case 'custom':
247
+ thisAccessory.services.forEach((service) => {
248
+ service.characteristics.forEach((characteristic) => {
249
+ switch (toLongFormUUID(characteristic.UUID)) {
250
+ case Characteristic.CurrentTemperature.UUID:
251
+ this.signatures.push({
252
+ signature: '0102',
253
+ length: 4,
254
+ uuid: toShortFormUUID(characteristic.UUID),
255
+ factor: 100,
256
+ entry: 'temp',
257
+ })
258
+ break
259
+ case Characteristic.CurrentRelativeHumidity.UUID:
260
+ this.signatures.push({
261
+ signature: '0202',
262
+ length: 4,
263
+ uuid: toShortFormUUID(characteristic.UUID),
264
+ factor: 100,
265
+ entry: 'humidity',
266
+ })
267
+ break
268
+ case 'E863F10F-079E-48FF-8F27-9C2605A29F52':
269
+ this.signatures.push({
270
+ signature: '0302',
271
+ length: 4,
272
+ uuid: toShortFormUUID(characteristic.UUID),
273
+ factor: 10,
274
+ entry: 'pressure',
275
+ })
276
+ break
277
+ case 'E863F10B-079E-48FF-8F27-9C2605A29F52':
278
+ this.signatures.push({
279
+ signature: '0702',
280
+ length: 4,
281
+ uuid: toShortFormUUID(characteristic.UUID),
282
+ factor: 10,
283
+ entry: 'ppm',
284
+ })
285
+ break
286
+ case Characteristic.ContactSensorState.UUID:
287
+ this.signatures.push({
288
+ signature: '0601',
289
+ length: 2,
290
+ uuid: toShortFormUUID(characteristic.UUID),
291
+ factor: 1,
292
+ entry: 'contact',
293
+ })
294
+ break
295
+ case 'E863F10D-079E-48FF-8F27-9C2605A29F52':
296
+ this.signatures.push({
297
+ signature: '0702',
298
+ length: 4,
299
+ uuid: toShortFormUUID(characteristic.UUID),
300
+ factor: 10,
301
+ entry: 'power',
302
+ })
303
+ break
304
+ case Characteristic.On.UUID:
305
+ this.signatures.push({
306
+ signature: '0e01',
307
+ length: 2,
308
+ uuid: toShortFormUUID(characteristic.UUID),
309
+ factor: 1,
310
+ entry: 'status',
311
+ })
312
+ break
313
+ case Characteristic.MotionDetected.UUID:
314
+ this.signatures.push({
315
+ signature: '1c01',
316
+ length: 2,
317
+ uuid: toShortFormUUID(characteristic.UUID),
318
+ factor: 1,
319
+ entry: 'motion',
320
+ })
321
+ break
322
+ }
323
+ })
324
+ })
325
+ this.accessoryType116 = ` 0${
326
+ this.signatures.length.toString()
327
+ } ${
328
+ this.signatures
329
+ .sort((a, b) => (a.signature > b.signature ? 1 : -1))
330
+ .map(a => a.signature)
331
+ .join(' ')
332
+ } `
333
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
334
+ break
335
+ case 'aqua':
336
+ this.accessoryType116 = '03 1f01 2a08 2302'
337
+ this.accessoryType117 = '05'
338
+ this.accessoryType117bis = '07'
339
+ break
340
+ case 'thermo':
341
+ this.accessoryType116 = '05 0102 1102 1001 1201 1d01'
342
+ this.accessoryType117 = '1f'
343
+ break
344
+ }
345
+ this.accessoryType = accessoryType
346
+ this.firstEntry = 0
347
+ this.lastEntry = 0
348
+ this.history = ['noValue']
349
+ this.memorySize = 4032
350
+ this.usedMemory = 0
351
+ this.currentEntry = 1
352
+ this.transfer = false
353
+ this.setTime = true
354
+ this.restarted = true
355
+ this.refTime = 0
356
+ this.memAddress = 0
357
+ this.dataStream = ''
358
+ this.saving = false
359
+ this.registerEvents()
360
+ }
361
+
362
+ calculateAverage(params) {
363
+ const backLog = params.backLog || []
364
+ const previousAvrg = params.previousAvrg || {}
365
+ const { timer } = params
366
+ const fakegato = this.service
367
+ const calc = {
368
+ sum: {},
369
+ num: {},
370
+ avrg: {},
371
+ }
372
+ for (const h in backLog) {
373
+ if (Object.prototype.hasOwnProperty.call(backLog, h)) {
374
+ for (const key in backLog[h]) {
375
+ if (Object.prototype.hasOwnProperty.call(backLog[h], key) && key !== 'time') {
376
+ if (!calc.sum[key]) {
377
+ calc.sum[key] = 0
378
+ }
379
+ if (!calc.num[key]) {
380
+ calc.num[key] = 0
381
+ }
382
+ calc.sum[key] += backLog[h][key]
383
+ calc.num[key]++
384
+ calc.avrg[key] = precisionRound(calc.sum[key] / calc.num[key], 2)
385
+ }
386
+ }
387
+ }
388
+ }
389
+ calc.avrg.time = Math.round(new Date().valueOf() / 1000)
390
+
391
+ for (const key in previousAvrg) {
392
+ if (Object.prototype.hasOwnProperty.call(previousAvrg, key) && key !== 'time') {
393
+ if (!backLog.length || calc.avrg[key] === undefined) {
394
+ calc.avrg[key] = previousAvrg[key]
395
+ }
396
+ }
397
+ }
398
+
399
+ if (Object.keys(calc.avrg).length > 1) {
400
+ fakegato._addEntry(calc.avrg)
401
+ timer.emptyData(fakegato)
402
+ }
403
+ return calc.avrg
404
+ }
405
+
406
+ registerEvents() {
407
+ this.log('[%s] FGH registering events.', thisAccessory.displayName)
408
+ this.service = thisAccessory.getService(FakeGatoHistoryService)
409
+ if (this.service === undefined) {
410
+ this.service = thisAccessory.addService(
411
+ FakeGatoHistoryService,
412
+ `${ucfirst(thisAccessory.displayName)} History`,
413
+ this.accessoryType,
414
+ )
415
+ }
416
+ this.service.getCharacteristic(S2R2Characteristic).onGet(() => this.getCurrentS2R2())
417
+ this.service.getCharacteristic(S2W1Characteristic).onSet((value) => {
418
+ this.setCurrentS2W1(value)
419
+ })
420
+ this.service.getCharacteristic(S2W2Characteristic).onSet((value) => {
421
+ this.setCurrentS2W2(value)
422
+ })
423
+ }
424
+
425
+ sendHistory(address) {
426
+ if (address !== 0) {
427
+ this.currentEntry = address
428
+ } else {
429
+ this.currentEntry = 1
430
+ }
431
+ this.transfer = true
432
+ }
433
+
434
+ addEntry(entry) {
435
+ entry.time = Math.round(new Date().valueOf() / 1000)
436
+ switch (this.accessoryType) {
437
+ case 'door':
438
+ case 'motion':
439
+ case 'switch':
440
+ homebridge.globalFakeGatoTimer.addData({
441
+ entry,
442
+ service: this,
443
+ immediateCallback: true,
444
+ })
445
+ break
446
+ case 'aqua':
447
+ this._addEntry({
448
+ time: entry.time,
449
+ status: entry.status,
450
+ waterAmount: entry.waterAmount,
451
+ })
452
+ break
453
+ case 'weather':
454
+ homebridge.globalFakeGatoTimer.addData({
455
+ entry,
456
+ service: this,
457
+ })
458
+ break
459
+ case 'room':
460
+ homebridge.globalFakeGatoTimer.addData({
461
+ entry,
462
+ service: this,
463
+ })
464
+ break
465
+ case 'energy':
466
+ homebridge.globalFakeGatoTimer.addData({
467
+ entry,
468
+ service: this,
469
+ })
470
+ break
471
+ case 'custom':
472
+ if ('power' in entry || 'temp' in entry || 'humidity' in entry) {
473
+ homebridge.globalFakeGatoTimer.addData({
474
+ entry,
475
+ service: this,
476
+ })
477
+ } else {
478
+ this._addEntry(entry)
479
+ }
480
+ break
481
+ default:
482
+ this._addEntry(entry)
483
+ break
484
+ }
485
+ }
486
+
487
+ _addEntry(entry) {
488
+ if (this.loaded) {
489
+ const entry2address = val => val % this.memorySize
490
+ let val
491
+ if (this.usedMemory < this.memorySize) {
492
+ this.usedMemory++
493
+ this.firstEntry = 0
494
+ this.lastEntry = this.usedMemory
495
+ } else {
496
+ this.firstEntry++
497
+ this.lastEntry = this.firstEntry + this.usedMemory
498
+ if (this.restarted) {
499
+ this.history[entry2address(this.lastEntry)] = {
500
+ time: entry.time,
501
+ setRefTime: 1,
502
+ }
503
+ this.firstEntry++
504
+ this.lastEntry = this.firstEntry + this.usedMemory
505
+ this.restarted = false
506
+ }
507
+ }
508
+ if (this.refTime === 0) {
509
+ this.refTime = entry.time - _EPOCH
510
+ this.history[this.lastEntry] = {
511
+ time: entry.time,
512
+ setRefTime: 1,
513
+ }
514
+ this.initialTime = entry.time
515
+ this.lastEntry++
516
+ this.usedMemory++
517
+ }
518
+ this.history[entry2address(this.lastEntry)] = entry
519
+ if (this.usedMemory < this.memorySize) {
520
+ val = Format(
521
+ '%s00000000%s%s%s%s%s000000000101',
522
+ numToHex(swap32(entry.time - this.refTime - _EPOCH), 8),
523
+ numToHex(swap32(this.refTime), 8),
524
+ this.accessoryType116,
525
+ numToHex(swap16(this.usedMemory + 1), 4),
526
+ numToHex(swap16(this.memorySize), 4),
527
+ numToHex(swap32(this.firstEntry), 8),
528
+ )
529
+ } else {
530
+ val = Format(
531
+ '%s00000000%s%s%s%s%s000000000101',
532
+ numToHex(swap32(entry.time - this.refTime - _EPOCH), 8),
533
+ numToHex(swap32(this.refTime), 8),
534
+ this.accessoryType116,
535
+ numToHex(swap16(this.usedMemory), 4),
536
+ numToHex(swap16(this.memorySize), 4),
537
+ numToHex(swap32(this.firstEntry + 1), 8),
538
+ )
539
+ }
540
+ this.service.getCharacteristic(S2R1Characteristic).setValue(hexToBase64(val))
541
+ this.log(
542
+ '[%s] FGH first entry [%s] last entry [%s] used memory [%s] 116 [%s].',
543
+ this.accessoryName,
544
+ this.firstEntry.toString(16),
545
+ this.lastEntry.toString(16),
546
+ this.usedMemory.toString(16),
547
+ val,
548
+ )
549
+ this.save()
550
+ } else {
551
+ setTimeout(() => this._addEntry(entry), 100)
552
+ }
553
+ }
554
+
555
+ getInitialTime() {
556
+ return this.initialTime || 0
557
+ }
558
+
559
+ setExtraPersistedData(extra) {
560
+ this.extra = extra
561
+ }
562
+
563
+ getExtraPersistedData() {
564
+ return this.extra
565
+ }
566
+
567
+ isHistoryLoaded() {
568
+ return this.loaded
569
+ }
570
+
571
+ save() {
572
+ if (this.loaded) {
573
+ const data = {
574
+ firstEntry: this.firstEntry,
575
+ lastEntry: this.lastEntry,
576
+ usedMemory: this.usedMemory,
577
+ refTime: this.refTime,
578
+ initialTime: this.initialTime,
579
+ history: this.history,
580
+ extra: this.extra,
581
+ }
582
+ homebridge.globalFakeGatoStorage.write({
583
+ service: this,
584
+ data: typeof data === 'object' ? JSON.stringify(data) : data,
585
+ })
586
+ } else {
587
+ setTimeout(() => this.save(), 100)
588
+ }
589
+ }
590
+
591
+ load(cb) {
592
+ this.log('[%s] FGH loading.', this.accessoryName)
593
+ homebridge.globalFakeGatoStorage.read({
594
+ service: this,
595
+ callback: function (err, data) {
596
+ if (!err) {
597
+ if (data) {
598
+ try {
599
+ // this.log('[%s] read data [%s].', this.accessoryName, data)
600
+ const jsonFile = typeof data === 'object' ? data : JSON.parse(data)
601
+ this.firstEntry = jsonFile.firstEntry
602
+ this.lastEntry = jsonFile.lastEntry
603
+ this.usedMemory = jsonFile.usedMemory
604
+ this.refTime = jsonFile.refTime
605
+ this.initialTime = jsonFile.initialTime
606
+ this.history = jsonFile.history
607
+ this.extra = jsonFile.extra
608
+ } catch (e) {
609
+ this.log('FGH error fetching data - invalid JSON [%s].', e)
610
+ cb(e, false)
611
+ }
612
+ cb(null, true)
613
+ }
614
+ } else {
615
+ cb(null, false)
616
+ }
617
+ }.bind(this),
618
+ })
619
+ }
620
+
621
+ cleanPersist() {
622
+ this.log('FGH cleaning.')
623
+ homebridge.globalFakeGatoStorage.remove({ service: this })
624
+ }
625
+
626
+ getCurrentS2R2() {
627
+ const entry2address = val => val % this.memorySize
628
+ if (this.currentEntry <= this.lastEntry && this.transfer) {
629
+ this.memAddress = entry2address(this.currentEntry)
630
+ for (let i = 0; i < 11; i += 1) {
631
+ if (
632
+ this.history[this.memAddress].setRefTime === 1
633
+ || this.setTime
634
+ || this.currentEntry === this.firstEntry + 1
635
+ ) {
636
+ this.dataStream += Format(
637
+ ',15%s 0100 0000 81%s0000 0000 00 0000',
638
+ numToHex(swap32(this.currentEntry), 8),
639
+ numToHex(swap32(this.refTime), 8),
640
+ )
641
+ this.setTime = false
642
+ } else {
643
+ this.log(
644
+ '[%s] FGH entry [%s] address [%s].',
645
+ this.accessoryName,
646
+ this.currentEntry,
647
+ this.memAddress,
648
+ )
649
+ switch (this.accessoryType) {
650
+ case 'weather':
651
+ this.dataStream += Format(
652
+ ',10 %s%s-%s:%s %s %s',
653
+ numToHex(swap32(this.currentEntry), 8),
654
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
655
+ this.accessoryType117,
656
+ numToHex(swap16(this.history[this.memAddress].temp * 100), 4),
657
+ numToHex(swap16(this.history[this.memAddress].humidity * 100), 4),
658
+ numToHex(swap16(this.history[this.memAddress].pressure * 10), 4),
659
+ )
660
+ break
661
+ case 'energy':
662
+ this.dataStream += Format(
663
+ ',14 %s%s-%s:0000 0000 %s 0000 0000',
664
+ numToHex(swap32(this.currentEntry), 8),
665
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
666
+ this.accessoryType117,
667
+ numToHex(swap16(this.history[this.memAddress].power * 10), 4),
668
+ )
669
+ break
670
+ case 'room':
671
+ this.dataStream += Format(
672
+ ',13 %s%s%s%s%s%s0000 00',
673
+ numToHex(swap32(this.currentEntry), 8),
674
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
675
+ this.accessoryType117,
676
+ numToHex(swap16(this.history[this.memAddress].temp * 100), 4),
677
+ numToHex(swap16(this.history[this.memAddress].humidity * 100), 4),
678
+ numToHex(swap16(this.history[this.memAddress].ppm), 4),
679
+ )
680
+ break
681
+ case 'door':
682
+ case 'motion':
683
+ case 'switch':
684
+ this.dataStream += Format(
685
+ ',0b %s%s%s%s',
686
+ numToHex(swap32(this.currentEntry), 8),
687
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
688
+ this.accessoryType117,
689
+ numToHex(this.history[this.memAddress].status, 2),
690
+ )
691
+ break
692
+ case 'aqua':
693
+ if (this.history[this.memAddress].status) {
694
+ this.dataStream += Format(
695
+ ',0d %s%s%s%s 300c',
696
+ numToHex(swap32(this.currentEntry), 8),
697
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
698
+ this.accessoryType117,
699
+ numToHex(this.history[this.memAddress].status, 2),
700
+ )
701
+ } else {
702
+ this.dataStream += Format(
703
+ ',15 %s%s%s%s%s 00000000 300c',
704
+ numToHex(swap32(this.currentEntry), 8),
705
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
706
+ this.accessoryType117bis,
707
+ numToHex(this.history[this.memAddress].status, 2),
708
+ numToHex(swap32(this.history[this.memAddress].waterAmount), 8),
709
+ )
710
+ }
711
+ break
712
+ case 'thermo':
713
+ this.dataStream += Format(
714
+ ',11 %s%s%s%s%s%s 0000',
715
+ numToHex(swap32(this.currentEntry), 8),
716
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
717
+ this.accessoryType117,
718
+ numToHex(swap16(this.history[this.memAddress].currentTemp * 100), 4),
719
+ numToHex(swap16(this.history[this.memAddress].setTemp * 100), 4),
720
+ numToHex(this.history[this.memAddress].valvePosition, 2),
721
+ )
722
+ break
723
+ case 'custom': {
724
+ const result = []
725
+ let bitmask = 0
726
+ const dataStream = Format(
727
+ '%s%s',
728
+ numToHex(swap32(this.currentEntry), 8),
729
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
730
+ )
731
+ for (const [k, v] of Object.entries(this.history[this.memAddress])) {
732
+ switch (k) {
733
+ case 'time':
734
+ break
735
+ default:
736
+ for (let x = 0, iLen = this.signatures.length; x < iLen; x++) {
737
+ if (this.signatures[x].entry === k) {
738
+ switch (this.signatures[x].length) {
739
+ case 8:
740
+ result[x] = Format(
741
+ '%s',
742
+ numToHex(
743
+ swap32(v * this.signatures[x].factor),
744
+ this.signatures[x].length,
745
+ ),
746
+ )
747
+ break
748
+ case 4:
749
+ result[x] = Format(
750
+ '%s',
751
+ numToHex(
752
+ swap16(v * this.signatures[x].factor),
753
+ this.signatures[x].length,
754
+ ),
755
+ )
756
+ break
757
+ case 2:
758
+ result[x] = Format(
759
+ '%s',
760
+ numToHex(v * this.signatures[x].factor, this.signatures[x].length),
761
+ )
762
+ break
763
+ }
764
+ bitmask += 2 ** x
765
+ }
766
+ }
767
+ }
768
+ }
769
+ const results = `${dataStream} ${numToHex(bitmask, 2)} ${result.map(a => a).join(' ')}`
770
+ this.dataStream
771
+ += ` ${
772
+ numToHex(results.replace(/[^0-9A-F]/gi, '').length / 2 + 1)
773
+ } ${
774
+ results
775
+ },`
776
+ break
777
+ }
778
+ }
779
+ }
780
+ this.currentEntry++
781
+ this.memAddress = entry2address(this.currentEntry)
782
+ if (this.currentEntry > this.lastEntry) {
783
+ break
784
+ }
785
+ }
786
+ this.log('[%s] FGH data [%s].', this.accessoryName, this.dataStream)
787
+ const toReturn = hexToBase64(this.dataStream)
788
+ this.dataStream = ''
789
+ return toReturn
790
+ }
791
+ this.transfer = false
792
+ return hexToBase64('00')
793
+ }
794
+
795
+ setCurrentS2W1(value) {
796
+ this.log('[%s] FGH data req [%s].', this.accessoryName, base64ToHex(value))
797
+ const valHex = base64ToHex(value)
798
+ const substring = valHex.substring(4, 12)
799
+ const valInt = Number.parseInt(substring, 16)
800
+ const address = swap32(valInt)
801
+ const hexAddress = address.toString('16')
802
+ this.log('[%s] FGH address req [%s].', this.accessoryName, hexAddress)
803
+ this.sendHistory(address)
804
+ }
805
+
806
+ setCurrentS2W2(value) {
807
+ this.log('[%s] FGH clock adj [%s].', this.accessoryName, base64ToHex(value))
808
+ }
809
+ }
810
+
811
+ FakeGatoHistoryService.UUID = 'E863F007-079E-48FF-8F27-9C2605A29F52'
812
+
813
+ return FakeGatoHistory
814
+ }