@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,447 @@
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 { 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.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.inUsePowerThreshold = this.accessory.context.options.inUsePowerThreshold
20
+ || platformConsts.defaultValues.inUsePowerThreshold
21
+ this.name = accessory.displayName
22
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
23
+ ? platform.config.cloudRefreshRate
24
+ : platformConsts.defaultValues.cloudRefreshRate
25
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
26
+ ? platform.config.refreshRate
27
+ : platformConsts.defaultValues.refreshRate
28
+ this.pollInterval = accessory.context.connection === 'local'
29
+ ? localRefreshRate
30
+ : cloudRefreshRate
31
+ this.temperatureSource = accessory.context.options.temperatureSource;
32
+
33
+ // If the accessory has any old services then remove them
34
+ ['Switch', 'Outlet', 'AirPurifier'].forEach((service) => {
35
+ if (this.accessory.getService(this.hapServ[service])) {
36
+ this.accessory.removeService(this.accessory.getService(this.hapServ[service]))
37
+ }
38
+ })
39
+
40
+ // Set up the accessory with default target temp when added the first time
41
+ if (!hasProperty(this.accessory.context, 'cacheTarget')) {
42
+ this.accessory.context.cacheTarget = 20
43
+ }
44
+
45
+ // Check to make sure user has not switched from cooler to heater
46
+ if (this.accessory.context.cacheType !== 'cooler') {
47
+ // Remove and re-setup as a HeaterCooler
48
+ if (this.accessory.getService(this.hapServ.HeaterCooler)) {
49
+ this.accessory.removeService(this.accessory.getService(this.hapServ.HeaterCooler))
50
+ }
51
+ this.accessory.context.cacheType = 'cooler'
52
+ this.accessory.context.cacheTarget = 20
53
+ }
54
+
55
+ // Add the heater service if it doesn't already exist
56
+ this.service = this.accessory.getService(this.hapServ.HeaterCooler)
57
+ || this.accessory.addService(this.hapServ.HeaterCooler)
58
+
59
+ // Set custom properties of the current temperature characteristic
60
+ this.service.getCharacteristic(this.hapChar.CurrentTemperature).setProps({
61
+ minStep: 0.1,
62
+ })
63
+ this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
64
+
65
+ // Add the set handler to the heater active characteristic
66
+ this.service
67
+ .getCharacteristic(this.hapChar.Active)
68
+ .onSet(async value => this.internalStateUpdate(value))
69
+
70
+ // Add options to the target state characteristic
71
+ this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({
72
+ minValue: 0,
73
+ maxValue: 0,
74
+ validValues: [0],
75
+ })
76
+
77
+ // Add the set handler to the target temperature characteristic
78
+ this.service
79
+ .getCharacteristic(this.hapChar.HeatingThresholdTemperature)
80
+ .updateValue(this.accessory.context.cacheTarget)
81
+ .setProps({ minStep: 0.5 })
82
+ .onSet(async value => this.internalTargetTempUpdate(value))
83
+
84
+ // Initialise these caches now since they aren't determined by the initial externalUpdate()
85
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1
86
+ this.cacheCool = this.cacheState
87
+ && this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).value === 3
88
+
89
+ // Pass the accessory to Fakegato to set up with Eve
90
+ this.accessory.eveService = new platform.eveService('custom', this.accessory, { log: () => {} })
91
+
92
+ // Create the queue used for sending device requests
93
+ this.updateInProgress = false
94
+ this.queue = new PQueue({
95
+ concurrency: 1,
96
+ interval: 250,
97
+ intervalCap: 1,
98
+ timeout: 10000,
99
+ throwOnTimeout: true,
100
+ })
101
+ this.queue.on('idle', () => {
102
+ this.updateInProgress = false
103
+ })
104
+
105
+ // Set up the mqtt client for cloud devices to send and receive device updates
106
+ if (accessory.context.connection !== 'local') {
107
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
108
+ this.accessory.mqtt.connect()
109
+ }
110
+
111
+ // Always request a device update on startup, then start the interval for polling
112
+ setTimeout(() => this.requestUpdate(true), 2000)
113
+ this.accessory.refreshInterval = setInterval(
114
+ () => this.requestUpdate(),
115
+ this.pollInterval * 1000,
116
+ )
117
+
118
+ // Set up an interval to get regular temperature updates
119
+ setTimeout(() => {
120
+ this.getTemperature()
121
+ this.accessory.powerInterval = setInterval(
122
+ () => this.getTemperature(),
123
+ 120000,
124
+ )
125
+ }, 5000)
126
+
127
+ // Output the customised options to the log
128
+ const opts = JSON.stringify({
129
+ connection: this.accessory.context.connection,
130
+ showAs: 'cooler',
131
+ temperatureSource: this.temperatureSource,
132
+ })
133
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
134
+ }
135
+
136
+ async internalStateUpdate(value) {
137
+ try {
138
+ // Add the request to the queue so updates are sent apart
139
+ await this.queue.add(async () => {
140
+ let newState
141
+ let newCool
142
+ let newValue
143
+ if (value !== 0) {
144
+ newState = true
145
+ if (this.cacheTemp > this.accessory.context.cacheTarget) {
146
+ newValue = true
147
+ newCool = true
148
+ }
149
+ }
150
+
151
+ // Only send the update if either:
152
+ // * The new value (state) is OFF and the cacheCool was ON
153
+ // * The new value (state) is ON and newCool is 'on'
154
+ if ((value === 0 && this.cacheCool) || (value === 1 && newCool)) {
155
+ // This flag stops the plugin from requesting updates while pending on others
156
+ this.updateInProgress = true
157
+
158
+ // The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
159
+ let namespace
160
+ let payload
161
+ if (this.isToggleX) {
162
+ namespace = 'Appliance.Control.ToggleX'
163
+ payload = {
164
+ togglex: {
165
+ onoff: newValue ? 1 : 0,
166
+ channel: 0,
167
+ },
168
+ }
169
+ } else {
170
+ namespace = 'Appliance.Control.Toggle'
171
+ payload = {
172
+ toggle: {
173
+ onoff: newValue ? 1 : 0,
174
+ },
175
+ }
176
+ }
177
+
178
+ // Use the platform function to send the update to the device
179
+ await this.platform.sendUpdate(this.accessory, {
180
+ namespace,
181
+ payload,
182
+ })
183
+ }
184
+ if (newState !== this.cacheState) {
185
+ this.cacheState = newState
186
+ this.accessory.log(`${platformLang.curState} [${this.cacheState ? 'on' : 'off'}]`)
187
+ }
188
+ if (newCool !== this.cacheCool) {
189
+ this.cacheCool = newCool
190
+ this.accessory.log(`${platformLang.curCool} [${this.cacheCool ? 'on' : 'off'}]`)
191
+ }
192
+ const newOnState = this.cacheCool ? 3 : 1
193
+ this.service.updateCharacteristic(
194
+ this.hapChar.CurrentHeaterCoolerState,
195
+ value === 1 ? newOnState : 0,
196
+ )
197
+ })
198
+ } catch (err) {
199
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
200
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
201
+ setTimeout(() => {
202
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState ? 1 : 0)
203
+ }, 2000)
204
+ throw new this.hapErr(-70402)
205
+ }
206
+ }
207
+
208
+ async internalTargetTempUpdate(value) {
209
+ try {
210
+ // Add the request to the queue so updates are sent apart
211
+ await this.queue.add(async () => {
212
+ if (value === this.accessory.context.cacheTarget) {
213
+ return
214
+ }
215
+ this.accessory.context.cacheTarget = value
216
+ this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
217
+ if (!this.cacheState) {
218
+ return
219
+ }
220
+ let newCool
221
+ let newValue
222
+ if (this.cacheTemp > value) {
223
+ newValue = true
224
+ newCool = true
225
+ }
226
+ if (newCool === this.cacheCool) {
227
+ return
228
+ }
229
+ // This flag stops the plugin from requesting updates while pending on others
230
+ this.updateInProgress = true
231
+
232
+ // The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
233
+ let namespace
234
+ let payload
235
+ if (this.isToggleX) {
236
+ namespace = 'Appliance.Control.ToggleX'
237
+ payload = {
238
+ togglex: {
239
+ onoff: newValue ? 1 : 0,
240
+ channel: 0,
241
+ },
242
+ }
243
+ } else {
244
+ namespace = 'Appliance.Control.Toggle'
245
+ payload = {
246
+ toggle: {
247
+ onoff: newValue ? 1 : 0,
248
+ },
249
+ }
250
+ }
251
+
252
+ // Use the platform function to send the update to the device
253
+ await this.platform.sendUpdate(this.accessory, {
254
+ namespace,
255
+ payload,
256
+ })
257
+
258
+ // Cache and log
259
+ this.cacheCool = newCool
260
+
261
+ this.accessory.log(`${platformLang.curCool} [${this.cacheCool ? 'on' : 'off'}]`)
262
+
263
+ this.service.updateCharacteristic(
264
+ this.hapChar.CurrentHeaterCoolerState,
265
+ this.cacheCool ? 3 : 1,
266
+ )
267
+ })
268
+ } catch (err) {
269
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
270
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
271
+ setTimeout(() => {
272
+ this.service.updateCharacteristic(
273
+ this.hapChar.HeatingThresholdTemperature,
274
+ this.accessory.context.cacheTarget,
275
+ )
276
+ }, 2000)
277
+ throw new this.hapErr(-70402)
278
+ }
279
+ }
280
+
281
+ async internalCurrentTempUpdate() {
282
+ try {
283
+ // Add the request to the queue so updates are sent apart
284
+ await this.queue.add(async () => {
285
+ if (!this.cacheState) {
286
+ return
287
+ }
288
+ let newCool
289
+ let newValue
290
+ if (this.cacheTemp > this.accessory.context.cacheTarget) {
291
+ newValue = true
292
+ newCool = true
293
+ }
294
+ if (newCool === this.cacheCool) {
295
+ return
296
+ }
297
+ // This flag stops the plugin from requesting updates while pending on others
298
+ this.updateInProgress = true
299
+
300
+ // The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
301
+ let namespace
302
+ let payload
303
+ if (this.isToggleX) {
304
+ namespace = 'Appliance.Control.ToggleX'
305
+ payload = {
306
+ togglex: {
307
+ onoff: newValue ? 1 : 0,
308
+ channel: 0,
309
+ },
310
+ }
311
+ } else {
312
+ namespace = 'Appliance.Control.Toggle'
313
+ payload = {
314
+ toggle: {
315
+ onoff: newValue ? 1 : 0,
316
+ },
317
+ }
318
+ }
319
+
320
+ // Use the platform function to send the update to the device
321
+ await this.platform.sendUpdate(this.accessory, {
322
+ namespace,
323
+ payload,
324
+ })
325
+
326
+ // Cache and log
327
+ this.cacheCool = newCool
328
+
329
+ this.accessory.log(`${platformLang.curCool} [${this.cacheCool ? 'on' : 'off'}]`)
330
+ this.service.updateCharacteristic(
331
+ this.hapChar.CurrentHeaterCoolerState,
332
+ this.cacheCool ? 2 : 1,
333
+ )
334
+ })
335
+ } catch (err) {
336
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
337
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
338
+ }
339
+ }
340
+
341
+ async getTemperature() {
342
+ try {
343
+ // Skip polling if the storage hasn't initialised properly
344
+ if (!this.platform.storageClientData) {
345
+ return
346
+ }
347
+
348
+ const newTemp = await this.platform.storageData.getItem(`${this.temperatureSource}_temp`)
349
+ if (newTemp && newTemp !== this.cacheTemp) {
350
+ this.cacheTemp = newTemp
351
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
352
+ this.accessory.eveService.addEntry({ temp: this.cacheTemp })
353
+
354
+ this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
355
+ await this.internalCurrentTempUpdate()
356
+ }
357
+ } catch (err) {
358
+ this.accessory.logWarn(parseError(err))
359
+ }
360
+ }
361
+
362
+ async requestUpdate(firstRun = false) {
363
+ try {
364
+ // Don't continue if an update is currently being sent to the device
365
+ if (this.updateInProgress) {
366
+ return
367
+ }
368
+
369
+ // Add the request to the queue so updates are sent apart
370
+ await this.queue.add(async () => {
371
+ // This flag stops the plugin from requesting updates while pending on others
372
+ this.updateInProgress = true
373
+
374
+ // Send the request
375
+ const res = await this.platform.sendUpdate(this.accessory, {
376
+ namespace: 'Appliance.System.All',
377
+ payload: {},
378
+ })
379
+
380
+ // Log the received data
381
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
382
+
383
+ // Check the response is in a useful format
384
+ const data = res.data.payload
385
+ if (data.all) {
386
+ if (firstRun && data.all.digest) {
387
+ if (data.all.digest.togglex && data.all.digest.togglex[0]) {
388
+ this.isToggleX = true
389
+ }
390
+ }
391
+
392
+ // A flag to check if we need to update the accessory context
393
+ let needsUpdate = false
394
+
395
+ // Get the mac address and hardware version of the device
396
+ if (data.all.system) {
397
+ // Mac address and hardware don't change regularly so only get on first poll
398
+ if (firstRun && data.all.system.hardware) {
399
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
400
+ this.accessory.context.hardware = data.all.system.hardware.version
401
+ }
402
+
403
+ // Get the ip address and firmware of the device
404
+ if (data.all.system.firmware) {
405
+ // Check for an IP change each and every time the device is polled
406
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
407
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
408
+ needsUpdate = true
409
+ }
410
+
411
+ // Firmware doesn't change regularly so only get on first poll
412
+ if (firstRun) {
413
+ this.accessory.context.firmware = data.all.system.firmware.version
414
+ }
415
+ }
416
+ }
417
+
418
+ // Get the cloud online status of the device
419
+ if (data.all.system.online) {
420
+ const isOnline = data.all.system.online.status === 1
421
+ if (this.accessory.context.isOnline !== isOnline) {
422
+ this.accessory.context.isOnline = isOnline
423
+ needsUpdate = true
424
+ }
425
+ }
426
+
427
+ // Update the accessory cache if anything has changed
428
+ if (needsUpdate || firstRun) {
429
+ this.platform.updateAccessory(this.accessory)
430
+ }
431
+ }
432
+ })
433
+ } catch (err) {
434
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
435
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
436
+
437
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
438
+ if (
439
+ (this.accessory.context.isOnline || firstRun)
440
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
441
+ ) {
442
+ this.accessory.context.isOnline = false
443
+ this.platform.updateAccessory(this.accessory)
444
+ }
445
+ }
446
+ }
447
+ }