@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,403 @@
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.name = accessory.displayName
20
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
21
+ ? platform.config.cloudRefreshRate
22
+ : platformConsts.defaultValues.cloudRefreshRate
23
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
24
+ ? platform.config.refreshRate
25
+ : platformConsts.defaultValues.refreshRate
26
+ this.pollInterval = accessory.context.connection === 'local'
27
+ ? localRefreshRate
28
+ : cloudRefreshRate
29
+ this.hk2mr = speed => speed / 25
30
+ this.hk2Label = (speed) => {
31
+ if (speed === 0) {
32
+ return 'off'
33
+ }
34
+ if (speed === 25) {
35
+ return 'sleep'
36
+ }
37
+ if (speed === 50) {
38
+ return 'low'
39
+ }
40
+ if (speed === 75) {
41
+ return 'medium'
42
+ }
43
+ return 'high'
44
+ }
45
+ this.mr2hk = speed => speed * 25
46
+
47
+ // Add the purifier service if it doesn't already exist
48
+ this.service = this.accessory.getService(this.hapServ.AirPurifier)
49
+ || this.accessory.addService(this.hapServ.AirPurifier)
50
+
51
+ // Add the set handler to the purifier on/off characteristic
52
+ this.service
53
+ .getCharacteristic(this.hapChar.Active)
54
+ .onSet(async value => this.internalStateUpdate(value))
55
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value
56
+
57
+ // Add options to the purifier target state characteristic
58
+ this.service
59
+ .getCharacteristic(this.hapChar.TargetAirPurifierState)
60
+ .setProps({
61
+ minValue: 1,
62
+ maxValue: 1,
63
+ validValues: [1],
64
+ })
65
+ .updateValue(1)
66
+
67
+ // Add the set handler to the purifier speed characteristic
68
+ this.service
69
+ .getCharacteristic(this.hapChar.RotationSpeed)
70
+ .setProps({
71
+ minStep: 25,
72
+ validValues: [0, 25, 50, 75, 100],
73
+ })
74
+ .onSet(async value => this.internalSpeedUpdate(value))
75
+ this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
76
+
77
+ // Add the set handler to the purifier child lock characteristic
78
+ this.service
79
+ .getCharacteristic(this.hapChar.LockPhysicalControls)
80
+ .onSet(async value => this.internalLockUpdate(value))
81
+ this.cacheLock = this.service.getCharacteristic(this.hapChar.LockPhysicalControls).value
82
+
83
+ // Create the queue used for sending device requests
84
+ this.updateInProgress = false
85
+ this.queue = new PQueue({
86
+ concurrency: 1,
87
+ interval: 250,
88
+ intervalCap: 1,
89
+ timeout: 10000,
90
+ throwOnTimeout: true,
91
+ })
92
+ this.queue.on('idle', () => {
93
+ this.updateInProgress = false
94
+ })
95
+
96
+ // Set up the mqtt client for cloud devices to send and receive device updates
97
+ if (accessory.context.connection !== 'local') {
98
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
99
+ this.accessory.mqtt.connect()
100
+ }
101
+
102
+ // Always request a device update on startup, then start the interval for polling
103
+ setTimeout(() => this.requestUpdate(true), 2000)
104
+ this.accessory.refreshInterval = setInterval(
105
+ () => this.requestUpdate(),
106
+ this.pollInterval * 1000,
107
+ )
108
+
109
+ // Output the customised options to the log
110
+ const opts = JSON.stringify({
111
+ connection: this.accessory.context.connection,
112
+ })
113
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
114
+ }
115
+
116
+ async internalStateUpdate(value) {
117
+ try {
118
+ // Add the request to the queue so updates are sent apart
119
+ await this.queue.add(async () => {
120
+ // Don't continue if the state is the same as before
121
+ if (value === this.cacheState) {
122
+ return
123
+ }
124
+
125
+ // This flag stops the plugin from requesting updates while pending on others
126
+ this.updateInProgress = true
127
+
128
+ // Generate the payload and namespace
129
+ const namespace = 'Appliance.Control.ToggleX'
130
+ const payload = {
131
+ togglex: {
132
+ onoff: value ? 1 : 0,
133
+ channel: 0,
134
+ },
135
+ }
136
+
137
+ // Use the platform function to send the update to the device
138
+ await this.platform.sendUpdate(this.accessory, {
139
+ namespace,
140
+ payload,
141
+ })
142
+
143
+ // Update the cache and log the update has been successful
144
+ this.cacheState = value
145
+ this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, value === 1 ? 2 : 0)
146
+ this.accessory.log(`${platformLang.curState} [${value === 1 ? 'purifying' : 'off'}]`)
147
+ })
148
+ } catch (err) {
149
+ // Catch any errors whilst updating the device
150
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
151
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
152
+ setTimeout(() => {
153
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState)
154
+ }, 2000)
155
+ throw new this.hapErr(-70402)
156
+ }
157
+ }
158
+
159
+ async internalSpeedUpdate(value) {
160
+ try {
161
+ // Add the request to the queue so updates are sent apart
162
+ await this.queue.add(async () => {
163
+ // Some homekit apps might not support the valid values of 0, 50 and 100
164
+ if (value === 0) {
165
+ return
166
+ }
167
+ if (value <= 33) {
168
+ value = 25
169
+ } else if (value <= 66) {
170
+ value = 50
171
+ } else if (value <= 99) {
172
+ value = 75
173
+ } else {
174
+ value = 100
175
+ }
176
+
177
+ // Don't continue if the state is the same as before
178
+ const mrVal = this.hk2mr(value)
179
+ if (mrVal === this.cacheSpeed) {
180
+ return
181
+ }
182
+
183
+ // This flag stops the plugin from requesting updates while pending on others
184
+ this.updateInProgress = true
185
+
186
+ // Generate the payload and namespace
187
+ const namespace = 'Appliance.Control.Fan'
188
+ const payload = {
189
+ fan: {
190
+ speed: mrVal,
191
+ channel: 0,
192
+ },
193
+ }
194
+
195
+ // Use the platform function to send the update to the device
196
+ await this.platform.sendUpdate(this.accessory, {
197
+ namespace,
198
+ payload,
199
+ })
200
+
201
+ // Update the cache and log the update has been successful
202
+ this.cacheSpeed = mrVal
203
+ this.accessory.log(`${platformLang.curSpeed} [${this.hk2Label(value)}]`)
204
+ })
205
+ } catch (err) {
206
+ // Catch any errors whilst updating the device
207
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
208
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
209
+ setTimeout(() => {
210
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.mr2hk(this.cacheSpeed))
211
+ }, 2000)
212
+ throw new this.hapErr(-70402)
213
+ }
214
+ }
215
+
216
+ async internalLockUpdate(value) {
217
+ try {
218
+ // Add the request to the queue so updates are sent apart
219
+ await this.queue.add(async () => {
220
+ // Don't continue if the state is the same as before
221
+ if (value === this.cacheLock) {
222
+ return
223
+ }
224
+
225
+ // This flag stops the plugin from requesting updates while pending on others
226
+ this.updateInProgress = true
227
+
228
+ // Generate the payload and namespace
229
+ const namespace = 'Appliance.Control.PhysicalLock'
230
+ const payload = {
231
+ lock: {
232
+ onoff: value ? 1 : 0,
233
+ channel: 0,
234
+ },
235
+ }
236
+
237
+ // Use the platform function to send the update to the device
238
+ await this.platform.sendUpdate(this.accessory, {
239
+ namespace,
240
+ payload,
241
+ })
242
+
243
+ // Update the cache and log the update has been successful
244
+ this.cacheLock = value
245
+ this.accessory.log(`${platformLang.curLock} [${value === 1 ? 'on' : 'off'}]`)
246
+ })
247
+ } catch (err) {
248
+ // Catch any errors whilst updating the device
249
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
250
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
251
+ setTimeout(() => {
252
+ this.service.updateCharacteristic(this.hapChar.LockPhysicalControls, this.cacheLock)
253
+ }, 2000)
254
+ throw new this.hapErr(-70402)
255
+ }
256
+ }
257
+
258
+ async requestUpdate(firstRun = false) {
259
+ try {
260
+ // Don't continue if an update is currently being sent to the device
261
+ if (this.updateInProgress) {
262
+ return
263
+ }
264
+
265
+ // Add the request to the queue so updates are sent apart
266
+ await this.queue.add(async () => {
267
+ // This flag stops the plugin from requesting updates while pending on others
268
+ this.updateInProgress = true
269
+
270
+ // Send the request
271
+ const res = await this.platform.sendUpdate(this.accessory, {
272
+ namespace: 'Appliance.System.All',
273
+ payload: {},
274
+ })
275
+
276
+ // Log the received data
277
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
278
+
279
+ // Check the response is in a useful format
280
+ const data = res.data.payload
281
+ if (data.all) {
282
+ if (data.all.digest) {
283
+ if (data.all.digest.togglex && data.all.digest.togglex[0]) {
284
+ this.applyUpdate(data.all.digest.togglex[0])
285
+ }
286
+ }
287
+
288
+ // A flag to check if we need to update the accessory context
289
+ let needsUpdate = false
290
+
291
+ // Get the mac address and hardware version of the device
292
+ if (data.all.system) {
293
+ // Mac address and hardware don't change regularly so only get on first poll
294
+ if (firstRun && data.all.system.hardware) {
295
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
296
+ this.accessory.context.hardware = data.all.system.hardware.version
297
+ }
298
+
299
+ // Get the ip address and firmware of the device
300
+ if (data.all.system.firmware) {
301
+ // Check for an IP change each and every time the device is polled
302
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
303
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
304
+ needsUpdate = true
305
+ }
306
+
307
+ // Firmware doesn't change regularly so only get on first poll
308
+ if (firstRun) {
309
+ this.accessory.context.firmware = data.all.system.firmware.version
310
+ }
311
+ }
312
+ }
313
+
314
+ // Get the cloud online status of the device
315
+ if (data.all.system.online) {
316
+ const isOnline = data.all.system.online.status === 1
317
+ if (this.accessory.context.isOnline !== isOnline) {
318
+ this.accessory.context.isOnline = isOnline
319
+ needsUpdate = true
320
+ }
321
+ }
322
+
323
+ // Update the accessory cache if anything has changed
324
+ if (needsUpdate || firstRun) {
325
+ this.platform.updateAccessory(this.accessory)
326
+ }
327
+ }
328
+ })
329
+ } catch (err) {
330
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
331
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
332
+
333
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
334
+ if (
335
+ (this.accessory.context.isOnline || firstRun)
336
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
337
+ ) {
338
+ this.accessory.context.isOnline = false
339
+ this.platform.updateAccessory(this.accessory)
340
+ }
341
+ }
342
+ }
343
+
344
+ receiveUpdate(params) {
345
+ try {
346
+ // Log the received data
347
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
348
+
349
+ // Check the response is in a useful format
350
+ const data = params.payload
351
+ if (data.togglex && data.togglex[0]) {
352
+ this.applyUpdate(data.togglex[0])
353
+ }
354
+ if (data.fan && data.fan[0]) {
355
+ this.applyUpdate(data.fan[0])
356
+ }
357
+ if (data.lock && data.lock[0]) {
358
+ this.applyUpdate({ lock: data.lock[0].onoff })
359
+ }
360
+ } catch (err) {
361
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
362
+ }
363
+ }
364
+
365
+ applyUpdate(data) {
366
+ // Check the data is in a format which contains the value we need
367
+ if (hasProperty(data, 'onoff')) {
368
+ // newState is given as 0 or 1 -> active characteristic also needs 0 or 1
369
+ const newState = data.onoff
370
+
371
+ // Check against the cache and update HomeKit and the cache if needed
372
+ if (this.cacheState !== newState) {
373
+ this.service.updateCharacteristic(this.hapChar.Active, newState)
374
+ this.service.updateCharacteristic(
375
+ this.hapChar.CurrentAirPurifierState,
376
+ newState === 1 ? 2 : 0,
377
+ )
378
+ this.cacheState = newState
379
+ this.accessory.log(`${platformLang.curState} [${newState ? 'purifying' : 'off'}]`)
380
+ }
381
+ }
382
+ if (hasProperty(data, 'speed')) {
383
+ const newSpeed = data.speed
384
+ if (this.cacheSpeed !== newSpeed) {
385
+ this.cacheSpeed = newSpeed
386
+ const hkValue = this.mr2hk(this.cacheSpeed)
387
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkValue)
388
+ this.accessory.log(`${platformLang.curSpeed} [${this.hk2Label(hkValue)}]`)
389
+ }
390
+ }
391
+ if (hasProperty(data, 'lock')) {
392
+ // newState is given as 0 or 1 -> active characteristic also needs 0 or 1
393
+ const newLock = data.lock
394
+
395
+ // Check against the cache and update HomeKit and the cache if needed
396
+ if (this.cacheLock !== newLock) {
397
+ this.service.updateCharacteristic(this.hapChar.LockPhysicalControls, newLock)
398
+ this.cacheLock = newLock
399
+ this.accessory.log(`${platformLang.curLock} [${newLock ? 'on' : 'off'}]`)
400
+ }
401
+ }
402
+ }
403
+ }