@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,376 @@
1
+ import PQueue from 'p-queue'
2
+ import { TimeoutError } from 'p-timeout'
3
+
4
+ import platformConsts from '../utils/constants.js'
5
+ import { generateRandomString, hasProperty, parseError } from '../utils/functions.js'
6
+ import platformLang from '../utils/lang-en.js'
7
+
8
+ export default class {
9
+ constructor(platform, accessory, priAcc) {
10
+ // Set up variables from the platform
11
+ this.eveChar = platform.eveChar
12
+ this.hapChar = platform.api.hap.Characteristic
13
+ this.hapErr = platform.api.hap.HapStatusError
14
+ this.hapServ = platform.api.hap.Service
15
+ this.platform = platform
16
+
17
+ // Set up variables from the accessory
18
+ this.accessory = accessory
19
+ this.operationTime = this.accessory.context.options.garageDoorOpeningTime
20
+ || platformConsts.defaultValues.garageDoorOpeningTime
21
+ this.name = accessory.displayName
22
+ this.priAcc = priAcc
23
+ this.states = {
24
+ 0: 'open',
25
+ 1: 'closed',
26
+ 2: 'opening',
27
+ 3: 'closing',
28
+ 4: 'stopped',
29
+ }
30
+
31
+ // Add the garage door service if it doesn't already exist
32
+ this.service = this.accessory.getService(this.hapServ.GarageDoorOpener)
33
+ || this.accessory.addService(this.hapServ.GarageDoorOpener)
34
+
35
+ // Add some extra Eve characteristics
36
+ if (!this.service.testCharacteristic(this.eveChar.LastActivation)) {
37
+ this.service.addCharacteristic(this.eveChar.LastActivation)
38
+ }
39
+ if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) {
40
+ this.service.addCharacteristic(this.eveChar.ResetTotal)
41
+ }
42
+ if (!this.service.testCharacteristic(this.eveChar.TimesOpened)) {
43
+ this.service.addCharacteristic(this.eveChar.TimesOpened)
44
+ }
45
+
46
+ // Add the set handler to the garage door target state characteristic
47
+ this.service
48
+ .getCharacteristic(this.hapChar.TargetDoorState)
49
+ .onSet(value => this.internalTargetUpdate(value))
50
+ this.cacheTarget = this.service.getCharacteristic(this.hapChar.TargetDoorState).value
51
+ this.cacheCurrent = this.service.getCharacteristic(this.hapChar.CurrentDoorState).value
52
+
53
+ // Add the set handler to the garage door reset total characteristic
54
+ this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => {
55
+ this.service.updateCharacteristic(this.eveChar.TimesOpened, 0)
56
+ })
57
+
58
+ // Update the obstruction detected to false on plugin load
59
+ this.service.updateCharacteristic(this.hapChar.ObstructionDetected, false)
60
+
61
+ // Pass the accessory to Fakegato to set up with Eve
62
+ this.accessory.eveService = new platform.eveService('door', this.accessory, { log: () => {} })
63
+ this.accessory.eveService.addEntry({ status: this.cacheCurrent === 0 ? 0 : 1 })
64
+
65
+ // Create the queue used for sending device requests
66
+ this.updateInProgress = false
67
+ this.queue = new PQueue({
68
+ concurrency: 1,
69
+ interval: 250,
70
+ intervalCap: 1,
71
+ timeout: 10000,
72
+ throwOnTimeout: true,
73
+ })
74
+ this.queue.on('idle', () => {
75
+ this.updateInProgress = false
76
+ })
77
+
78
+ // Output the customised options to the log
79
+ const opts = JSON.stringify({
80
+ connection: this.accessory.context.connection,
81
+ garageDoorOpeningTime: this.operationTime,
82
+ hideChannels: accessory.context.options.hideChannels,
83
+ })
84
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
85
+ }
86
+
87
+ async internalTargetUpdate(value) {
88
+ // Add the request to the queue so updates are sent apart
89
+ try {
90
+ await this.queue.add(async () => {
91
+ let action
92
+ let newTarget = value
93
+ let newCurrent
94
+ if (value === 1) {
95
+ // Request to close the garage door
96
+ if (this.cacheCurrent === 0) {
97
+ // The door is currently open
98
+ // ACTION: close the door
99
+ action = 'close'
100
+
101
+ // Mark the current door state as closing
102
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
103
+ newCurrent = 3
104
+ } else if (this.cacheCurrent === 1) {
105
+ // The door is currently closed
106
+ // ACTION: none
107
+ // Mark the current door state as closed
108
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
109
+ newCurrent = 1
110
+ } else if (this.cacheCurrent === 2) {
111
+ // The door is currently opening
112
+ // ACTION: close the door
113
+ action = 'close'
114
+
115
+ // Mark the target state as close and current door state as closing
116
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 1)
117
+ newTarget = 1
118
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
119
+ newCurrent = 3
120
+ } else if (this.cacheCurrent === 3) {
121
+ // The door is currently closing
122
+ // ACTION: none
123
+ // Mark the current door state as closing
124
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
125
+ newCurrent = 3
126
+ }
127
+ } else if (value === 0) {
128
+ // Request to open the door
129
+ if (this.cacheCurrent === 0) {
130
+ // The door is currently open
131
+ // ACTION: none
132
+ // Mark the current door state as open
133
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
134
+ newCurrent = 0
135
+ } else if (this.cacheCurrent === 1) {
136
+ // The door is currently closed
137
+ // ACTION: open the door
138
+ action = 'open'
139
+
140
+ // Mark the current door state as opening
141
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
142
+ newCurrent = 2
143
+ } else if (this.cacheCurrent === 2) {
144
+ // The door is currently opening
145
+ // ACTION: none
146
+
147
+ // Mark the current door state as opening
148
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
149
+ newCurrent = 2
150
+ } else if (this.cacheCurrent === 3) {
151
+ // The door is currently closing
152
+ // ACTION: open the door
153
+ action = 'open'
154
+
155
+ // Mark the target state as open and current state as opening
156
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
157
+ newTarget = 0
158
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
159
+ newCurrent = 2
160
+ }
161
+ }
162
+
163
+ // Only send an update if we need to
164
+ if (action) {
165
+ this.ignoreIncoming = true
166
+ setTimeout(() => {
167
+ this.ignoreIncoming = false
168
+ }, 3000)
169
+
170
+ // Generate the payload and namespace for the correct device model
171
+ const namespace = 'Appliance.GarageDoor.State'
172
+ const payload = {
173
+ state: {
174
+ channel: this.accessory.context.channel,
175
+ open: action === 'open' ? 1 : 0,
176
+ uuid: this.accessory.context.serialNumber,
177
+ },
178
+ }
179
+
180
+ // Use the platform function to send the update to the device
181
+ await this.platform.sendUpdate(this.priAcc, {
182
+ namespace,
183
+ payload,
184
+ })
185
+ }
186
+
187
+ // Update the cache target state if different
188
+ if (this.cacheTarget !== newTarget) {
189
+ this.cacheTarget = newTarget
190
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
191
+ }
192
+
193
+ // Update the cache current state if different
194
+ if (this.cacheCurrent !== newCurrent) {
195
+ this.cacheCurrent = newCurrent
196
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
197
+ }
198
+
199
+ /*
200
+ CASE: garage has been opened
201
+ target has been set to [open] and current has been set to [opening]
202
+ wait for the operation time to elapse and set the current to [open]
203
+ */
204
+ if (action === 'open') {
205
+ const updateKey = generateRandomString(5)
206
+ this.updateKey = updateKey
207
+
208
+ // Update the Eve times opened characteristic
209
+ this.accessory.eveService.addEntry({ status: 0 })
210
+ this.service.updateCharacteristic(
211
+ this.eveChar.TimesOpened,
212
+ this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
213
+ )
214
+ const initialTime = this.accessory.eveService.getInitialTime()
215
+ this.service.updateCharacteristic(
216
+ this.eveChar.LastActivation,
217
+ Math.round(new Date().valueOf() / 1000) - initialTime,
218
+ )
219
+ setTimeout(() => {
220
+ if (updateKey !== this.updateKey) {
221
+ return
222
+ }
223
+ if (this.service.getCharacteristic(this.hapChar.CurrentDoorState).value !== 2) {
224
+ return
225
+ }
226
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
227
+ this.cacheCurrent = 0
228
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
229
+ }, this.operationTime * 1000)
230
+ }
231
+
232
+ /*
233
+ CASE: garage has been closed
234
+ target has been set to [close] and current has been set to [closing]
235
+ wait for the plugin to get a definite closed response from Meross
236
+ For security reasons, I don't want to rely on operation time for the garage to
237
+ definitely show as closed
238
+ Set a timer for operation time plus 15 seconds, and if the garage is still closing then
239
+ mark target and current state as open
240
+ Also setup quicker polling every 3 seconds when in local mode to get the closed status
241
+ */
242
+ if (action === 'close') {
243
+ const updateKey = generateRandomString(5)
244
+ if (this.accessory.context.connection === 'local') {
245
+ this.extremePolling = setInterval(() => this.priAcc.control.requestUpdate(), 3000)
246
+ }
247
+ this.updateKey = updateKey
248
+ setTimeout(() => {
249
+ if (updateKey !== this.updateKey) {
250
+ return
251
+ }
252
+ if (this.service.getCharacteristic(this.hapChar.CurrentDoorState).value !== 3) {
253
+ return
254
+ }
255
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
256
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
257
+ this.cacheTarget = 0
258
+ this.cacheCurrent = 0
259
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
260
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
261
+
262
+ // Cancel any 'extreme' polling intervals from setting the garage to close
263
+ if (this.extremePolling) {
264
+ clearInterval(this.extremePolling)
265
+ this.extremePolling = false
266
+ }
267
+ }, (this.operationTime + 15) * 1000)
268
+ }
269
+ })
270
+ } catch (err) {
271
+ // Catch any errors whilst updating the device
272
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
273
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
274
+ setTimeout(() => {
275
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.cacheTarget)
276
+ }, 2000)
277
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, new this.hapErr(-70402))
278
+ }
279
+ }
280
+
281
+ applyUpdate(data) {
282
+ // data will be in the format {"channel":1,"doorEnable":1,"open":0,"lmTime":1628623166}
283
+ // Don't bother whilst the ignore incoming is set to true
284
+ if (this.ignoreIncoming) {
285
+ return
286
+ }
287
+
288
+ // When operated externally, the plugin does not bother with 'opening' and 'closing' status
289
+ // Open means magnetic sensor not detected, doesn't really mean the door is open
290
+ if (hasProperty(data, 'open')) {
291
+ const isOpen = data.open === 1
292
+ switch (this.cacheCurrent) {
293
+ case 0:
294
+ case 2: {
295
+ // Homebridge has garage as open or opening
296
+ if (isOpen) {
297
+ // Meross has reported open
298
+ // Nothing to do
299
+ } else {
300
+ // Meross has reported closed
301
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 1)
302
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
303
+ this.accessory.eveService.addEntry({ status: 1 })
304
+ this.cacheCurrent = 1
305
+ this.cacheTarget = 1
306
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
307
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
308
+ }
309
+ break
310
+ }
311
+ case 1: {
312
+ // Homebridge has garage as closed
313
+ if (isOpen) {
314
+ // Meross has reported open
315
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
316
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
317
+ this.accessory.eveService.addEntry({ status: 0 })
318
+ this.service.updateCharacteristic(
319
+ this.eveChar.TimesOpened,
320
+ this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
321
+ )
322
+ const initialTime = this.accessory.eveService.getInitialTime()
323
+ this.service.updateCharacteristic(
324
+ this.eveChar.LastActivation,
325
+ Math.round(new Date().valueOf() / 1000) - initialTime,
326
+ )
327
+ this.cacheCurrent = 0
328
+ this.cacheTarget = 0
329
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
330
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
331
+ } else {
332
+ // Meross has reported closed
333
+ // Nothing to do
334
+ }
335
+ break
336
+ }
337
+ case 3: {
338
+ // Homebridge has garage as closing
339
+ if (isOpen) {
340
+ // Meross has reported open
341
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
342
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
343
+ this.accessory.eveService.addEntry({ status: 0 })
344
+ this.service.updateCharacteristic(
345
+ this.eveChar.TimesOpened,
346
+ this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
347
+ )
348
+ const initialTime = this.accessory.eveService.getInitialTime()
349
+ this.service.updateCharacteristic(
350
+ this.eveChar.LastActivation,
351
+ Math.round(new Date().valueOf() / 1000) - initialTime,
352
+ )
353
+ this.cacheCurrent = 0
354
+ this.cacheTarget = 0
355
+ this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
356
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
357
+ } else {
358
+ // Meross has reported closed
359
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
360
+ this.accessory.eveService.addEntry({ status: 1 })
361
+ this.cacheCurrent = 1
362
+ this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
363
+ }
364
+
365
+ // Cancel any 'extreme' polling intervals from setting the garage to close
366
+ if (this.extremePolling) {
367
+ clearInterval(this.extremePolling)
368
+ this.extremePolling = false
369
+ }
370
+ break
371
+ }
372
+ default:
373
+ }
374
+ }
375
+ }
376
+ }