@homebridge-plugins/homebridge-printer 0.0.0-stage → 1.0.1-beta.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.
@@ -0,0 +1,833 @@
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.substring(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.PAIRED_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.PAIRED_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.PAIRED_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.PAIRED_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 if (loaded) {
143
+ this.log('FGH loaded from storage.')
144
+ }
145
+ // Ready either way. A history that could not be read starts
146
+ // empty, which is the only thing left to do with it - leaving
147
+ // this unset instead would park save() in a 100ms retry that
148
+ // never ends, so a single unreadable file cost a busy loop for
149
+ // the lifetime of the plugin
150
+ this.loaded = true
151
+ },
152
+ )
153
+ }.bind(this),
154
+ })
155
+ switch (accessoryType) {
156
+ case 'weather':
157
+ this.accessoryType116 = '03 0102 0202 0302'
158
+ this.accessoryType117 = '07'
159
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
160
+ break
161
+ case 'energy':
162
+ this.accessoryType116 = '04 0102 0202 0702 0f03'
163
+ this.accessoryType117 = '1f'
164
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
165
+ break
166
+ case 'room':
167
+ this.accessoryType116 = '04 0102 0202 0402 0f03'
168
+ this.accessoryType117 = '0f'
169
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
170
+ break
171
+ case 'door':
172
+ this.accessoryType116 = '01 0601'
173
+ this.accessoryType117 = '01'
174
+ homebridge.globalFakeGatoTimer.subscribe(this, function (params) {
175
+ const backLog = params.backLog || []
176
+ const { immediate } = params
177
+ const fakegato = this.service
178
+ const actualEntry = {}
179
+ if (backLog.length) {
180
+ if (immediate) {
181
+ actualEntry.time = backLog[0].time
182
+ actualEntry.status = backLog[0].status
183
+ } else {
184
+ actualEntry.time = Math.round(new Date().valueOf() / 1000)
185
+ actualEntry.status = backLog[0].status
186
+ }
187
+ fakegato.log(
188
+ '[%s] FG timer: callbackDoor [%s] immediate [%s].',
189
+ fakegato.accessoryName,
190
+ actualEntry,
191
+ immediate,
192
+ )
193
+ fakegato._addEntry(actualEntry)
194
+ }
195
+ })
196
+ break
197
+ case 'motion':
198
+ this.accessoryType116 = '02 1301 1c01'
199
+ this.accessoryType117 = '02'
200
+ homebridge.globalFakeGatoTimer.subscribe(this, function (params) {
201
+ const backLog = params.backLog || []
202
+ const { immediate } = params
203
+ const fakegato = this.service
204
+ const actualEntry = {}
205
+ if (backLog.length) {
206
+ if (immediate) {
207
+ actualEntry.time = backLog[0].time
208
+ actualEntry.status = backLog[0].status
209
+ } else {
210
+ actualEntry.time = Math.round(new Date().valueOf() / 1000)
211
+ actualEntry.status = backLog[0].status
212
+ }
213
+ fakegato.log(
214
+ '[%s] FG timer: callbackMotion [%s] immediate [%s].',
215
+ fakegato.accessoryName,
216
+ actualEntry,
217
+ immediate,
218
+ )
219
+ fakegato._addEntry(actualEntry)
220
+ }
221
+ })
222
+ break
223
+ case 'switch':
224
+ this.accessoryType116 = '01 0e01'
225
+ this.accessoryType117 = '01'
226
+ homebridge.globalFakeGatoTimer.subscribe(this, function (params) {
227
+ const backLog = params.backLog || []
228
+ const { immediate } = params
229
+ const fakegato = this.service
230
+ const actualEntry = {}
231
+ if (backLog.length) {
232
+ if (immediate) {
233
+ actualEntry.time = backLog[0].time
234
+ actualEntry.status = backLog[0].status
235
+ } else {
236
+ actualEntry.time = Math.round(new Date().valueOf() / 1000)
237
+ actualEntry.status = backLog[0].status
238
+ }
239
+ fakegato.log(
240
+ '[%s] FG timer: callbackSwitch [%s] immediate [%s].',
241
+ fakegato.accessoryName,
242
+ actualEntry,
243
+ immediate,
244
+ )
245
+ fakegato._addEntry(actualEntry)
246
+ }
247
+ })
248
+ break
249
+ case 'custom':
250
+ thisAccessory.services.forEach((service) => {
251
+ service.characteristics.forEach((characteristic) => {
252
+ switch (toLongFormUUID(characteristic.UUID)) {
253
+ case Characteristic.CurrentTemperature.UUID:
254
+ this.signatures.push({
255
+ signature: '0102',
256
+ length: 4,
257
+ uuid: toShortFormUUID(characteristic.UUID),
258
+ factor: 100,
259
+ entry: 'temp',
260
+ })
261
+ break
262
+ case Characteristic.CurrentRelativeHumidity.UUID:
263
+ this.signatures.push({
264
+ signature: '0202',
265
+ length: 4,
266
+ uuid: toShortFormUUID(characteristic.UUID),
267
+ factor: 100,
268
+ entry: 'humidity',
269
+ })
270
+ break
271
+ case 'E863F10F-079E-48FF-8F27-9C2605A29F52':
272
+ this.signatures.push({
273
+ signature: '0302',
274
+ length: 4,
275
+ uuid: toShortFormUUID(characteristic.UUID),
276
+ factor: 10,
277
+ entry: 'pressure',
278
+ })
279
+ break
280
+ case 'E863F10B-079E-48FF-8F27-9C2605A29F52':
281
+ this.signatures.push({
282
+ signature: '0702',
283
+ length: 4,
284
+ uuid: toShortFormUUID(characteristic.UUID),
285
+ factor: 10,
286
+ entry: 'ppm',
287
+ })
288
+ break
289
+ case Characteristic.ContactSensorState.UUID:
290
+ this.signatures.push({
291
+ signature: '0601',
292
+ length: 2,
293
+ uuid: toShortFormUUID(characteristic.UUID),
294
+ factor: 1,
295
+ entry: 'contact',
296
+ })
297
+ break
298
+ case 'E863F10D-079E-48FF-8F27-9C2605A29F52':
299
+ this.signatures.push({
300
+ signature: '0702',
301
+ length: 4,
302
+ uuid: toShortFormUUID(characteristic.UUID),
303
+ factor: 10,
304
+ entry: 'power',
305
+ })
306
+ break
307
+ case Characteristic.On.UUID:
308
+ this.signatures.push({
309
+ signature: '0e01',
310
+ length: 2,
311
+ uuid: toShortFormUUID(characteristic.UUID),
312
+ factor: 1,
313
+ entry: 'status',
314
+ })
315
+ break
316
+ case Characteristic.MotionDetected.UUID:
317
+ this.signatures.push({
318
+ signature: '1c01',
319
+ length: 2,
320
+ uuid: toShortFormUUID(characteristic.UUID),
321
+ factor: 1,
322
+ entry: 'motion',
323
+ })
324
+ break
325
+ }
326
+ })
327
+ })
328
+ this.accessoryType116 = ` 0${
329
+ this.signatures.length.toString()
330
+ } ${
331
+ this.signatures
332
+ .sort((a, b) => (a.signature > b.signature
333
+ ? 1
334
+ : -1))
335
+ .map(a => a.signature)
336
+ .join(' ')
337
+ } `
338
+ homebridge.globalFakeGatoTimer.subscribe(this, this.calculateAverage)
339
+ break
340
+ case 'aqua':
341
+ this.accessoryType116 = '03 1f01 2a08 2302'
342
+ this.accessoryType117 = '05'
343
+ this.accessoryType117bis = '07'
344
+ break
345
+ case 'thermo':
346
+ this.accessoryType116 = '05 0102 1102 1001 1201 1d01'
347
+ this.accessoryType117 = '1f'
348
+ break
349
+ }
350
+ this.accessoryType = accessoryType
351
+ this.firstEntry = 0
352
+ this.lastEntry = 0
353
+ this.history = ['noValue']
354
+ this.memorySize = 4032
355
+ this.usedMemory = 0
356
+ this.currentEntry = 1
357
+ this.transfer = false
358
+ this.setTime = true
359
+ this.restarted = true
360
+ this.refTime = 0
361
+ this.memAddress = 0
362
+ this.dataStream = ''
363
+ this.saving = false
364
+ this.registerEvents()
365
+ }
366
+
367
+ calculateAverage(params) {
368
+ const backLog = params.backLog || []
369
+ const previousAvrg = params.previousAvrg || {}
370
+ const { timer } = params
371
+ const fakegato = this.service
372
+ const calc = {
373
+ sum: {},
374
+ num: {},
375
+ avrg: {},
376
+ }
377
+ for (const h in backLog) {
378
+ if (Object.hasOwn(backLog, h)) {
379
+ for (const key in backLog[h]) {
380
+ if (Object.hasOwn(backLog[h], key) && key !== 'time') {
381
+ if (!calc.sum[key]) {
382
+ calc.sum[key] = 0
383
+ }
384
+ if (!calc.num[key]) {
385
+ calc.num[key] = 0
386
+ }
387
+ calc.sum[key] += backLog[h][key]
388
+ calc.num[key]++
389
+ calc.avrg[key] = precisionRound(calc.sum[key] / calc.num[key], 2)
390
+ }
391
+ }
392
+ }
393
+ }
394
+ calc.avrg.time = Math.round(new Date().valueOf() / 1000)
395
+
396
+ for (const key in previousAvrg) {
397
+ if (Object.hasOwn(previousAvrg, key) && key !== 'time') {
398
+ if (!backLog.length || calc.avrg[key] === undefined) {
399
+ calc.avrg[key] = previousAvrg[key]
400
+ }
401
+ }
402
+ }
403
+
404
+ if (Object.keys(calc.avrg).length > 1) {
405
+ fakegato._addEntry(calc.avrg)
406
+ timer.emptyData(fakegato)
407
+ }
408
+ return calc.avrg
409
+ }
410
+
411
+ registerEvents() {
412
+ this.log('[%s] FGH registering events.', thisAccessory.displayName)
413
+ this.service = thisAccessory.getService(FakeGatoHistoryService)
414
+ if (this.service === undefined) {
415
+ this.service = thisAccessory.addService(
416
+ FakeGatoHistoryService,
417
+ `${ucfirst(thisAccessory.displayName)} History`,
418
+ this.accessoryType,
419
+ )
420
+ }
421
+ this.service.getCharacteristic(S2R2Characteristic).onGet(() => this.getCurrentS2R2())
422
+ this.service.getCharacteristic(S2W1Characteristic).onSet((value) => {
423
+ this.setCurrentS2W1(value)
424
+ })
425
+ this.service.getCharacteristic(S2W2Characteristic).onSet((value) => {
426
+ this.setCurrentS2W2(value)
427
+ })
428
+ }
429
+
430
+ sendHistory(address) {
431
+ if (address !== 0) {
432
+ this.currentEntry = address
433
+ } else {
434
+ this.currentEntry = 1
435
+ }
436
+ this.transfer = true
437
+ }
438
+
439
+ addEntry(entry) {
440
+ entry.time = Math.round(new Date().valueOf() / 1000)
441
+ switch (this.accessoryType) {
442
+ case 'door':
443
+ case 'motion':
444
+ case 'switch':
445
+ homebridge.globalFakeGatoTimer.addData({
446
+ entry,
447
+ service: this,
448
+ immediateCallback: true,
449
+ })
450
+ break
451
+ case 'aqua':
452
+ this._addEntry({
453
+ time: entry.time,
454
+ status: entry.status,
455
+ waterAmount: entry.waterAmount,
456
+ })
457
+ break
458
+ case 'weather':
459
+ homebridge.globalFakeGatoTimer.addData({
460
+ entry,
461
+ service: this,
462
+ })
463
+ break
464
+ case 'room':
465
+ homebridge.globalFakeGatoTimer.addData({
466
+ entry,
467
+ service: this,
468
+ })
469
+ break
470
+ case 'energy':
471
+ homebridge.globalFakeGatoTimer.addData({
472
+ entry,
473
+ service: this,
474
+ })
475
+ break
476
+ case 'custom':
477
+ if ('power' in entry || 'temp' in entry || 'humidity' in entry) {
478
+ homebridge.globalFakeGatoTimer.addData({
479
+ entry,
480
+ service: this,
481
+ })
482
+ } else {
483
+ this._addEntry(entry)
484
+ }
485
+ break
486
+ default:
487
+ this._addEntry(entry)
488
+ break
489
+ }
490
+ }
491
+
492
+ _addEntry(entry) {
493
+ if (this.loaded) {
494
+ const entry2address = val => val % this.memorySize
495
+ let val
496
+ if (this.usedMemory < this.memorySize) {
497
+ this.usedMemory++
498
+ this.firstEntry = 0
499
+ this.lastEntry = this.usedMemory
500
+ } else {
501
+ this.firstEntry++
502
+ this.lastEntry = this.firstEntry + this.usedMemory
503
+ if (this.restarted) {
504
+ this.history[entry2address(this.lastEntry)] = {
505
+ time: entry.time,
506
+ setRefTime: 1,
507
+ }
508
+ this.firstEntry++
509
+ this.lastEntry = this.firstEntry + this.usedMemory
510
+ this.restarted = false
511
+ }
512
+ }
513
+ if (this.refTime === 0) {
514
+ this.refTime = entry.time - _EPOCH
515
+ this.history[this.lastEntry] = {
516
+ time: entry.time,
517
+ setRefTime: 1,
518
+ }
519
+ this.initialTime = entry.time
520
+ this.lastEntry++
521
+ this.usedMemory++
522
+ }
523
+ this.history[entry2address(this.lastEntry)] = entry
524
+ if (this.usedMemory < this.memorySize) {
525
+ val = Format(
526
+ '%s00000000%s%s%s%s%s000000000101',
527
+ numToHex(swap32(entry.time - this.refTime - _EPOCH), 8),
528
+ numToHex(swap32(this.refTime), 8),
529
+ this.accessoryType116,
530
+ numToHex(swap16(this.usedMemory + 1), 4),
531
+ numToHex(swap16(this.memorySize), 4),
532
+ numToHex(swap32(this.firstEntry), 8),
533
+ )
534
+ } else {
535
+ val = Format(
536
+ '%s00000000%s%s%s%s%s000000000101',
537
+ numToHex(swap32(entry.time - this.refTime - _EPOCH), 8),
538
+ numToHex(swap32(this.refTime), 8),
539
+ this.accessoryType116,
540
+ numToHex(swap16(this.usedMemory), 4),
541
+ numToHex(swap16(this.memorySize), 4),
542
+ numToHex(swap32(this.firstEntry + 1), 8),
543
+ )
544
+ }
545
+ this.service.getCharacteristic(S2R1Characteristic).setValue(hexToBase64(val))
546
+ this.log(
547
+ '[%s] FGH first entry [%s] last entry [%s] used memory [%s] 116 [%s].',
548
+ this.accessoryName,
549
+ this.firstEntry.toString(16),
550
+ this.lastEntry.toString(16),
551
+ this.usedMemory.toString(16),
552
+ val,
553
+ )
554
+ this.save()
555
+ } else {
556
+ setTimeout(() => this._addEntry(entry), 100)
557
+ }
558
+ }
559
+
560
+ getInitialTime() {
561
+ return this.initialTime || 0
562
+ }
563
+
564
+ setExtraPersistedData(extra) {
565
+ this.extra = extra
566
+ }
567
+
568
+ getExtraPersistedData() {
569
+ return this.extra
570
+ }
571
+
572
+ isHistoryLoaded() {
573
+ return this.loaded
574
+ }
575
+
576
+ save() {
577
+ if (this.loaded) {
578
+ const data = {
579
+ firstEntry: this.firstEntry,
580
+ lastEntry: this.lastEntry,
581
+ usedMemory: this.usedMemory,
582
+ refTime: this.refTime,
583
+ initialTime: this.initialTime,
584
+ history: this.history,
585
+ extra: this.extra,
586
+ }
587
+ homebridge.globalFakeGatoStorage.write({
588
+ service: this,
589
+ data: typeof data === 'object'
590
+ ? JSON.stringify(data)
591
+ : data,
592
+ })
593
+ } else {
594
+ setTimeout(() => this.save(), 100)
595
+ }
596
+ }
597
+
598
+ load(cb) {
599
+ this.log('[%s] FGH loading.', this.accessoryName)
600
+ homebridge.globalFakeGatoStorage.read({
601
+ service: this,
602
+ callback: function (err, data) {
603
+ // No history stored yet, or a file with nothing in it. There is
604
+ // nothing to restore either way, and the caller still has to be told
605
+ // so: an empty file used to answer nothing at all, which left the
606
+ // service waiting to be loaded while save() retried every 100ms for
607
+ // as long as the plugin ran
608
+ if (err || !data) {
609
+ cb(null, false)
610
+ return
611
+ }
612
+
613
+ try {
614
+ // this.log('[%s] read data [%s].', this.accessoryName, data)
615
+ const jsonFile = typeof data === 'object'
616
+ ? data
617
+ : JSON.parse(data)
618
+ this.firstEntry = jsonFile.firstEntry
619
+ this.lastEntry = jsonFile.lastEntry
620
+ this.usedMemory = jsonFile.usedMemory
621
+ this.refTime = jsonFile.refTime
622
+ this.initialTime = jsonFile.initialTime
623
+ this.history = jsonFile.history
624
+ this.extra = jsonFile.extra
625
+ } catch (e) {
626
+ // A file that will not parse was cut short. The entries are gone
627
+ // whatever happens next, so the caller is told once and the service
628
+ // starts empty - this used to report the failure and then
629
+ // immediately report success as well, over the top of it
630
+ this.log('FGH error fetching data - invalid JSON [%s].', e)
631
+ cb(e, false)
632
+ return
633
+ }
634
+
635
+ cb(null, true)
636
+ }.bind(this),
637
+ })
638
+ }
639
+
640
+ cleanPersist() {
641
+ this.log('FGH cleaning.')
642
+ homebridge.globalFakeGatoStorage.remove({ service: this })
643
+ }
644
+
645
+ getCurrentS2R2() {
646
+ const entry2address = val => val % this.memorySize
647
+ if (this.currentEntry <= this.lastEntry && this.transfer) {
648
+ this.memAddress = entry2address(this.currentEntry)
649
+ for (let i = 0; i < 11; i += 1) {
650
+ if (
651
+ this.history[this.memAddress].setRefTime === 1
652
+ || this.setTime
653
+ || this.currentEntry === this.firstEntry + 1
654
+ ) {
655
+ this.dataStream += Format(
656
+ ',15%s 0100 0000 81%s0000 0000 00 0000',
657
+ numToHex(swap32(this.currentEntry), 8),
658
+ numToHex(swap32(this.refTime), 8),
659
+ )
660
+ this.setTime = false
661
+ } else {
662
+ this.log(
663
+ '[%s] FGH entry [%s] address [%s].',
664
+ this.accessoryName,
665
+ this.currentEntry,
666
+ this.memAddress,
667
+ )
668
+ switch (this.accessoryType) {
669
+ case 'weather':
670
+ this.dataStream += Format(
671
+ ',10 %s%s-%s:%s %s %s',
672
+ numToHex(swap32(this.currentEntry), 8),
673
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
674
+ this.accessoryType117,
675
+ numToHex(swap16(this.history[this.memAddress].temp * 100), 4),
676
+ numToHex(swap16(this.history[this.memAddress].humidity * 100), 4),
677
+ numToHex(swap16(this.history[this.memAddress].pressure * 10), 4),
678
+ )
679
+ break
680
+ case 'energy':
681
+ this.dataStream += Format(
682
+ ',14 %s%s-%s:0000 0000 %s 0000 0000',
683
+ numToHex(swap32(this.currentEntry), 8),
684
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
685
+ this.accessoryType117,
686
+ numToHex(swap16(this.history[this.memAddress].power * 10), 4),
687
+ )
688
+ break
689
+ case 'room':
690
+ this.dataStream += Format(
691
+ ',13 %s%s%s%s%s%s0000 00',
692
+ numToHex(swap32(this.currentEntry), 8),
693
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
694
+ this.accessoryType117,
695
+ numToHex(swap16(this.history[this.memAddress].temp * 100), 4),
696
+ numToHex(swap16(this.history[this.memAddress].humidity * 100), 4),
697
+ numToHex(swap16(this.history[this.memAddress].ppm), 4),
698
+ )
699
+ break
700
+ case 'door':
701
+ case 'motion':
702
+ case 'switch':
703
+ this.dataStream += Format(
704
+ ',0b %s%s%s%s',
705
+ numToHex(swap32(this.currentEntry), 8),
706
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
707
+ this.accessoryType117,
708
+ numToHex(this.history[this.memAddress].status, 2),
709
+ )
710
+ break
711
+ case 'aqua':
712
+ if (this.history[this.memAddress].status) {
713
+ this.dataStream += Format(
714
+ ',0d %s%s%s%s 300c',
715
+ numToHex(swap32(this.currentEntry), 8),
716
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
717
+ this.accessoryType117,
718
+ numToHex(this.history[this.memAddress].status, 2),
719
+ )
720
+ } else {
721
+ this.dataStream += Format(
722
+ ',15 %s%s%s%s%s 00000000 300c',
723
+ numToHex(swap32(this.currentEntry), 8),
724
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
725
+ this.accessoryType117bis,
726
+ numToHex(this.history[this.memAddress].status, 2),
727
+ numToHex(swap32(this.history[this.memAddress].waterAmount), 8),
728
+ )
729
+ }
730
+ break
731
+ case 'thermo':
732
+ this.dataStream += Format(
733
+ ',11 %s%s%s%s%s%s 0000',
734
+ numToHex(swap32(this.currentEntry), 8),
735
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
736
+ this.accessoryType117,
737
+ numToHex(swap16(this.history[this.memAddress].currentTemp * 100), 4),
738
+ numToHex(swap16(this.history[this.memAddress].setTemp * 100), 4),
739
+ numToHex(this.history[this.memAddress].valvePosition, 2),
740
+ )
741
+ break
742
+ case 'custom': {
743
+ const result = []
744
+ let bitmask = 0
745
+ const dataStream = Format(
746
+ '%s%s',
747
+ numToHex(swap32(this.currentEntry), 8),
748
+ numToHex(swap32(this.history[this.memAddress].time - this.refTime - _EPOCH), 8),
749
+ )
750
+ for (const [k, v] of Object.entries(this.history[this.memAddress])) {
751
+ switch (k) {
752
+ case 'time':
753
+ break
754
+ default:
755
+ for (let x = 0, iLen = this.signatures.length; x < iLen; x++) {
756
+ if (this.signatures[x].entry === k) {
757
+ switch (this.signatures[x].length) {
758
+ case 8:
759
+ result[x] = Format(
760
+ '%s',
761
+ numToHex(
762
+ swap32(v * this.signatures[x].factor),
763
+ this.signatures[x].length,
764
+ ),
765
+ )
766
+ break
767
+ case 4:
768
+ result[x] = Format(
769
+ '%s',
770
+ numToHex(
771
+ swap16(v * this.signatures[x].factor),
772
+ this.signatures[x].length,
773
+ ),
774
+ )
775
+ break
776
+ case 2:
777
+ result[x] = Format(
778
+ '%s',
779
+ numToHex(v * this.signatures[x].factor, this.signatures[x].length),
780
+ )
781
+ break
782
+ }
783
+ bitmask += 2 ** x
784
+ }
785
+ }
786
+ }
787
+ }
788
+ const results = `${dataStream} ${numToHex(bitmask, 2)} ${result.map(a => a).join(' ')}`
789
+ this.dataStream
790
+ += ` ${
791
+ numToHex(results.replace(/[^0-9A-F]/gi, '').length / 2 + 1)
792
+ } ${
793
+ results
794
+ },`
795
+ break
796
+ }
797
+ }
798
+ }
799
+ this.currentEntry++
800
+ this.memAddress = entry2address(this.currentEntry)
801
+ if (this.currentEntry > this.lastEntry) {
802
+ break
803
+ }
804
+ }
805
+ this.log('[%s] FGH data [%s].', this.accessoryName, this.dataStream)
806
+ const toReturn = hexToBase64(this.dataStream)
807
+ this.dataStream = ''
808
+ return toReturn
809
+ }
810
+ this.transfer = false
811
+ return hexToBase64('00')
812
+ }
813
+
814
+ setCurrentS2W1(value) {
815
+ this.log('[%s] FGH data req [%s].', this.accessoryName, base64ToHex(value))
816
+ const valHex = base64ToHex(value)
817
+ const substring = valHex.substring(4, 12)
818
+ const valInt = Number.parseInt(substring, 16)
819
+ const address = swap32(valInt)
820
+ const hexAddress = address.toString(16)
821
+ this.log('[%s] FGH address req [%s].', this.accessoryName, hexAddress)
822
+ this.sendHistory(address)
823
+ }
824
+
825
+ setCurrentS2W2(value) {
826
+ this.log('[%s] FGH clock adj [%s].', this.accessoryName, base64ToHex(value))
827
+ }
828
+ }
829
+
830
+ FakeGatoHistoryService.UUID = 'E863F007-079E-48FF-8F27-9C2605A29F52'
831
+
832
+ return FakeGatoHistory
833
+ }