@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,532 @@
1
+ import PQueue from 'p-queue'
2
+ import { TimeoutError } from 'p-timeout'
3
+
4
+ import mqttClient from '../connection/mqtt.js'
5
+ import platformConsts from '../utils/constants.js'
6
+ import {
7
+ generateRandomString,
8
+ hasProperty,
9
+ parseError,
10
+ sleep,
11
+ } from '../utils/functions.js'
12
+ import platformLang from '../utils/lang-en.js'
13
+
14
+ export default class {
15
+ constructor(platform, accessory, accessoryLight) {
16
+ // Set up variables from the platform
17
+ this.cusChar = platform.cusChar
18
+ this.hapChar = platform.api.hap.Characteristic
19
+ this.hapErr = platform.api.hap.HapStatusError
20
+ this.hapServ = platform.api.hap.Service
21
+ this.platform = platform
22
+
23
+ // Set up variables from the accessory
24
+ this.accessory = accessory
25
+ this.accessoryLight = accessoryLight
26
+ this.name = accessory.displayName
27
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
28
+ ? platform.config.cloudRefreshRate
29
+ : platformConsts.defaultValues.cloudRefreshRate
30
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
31
+ ? platform.config.refreshRate
32
+ : platformConsts.defaultValues.refreshRate
33
+ this.pollInterval = accessory.context.connection === 'local'
34
+ ? localRefreshRate
35
+ : cloudRefreshRate
36
+
37
+ this.channelList = {
38
+ 1: 'Cicada Chirping',
39
+ 2: 'Rain Sound',
40
+ 3: 'Ripple Sound',
41
+ 4: 'Birdsong',
42
+ 5: 'Lullaby',
43
+ 6: 'Fan Sound',
44
+ 7: 'Crystal Ball',
45
+ 8: 'Music Box',
46
+ 9: 'White Noise',
47
+ 10: 'Thunder',
48
+ 11: 'Ocean Wave',
49
+ }
50
+
51
+ this.volumeHK2MR = (input) => {
52
+ if (input === 0) {
53
+ return 0
54
+ }
55
+ if (input < 7) {
56
+ return 1
57
+ }
58
+ if (input < 13) {
59
+ return 2
60
+ }
61
+ if (input < 19) {
62
+ return 3
63
+ }
64
+ if (input < 25) {
65
+ return 4
66
+ }
67
+ if (input < 31) {
68
+ return 5
69
+ }
70
+ if (input < 37) {
71
+ return 6
72
+ }
73
+ if (input < 43) {
74
+ return 7
75
+ }
76
+ if (input < 49) {
77
+ return 8
78
+ }
79
+ if (input < 55) {
80
+ return 9
81
+ }
82
+ if (input < 61) {
83
+ return 10
84
+ }
85
+ if (input < 67) {
86
+ return 11
87
+ }
88
+ if (input < 73) {
89
+ return 12
90
+ }
91
+ if (input < 79) {
92
+ return 13
93
+ }
94
+ if (input < 85) {
95
+ return 14
96
+ }
97
+ if (input < 91) {
98
+ return 15
99
+ }
100
+ return 16
101
+ }
102
+
103
+ this.volumeMR2HK = (input) => {
104
+ if (input === 16) {
105
+ return 100
106
+ }
107
+ return input * 6
108
+ }
109
+
110
+ // Add the tv service if it doesn't already exist
111
+ this.service = this.accessory.getService(this.hapServ.Television)
112
+ || this.accessory.addService(this.hapServ.Television)
113
+
114
+ // Add a lightbulb service to the light accessory if it doesn't exist
115
+ this.serviceLight = this.accessoryLight.getService(this.hapServ.Lightbulb)
116
+ || this.accessoryLight.addService(this.hapServ.Lightbulb)
117
+
118
+ // Remove any old tv speaker services
119
+ if (this.accessory.getService(this.hapServ.TelevisionSpeaker)) {
120
+ this.accessory.removeService(this.accessory.getService(this.hapServ.TelevisionSpeaker))
121
+ }
122
+
123
+ // Add the tv speaker (fan) service if it doesn't already exist
124
+ this.speakerService = this.accessory.getService(this.hapServ.Fan) || this.accessory.addService(this.hapServ.Fan)
125
+
126
+ // Add the set handler to the tv active characteristic
127
+ this.service
128
+ .getCharacteristic(this.hapChar.Active)
129
+ .onSet(async value => this.internalActiveUpdate(value))
130
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value
131
+ this.cacheStateMR = this.cacheState === 1 ? 0 : 1
132
+
133
+ // Add the set handler to the switch active identifier characteristic
134
+ this.service
135
+ .getCharacteristic(this.hapChar.ActiveIdentifier)
136
+ .onSet(async value => this.internalChannelUpdate(value))
137
+ this.cacheChannel = this.service.getCharacteristic(this.hapChar.ActiveIdentifier).value
138
+
139
+ // Handle volume control
140
+ this.speakerService
141
+ .getCharacteristic(this.hapChar.RotationSpeed)
142
+ .onSet(async value => this.internalVolumeUpdate(value))
143
+ this.cacheVolume = this.speakerService.getCharacteristic(this.hapChar.RotationSpeed).value
144
+ this.cacheVolumeMR = this.volumeHK2MR(this.cacheVolume)
145
+
146
+ Object.entries(this.channelList).forEach((entry) => {
147
+ const [id, scene] = entry
148
+ const service = this.accessory.getService(scene)
149
+ || this.accessory.addService(this.hapServ.InputSource, scene, id)
150
+ service
151
+ .setCharacteristic(this.hapChar.Identifier, id)
152
+ .setCharacteristic(this.hapChar.ConfiguredName, scene)
153
+ .setCharacteristic(this.hapChar.IsConfigured, 1)
154
+ .setCharacteristic(this.hapChar.InputSourceType, 3)
155
+ this.service.addLinkedService(service)
156
+ })
157
+
158
+ // Add the baby scene custom characteristics
159
+ if (!this.serviceLight.testCharacteristic(this.cusChar.BabySceneOne)) {
160
+ this.serviceLight.addCharacteristic(this.cusChar.BabySceneOne)
161
+ }
162
+ this.serviceLight
163
+ .getCharacteristic(this.cusChar.BabySceneOne)
164
+ .onSet(async value => this.internalSceneUpdate(value, 3, 'BabySceneOne'))
165
+ if (!this.serviceLight.testCharacteristic(this.cusChar.BabySceneTwo)) {
166
+ this.serviceLight.addCharacteristic(this.cusChar.BabySceneTwo)
167
+ }
168
+ this.serviceLight
169
+ .getCharacteristic(this.cusChar.BabySceneTwo)
170
+ .onSet(async value => this.internalSceneUpdate(value, 4, 'BabySceneTwo'))
171
+ if (!this.serviceLight.testCharacteristic(this.cusChar.BabySceneThree)) {
172
+ this.serviceLight.addCharacteristic(this.cusChar.BabySceneThree)
173
+ }
174
+ this.serviceLight
175
+ .getCharacteristic(this.cusChar.BabySceneThree)
176
+ .onSet(async value => this.internalSceneUpdate(value, 1, 'BabySceneThree'))
177
+ if (!this.serviceLight.testCharacteristic(this.cusChar.BabySceneFour)) {
178
+ this.serviceLight.addCharacteristic(this.cusChar.BabySceneFour)
179
+ }
180
+ this.serviceLight
181
+ .getCharacteristic(this.cusChar.BabySceneFour)
182
+ .onSet(async value => this.internalSceneUpdate(value, 2, 'BabySceneFour'))
183
+
184
+ // Create the queue used for sending device requests
185
+ this.updateInProgress = false
186
+ this.queue = new PQueue({
187
+ concurrency: 1,
188
+ interval: 250,
189
+ intervalCap: 1,
190
+ timeout: 10000,
191
+ throwOnTimeout: true,
192
+ })
193
+ this.queue.on('idle', () => {
194
+ this.updateInProgress = false
195
+ })
196
+
197
+ // Set up the mqtt client for cloud devices to send and receive device updates
198
+ if (accessory.context.connection !== 'local') {
199
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
200
+ this.accessory.mqtt.connect()
201
+ }
202
+
203
+ // Always request a device update on startup, then start the interval for polling
204
+ setTimeout(() => this.requestUpdate(true), 2000)
205
+ this.accessory.refreshInterval = setInterval(
206
+ () => this.requestUpdate(),
207
+ this.pollInterval * 1000,
208
+ )
209
+
210
+ // Output the customised options to the log
211
+ const opts = JSON.stringify({
212
+ connection: this.accessory.context.connection,
213
+ })
214
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
215
+ }
216
+
217
+ async internalActiveUpdate(value) {
218
+ try {
219
+ // Add the request to the queue so updates are sent apart
220
+ await this.queue.add(async () => {
221
+ // Don't continue if the state is the same as before
222
+ if (value === this.cacheState) {
223
+ return
224
+ }
225
+
226
+ // This flag stops the plugin from requesting updates while pending on others
227
+ this.updateInProgress = true
228
+
229
+ // Generate the namespace and payload
230
+ const namespace = 'Appliance.Control.Mp3'
231
+ const payload = {
232
+ mp3: {
233
+ mute: value === 1 ? 0 : 1,
234
+ },
235
+ }
236
+
237
+ // Use the platform function to send the update to the device
238
+ await this.platform.sendUpdate(this.accessory, {
239
+ namespace,
240
+ payload,
241
+ })
242
+
243
+ // Update the cache
244
+ this.cacheState = value
245
+ this.cacheStateMR = value === 1 ? 0 : 1
246
+ this.accessory.log(`${platformLang.curState} [${value === 1 ? 'on' : 'off'}]`)
247
+
248
+ // Also update the speaker (fan) service
249
+ this.speakerService.updateCharacteristic(this.hapChar.On, value === 1)
250
+ })
251
+ } catch (err) {
252
+ // Catch any errors whilst updating the device
253
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
254
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
255
+ setTimeout(() => {
256
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState)
257
+ }, 2000)
258
+ throw new this.hapErr(-70402)
259
+ }
260
+ }
261
+
262
+ async internalChannelUpdate(value) {
263
+ try {
264
+ // Add the request to the queue so updates are sent apart
265
+ await this.queue.add(async () => {
266
+ // Don't continue if the state is the same as before
267
+ if (value === this.cacheChannel) {
268
+ return
269
+ }
270
+
271
+ // This flag stops the plugin from requesting updates while pending on others
272
+ this.updateInProgress = true
273
+
274
+ // Generate the namespace and payload
275
+ const namespace = 'Appliance.Control.Mp3'
276
+ const payload = {
277
+ mp3: {
278
+ song: value,
279
+ },
280
+ }
281
+
282
+ // Use the platform function to send the update to the device
283
+ await this.platform.sendUpdate(this.accessory, {
284
+ namespace,
285
+ payload,
286
+ })
287
+
288
+ // Update the cache
289
+ this.cacheChannel = value
290
+ this.accessory.log(`${platformLang.curSong} [${this.channelList[this.cacheChannel]}]`)
291
+ })
292
+ } catch (err) {
293
+ // Catch any errors whilst updating the device
294
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
295
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
296
+ setTimeout(() => {
297
+ this.service.updateCharacteristic(this.hapChar.ActiveIdentifier, this.cacheChannel)
298
+ }, 2000)
299
+ throw new this.hapErr(-70402)
300
+ }
301
+ }
302
+
303
+ async internalVolumeUpdate(value) {
304
+ try {
305
+ // Add the request to the queue so updates are sent apart
306
+ await this.queue.add(async () => {
307
+ // Avoid multiple changes in short space of time
308
+ const updateKey = generateRandomString(5)
309
+ this.updateKey = updateKey
310
+ await sleep(300)
311
+ if (updateKey !== this.updateKey) {
312
+ return
313
+ }
314
+
315
+ // Calculate the value Meross needs
316
+ const volumeMR = this.volumeHK2MR(value)
317
+
318
+ if (volumeMR === this.cacheVolumeMR) {
319
+ return
320
+ }
321
+
322
+ // This flag stops the plugin from requesting updates while pending on others
323
+ this.updateInProgress = true
324
+
325
+ // Generate the namespace and payload
326
+ const namespace = 'Appliance.Control.Mp3'
327
+ const payload = {
328
+ mp3: {
329
+ volume: volumeMR,
330
+ },
331
+ }
332
+
333
+ // Use the platform function to send the update to the device
334
+ await this.platform.sendUpdate(this.accessory, {
335
+ namespace,
336
+ payload,
337
+ })
338
+
339
+ // Update the cache
340
+ this.cacheVolume = value
341
+ this.cacheVolumeMR = volumeMR
342
+
343
+ this.accessory.log(`${platformLang.curVol} [${this.cacheVolumeMR}/16]`)
344
+ })
345
+ } catch (err) {
346
+ // Catch any errors whilst updating the device
347
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
348
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
349
+ setTimeout(() => {
350
+ this.speakerService.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheVolume)
351
+ }, 2000)
352
+ throw new this.hapErr(-70402)
353
+ }
354
+ }
355
+
356
+ async internalSceneUpdate(value, scene, charToUpdate) {
357
+ try {
358
+ // Add the request to the queue so updates are sent apart
359
+ await this.queue.add(async () => {
360
+ // This flag stops the plugin from requesting updates while pending on others
361
+ this.updateInProgress = true
362
+
363
+ // Generate the namespace and payload
364
+ const namespace = 'Appliance.Control.Light'
365
+ const payload = {
366
+ light: {
367
+ effect: value ? scene : 0,
368
+ channel: 0,
369
+ },
370
+ }
371
+
372
+ // Use the platform function to send the update to the device
373
+ await this.platform.sendUpdate(this.accessory, {
374
+ namespace,
375
+ payload,
376
+ })
377
+
378
+ // Update the cache
379
+ this.accessory.log(`${platformLang.curScene} [${value ? scene : 0}]`);
380
+
381
+ // Also turn the other scenes off
382
+ ['BabySceneOne', 'BabySceneTwo', 'BabySceneThree', 'BabySceneFour'].forEach((char) => {
383
+ if (char !== charToUpdate) {
384
+ if (this.serviceLight.getCharacteristic(this.cusChar[char]).value) {
385
+ this.serviceLight.updateCharacteristic(this.cusChar[char], false)
386
+ }
387
+ }
388
+ })
389
+ })
390
+ } catch (err) {
391
+ // Catch any errors whilst updating the device
392
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
393
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
394
+ setTimeout(() => {
395
+ this.serviceLight.updateCharacteristic(this.cusChar[charToUpdate], false)
396
+ }, 2000)
397
+ throw new this.hapErr(-70402)
398
+ }
399
+ }
400
+
401
+ async requestUpdate(firstRun = false) {
402
+ try {
403
+ // Don't continue if an update is currently being sent to the device
404
+ if (this.updateInProgress) {
405
+ return
406
+ }
407
+
408
+ // Add the request to the queue so updates are sent apart
409
+ await this.queue.add(async () => {
410
+ // This flag stops the plugin from requesting updates while pending on others
411
+ this.updateInProgress = true
412
+
413
+ // Send the request
414
+ const res = await this.platform.sendUpdate(this.accessory, {
415
+ namespace: 'Appliance.System.All',
416
+ payload: {},
417
+ })
418
+
419
+ // Log the received data
420
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
421
+
422
+ // Check the response is in a useful format
423
+ const data = res.data.payload
424
+ if (data.all) {
425
+ if (data.all.digest && data.all.digest.mp3) {
426
+ this.applyUpdate(data.all.digest.mp3)
427
+ }
428
+
429
+ // A flag to check if we need to update the accessory context
430
+ let needsUpdate = false
431
+
432
+ // Get the mac address and hardware version of the device
433
+ if (data.all.system) {
434
+ // Mac address and hardware don't change regularly so only get on first poll
435
+ if (firstRun && data.all.system.hardware) {
436
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
437
+ this.accessory.context.hardware = data.all.system.hardware.version
438
+ }
439
+
440
+ // Get the ip address and firmware of the device
441
+ if (data.all.system.firmware) {
442
+ // Check for an IP change each and every time the device is polled
443
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
444
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
445
+ needsUpdate = true
446
+ }
447
+
448
+ // Firmware doesn't change regularly so only get on first poll
449
+ if (firstRun) {
450
+ this.accessory.context.firmware = data.all.system.firmware.version
451
+ }
452
+ }
453
+ }
454
+
455
+ // Get the cloud online status of the device
456
+ if (data.all.system.online) {
457
+ const isOnline = data.all.system.online.status === 1
458
+ if (this.accessory.context.isOnline !== isOnline) {
459
+ this.accessory.context.isOnline = isOnline
460
+ needsUpdate = true
461
+ }
462
+ }
463
+
464
+ // Update the accessory cache if anything has changed
465
+ if (needsUpdate || firstRun) {
466
+ this.platform.updateAccessory(this.accessory)
467
+ }
468
+ }
469
+ })
470
+ } catch (err) {
471
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
472
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
473
+
474
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
475
+ if (
476
+ (this.accessory.context.isOnline || firstRun)
477
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
478
+ ) {
479
+ this.accessory.context.isOnline = false
480
+ this.platform.updateAccessory(this.accessory)
481
+ }
482
+ }
483
+ }
484
+
485
+ receiveUpdate(params) {
486
+ try {
487
+ // Log the received data
488
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
489
+ if (params.payload && params.payload.mp3) {
490
+ this.applyUpdate(params.payload.mp3)
491
+ }
492
+ } catch (err) {
493
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
494
+ }
495
+ }
496
+
497
+ applyUpdate(data) {
498
+ // For TV Active characteristic
499
+ if (hasProperty(data, 'mute')) {
500
+ // Compare to this.cacheState (0, 1)
501
+ if (data.mute !== this.cacheStateMR) {
502
+ this.cacheStateMR = data.mute
503
+ this.cacheState = data.mute === 1 ? 0 : 1
504
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState)
505
+ this.speakerService.updateCharacteristic(this.hapChar.On, this.cacheState === 1)
506
+ this.accessory.log(`${platformLang.curState} [${this.cacheState === 1 ? 'on' : 'off'}]`)
507
+ }
508
+ }
509
+
510
+ // For TV Speaker (Fan) characteristic
511
+ if (hasProperty(data, 'volume')) {
512
+ // Compare to this.cacheVolume (0, ..., 100) or better this.cacheVolumeMR (0, 16)
513
+ // data.volume is (0, 16)
514
+ if (data.volume !== this.cacheVolumeMR) {
515
+ this.cacheVolumeMR = data.volume
516
+ this.cacheVolume = this.volumeMR2HK(this.cacheVolumeMR)
517
+ this.speakerService.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheVolume)
518
+ this.accessory.log(`${platformLang.curVol} [${this.cacheVolumeMR}/16].`)
519
+ }
520
+ }
521
+
522
+ // For TV ActiveIdentifier characteristic
523
+ if (data.song) {
524
+ // Compare to this.cacheChannel (1, ..., 11)
525
+ if (data.song !== this.cacheChannel) {
526
+ this.cacheChannel = data.song
527
+ this.service.updateCharacteristic(this.hapChar.ActiveIdentifier, this.cacheChannel)
528
+ this.accessory.log(`${platformLang.curSong} [${this.channelList[this.cacheChannel]}]`)
529
+ }
530
+ }
531
+ }
532
+ }