@homebridge-plugins/homebridge-meross 10.19.0 → 10.19.1-beta.1
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.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/config.schema.json +1 -0
- package/lib/device/garage-single.js +22 -8
- package/lib/device/garage-sub.js +22 -8
- package/lib/device/hub-main.js +7 -0
- package/lib/device/hub-smoke.js +25 -2
- package/lib/device/light-cct.js +22 -4
- package/lib/device/light-rgb.js +43 -9
- package/lib/homebridge-ui/public/index.html +1 -1
- package/lib/platform.js +54 -38
- package/lib/utils/constants.js +8 -0
- package/lib/utils/lang-en.js +2 -1
- package/package.json +1 -1
- package/lib/utils/functions.test.js +0 -78
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-meross` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v10.19.1 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- docs: add node 26 to the supported node versions
|
|
10
|
+
- chore: exclude test files and the test config from the published package
|
|
11
|
+
- fix: restore debug logging when the plugin runs in a child bridge
|
|
12
|
+
- fix: only turn adaptive lighting off when a colour is genuinely chosen elsewhere
|
|
13
|
+
- fix: send a colour change when only the saturation moves, and mend the al cleanup
|
|
14
|
+
- fix: keep the cloud accessories when the device list could not be fetched
|
|
15
|
+
- fix: stop the hub's follow-up request growing on every poll until it fails
|
|
16
|
+
- fix: stop the polling timers on shutdown for a local-only setup, not just a cloud one
|
|
17
|
+
- fix: stop the garage's quick polling when the door is operated again mid-close
|
|
18
|
+
- fix: apply the smoke sensor's battery reading, instead of failing on every update
|
|
19
|
+
- fix: reject a garage opening time so large it would make the reset timer fire immediately
|
|
20
|
+
- test: record what every device handler builds, so a refactor can be shown to change nothing
|
|
21
|
+
|
|
5
22
|
## v10.19.0 (2026-08-01)
|
|
6
23
|
|
|
7
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Homebridge plugin to integrate Meross devices into HomeKit
|
|
|
28
28
|
### Prerequisites
|
|
29
29
|
|
|
30
30
|
- To use this plugin, you will need to already have:
|
|
31
|
-
- [Node](https://nodejs.org): latest version of `v22` or `
|
|
31
|
+
- [Node](https://nodejs.org): latest version of `v22`, `v24` or `v26` - any other major version is not supported.
|
|
32
32
|
- [Homebridge](https://homebridge.io): `v1.6` or above - refer to link for more information and installation instructions.
|
|
33
33
|
|
|
34
34
|
### Setup
|
package/config.schema.json
CHANGED
|
@@ -1486,6 +1486,7 @@
|
|
|
1486
1486
|
"type": "integer",
|
|
1487
1487
|
"placeholder": "20",
|
|
1488
1488
|
"minimum": 1,
|
|
1489
|
+
"maximum": 2073600,
|
|
1489
1490
|
"condition": {
|
|
1490
1491
|
"functionBody": "return (model.garageDevices && model.garageDevices[arrayIndices] && model.garageDevices[arrayIndices].serialNumber && model.garageDevices[arrayIndices].serialNumber.length === 32 && !model.garageDevices[arrayIndices].ignoreDevice);"
|
|
1491
1492
|
}
|
|
@@ -105,6 +105,18 @@ export default class {
|
|
|
105
105
|
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Cancel the quick polling started when the door was told to close. Kept in one
|
|
110
|
+
* place so every path that ends a close - a new operation, the door settling,
|
|
111
|
+
* or Homebridge shutting down - stops it rather than leaving it running.
|
|
112
|
+
*/
|
|
113
|
+
stopExtremePolling() {
|
|
114
|
+
if (this.extremePolling) {
|
|
115
|
+
clearInterval(this.extremePolling)
|
|
116
|
+
this.extremePolling = false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
async internalTargetUpdate(value) {
|
|
109
121
|
// Add the request to the queue so updates are sent apart
|
|
110
122
|
try {
|
|
@@ -226,6 +238,13 @@ export default class {
|
|
|
226
238
|
const updateKey = generateRandomString(5)
|
|
227
239
|
this.updateKey = updateKey
|
|
228
240
|
|
|
241
|
+
// A new operation supersedes any close still in progress, so its quick
|
|
242
|
+
// polling has to stop here. Without this it was orphaned: the close
|
|
243
|
+
// timeout returned early on the changed key and never reached its own
|
|
244
|
+
// clearInterval, so the device was polled every 3 seconds for ever, and
|
|
245
|
+
// each open-during-close cycle leaked another one.
|
|
246
|
+
this.stopExtremePolling()
|
|
247
|
+
|
|
229
248
|
// Update the Eve times opened characteristic
|
|
230
249
|
this.accessory.eveService.addEntry({ status: 0 })
|
|
231
250
|
this.service.updateCharacteristic(
|
|
@@ -262,6 +281,7 @@ export default class {
|
|
|
262
281
|
*/
|
|
263
282
|
if (action === 'close') {
|
|
264
283
|
const updateKey = generateRandomString(5)
|
|
284
|
+
this.stopExtremePolling()
|
|
265
285
|
if (this.accessory.context.connection === 'local') {
|
|
266
286
|
this.extremePolling = setInterval(() => this.requestUpdate(), 3000)
|
|
267
287
|
}
|
|
@@ -281,10 +301,7 @@ export default class {
|
|
|
281
301
|
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
282
302
|
|
|
283
303
|
// Cancel any 'extreme' polling intervals from setting the garage to close
|
|
284
|
-
|
|
285
|
-
clearInterval(this.extremePolling)
|
|
286
|
-
this.extremePolling = false
|
|
287
|
-
}
|
|
304
|
+
this.stopExtremePolling()
|
|
288
305
|
}, (this.operationTime + 15) * 1000)
|
|
289
306
|
}
|
|
290
307
|
})
|
|
@@ -482,10 +499,7 @@ export default class {
|
|
|
482
499
|
}
|
|
483
500
|
|
|
484
501
|
// Cancel any 'extreme' polling intervals from setting the garage to close
|
|
485
|
-
|
|
486
|
-
clearInterval(this.extremePolling)
|
|
487
|
-
this.extremePolling = false
|
|
488
|
-
}
|
|
502
|
+
this.stopExtremePolling()
|
|
489
503
|
break
|
|
490
504
|
}
|
|
491
505
|
default:
|
package/lib/device/garage-sub.js
CHANGED
|
@@ -84,6 +84,18 @@ export default class {
|
|
|
84
84
|
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Cancel the quick polling started when the door was told to close. Kept in one
|
|
89
|
+
* place so every path that ends a close - a new operation, the door settling,
|
|
90
|
+
* or Homebridge shutting down - stops it rather than leaving it running.
|
|
91
|
+
*/
|
|
92
|
+
stopExtremePolling() {
|
|
93
|
+
if (this.extremePolling) {
|
|
94
|
+
clearInterval(this.extremePolling)
|
|
95
|
+
this.extremePolling = false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
async internalTargetUpdate(value) {
|
|
88
100
|
// Add the request to the queue so updates are sent apart
|
|
89
101
|
try {
|
|
@@ -205,6 +217,13 @@ export default class {
|
|
|
205
217
|
const updateKey = generateRandomString(5)
|
|
206
218
|
this.updateKey = updateKey
|
|
207
219
|
|
|
220
|
+
// A new operation supersedes any close still in progress, so its quick
|
|
221
|
+
// polling has to stop here. Without this it was orphaned: the close
|
|
222
|
+
// timeout returned early on the changed key and never reached its own
|
|
223
|
+
// clearInterval, so the device was polled every 3 seconds for ever, and
|
|
224
|
+
// each open-during-close cycle leaked another one.
|
|
225
|
+
this.stopExtremePolling()
|
|
226
|
+
|
|
208
227
|
// Update the Eve times opened characteristic
|
|
209
228
|
this.accessory.eveService.addEntry({ status: 0 })
|
|
210
229
|
this.service.updateCharacteristic(
|
|
@@ -241,6 +260,7 @@ export default class {
|
|
|
241
260
|
*/
|
|
242
261
|
if (action === 'close') {
|
|
243
262
|
const updateKey = generateRandomString(5)
|
|
263
|
+
this.stopExtremePolling()
|
|
244
264
|
if (this.accessory.context.connection === 'local') {
|
|
245
265
|
this.extremePolling = setInterval(() => this.priAcc.control.requestUpdate(), 3000)
|
|
246
266
|
}
|
|
@@ -260,10 +280,7 @@ export default class {
|
|
|
260
280
|
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
261
281
|
|
|
262
282
|
// Cancel any 'extreme' polling intervals from setting the garage to close
|
|
263
|
-
|
|
264
|
-
clearInterval(this.extremePolling)
|
|
265
|
-
this.extremePolling = false
|
|
266
|
-
}
|
|
283
|
+
this.stopExtremePolling()
|
|
267
284
|
}, (this.operationTime + 15) * 1000)
|
|
268
285
|
}
|
|
269
286
|
})
|
|
@@ -363,10 +380,7 @@ export default class {
|
|
|
363
380
|
}
|
|
364
381
|
|
|
365
382
|
// Cancel any 'extreme' polling intervals from setting the garage to close
|
|
366
|
-
|
|
367
|
-
clearInterval(this.extremePolling)
|
|
368
|
-
this.extremePolling = false
|
|
369
|
-
}
|
|
383
|
+
this.stopExtremePolling()
|
|
370
384
|
break
|
|
371
385
|
}
|
|
372
386
|
default:
|
package/lib/device/hub-main.js
CHANGED
|
@@ -92,6 +92,13 @@ export default class {
|
|
|
92
92
|
&& data.all.digest.hub.subdevice
|
|
93
93
|
&& Array.isArray(data.all.digest.hub.subdevice)
|
|
94
94
|
) {
|
|
95
|
+
// Rebuild these each poll. They used to be appended to and never
|
|
96
|
+
// cleared, so with one valve on the hub and the default 300s refresh
|
|
97
|
+
// the follow-up request carried 288 copies of the same id after a day,
|
|
98
|
+
// growing until the hub stopped answering and state stopped updating.
|
|
99
|
+
this.mtsList = []
|
|
100
|
+
this.waterList = []
|
|
101
|
+
|
|
95
102
|
data.all.digest.hub.subdevice.forEach((subdevice) => {
|
|
96
103
|
// Check whether the homebridge accessory this relates to exists
|
|
97
104
|
const subAcc = this.devicesInHB.get(
|
package/lib/device/hub-smoke.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import platformConsts from '../utils/constants.js'
|
|
2
|
-
import { parseError } from '../utils/functions.js'
|
|
2
|
+
import { hasProperty, parseError } from '../utils/functions.js'
|
|
3
3
|
import platformLang from '../utils/lang-en.js'
|
|
4
4
|
|
|
5
5
|
export default class {
|
|
@@ -32,7 +32,30 @@ export default class {
|
|
|
32
32
|
|
|
33
33
|
applyUpdate(data) {
|
|
34
34
|
try {
|
|
35
|
-
this.log
|
|
35
|
+
// This warned every incoming frame through `this.log`, which the class never
|
|
36
|
+
// defines - so it threw on every update, the catch below turned that into a
|
|
37
|
+
// warning the owner could do nothing about, and the battery reading was
|
|
38
|
+
// never applied at all. Routine frames belong in the debug log.
|
|
39
|
+
this.accessory.logDebug(JSON.stringify(data))
|
|
40
|
+
|
|
41
|
+
// Battery % from reported voltage, the same way the other hub sensors do it
|
|
42
|
+
if (hasProperty(data, 'voltage')) {
|
|
43
|
+
// 1. Reduce/enlarge value so in [2000, 3000]
|
|
44
|
+
let newVoltage = Math.min(Math.max(data.voltage, 2000), 3000)
|
|
45
|
+
|
|
46
|
+
// 2. Scale this from [2000, 3000] to [0, 100] and round to nearest whole number
|
|
47
|
+
newVoltage = Math.round((newVoltage - 2000) / 10)
|
|
48
|
+
|
|
49
|
+
// This should be a rough estimate of the battery %
|
|
50
|
+
if (newVoltage !== this.cacheBatt) {
|
|
51
|
+
this.cacheBatt = newVoltage
|
|
52
|
+
this.battService.updateCharacteristic(this.hapChar.BatteryLevel, this.cacheBatt)
|
|
53
|
+
this.battService.updateCharacteristic(
|
|
54
|
+
this.hapChar.StatusLowBattery,
|
|
55
|
+
this.cacheBatt < this.lowBattThreshold ? 1 : 0,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
36
59
|
} catch (err) {
|
|
37
60
|
this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
|
|
38
61
|
}
|
package/lib/device/light-cct.js
CHANGED
|
@@ -43,10 +43,22 @@ export default class {
|
|
|
43
43
|
this.service = this.accessory.getService(this.hapServ.Lightbulb)
|
|
44
44
|
|| this.accessory.addService(this.hapServ.Lightbulb)
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// Adaptive lighting has just been switched off, so take away the three
|
|
47
|
+
// characteristics that make Home offer it. Only these need to go: the whole
|
|
48
|
+
// Lightbulb service used to be thrown away and rebuilt here, which does hide
|
|
49
|
+
// the icon but presents Home with what looks like a different light, and an
|
|
50
|
+
// owner can lose that light's room and its automations as a result.
|
|
47
51
|
if (this.alShift === -1 && this.accessory.context.adaptiveLighting) {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
[
|
|
53
|
+
'SupportedCharacteristicValueTransitionConfiguration',
|
|
54
|
+
'CharacteristicValueTransitionControl',
|
|
55
|
+
'CharacteristicValueActiveTransitionCount',
|
|
56
|
+
].forEach((charName) => {
|
|
57
|
+
const char = this.hapChar[charName]
|
|
58
|
+
if (char && this.service.testCharacteristic(char)) {
|
|
59
|
+
this.service.removeCharacteristic(this.service.getCharacteristic(char))
|
|
60
|
+
}
|
|
61
|
+
})
|
|
50
62
|
this.accessory.context.adaptiveLighting = false
|
|
51
63
|
}
|
|
52
64
|
|
|
@@ -463,7 +475,13 @@ export default class {
|
|
|
463
475
|
this.cacheMired = hkTemp
|
|
464
476
|
const kelvin = Math.round(1000000 / this.cacheMired)
|
|
465
477
|
this.accessory.log(`${platformLang.curLightTemp} [${this.cacheMired} / ${kelvin}]`)
|
|
466
|
-
|
|
478
|
+
// A light repeating back the temperature adaptive lighting just set
|
|
479
|
+
// can land a few mireds off through the conversion, and treating that
|
|
480
|
+
// as the owner's doing would switch adaptive lighting off moments
|
|
481
|
+
// after it was switched on. Further apart than that and somebody set
|
|
482
|
+
// this temperature themselves, outside HomeKit - which nothing else
|
|
483
|
+
// would notice, so adaptive lighting has to be stopped here.
|
|
484
|
+
if (dif > 10 && this.alController?.isAdaptiveLightingActive?.()) {
|
|
467
485
|
this.alController.disableAdaptiveLighting()
|
|
468
486
|
this.accessory.log(platformLang.alDisabled)
|
|
469
487
|
}
|
package/lib/device/light-rgb.js
CHANGED
|
@@ -49,10 +49,22 @@ export default class {
|
|
|
49
49
|
this.service = this.accessory.getService(this.hapServ.Lightbulb)
|
|
50
50
|
|| this.accessory.addService(this.hapServ.Lightbulb)
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// Adaptive lighting has just been switched off, so take away the three
|
|
53
|
+
// characteristics that make Home offer it. Only these need to go: the whole
|
|
54
|
+
// Lightbulb service used to be thrown away and rebuilt here, which does hide
|
|
55
|
+
// the icon but presents Home with what looks like a different light, and an
|
|
56
|
+
// owner can lose that light's room and its automations as a result.
|
|
53
57
|
if (this.alShift === -1 && this.accessory.context.adaptiveLighting) {
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
[
|
|
59
|
+
'SupportedCharacteristicValueTransitionConfiguration',
|
|
60
|
+
'CharacteristicValueTransitionControl',
|
|
61
|
+
'CharacteristicValueActiveTransitionCount',
|
|
62
|
+
].forEach((charName) => {
|
|
63
|
+
const char = this.hapChar[charName]
|
|
64
|
+
if (char && this.service.testCharacteristic(char)) {
|
|
65
|
+
this.service.removeCharacteristic(this.service.getCharacteristic(char))
|
|
66
|
+
}
|
|
67
|
+
})
|
|
56
68
|
this.accessory.context.adaptiveLighting = false
|
|
57
69
|
}
|
|
58
70
|
|
|
@@ -73,6 +85,14 @@ export default class {
|
|
|
73
85
|
this.service
|
|
74
86
|
.getCharacteristic(this.hapChar.Hue)
|
|
75
87
|
.onSet(async value => this.internalColourUpdate(value))
|
|
88
|
+
// Saturation needs a handler of its own. HomeKit writes hue and saturation
|
|
89
|
+
// separately, so with only hue listened for, dragging saturation alone -
|
|
90
|
+
// the same colour made paler or richer - reached nothing at all. Both
|
|
91
|
+
// handlers share one update, and the debounce inside it collapses a
|
|
92
|
+
// hue-and-saturation pair into a single command.
|
|
93
|
+
this.service
|
|
94
|
+
.getCharacteristic(this.hapChar.Saturation)
|
|
95
|
+
.onSet(async () => this.internalColourUpdate(this.service.getCharacteristic(this.hapChar.Hue).value))
|
|
76
96
|
this.cacheHue = this.service.getCharacteristic(this.hapChar.Hue).value
|
|
77
97
|
this.cacheSat = this.service.getCharacteristic(this.hapChar.Saturation).value
|
|
78
98
|
|
|
@@ -495,11 +515,19 @@ export default class {
|
|
|
495
515
|
this.cacheHue = newHue
|
|
496
516
|
this.cacheSat = newSat
|
|
497
517
|
this.accessory.log(`${platformLang.curLightColour} [${this.cacheHue}, ${this.cacheSat}] / [${r}, ${g}, ${b}]`)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
518
|
+
|
|
519
|
+
// Somebody chose this colour outside HomeKit - in the Meross app, on
|
|
520
|
+
// a remote, or at the wall. Homebridge catches changes made in the
|
|
521
|
+
// Home app itself, but nothing else sees these, so adaptive lighting
|
|
522
|
+
// would carry on and move the light off that colour within minutes.
|
|
523
|
+
//
|
|
524
|
+
// This used to sit outside the check above and fire on every report,
|
|
525
|
+
// so a routine refresh that simply repeated the current colour back
|
|
526
|
+
// switched adaptive lighting off seconds after it was switched on.
|
|
527
|
+
if (this.alController?.isAdaptiveLightingActive?.()) {
|
|
528
|
+
this.alController.disableAdaptiveLighting()
|
|
529
|
+
this.accessory.log(platformLang.alDisabled)
|
|
530
|
+
}
|
|
503
531
|
}
|
|
504
532
|
}
|
|
505
533
|
if (
|
|
@@ -517,7 +545,13 @@ export default class {
|
|
|
517
545
|
this.cacheMired = hkTemp
|
|
518
546
|
const kelvin = Math.round(1000000 / this.cacheMired)
|
|
519
547
|
this.accessory.log(`${platformLang.curLightTemp} [${this.cacheMired} / ${kelvin}]`)
|
|
520
|
-
|
|
548
|
+
// A light repeating back the temperature adaptive lighting just set
|
|
549
|
+
// can land a few mireds off through the conversion, and treating that
|
|
550
|
+
// as the owner's doing would switch adaptive lighting off moments
|
|
551
|
+
// after it was switched on. Further apart than that and somebody set
|
|
552
|
+
// this temperature themselves, outside HomeKit - which nothing else
|
|
553
|
+
// would notice, so adaptive lighting has to be stopped here.
|
|
554
|
+
if (dif > 10 && this.alController?.isAdaptiveLightingActive?.()) {
|
|
521
555
|
this.alController.disableAdaptiveLighting()
|
|
522
556
|
this.accessory.log(platformLang.alDisabled)
|
|
523
557
|
}
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
<ul>
|
|
198
198
|
<li>To use this plugin, you will need to already have:</li>
|
|
199
199
|
<ul>
|
|
200
|
-
<li><a href="https://nodejs.org" target="_blank">Node</a>: latest version of <code>v22</code> or <code>
|
|
200
|
+
<li><a href="https://nodejs.org" target="_blank">Node</a>: latest version of <code>v22</code>, <code>v24</code> or <code>v26</code> - any other major version is not supported.</li>
|
|
201
201
|
<li><a href="https://homebridge.io" target="_blank">Homebridge</a>: <code>v1.6</code> or above - refer to link for more information and installation instructions.</li>
|
|
202
202
|
</ul>
|
|
203
203
|
</ul>
|
package/lib/platform.js
CHANGED
|
@@ -29,7 +29,6 @@ export default class {
|
|
|
29
29
|
try {
|
|
30
30
|
this.api = api
|
|
31
31
|
this.log = log
|
|
32
|
-
this.isBeta = process.argv.includes('-D')
|
|
33
32
|
this.cloudClient = false
|
|
34
33
|
this.deviceConf = {}
|
|
35
34
|
this.devicesInHB = new Map()
|
|
@@ -91,6 +90,9 @@ export default class {
|
|
|
91
90
|
const logIncrease = (k, min) => {
|
|
92
91
|
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
|
|
93
92
|
}
|
|
93
|
+
const logDecrease = (k, max) => {
|
|
94
|
+
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgHigh, max)
|
|
95
|
+
}
|
|
94
96
|
const logQuotes = (k) => {
|
|
95
97
|
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
|
|
96
98
|
}
|
|
@@ -163,6 +165,9 @@ export default class {
|
|
|
163
165
|
} else if (intVal < platformConsts.minValues[k]) {
|
|
164
166
|
logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
|
|
165
167
|
this.deviceConf[id][k] = platformConsts.minValues[k]
|
|
168
|
+
} else if (platformConsts.maxValues[k] !== undefined && intVal > platformConsts.maxValues[k]) {
|
|
169
|
+
logDecrease(`${key}.${id}.${k}`, platformConsts.maxValues[k])
|
|
170
|
+
this.deviceConf[id][k] = platformConsts.maxValues[k]
|
|
166
171
|
} else {
|
|
167
172
|
this.deviceConf[id][k] = intVal
|
|
168
173
|
}
|
|
@@ -304,13 +309,16 @@ export default class {
|
|
|
304
309
|
this.log('%s.', platformLang.initialised)
|
|
305
310
|
|
|
306
311
|
// Sort out some logging functions
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
// Homebridge already decides whether debug lines are shown, and it gets it
|
|
313
|
+
// right in a child bridge too. This used to work it out for itself by
|
|
314
|
+
// looking for `-D` in the plugin's own process arguments, which a child
|
|
315
|
+
// bridge does not receive unless that bridge has its own debug setting
|
|
316
|
+
// turned on - so debug logging silently disappeared for anyone running the
|
|
317
|
+
// plugin in a child bridge with debug enabled globally.
|
|
318
|
+
//
|
|
319
|
+
// `debugWarn` has no equivalent on Homebridge's logger, so it goes out as
|
|
320
|
+
// a debug line too rather than being invented here.
|
|
321
|
+
this.log.debugWarn = (...args) => this.log.debug(...args)
|
|
314
322
|
|
|
315
323
|
// Require any libraries that the accessory instances use
|
|
316
324
|
this.cusChar = new platformChars(this.api)
|
|
@@ -339,6 +347,7 @@ export default class {
|
|
|
339
347
|
// If the user has configured cloud username and password then get a device list
|
|
340
348
|
this.accountDetails = {}
|
|
341
349
|
let cloudDevices = []
|
|
350
|
+
let cloudListFetched = false
|
|
342
351
|
try {
|
|
343
352
|
if (!this.config.username || !this.config.password) {
|
|
344
353
|
throw new Error(platformLang.missingCreds)
|
|
@@ -381,6 +390,7 @@ export default class {
|
|
|
381
390
|
}
|
|
382
391
|
|
|
383
392
|
// Initialise the cloud configured devices into Homebridge
|
|
393
|
+
cloudListFetched = true
|
|
384
394
|
cloudDevices.forEach(device => this.initialiseDevice(device))
|
|
385
395
|
} catch (err) {
|
|
386
396
|
const eText = parseError(err, [platformLang.mfaFail, platformLang.missingCreds, platformLang.accTokenInvalid])
|
|
@@ -434,7 +444,12 @@ export default class {
|
|
|
434
444
|
switch (accessory.context.connection) {
|
|
435
445
|
case 'cloud':
|
|
436
446
|
case 'hybrid':
|
|
437
|
-
|
|
447
|
+
// Only a list that actually arrived says anything about what is on the
|
|
448
|
+
// account. `cloudDevices` is also empty when the login or the device
|
|
449
|
+
// request failed, and an owner with a user key set reaches this code
|
|
450
|
+
// in that case - so one bad morning at Meross used to wipe their whole
|
|
451
|
+
// setup out of HomeKit, rooms, scenes and automations included.
|
|
452
|
+
if (cloudListFetched && !cloudDevices.some(el => el.uuid === accessory.context.serialNumber)) {
|
|
438
453
|
this.removeAccessory(accessory)
|
|
439
454
|
}
|
|
440
455
|
break
|
|
@@ -464,42 +479,43 @@ export default class {
|
|
|
464
479
|
pluginShutdown() {
|
|
465
480
|
// A function that is called when the plugin fails to load or Homebridge restarts
|
|
466
481
|
try {
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
482
|
+
// Stop every accessory's timers. Only the mqtt disconnect needs a cloud
|
|
483
|
+
// client: this whole block used to sit behind that check, so a local-only
|
|
484
|
+
// setup - no username or password, just a user key - kept polling every
|
|
485
|
+
// device while Homebridge was tearing down, and kept the process alive
|
|
486
|
+
this.devicesInHB.forEach((accessory) => {
|
|
487
|
+
if (this.cloudClient && accessory.mqtt) {
|
|
488
|
+
accessory.mqtt.disconnect()
|
|
489
|
+
}
|
|
490
|
+
if (accessory.refreshInterval) {
|
|
491
|
+
clearInterval(accessory.refreshInterval)
|
|
492
|
+
}
|
|
493
|
+
if (accessory.powerInterval) {
|
|
494
|
+
clearInterval(accessory.powerInterval)
|
|
495
|
+
}
|
|
496
|
+
accessory.control?.stopExtremePolling?.()
|
|
497
|
+
})
|
|
481
498
|
} catch (err) {
|
|
482
499
|
// No need to show errors at this point
|
|
483
500
|
}
|
|
484
501
|
}
|
|
485
502
|
|
|
486
503
|
applyAccessoryLogging(accessory) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
499
|
-
}
|
|
500
|
-
accessory.logDebug = () => {}
|
|
501
|
-
accessory.logDebugWarn = () => {}
|
|
504
|
+
// Debug lines always go to Homebridge's debug logger, which prints them only
|
|
505
|
+
// when debug is on - so there is nothing to decide here. Working it out from
|
|
506
|
+
// the process arguments instead is what lost debug logging entirely for
|
|
507
|
+
// anyone running the plugin in a child bridge.
|
|
508
|
+
accessory.logDebug = msg => this.log.debug('[%s] %s.', accessory.displayName, msg)
|
|
509
|
+
accessory.logDebugWarn = accessory.logDebug
|
|
510
|
+
|
|
511
|
+
if (this.config.disableDeviceLogging) {
|
|
512
|
+
accessory.log = () => {}
|
|
513
|
+
accessory.logWarn = () => {}
|
|
514
|
+
return
|
|
502
515
|
}
|
|
516
|
+
|
|
517
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
518
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
503
519
|
}
|
|
504
520
|
|
|
505
521
|
async initialiseDevice(device) {
|
package/lib/utils/constants.js
CHANGED
|
@@ -50,6 +50,14 @@ export default {
|
|
|
50
50
|
refreshRate: 5,
|
|
51
51
|
},
|
|
52
52
|
|
|
53
|
+
// A Node timer holds its delay in a signed 32-bit integer. Over 2147483647 ms
|
|
54
|
+
// it does not throw - it quietly becomes 1 ms. refreshRate and cloudRefreshRate
|
|
55
|
+
// are already capped at 600 further down, but the garage opening time was not,
|
|
56
|
+
// so a large value made the door's reset timer fire immediately and repeatedly.
|
|
57
|
+
maxValues: {
|
|
58
|
+
garageDoorOpeningTime: 2073600,
|
|
59
|
+
},
|
|
60
|
+
|
|
53
61
|
allowed: {
|
|
54
62
|
singleDevices: [
|
|
55
63
|
'name',
|
package/lib/utils/lang-en.js
CHANGED
|
@@ -6,13 +6,14 @@ export default {
|
|
|
6
6
|
accTokenNoExist: 'does not exist, this is normally not an issue',
|
|
7
7
|
accTokenStoreErr: 'could not store access token as',
|
|
8
8
|
accTokenUserChange: 'username has changed',
|
|
9
|
-
alDisabled: 'adaptive lighting
|
|
9
|
+
alDisabled: 'adaptive lighting turned off, as the colour was changed outside HomeKit',
|
|
10
10
|
brand: 'Meross Technology',
|
|
11
11
|
cfgDef: 'is not a valid number so using default of',
|
|
12
12
|
cfgDup: 'will be ignored since another entry with this ID already exists',
|
|
13
13
|
cfgIgn: 'is not configured correctly so ignoring',
|
|
14
14
|
cfgIgnItem: 'has an invalid entry which will be ignored',
|
|
15
15
|
cfgItem: 'Config entry',
|
|
16
|
+
cfgHigh: 'is set too high so reducing to',
|
|
16
17
|
cfgLow: 'is set too low so increasing to',
|
|
17
18
|
cfgMoveEntry: 'configuration entry should be moved to appropriate section in the UI',
|
|
18
19
|
cfgRmv: 'is unused and can be removed',
|
package/package.json
CHANGED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer'
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
|
|
5
|
-
import { encodeParams, generateRandomString, hasProperty, parseError } from './functions.js'
|
|
6
|
-
|
|
7
|
-
describe('encodeParams', () => {
|
|
8
|
-
it('base64 encodes the json body the meross cloud expects', () => {
|
|
9
|
-
const encoded = encodeParams({ email: 'a@b.com', password: 'x' })
|
|
10
|
-
expect(JSON.parse(Buffer.from(encoded, 'base64').toString())).toEqual({
|
|
11
|
-
email: 'a@b.com',
|
|
12
|
-
password: 'x',
|
|
13
|
-
})
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('produces valid base64 with no line breaks', () => {
|
|
17
|
-
// The value goes straight into a request body, so a wrapped or padded
|
|
18
|
-
// encoding would be rejected by the cloud.
|
|
19
|
-
const encoded = encodeParams({ a: 'x'.repeat(200) })
|
|
20
|
-
expect(encoded).toMatch(/^[A-Z0-9+/]+=*$/i)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('handles an empty object', () => {
|
|
24
|
-
expect(Buffer.from(encodeParams({}), 'base64').toString()).toBe('{}')
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('generateRandomString', () => {
|
|
29
|
-
it('returns the requested length', () => {
|
|
30
|
-
expect(generateRandomString(16)).toHaveLength(16)
|
|
31
|
-
expect(generateRandomString(1)).toHaveLength(1)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('uses only lowercase letters and digits', () => {
|
|
35
|
-
// The nonce is signed alongside the request, so any character outside the
|
|
36
|
-
// documented set risks a signature mismatch.
|
|
37
|
-
expect(generateRandomString(200)).toMatch(/^[a-z0-9]+$/)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('does not return the same value twice in a row', () => {
|
|
41
|
-
expect(generateRandomString(32)).not.toBe(generateRandomString(32))
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('hasProperty', () => {
|
|
46
|
-
it('detects own properties only', () => {
|
|
47
|
-
expect(hasProperty({ a: 1 }, 'a')).toBe(true)
|
|
48
|
-
expect(hasProperty({ a: undefined }, 'a')).toBe(true)
|
|
49
|
-
expect(hasProperty({}, 'a')).toBe(false)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('ignores inherited properties', () => {
|
|
53
|
-
// A device payload with no "toString" key must not read as having one.
|
|
54
|
-
expect(hasProperty({}, 'toString')).toBe(false)
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe('parseError', () => {
|
|
59
|
-
it('appends the first stack frame to the message', () => {
|
|
60
|
-
const err = new Error('boom')
|
|
61
|
-
err.stack = 'Error: boom\n at thing (/a.js:1:1)'
|
|
62
|
-
expect(parseError(err)).toBe('boom at thing (/a.js:1:1)')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('returns just the message when the error is listed in hideStack', () => {
|
|
66
|
-
// Routine failures - a device being unreachable - are logged without a
|
|
67
|
-
// stack so the log stays readable.
|
|
68
|
-
const err = new Error('offline')
|
|
69
|
-
err.stack = 'Error: offline\n at thing (/a.js:1:1)'
|
|
70
|
-
expect(parseError(err, ['offline'])).toBe('offline')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('returns the message when there is no stack at all', () => {
|
|
74
|
-
const err = new Error('nostack')
|
|
75
|
-
err.stack = ''
|
|
76
|
-
expect(parseError(err)).toBe('nostack')
|
|
77
|
-
})
|
|
78
|
-
})
|