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