@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 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 `v24` - any other major version is not supported.
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
@@ -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
- if (this.extremePolling) {
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
- if (this.extremePolling) {
486
- clearInterval(this.extremePolling)
487
- this.extremePolling = false
488
- }
502
+ this.stopExtremePolling()
489
503
  break
490
504
  }
491
505
  default:
@@ -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
- if (this.extremePolling) {
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
- if (this.extremePolling) {
367
- clearInterval(this.extremePolling)
368
- this.extremePolling = false
369
- }
383
+ this.stopExtremePolling()
370
384
  break
371
385
  }
372
386
  default:
@@ -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(
@@ -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.warn('[%s]\n%s.', this.name, JSON.stringify(data))
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
  }
@@ -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
- // If adaptive lighting has just been disabled then remove and re-add service to hide AL icon
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
- this.accessory.removeService(this.service)
49
- this.service = this.accessory.addService(this.hapServ.Lightbulb)
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
- if (dif > 10 && this.alController && this.alController.isAdaptiveLightingActive()) {
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
  }
@@ -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
- // If adaptive lighting has just been disabled then remove and re-add service to hide AL icon
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
- this.accessory.removeService(this.service)
55
- this.service = this.accessory.addService(this.hapServ.Lightbulb)
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
- // Disable adaptive lighting
500
- if (this.alController && this.alController.isAdaptiveLightingActive()) {
501
- this.alController.disableAdaptiveLighting()
502
- this.accessory.log(platformLang.alDisabled)
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
- if (dif > 10 && this.alController && this.alController.isAdaptiveLightingActive()) {
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>v24</code> - any other major version is not supported.</li>
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
- if (this.isBeta) {
308
- this.log.debug = this.log
309
- this.log.debugWarn = this.log.warn
310
- } else {
311
- this.log.debug = () => {}
312
- this.log.debugWarn = () => {}
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
- if (!cloudDevices.some(el => el.uuid === accessory.context.serialNumber)) {
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
- // Close the mqtt connection for the accessories with an open connection
468
- if (this.cloudClient) {
469
- this.devicesInHB.forEach((accessory) => {
470
- if (accessory.mqtt) {
471
- accessory.mqtt.disconnect()
472
- }
473
- if (accessory.refreshInterval) {
474
- clearInterval(accessory.refreshInterval)
475
- }
476
- if (accessory.powerInterval) {
477
- clearInterval(accessory.powerInterval)
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
- if (this.isBeta) {
488
- accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
489
- accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
490
- accessory.logDebug = msg => this.log('[%s] %s.', accessory.displayName, msg)
491
- accessory.logDebugWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
492
- } else {
493
- if (this.config.disableDeviceLogging) {
494
- accessory.log = () => {}
495
- accessory.logWarn = () => {}
496
- } else {
497
- accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
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) {
@@ -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',
@@ -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 disabled due to change of colour detected',
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
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-meross",
3
3
  "alias": "Meross",
4
4
  "type": "module",
5
- "version": "10.19.0",
5
+ "version": "10.19.1-beta.1",
6
6
  "description": "Homebridge plugin to integrate Meross devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",
@@ -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
- })