@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,474 @@
1
+ import PQueue from 'p-queue'
2
+ import { TimeoutError } from 'p-timeout'
3
+
4
+ import mqttClient from '../connection/mqtt.js'
5
+ import { hk2mrCT, mr2hkCT } from '../utils/colour.js'
6
+ import platformConsts from '../utils/constants.js'
7
+ import {
8
+ generateRandomString,
9
+ hasProperty,
10
+ parseError,
11
+ sleep,
12
+ } from '../utils/functions.js'
13
+ import platformLang from '../utils/lang-en.js'
14
+
15
+ export default class {
16
+ constructor(platform, accessory) {
17
+ // Set up variables from the platform
18
+ this.cusChar = platform.cusChar
19
+ this.hapChar = platform.api.hap.Characteristic
20
+ this.hapErr = platform.api.hap.HapStatusError
21
+ this.hapServ = platform.api.hap.Service
22
+ this.platform = platform
23
+
24
+ // Set up variables from the accessory
25
+ this.accessory = accessory
26
+ this.alShift = this.accessory.context.options.adaptiveLightingShift
27
+ || platformConsts.defaultValues.adaptiveLightingShift
28
+ this.brightnessStep = this.accessory.context.options.brightnessStep || platformConsts.defaultValues.brightnessStep
29
+ this.brightnessStep = Math.min(this.brightnessStep, 100)
30
+ this.name = accessory.displayName
31
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
32
+ ? platform.config.cloudRefreshRate
33
+ : platformConsts.defaultValues.cloudRefreshRate
34
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
35
+ ? platform.config.refreshRate
36
+ : platformConsts.defaultValues.refreshRate
37
+ this.pollInterval = accessory.context.connection === 'local'
38
+ ? localRefreshRate
39
+ : cloudRefreshRate
40
+ this.hasNightLight = accessory.context.model === 'MSL210'
41
+
42
+ // Add the lightbulb service if it doesn't already exist
43
+ this.service = this.accessory.getService(this.hapServ.Lightbulb)
44
+ || this.accessory.addService(this.hapServ.Lightbulb)
45
+
46
+ // If adaptive lighting has just been disabled then remove and re-add service to hide AL icon
47
+ if (this.alShift === -1 && this.accessory.context.adaptiveLighting) {
48
+ this.accessory.removeService(this.service)
49
+ this.service = this.accessory.addService(this.hapServ.Lightbulb)
50
+ this.accessory.context.adaptiveLighting = false
51
+ }
52
+
53
+ // Add the set handler to the lightbulb on/off characteristic
54
+ this.service
55
+ .getCharacteristic(this.hapChar.On)
56
+ .onSet(async value => this.internalStateUpdate(value))
57
+ this.cacheState = this.service.getCharacteristic(this.hapChar.On).value
58
+
59
+ // Add the set handler to the lightbulb brightness characteristic
60
+ this.service
61
+ .getCharacteristic(this.hapChar.Brightness)
62
+ .setProps({ minStep: this.brightnessStep })
63
+ .onSet(async value => this.internalBrightnessUpdate(value))
64
+ this.cacheBright = this.service.getCharacteristic(this.hapChar.Brightness).value
65
+
66
+ // Add the set handler to the lightbulb colour temperature characteristic
67
+ this.service
68
+ .getCharacteristic(this.hapChar.ColorTemperature)
69
+ .onSet(async value => this.internalCTUpdate(value))
70
+ this.cacheMired = this.service.getCharacteristic(this.hapChar.ColorTemperature).value
71
+
72
+ // Add the night mode characteristics if supported model
73
+ if (this.hasNightLight) {
74
+ if (!this.service.testCharacteristic(this.cusChar.LightNightWarm)) {
75
+ this.service.addCharacteristic(this.cusChar.LightNightWarm)
76
+ }
77
+ if (!this.service.testCharacteristic(this.cusChar.LightNightWhite)) {
78
+ this.service.addCharacteristic(this.cusChar.LightNightWhite)
79
+ }
80
+ this.service
81
+ .getCharacteristic(this.cusChar.LightNightWarm)
82
+ .onSet(async value => this.internalNightUpdate(6, value))
83
+ this.service
84
+ .getCharacteristic(this.cusChar.LightNightWarm)
85
+ .onSet(async value => this.internalNightUpdate(5, value))
86
+
87
+ // Object of effect to description
88
+ this.nightModes = {
89
+ 5: 'night warm',
90
+ 6: 'night white',
91
+ }
92
+ }
93
+
94
+ // Set up the adaptive lighting controller if not disabled by user
95
+ if (this.alShift !== -1) {
96
+ this.alController = new platform.api.hap.AdaptiveLightingController(this.service, {
97
+ customTemperatureAdjustment: this.alShift,
98
+ })
99
+ this.accessory.configureController(this.alController)
100
+ this.accessory.context.adaptiveLighting = true
101
+ }
102
+
103
+ // Create the queue used for sending device requests
104
+ this.updateInProgress = false
105
+ this.queue = new PQueue({
106
+ concurrency: 1,
107
+ interval: 250,
108
+ intervalCap: 1,
109
+ timeout: 10000,
110
+ throwOnTimeout: true,
111
+ })
112
+ this.queue.on('idle', () => {
113
+ this.updateInProgress = false
114
+ })
115
+
116
+ // Set up the mqtt client for cloud devices to send and receive device updates
117
+ if (accessory.context.connection !== 'local') {
118
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
119
+ this.accessory.mqtt.connect()
120
+ }
121
+
122
+ // Always request a device update on startup, then start the interval for polling
123
+ setTimeout(() => this.requestUpdate(true), 2000)
124
+ this.accessory.refreshInterval = setInterval(
125
+ () => this.requestUpdate(),
126
+ this.pollInterval * 1000,
127
+ )
128
+
129
+ // Output the customised options to the log
130
+ const opts = JSON.stringify({
131
+ adaptiveLightingShift: this.alShift,
132
+ brightnessStep: this.brightnessStep,
133
+ connection: this.accessory.context.connection,
134
+ })
135
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
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 internalCTUpdate(value) {
233
+ try {
234
+ // Add the request to the queue so updates are sent apart
235
+ await this.queue.add(async () => {
236
+ // Don't continue if the state is the same as before
237
+ if (this.cacheMired === value) {
238
+ return
239
+ }
240
+
241
+ // Avoid multiple changes in short space of time
242
+ const updateKey = generateRandomString(5)
243
+ this.updateKeyCT = updateKey
244
+ await sleep(300)
245
+ if (updateKey !== this.updateKeyCT) {
246
+ return
247
+ }
248
+
249
+ // This flag stops the plugin from requesting updates while pending on others
250
+ this.updateInProgress = true
251
+
252
+ // Don't continue if the new value is the same as before
253
+ if (!this.cacheState || this.cacheMired === value) {
254
+ return
255
+ }
256
+
257
+ // Generate the payload to send
258
+ const payload = {
259
+ light: {
260
+ temperature: hk2mrCT(value),
261
+ capacity: 2,
262
+ channel: 0,
263
+ },
264
+ }
265
+
266
+ // Generate the namespace
267
+ const namespace = 'Appliance.Control.Light'
268
+
269
+ // Use the platform function to send the update to the device
270
+ await this.platform.sendUpdate(this.accessory, {
271
+ namespace,
272
+ payload,
273
+ })
274
+
275
+ // Update the cache and log the update has been successful
276
+ this.cacheMired = value
277
+ const kelvin = Math.round(1000000 / this.cacheMired)
278
+ const al = this.alController && this.alController.isAdaptiveLightingActive()
279
+ ? ` ${platformLang.viaAL}`
280
+ : ''
281
+ this.accessory.log(`${platformLang.curLightTemp} [${this.cacheMired} / ${kelvin}]${al}`)
282
+ })
283
+ } catch (err) {
284
+ const eText = parseError(err)
285
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
286
+ setTimeout(() => {
287
+ this.service.updateCharacteristic(this.hapChar.ColorTemperature, this.cacheMired)
288
+ }, 2000)
289
+ throw new this.hapErr(-70402)
290
+ }
291
+ }
292
+
293
+ async internalNightUpdate(effect, value) {
294
+ try {
295
+ // Add the request to the queue so updates are sent apart
296
+ await this.queue.add(async () => {
297
+ // This flag stops the plugin from requesting updates while pending on others
298
+ this.updateInProgress = true
299
+
300
+ // Temp return whilst payload is unknown
301
+ this.accessory.log(`${effect} ${value}`)
302
+
303
+ /*
304
+ // Generate the payload and namespace
305
+ const namespace = 'Appliance.Control.Light';
306
+ const payload = {
307
+ // Unknown
308
+ };
309
+
310
+ // Use the platform function to send the update to the device
311
+ await this.platform.sendUpdate(this.accessory, {
312
+ namespace,
313
+ payload,
314
+ });
315
+
316
+ // Log the update has been successful
317
+ this.accessory.log('[%s] current mode [%s].', this.name, this.nightModes[effect]);
318
+ */
319
+ })
320
+ } catch (err) {
321
+ // Catch any errors whilst updating the device
322
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
323
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
324
+ setTimeout(() => {
325
+ this.service.updateCharacteristic(this.cusChar.On, this.cacheState)
326
+ }, 2000)
327
+ throw new this.hapErr(-70402)
328
+ }
329
+ }
330
+
331
+ async requestUpdate(firstRun = false) {
332
+ try {
333
+ // Don't continue if an update is currently being sent to the device
334
+ if (this.updateInProgress) {
335
+ return
336
+ }
337
+
338
+ // Add the request to the queue so updates are sent apart
339
+ await this.queue.add(async () => {
340
+ // This flag stops the plugin from requesting updates while pending on others
341
+ this.updateInProgress = true
342
+
343
+ // Send the request
344
+ const res = await this.platform.sendUpdate(this.accessory, {
345
+ namespace: 'Appliance.System.All',
346
+ payload: {},
347
+ })
348
+
349
+ // Log the received data
350
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
351
+
352
+ // Check the response is in a useful format
353
+ const data = res.data.payload
354
+ if (data.all) {
355
+ if (data.all.digest) {
356
+ this.applyUpdate(data.all.digest)
357
+ }
358
+
359
+ // A flag to check if we need to update the accessory context
360
+ let needsUpdate = false
361
+
362
+ // Get the mac address and hardware version of the device
363
+ if (data.all.system) {
364
+ // Mac address and hardware don't change regularly so only get on first poll
365
+ if (firstRun && data.all.system.hardware) {
366
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
367
+ this.accessory.context.hardware = data.all.system.hardware.version
368
+ }
369
+
370
+ // Get the ip address and firmware of the device
371
+ if (data.all.system.firmware) {
372
+ // Check for an IP change each and every time the device is polled
373
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
374
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
375
+ needsUpdate = true
376
+ }
377
+
378
+ // Firmware doesn't change regularly so only get on first poll
379
+ if (firstRun) {
380
+ this.accessory.context.firmware = data.all.system.firmware.version
381
+ }
382
+ }
383
+ }
384
+
385
+ // Get the cloud online status of the device
386
+ if (data.all.system.online) {
387
+ const isOnline = data.all.system.online.status === 1
388
+ if (this.accessory.context.isOnline !== isOnline) {
389
+ this.accessory.context.isOnline = isOnline
390
+ needsUpdate = true
391
+ }
392
+ }
393
+
394
+ // Update the accessory cache if anything has changed
395
+ if (needsUpdate || firstRun) {
396
+ this.platform.updateAccessory(this.accessory)
397
+ }
398
+ }
399
+ })
400
+ } catch (err) {
401
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
402
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
403
+
404
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
405
+ if (
406
+ (this.accessory.context.isOnline || firstRun)
407
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
408
+ ) {
409
+ this.accessory.context.isOnline = false
410
+ this.platform.updateAccessory(this.accessory)
411
+ }
412
+ }
413
+ }
414
+
415
+ receiveUpdate(params) {
416
+ try {
417
+ // Log the received data
418
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
419
+
420
+ // Validate the response, checking for payload property
421
+ if (!params.payload) {
422
+ throw new Error('invalid response received')
423
+ }
424
+ const data = params.payload
425
+ if (data.togglex || data.light) {
426
+ this.applyUpdate(data)
427
+ }
428
+ } catch (err) {
429
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
430
+ }
431
+ }
432
+
433
+ applyUpdate(data) {
434
+ if (data.togglex && data.togglex[0] && hasProperty(data.togglex[0], 'onoff')) {
435
+ // newState is given as 0 or 1 -> convert to bool for HomeKit
436
+ const newState = data.togglex[0].onoff === 1
437
+
438
+ // Check against the cache and update HomeKit and the cache if needed
439
+ if (this.cacheState !== newState) {
440
+ this.service.updateCharacteristic(this.hapChar.On, newState)
441
+ this.cacheState = newState
442
+ this.accessory.log(`${platformLang.curState} [${this.cacheState ? 'on' : 'off'}]`)
443
+ }
444
+ }
445
+ if (data.light) {
446
+ if (hasProperty(data.light, 'luminance')) {
447
+ const newBright = data.light.luminance
448
+
449
+ // Check against the cache and update HomeKit and the cache if needed
450
+ if (this.cacheBright !== newBright) {
451
+ this.service.updateCharacteristic(this.hapChar.Brightness, newBright)
452
+ this.cacheBright = newBright
453
+ this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`)
454
+ }
455
+ }
456
+ if (hasProperty(data.light, 'temperature')) {
457
+ const hkTemp = mr2hkCT(data.light.temperature)
458
+
459
+ // Check against the cache and update HomeKit and the cache if needed
460
+ if (this.cacheMired !== hkTemp) {
461
+ const dif = Math.abs(this.cacheMired - hkTemp)
462
+ this.service.updateCharacteristic(this.hapChar.ColorTemperature, hkTemp)
463
+ this.cacheMired = hkTemp
464
+ const kelvin = Math.round(1000000 / this.cacheMired)
465
+ this.accessory.log(`${platformLang.curLightTemp} [${this.cacheMired} / ${kelvin}]`)
466
+ if (dif > 10 && this.alController && this.alController.isAdaptiveLightingActive()) {
467
+ this.alController.disableAdaptiveLighting()
468
+ this.accessory.log(platformLang.alDisabled)
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ }