@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,495 @@
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 { generateRandomString, hasProperty, parseError } from '../utils/functions.js'
7
+ import platformLang from '../utils/lang-en.js'
8
+
9
+ export default class {
10
+ constructor(platform, accessory) {
11
+ // Set up variables from the platform
12
+ this.eveChar = platform.eveChar
13
+ this.hapChar = platform.api.hap.Characteristic
14
+ this.hapErr = platform.api.hap.HapStatusError
15
+ this.hapServ = platform.api.hap.Service
16
+ this.platform = platform
17
+
18
+ // Set up variables from the accessory
19
+ this.accessory = accessory
20
+ this.operationTime = this.accessory.context.options.garageDoorOpeningTime
21
+ || platformConsts.defaultValues.garageDoorOpeningTime
22
+ this.name = accessory.displayName
23
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
24
+ ? platform.config.cloudRefreshRate
25
+ : platformConsts.defaultValues.cloudRefreshRate
26
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
27
+ ? platform.config.refreshRate
28
+ : platformConsts.defaultValues.refreshRate
29
+ this.pollInterval = accessory.context.connection === 'local'
30
+ ? localRefreshRate
31
+ : cloudRefreshRate
32
+ this.states = {
33
+ 0: 'open',
34
+ 1: 'closed',
35
+ 2: 'opening',
36
+ 3: 'closing',
37
+ 4: 'stopped',
38
+ }
39
+
40
+ // Add the garage door service if it doesn't already exist
41
+ this.service = this.accessory.getService(this.hapServ.GarageDoorOpener)
42
+ || this.accessory.addService(this.hapServ.GarageDoorOpener)
43
+
44
+ // Add some extra Eve characteristics
45
+ if (!this.service.testCharacteristic(this.eveChar.LastActivation)) {
46
+ this.service.addCharacteristic(this.eveChar.LastActivation)
47
+ }
48
+ if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) {
49
+ this.service.addCharacteristic(this.eveChar.ResetTotal)
50
+ }
51
+ if (!this.service.testCharacteristic(this.eveChar.TimesOpened)) {
52
+ this.service.addCharacteristic(this.eveChar.TimesOpened)
53
+ }
54
+
55
+ // Add the set handler to the garage door target state characteristic
56
+ this.service
57
+ .getCharacteristic(this.hapChar.TargetDoorState)
58
+ .onSet(value => this.internalTargetUpdate(value))
59
+ this.cacheTarget = this.service.getCharacteristic(this.hapChar.TargetDoorState).value
60
+ this.cacheCurrent = this.service.getCharacteristic(this.hapChar.CurrentDoorState).value
61
+
62
+ // Add the set handler to the garage door reset total characteristic
63
+ this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => {
64
+ this.service.updateCharacteristic(this.eveChar.TimesOpened, 0)
65
+ })
66
+
67
+ // Update the obstruction detected to false on plugin load
68
+ this.service.updateCharacteristic(this.hapChar.ObstructionDetected, false)
69
+
70
+ // Pass the accessory to Fakegato to set up with Eve
71
+ this.accessory.eveService = new platform.eveService('door', this.accessory, { log: () => {} })
72
+ this.accessory.eveService.addEntry({ status: this.cacheCurrent === 0 ? 0 : 1 })
73
+
74
+ // Create the queue used for sending device requests
75
+ this.updateInProgress = false
76
+ this.queue = new PQueue({
77
+ concurrency: 1,
78
+ interval: 250,
79
+ intervalCap: 1,
80
+ timeout: 10000,
81
+ throwOnTimeout: true,
82
+ })
83
+ this.queue.on('idle', () => {
84
+ this.updateInProgress = false
85
+ })
86
+
87
+ // Set up the mqtt client for cloud devices to send and receive device updates
88
+ if (accessory.context.connection !== 'local') {
89
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
90
+ this.accessory.mqtt.connect()
91
+ }
92
+
93
+ // Always request a device update on startup, then start the interval for polling
94
+ setTimeout(() => this.requestUpdate(true), 2000)
95
+ this.accessory.refreshInterval = setInterval(
96
+ () => this.requestUpdate(),
97
+ this.pollInterval * 1000,
98
+ )
99
+
100
+ // Output the customised options to the log
101
+ const opts = JSON.stringify({
102
+ connection: this.accessory.context.connection,
103
+ garageDoorOpeningTime: this.operationTime,
104
+ })
105
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
106
+ }
107
+
108
+ async internalTargetUpdate(value) {
109
+ // Add the request to the queue so updates are sent apart
110
+ try {
111
+ await this.queue.add(async () => {
112
+ let action
113
+ let newTarget = value
114
+ let newCurrent
115
+ if (value === 1) {
116
+ // Request to close the garage door
117
+ if (this.cacheCurrent === 0) {
118
+ // The door is currently open
119
+ // ACTION: close the door
120
+ action = 'close'
121
+
122
+ // Mark the current door state as closing
123
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
124
+ newCurrent = 3
125
+ } else if (this.cacheCurrent === 1) {
126
+ // The door is currently closed
127
+ // ACTION: none
128
+ // Mark the current door state as closed
129
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
130
+ newCurrent = 1
131
+ } else if (this.cacheCurrent === 2) {
132
+ // The door is currently opening
133
+ // ACTION: close the door
134
+ action = 'close'
135
+
136
+ // Mark the target state as close and current door state as closing
137
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 1)
138
+ newTarget = 1
139
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
140
+ newCurrent = 3
141
+ } else if (this.cacheCurrent === 3) {
142
+ // The door is currently closing
143
+ // ACTION: none
144
+ // Mark the current door state as closing
145
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
146
+ newCurrent = 3
147
+ }
148
+ } else if (value === 0) {
149
+ // Request to open the door
150
+ if (this.cacheCurrent === 0) {
151
+ // The door is currently open
152
+ // ACTION: none
153
+ // Mark the current door state as open
154
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
155
+ newCurrent = 0
156
+ } else if (this.cacheCurrent === 1) {
157
+ // The door is currently closed
158
+ // ACTION: open the door
159
+ action = 'open'
160
+
161
+ // Mark the current door state as opening
162
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
163
+ newCurrent = 2
164
+ } else if (this.cacheCurrent === 2) {
165
+ // The door is currently opening
166
+ // ACTION: none
167
+
168
+ // Mark the current door state as opening
169
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
170
+ newCurrent = 2
171
+ } else if (this.cacheCurrent === 3) {
172
+ // The door is currently closing
173
+ // ACTION: open the door
174
+ action = 'open'
175
+
176
+ // Mark the target state as open and current state as opening
177
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
178
+ newTarget = 0
179
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
180
+ newCurrent = 2
181
+ }
182
+ }
183
+
184
+ // Only send an update if we need to
185
+ if (action) {
186
+ this.ignoreIncoming = true
187
+ setTimeout(() => {
188
+ this.ignoreIncoming = false
189
+ }, 3000)
190
+
191
+ // Generate the payload and namespace for the correct device model
192
+ const namespace = 'Appliance.GarageDoor.State'
193
+ const payload = {
194
+ state: {
195
+ channel: 0,
196
+ open: action === 'open' ? 1 : 0,
197
+ uuid: this.accessory.context.serialNumber,
198
+ },
199
+ }
200
+
201
+ // Use the platform function to send the update to the device
202
+ await this.platform.sendUpdate(this.accessory, {
203
+ namespace,
204
+ payload,
205
+ })
206
+ }
207
+
208
+ // Update the cache target state if different
209
+ if (this.cacheTarget !== newTarget) {
210
+ this.cacheTarget = newTarget
211
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
212
+ }
213
+
214
+ // Update the cache current state if different
215
+ if (this.cacheCurrent !== newCurrent) {
216
+ this.cacheCurrent = newCurrent
217
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
218
+ }
219
+
220
+ /*
221
+ CASE: garage has been opened
222
+ target has been set to [open] and current has been set to [opening]
223
+ wait for the operation time to elapse and set the current to [open]
224
+ */
225
+ if (action === 'open') {
226
+ const updateKey = generateRandomString(5)
227
+ this.updateKey = updateKey
228
+
229
+ // Update the Eve times opened characteristic
230
+ this.accessory.eveService.addEntry({ status: 0 })
231
+ this.service.updateCharacteristic(
232
+ this.eveChar.TimesOpened,
233
+ this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
234
+ )
235
+ const initialTime = this.accessory.eveService.getInitialTime()
236
+ this.service.updateCharacteristic(
237
+ this.eveChar.LastActivation,
238
+ Math.round(new Date().valueOf() / 1000) - initialTime,
239
+ )
240
+ setTimeout(() => {
241
+ if (updateKey !== this.updateKey) {
242
+ return
243
+ }
244
+ if (this.service.getCharacteristic(this.hapChar.CurrentDoorState).value !== 2) {
245
+ return
246
+ }
247
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
248
+ this.cacheCurrent = 0
249
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
250
+ }, this.operationTime * 1000)
251
+ }
252
+
253
+ /*
254
+ CASE: garage has been closed
255
+ target has been set to [close] and current has been set to [closing]
256
+ wait for the plugin to get a definite closed response from Meross
257
+ For security reasons, I don't want to rely on operation time for the garage to
258
+ definitely show as closed
259
+ Set a timer for operation time plus 15 seconds, and if the garage is still closing then
260
+ mark target and current state as open
261
+ Also setup quicker polling every 3 seconds when in local mode to get the closed status
262
+ */
263
+ if (action === 'close') {
264
+ const updateKey = generateRandomString(5)
265
+ if (this.accessory.context.connection === 'local') {
266
+ this.extremePolling = setInterval(() => this.requestUpdate(), 3000)
267
+ }
268
+ this.updateKey = updateKey
269
+ setTimeout(() => {
270
+ if (updateKey !== this.updateKey) {
271
+ return
272
+ }
273
+ if (this.service.getCharacteristic(this.hapChar.CurrentDoorState).value !== 3) {
274
+ return
275
+ }
276
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
277
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
278
+ this.cacheTarget = 0
279
+ this.cacheCurrent = 0
280
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
281
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
282
+
283
+ // Cancel any 'extreme' polling intervals from setting the garage to close
284
+ if (this.extremePolling) {
285
+ clearInterval(this.extremePolling)
286
+ this.extremePolling = false
287
+ }
288
+ }, (this.operationTime + 15) * 1000)
289
+ }
290
+ })
291
+ } catch (err) {
292
+ // Catch any errors whilst updating the device
293
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
294
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
295
+ setTimeout(() => {
296
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.cacheTarget)
297
+ }, 2000)
298
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, new this.hapErr(-70402))
299
+ }
300
+ }
301
+
302
+ async requestUpdate(firstRun = false) {
303
+ try {
304
+ // Don't continue if an update is currently being sent to the device
305
+ if (this.updateInProgress) {
306
+ return
307
+ }
308
+
309
+ // Add the request to the queue so updates are sent apart
310
+ await this.queue.add(async () => {
311
+ // This flag stops the plugin from requesting updates while pending on others
312
+ this.updateInProgress = true
313
+
314
+ // Send the request
315
+ const res = await this.platform.sendUpdate(this.accessory, {
316
+ namespace: 'Appliance.System.All',
317
+ payload: {},
318
+ })
319
+
320
+ // Log the received data
321
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
322
+
323
+ // Check the response is in a useful format
324
+ const data = res.data.payload
325
+ if (data.all) {
326
+ if (data.all.digest && data.all.digest.garageDoor && data.all.digest.garageDoor[0]) {
327
+ this.applyUpdate(data.all.digest.garageDoor[0])
328
+ }
329
+
330
+ // A flag to check if we need to update the accessory context
331
+ let needsUpdate = false
332
+
333
+ // Get the mac address and hardware version of the device
334
+ if (data.all.system) {
335
+ // Mac address and hardware don't change regularly so only get on first poll
336
+ if (firstRun && data.all.system.hardware) {
337
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
338
+ this.accessory.context.hardware = data.all.system.hardware.version
339
+ }
340
+
341
+ // Get the ip address and firmware of the device
342
+ if (data.all.system.firmware) {
343
+ // Check for an IP change each and every time the device is polled
344
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
345
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
346
+ needsUpdate = true
347
+ }
348
+
349
+ // Firmware doesn't change regularly so only get on first poll
350
+ if (firstRun) {
351
+ this.accessory.context.firmware = data.all.system.firmware.version
352
+ }
353
+ }
354
+ }
355
+
356
+ // Get the cloud online status of the device
357
+ if (data.all.system.online) {
358
+ const isOnline = data.all.system.online.status === 1
359
+ if (this.accessory.context.isOnline !== isOnline) {
360
+ this.accessory.context.isOnline = isOnline
361
+ needsUpdate = true
362
+ }
363
+ }
364
+
365
+ // Update the accessory cache if anything has changed
366
+ if (needsUpdate || firstRun) {
367
+ this.platform.updateAccessory(this.accessory)
368
+ }
369
+ }
370
+ })
371
+ } catch (err) {
372
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
373
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
374
+
375
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
376
+ if (
377
+ (this.accessory.context.isOnline || firstRun)
378
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
379
+ ) {
380
+ this.accessory.context.isOnline = false
381
+ this.platform.updateAccessory(this.accessory)
382
+ }
383
+ }
384
+ }
385
+
386
+ receiveUpdate(params) {
387
+ try {
388
+ // Log the received data
389
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
390
+
391
+ // Check the response is in a useful format
392
+ const data = params.payload
393
+ if (data.state && data.state[0]) {
394
+ this.applyUpdate(data.state[0])
395
+ }
396
+ } catch (err) {
397
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
398
+ }
399
+ }
400
+
401
+ applyUpdate(data) {
402
+ // Don't bother whilst the ignore incoming is set to true
403
+ if (this.ignoreIncoming) {
404
+ return
405
+ }
406
+
407
+ // When operated externally, the plugin does not bother with 'opening' and 'closing' status
408
+ // Open means magnetic sensor not detected, doesn't really mean the door is open
409
+ if (hasProperty(data, 'open')) {
410
+ const isOpen = data.open === 1
411
+ switch (this.cacheCurrent) {
412
+ case 0:
413
+ case 2: {
414
+ // Homebridge has garage as open or opening
415
+ if (isOpen) {
416
+ // Meross has reported open
417
+ // Nothing to do
418
+ } else {
419
+ // Meross has reported closed
420
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 1)
421
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
422
+ this.accessory.eveService.addEntry({ status: 1 })
423
+ this.cacheCurrent = 1
424
+ this.cacheTarget = 1
425
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
426
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
427
+ }
428
+ break
429
+ }
430
+ case 1: {
431
+ // Homebridge has garage as closed
432
+ if (isOpen) {
433
+ // Meross has reported open
434
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
435
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
436
+ this.accessory.eveService.addEntry({ status: 0 })
437
+ this.service.updateCharacteristic(
438
+ this.eveChar.TimesOpened,
439
+ this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
440
+ )
441
+ const initialTime = this.accessory.eveService.getInitialTime()
442
+ this.service.updateCharacteristic(
443
+ this.eveChar.LastActivation,
444
+ Math.round(new Date().valueOf() / 1000) - initialTime,
445
+ )
446
+ this.cacheCurrent = 0
447
+ this.cacheTarget = 0
448
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
449
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
450
+ } else {
451
+ // Meross has reported closed
452
+ // Nothing to do
453
+ }
454
+ break
455
+ }
456
+ case 3: {
457
+ // Homebridge has garage as closing
458
+ if (isOpen) {
459
+ // Meross has reported open
460
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
461
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
462
+ this.accessory.eveService.addEntry({ status: 0 })
463
+ this.service.updateCharacteristic(
464
+ this.eveChar.TimesOpened,
465
+ this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
466
+ )
467
+ const initialTime = this.accessory.eveService.getInitialTime()
468
+ this.service.updateCharacteristic(
469
+ this.eveChar.LastActivation,
470
+ Math.round(new Date().valueOf() / 1000) - initialTime,
471
+ )
472
+ this.cacheCurrent = 0
473
+ this.cacheTarget = 0
474
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
475
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
476
+ } else {
477
+ // Meross has reported closed
478
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
479
+ this.accessory.eveService.addEntry({ status: 1 })
480
+ this.cacheCurrent = 1
481
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
482
+ }
483
+
484
+ // Cancel any 'extreme' polling intervals from setting the garage to close
485
+ if (this.extremePolling) {
486
+ clearInterval(this.extremePolling)
487
+ this.extremePolling = false
488
+ }
489
+ break
490
+ }
491
+ default:
492
+ }
493
+ }
494
+ }
495
+ }