@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,493 @@
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.cusChar = platform.cusChar
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
+
31
+ this.mode2Label = {
32
+ 0: 'manual',
33
+ 1: 'heat',
34
+ 2: 'cool',
35
+ 3: 'auto',
36
+ 4: 'economy',
37
+ }
38
+ this.mode2Char = {
39
+ 0: false,
40
+ 1: this.cusChar.ValveHeatMode,
41
+ 2: this.cusChar.ValveCoolMode,
42
+ 3: this.cusChar.ValveAutoMode,
43
+ 4: this.cusChar.ValveEconomyMode,
44
+ }
45
+
46
+ // Add the thermostat service if it doesn't already exist
47
+ this.service = this.accessory.getService(this.hapServ.Thermostat)
48
+ || this.accessory.addService(this.hapServ.Thermostat)
49
+
50
+ this.service
51
+ .getCharacteristic(this.hapChar.TargetHeatingCoolingState)
52
+ .setProps({
53
+ minValue: 0,
54
+ maxValue: 1,
55
+ validValues: [0, 1],
56
+ })
57
+ .onSet(async value => this.internalStateUpdate(value))
58
+ this.cacheState = this.service.getCharacteristic(this.hapChar.TargetHeatingCoolingState).value
59
+
60
+ this.service
61
+ .getCharacteristic(this.hapChar.TargetTemperature)
62
+ .setProps({
63
+ minValue: 5,
64
+ maxValue: 35,
65
+ minStep: 0.5,
66
+ })
67
+ .onSet(async value => this.internalTargetUpdate(value))
68
+ this.cacheTarg = this.service.getCharacteristic(this.hapChar.TargetTemperature).value
69
+
70
+ this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
71
+ this.updateCache()
72
+
73
+ if (!this.service.testCharacteristic(this.cusChar.ValveHeatMode)) {
74
+ this.service.addCharacteristic(this.cusChar.ValveHeatMode)
75
+ }
76
+ this.service
77
+ .getCharacteristic(this.cusChar.ValveHeatMode)
78
+ .onSet(async value => this.internalModeUpdate(value, 1))
79
+ if (!this.service.testCharacteristic(this.cusChar.ValveCoolMode)) {
80
+ this.service.addCharacteristic(this.cusChar.ValveCoolMode)
81
+ }
82
+ this.service
83
+ .getCharacteristic(this.cusChar.ValveCoolMode)
84
+ .onSet(async value => this.internalModeUpdate(value, 2))
85
+ if (!this.service.testCharacteristic(this.cusChar.ValveAutoMode)) {
86
+ this.service.addCharacteristic(this.cusChar.ValveAutoMode)
87
+ }
88
+ this.service
89
+ .getCharacteristic(this.cusChar.ValveAutoMode)
90
+ .onSet(async value => this.internalModeUpdate(value, 3))
91
+ if (!this.service.testCharacteristic(this.cusChar.ValveEconomyMode)) {
92
+ this.service.addCharacteristic(this.cusChar.ValveEconomyMode)
93
+ }
94
+ this.cacheMode = 0
95
+ this.service
96
+ .getCharacteristic(this.cusChar.ValveEconomyMode)
97
+ .onSet(async value => this.internalModeUpdate(value, 4))
98
+ if (!this.service.testCharacteristic(this.cusChar.ValveWindowOpen)) {
99
+ this.service.addCharacteristic(this.cusChar.ValveWindowOpen)
100
+ }
101
+ this.cacheWindow = this.service.getCharacteristic(this.cusChar.ValveWindowOpen).value
102
+
103
+ // Pass the accessory to Fakegato to set up with Eve
104
+ this.accessory.eveService = new platform.eveService('custom', this.accessory, { log: () => {} })
105
+
106
+ // Create the queue used for sending device requests
107
+ this.updateInProgress = false
108
+ this.queue = new PQueue({
109
+ concurrency: 1,
110
+ interval: 250,
111
+ intervalCap: 1,
112
+ timeout: 10000,
113
+ throwOnTimeout: true,
114
+ })
115
+ this.queue.on('idle', () => {
116
+ this.updateInProgress = false
117
+ })
118
+
119
+ // Set up the mqtt client for cloud devices to send and receive device updates
120
+ if (accessory.context.connection !== 'local') {
121
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
122
+ this.accessory.mqtt.connect()
123
+ }
124
+
125
+ // Always request a device update on startup, then start the interval for polling
126
+ setTimeout(() => this.requestUpdate(true), 2000)
127
+ this.accessory.refreshInterval = setInterval(
128
+ () => this.requestUpdate(),
129
+ this.pollInterval * 1000,
130
+ )
131
+
132
+ // Output the customised options to the log
133
+ const opts = JSON.stringify({
134
+ connection: this.accessory.context.connection,
135
+ })
136
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
137
+ }
138
+
139
+ async internalStateUpdate(value) {
140
+ try {
141
+ // Add the request to the queue so updates are sent apart
142
+ await this.queue.add(async () => {
143
+ // Don't continue if the state is the same as before
144
+ if (value === this.cacheState) {
145
+ return
146
+ }
147
+
148
+ // This flag stops the plugin from requesting updates while pending on others
149
+ this.updateInProgress = true
150
+
151
+ // Generate the payload and namespace
152
+ const namespace = 'Appliance.Control.Thermostat.Mode'
153
+ const payload = {
154
+ mode: [
155
+ {
156
+ channel: 0,
157
+ onoff: value ? 1 : 0,
158
+ },
159
+ ],
160
+ }
161
+
162
+ // Use the platform function to send the update to the device
163
+ await this.platform.sendUpdate(this.accessory, {
164
+ namespace,
165
+ payload,
166
+ })
167
+
168
+ // Update the cache and log the update has been successful
169
+ this.cacheState = value
170
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
171
+ })
172
+ } catch (err) {
173
+ // Catch any errors whilst updating the device
174
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
175
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
176
+ setTimeout(() => {
177
+ this.service.updateCharacteristic(this.hapChar.TargetHeatingCoolingState, this.cacheState)
178
+ }, 2000)
179
+ throw new this.hapErr(-70402)
180
+ }
181
+ }
182
+
183
+ async internalModeUpdate(value, newMode) {
184
+ try {
185
+ // If turning off then set to manual mode
186
+ if (!value) {
187
+ newMode = 0
188
+ }
189
+
190
+ // Add the request to the queue so updates are sent apart
191
+ await this.queue.add(async () => {
192
+ // Don't continue if the state is the same as before
193
+ if (newMode === this.cacheMode) {
194
+ return
195
+ }
196
+
197
+ // This flag stops the plugin from requesting updates while pending on others
198
+ this.updateInProgress = true
199
+
200
+ // Generate the payload and namespace
201
+ const namespace = 'Appliance.Control.Thermostat.Mode'
202
+ const payload = {
203
+ mode: [
204
+ {
205
+ state: newMode,
206
+ },
207
+ ],
208
+ }
209
+
210
+ // Use the platform function to send the update to the device
211
+ await this.platform.sendUpdate(this.accessory, {
212
+ namespace,
213
+ payload,
214
+ })
215
+
216
+ // Update the cache and log the update has been successful
217
+ this.cacheState = value
218
+ this.accessory.log(`${platformLang.curMode} [${this.mode2Label[newMode]}]`)
219
+
220
+ // Turn the other modes off
221
+ Object.entries(this.mode2Char).forEach((entry) => {
222
+ const [mode, char] = entry
223
+ if (char && mode !== newMode.toString()) {
224
+ this.service.updateCharacteristic(char, false)
225
+ }
226
+ })
227
+ })
228
+ } catch (err) {
229
+ // Catch any errors whilst updating the device
230
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
231
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
232
+ setTimeout(() => {
233
+ this.service.updateCharacteristic(this.mode2Char[newMode], false)
234
+ }, 2000)
235
+ throw new this.hapErr(-70402)
236
+ }
237
+ }
238
+
239
+ async internalTargetUpdate(value) {
240
+ try {
241
+ // Add the request to the queue so updates are sent apart
242
+ await this.queue.add(async () => {
243
+ // Don't continue if the state is the same as before
244
+ if (value === this.cacheTarg) {
245
+ return
246
+ }
247
+
248
+ // This flag stops the plugin from requesting updates while pending on others
249
+ this.updateInProgress = true
250
+
251
+ // Generate the payload and namespace
252
+ const namespace = 'Appliance.Control.Thermostat.Mode'
253
+ const payload = {
254
+ mode: [
255
+ {
256
+ channel: 0,
257
+ mode: 4,
258
+ manualTemp: value * 10,
259
+ },
260
+ ],
261
+ }
262
+
263
+ // Use the platform function to send the update to the device
264
+ await this.platform.sendUpdate(this.accessory, {
265
+ namespace,
266
+ payload,
267
+ })
268
+
269
+ // Update the cache and log the update has been successful
270
+ this.cacheTarg = value
271
+ this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
272
+
273
+ // Update the current heating state
274
+ this.service.updateCharacteristic(
275
+ this.hapChar.CurrentHeatingCoolingState,
276
+ value > this.cacheTemp ? 1 : 0,
277
+ )
278
+
279
+ // Turn the modes off as back to manual mode
280
+ Object.values(this.mode2Char).forEach((char) => {
281
+ if (char) {
282
+ this.service.updateCharacteristic(char, false)
283
+ }
284
+ })
285
+ })
286
+ } catch (err) {
287
+ // Catch any errors whilst updating the device
288
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
289
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
290
+ setTimeout(() => {
291
+ this.service.updateCharacteristic(this.hapChar.TargetTemperature, this.cacheTarg)
292
+ }, 2000)
293
+ throw new this.hapErr(-70402)
294
+ }
295
+ }
296
+
297
+ async updateCache() {
298
+ // Don't continue if the storage client hasn't initialised properly
299
+ if (!this.platform.storageClientData) {
300
+ return
301
+ }
302
+
303
+ // Attempt to save the new temperature to the cache
304
+ try {
305
+ await this.platform.storageData.setItem(
306
+ `${this.accessory.context.serialNumber}_temp`,
307
+ this.cacheTemp,
308
+ )
309
+ } catch (err) {
310
+ this.accessory.logWarn(`${platformLang.storageWriteErr} ${parseError(err)}`)
311
+ }
312
+ }
313
+
314
+ async requestUpdate(firstRun = false) {
315
+ try {
316
+ // Don't continue if an update is currently being sent to the device
317
+ if (this.updateInProgress) {
318
+ return
319
+ }
320
+
321
+ // Add the request to the queue so updates are sent apart
322
+ await this.queue.add(async () => {
323
+ // This flag stops the plugin from requesting updates while pending on others
324
+ this.updateInProgress = true
325
+
326
+ // Send the request
327
+ const res = await this.platform.sendUpdate(this.accessory, {
328
+ namespace: 'Appliance.System.All',
329
+ payload: {},
330
+ })
331
+
332
+ // Log the received data
333
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
334
+
335
+ // Check the response is in a useful format
336
+ const data = res.data.payload
337
+ if (data.all) {
338
+ if (data.all.digest?.thermostat) {
339
+ this.applyUpdate(data.all.digest.thermostat)
340
+ }
341
+
342
+ // A flag to check if we need to update the accessory context
343
+ let needsUpdate = false
344
+
345
+ // Get the mac address and hardware version of the device
346
+ if (data.all.system) {
347
+ // Mac address and hardware don't change regularly so only get on first poll
348
+ if (firstRun && data.all.system.hardware) {
349
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
350
+ this.accessory.context.hardware = data.all.system.hardware.version
351
+ }
352
+
353
+ // Get the ip address and firmware of the device
354
+ if (data.all.system.firmware) {
355
+ // Check for an IP change each and every time the device is polled
356
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
357
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
358
+ needsUpdate = true
359
+ }
360
+
361
+ // Firmware doesn't change regularly so only get on first poll
362
+ if (firstRun) {
363
+ this.accessory.context.firmware = data.all.system.firmware.version
364
+ }
365
+ }
366
+ }
367
+
368
+ // Get the cloud online status of the device
369
+ if (data.all.system.online) {
370
+ const isOnline = data.all.system.online.status === 1
371
+ if (this.accessory.context.isOnline !== isOnline) {
372
+ this.accessory.context.isOnline = isOnline
373
+ needsUpdate = true
374
+ }
375
+ }
376
+
377
+ // Update the accessory cache if anything has changed
378
+ if (needsUpdate || firstRun) {
379
+ this.platform.updateAccessory(this.accessory)
380
+ }
381
+ }
382
+ })
383
+ } catch (err) {
384
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
385
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
386
+
387
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
388
+ if (
389
+ (this.accessory.context.isOnline || firstRun)
390
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
391
+ ) {
392
+ this.accessory.context.isOnline = false
393
+ this.platform.updateAccessory(this.accessory)
394
+ }
395
+ }
396
+ }
397
+
398
+ receiveUpdate(params) {
399
+ try {
400
+ // Log the received data
401
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
402
+ if (params.payload) {
403
+ this.applyUpdate(params.payload)
404
+ }
405
+ } catch (err) {
406
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
407
+ }
408
+ }
409
+
410
+ applyUpdate(data) {
411
+ try {
412
+ const modeData = data.mode?.[0]
413
+ if (modeData) {
414
+ let needsUpdate = false
415
+ if (hasProperty(modeData, 'state')) {
416
+ const newState = modeData.state
417
+
418
+ // Check against the cache and update HomeKit and the cache if needed
419
+ if (this.cacheState !== newState) {
420
+ this.service.updateCharacteristic(this.hapChar.TargetHeatingCoolingState, newState)
421
+ this.cacheState = newState
422
+ this.accessory.log(`${platformLang.curState} [${newState === 1 ? 'on' : 'off'}]`)
423
+ needsUpdate = true
424
+ }
425
+ }
426
+ if (hasProperty(modeData, 'targetTemp')) {
427
+ const newTarg = modeData.targetTemp / 10
428
+
429
+ // Check against the cache and update HomeKit and the cache if needed
430
+ if (this.cacheTarg !== newTarg) {
431
+ this.service.updateCharacteristic(this.hapChar.TargetTemperature, newTarg)
432
+ this.cacheTarg = newTarg
433
+ this.accessory.log(`${platformLang.curTarg} [${newTarg}°C]`)
434
+ needsUpdate = true
435
+ }
436
+ }
437
+ if (hasProperty(modeData, 'currentTemp')) {
438
+ const newTemp = modeData.currentTemp / 10
439
+
440
+ // Check against the cache and update HomeKit and the cache if needed
441
+ if (this.cacheTemp !== newTemp) {
442
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, newTemp)
443
+ this.cacheTemp = newTemp
444
+ this.accessory.eveService.addEntry({ temp: newTemp })
445
+ this.accessory.log(`${platformLang.curTemp} [${newTemp}°C]`)
446
+ needsUpdate = true
447
+
448
+ // Update the cache file with the new temperature
449
+ this.updateCache()
450
+ }
451
+ }
452
+
453
+ // Update the current heating state
454
+ if (needsUpdate) {
455
+ this.service.updateCharacteristic(
456
+ this.hapChar.CurrentHeatingCoolingState,
457
+ this.cacheState === 1 && this.cacheTarg > this.cacheTemp ? 1 : 0,
458
+ )
459
+ }
460
+
461
+ // Todo - data.openWindow and data.mode
462
+ // if (hasProperty(data, 'openWindow')) {
463
+ // const newWindow = data.openWindow === 1;
464
+ //
465
+ // // Check against the cache and update HomeKit and the cache if needed
466
+ // if (this.cacheWindow !== newWindow) {
467
+ // this.service.updateCharacteristic(this.cusChar.ValveWindowOpen, newWindow);
468
+ // this.cacheWindow = newWindow;
469
+ // this.accessory.log(`${platformLang.curWindow} [${newWindow ? 'open' : 'closed'}]`);
470
+ // }
471
+ // }
472
+ //
473
+ // if (hasProperty(data, 'mode')) {
474
+ // const newMode = data.mode;
475
+ //
476
+ // // Check against the cache and update HomeKit and the cache if needed
477
+ // if (this.cacheMode !== newMode) {
478
+ // Object.entries(this.mode2Char).forEach((entry) => {
479
+ // const [mode, char] = entry;
480
+ // if (char) {
481
+ // this.service.updateCharacteristic(char, mode === newMode.toString());
482
+ // }
483
+ // });
484
+ // this.cacheMode = newMode;
485
+ // this.accessory.log(`${platformLang.curMode} [${this.mode2Label[newMode]}]`);
486
+ // }
487
+ // }
488
+ }
489
+ } catch (err) {
490
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
491
+ }
492
+ }
493
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 simont77
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.