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