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