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