@homebridge-plugins/homebridge-wemo 7.7.0 → 7.7.1-beta.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.
- package/CHANGELOG.md +11 -0
- package/lib/device/link-bulb.js +42 -8
- package/lib/platform.js +49 -50
- package/lib/utils/lang-en.js +1 -1
- package/package.json +1 -1
- package/lib/utils/functions.test.js +0 -98
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-wemo` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v7.7.1 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- chore: exclude test files and the test config from the published package
|
|
10
|
+
- fix: restore debug logging when the plugin runs in a child bridge
|
|
11
|
+
- fix: only turn adaptive lighting off when a colour is genuinely chosen elsewhere
|
|
12
|
+
- fix: send a colour change when only the saturation moves, and mend the al cleanup
|
|
13
|
+
- fix: explain a port clash on the notification listener instead of crashing the bridge
|
|
14
|
+
- fix: stop gating a few log lines on a debug setting that was never readable
|
|
15
|
+
|
|
5
16
|
## v7.7.0 (2026-08-02)
|
|
6
17
|
|
|
7
18
|
### Changed
|
package/lib/device/link-bulb.js
CHANGED
|
@@ -57,10 +57,22 @@ export default class {
|
|
|
57
57
|
this.service = this.accessory.getService(this.hapServ.Lightbulb)
|
|
58
58
|
|| this.accessory.addService(this.hapServ.Lightbulb)
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// Adaptive lighting has just been switched off, so take away the three
|
|
61
|
+
// characteristics that make Home offer it. Only these need to go: the whole
|
|
62
|
+
// Lightbulb service used to be thrown away and rebuilt here, which does hide
|
|
63
|
+
// the icon but presents Home with what looks like a different light, and an
|
|
64
|
+
// owner can lose that light's room and its automations as a result.
|
|
61
65
|
if (this.alShift === -1 && this.accessory.context.adaptiveLighting) {
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
[
|
|
67
|
+
'SupportedCharacteristicValueTransitionConfiguration',
|
|
68
|
+
'CharacteristicValueTransitionControl',
|
|
69
|
+
'CharacteristicValueActiveTransitionCount',
|
|
70
|
+
].forEach((charName) => {
|
|
71
|
+
const char = this.hapChar[charName]
|
|
72
|
+
if (char && this.service.testCharacteristic(char)) {
|
|
73
|
+
this.service.removeCharacteristic(this.service.getCharacteristic(char))
|
|
74
|
+
}
|
|
75
|
+
})
|
|
64
76
|
this.accessory.context.adaptiveLighting = false
|
|
65
77
|
}
|
|
66
78
|
|
|
@@ -85,6 +97,14 @@ export default class {
|
|
|
85
97
|
this.service.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
|
|
86
98
|
await this.internalColourUpdate(value)
|
|
87
99
|
})
|
|
100
|
+
// Saturation needs a handler of its own. HomeKit writes hue and saturation
|
|
101
|
+
// separately, so with only hue listened for, dragging saturation alone -
|
|
102
|
+
// the same colour made paler or richer - reached nothing and the bulb
|
|
103
|
+
// never changed. Both handlers share one update, and the debounce inside
|
|
104
|
+
// it collapses a hue-and-saturation pair into a single command.
|
|
105
|
+
this.service.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
|
|
106
|
+
await this.internalColourUpdate(this.service.getCharacteristic(this.hapChar.Hue).value)
|
|
107
|
+
})
|
|
88
108
|
this.cacheHue = this.service.getCharacteristic(this.hapChar.Hue).value
|
|
89
109
|
this.cacheSat = this.service.getCharacteristic(this.hapChar.Saturation).value
|
|
90
110
|
} else {
|
|
@@ -109,6 +129,10 @@ export default class {
|
|
|
109
129
|
customTemperatureAdjustment: this.alShift,
|
|
110
130
|
})
|
|
111
131
|
this.accessory.configureController(this.alController)
|
|
132
|
+
// Without this the cleanup that removes the adaptive lighting
|
|
133
|
+
// characteristics can never run, because it is guarded on this flag -
|
|
134
|
+
// so switching adaptive lighting off left Home still offering it.
|
|
135
|
+
this.accessory.context.adaptiveLighting = true
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
|
|
@@ -289,13 +313,15 @@ export default class {
|
|
|
289
313
|
return
|
|
290
314
|
}
|
|
291
315
|
|
|
292
|
-
// Don't continue if this
|
|
293
|
-
|
|
316
|
+
// Don't continue if this colour is the same as before. Saturation is part
|
|
317
|
+
// of that question: comparing hue alone meant a change of saturation at
|
|
318
|
+
// the same hue looked like no change at all, and nothing was sent.
|
|
319
|
+
const currentSat = this.service.getCharacteristic(this.hapChar.Saturation).value
|
|
320
|
+
if (this.cacheHue === value && this.cacheSat === currentSat) {
|
|
294
321
|
return
|
|
295
322
|
}
|
|
296
323
|
|
|
297
324
|
// First convert to RGB
|
|
298
|
-
const currentSat = this.service.getCharacteristic(this.hapChar.Saturation).value
|
|
299
325
|
const [r, g, b] = hs2rgb(value, currentSat)
|
|
300
326
|
|
|
301
327
|
// Then convert the RGB to the values needed for Wemo
|
|
@@ -424,7 +450,10 @@ export default class {
|
|
|
424
450
|
// Log the change if appropriate
|
|
425
451
|
this.accessory.log(`${platformLang.curColour} [X:${valueX} Y:${valueY}]`)
|
|
426
452
|
|
|
427
|
-
//
|
|
453
|
+
// Somebody chose this colour outside HomeKit - in the Wemo app, on a
|
|
454
|
+
// remote, or at the wall. Homebridge catches changes made in the Home
|
|
455
|
+
// app itself, but nothing else sees these, so adaptive lighting would
|
|
456
|
+
// carry on and move the bulb off that colour within minutes.
|
|
428
457
|
if (this.alController?.isAdaptiveLightingActive()) {
|
|
429
458
|
this.alController.disableAdaptiveLighting()
|
|
430
459
|
this.accessory.log(platformLang.alDisabled)
|
|
@@ -449,7 +478,12 @@ export default class {
|
|
|
449
478
|
const mToK = Math.round(1000000 / value)
|
|
450
479
|
this.accessory.log(`${platformLang.curCCT} [${mToK}K / ${value}M]`)
|
|
451
480
|
|
|
452
|
-
//
|
|
481
|
+
// A bulb repeating back the temperature adaptive lighting just set can
|
|
482
|
+
// land a few mireds off through the conversion, and treating that as the
|
|
483
|
+
// owner's doing would switch adaptive lighting off moments after it was
|
|
484
|
+
// switched on. Further apart than that and somebody set this temperature
|
|
485
|
+
// themselves, outside HomeKit - which nothing else would notice, so
|
|
486
|
+
// adaptive lighting has to be stopped here.
|
|
453
487
|
if (!Number.isNaN(this.cacheMired)) {
|
|
454
488
|
const diff = Math.abs(value - this.cacheMired) > 20
|
|
455
489
|
if (this.alController?.isAdaptiveLightingActive() && diff) {
|
package/lib/platform.js
CHANGED
|
@@ -41,7 +41,6 @@ export default class {
|
|
|
41
41
|
try {
|
|
42
42
|
this.api = api
|
|
43
43
|
this.log = log
|
|
44
|
-
this.isBeta = process.argv.includes('-D')
|
|
45
44
|
|
|
46
45
|
// Configuration objects for accessories
|
|
47
46
|
this.ignoredDevices = []
|
|
@@ -344,13 +343,16 @@ export default class {
|
|
|
344
343
|
this.log('%s.', platformLang.initialised)
|
|
345
344
|
|
|
346
345
|
// Sort out some logging functions
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
346
|
+
// Homebridge already decides whether debug lines are shown, and it gets it
|
|
347
|
+
// right in a child bridge too. This used to work it out for itself by
|
|
348
|
+
// looking for `-D` in the plugin's own process arguments, which a child
|
|
349
|
+
// bridge does not receive unless that bridge has its own debug setting
|
|
350
|
+
// turned on - so debug logging silently disappeared for anyone running the
|
|
351
|
+
// plugin in a child bridge with debug enabled globally.
|
|
352
|
+
//
|
|
353
|
+
// `debugWarn` has no equivalent on Homebridge's logger, so it goes out as
|
|
354
|
+
// a debug line too rather than being invented here.
|
|
355
|
+
this.log.debugWarn = (...args) => this.log.debug(...args)
|
|
354
356
|
|
|
355
357
|
// Set up the discovery run counter
|
|
356
358
|
this.discoveryRun = -1
|
|
@@ -454,34 +456,38 @@ export default class {
|
|
|
454
456
|
res.end()
|
|
455
457
|
})
|
|
456
458
|
|
|
459
|
+
// A failure to listen - the configured port already taken, or a privileged
|
|
460
|
+
// port without the rights to use it - arrives as an 'error' event, not
|
|
461
|
+
// through the listen callback. Node throws an 'error' event that nobody is
|
|
462
|
+
// listening for, so without this the bridge dies on a port clash and
|
|
463
|
+
// restarts into the same clash. The callback below only ever runs on
|
|
464
|
+
// success, and node calls it with no arguments.
|
|
465
|
+
listenerServer.on('error', (err) => {
|
|
466
|
+
this.log.warn('%s: %s.', platformLang.listenerError, parseError(err))
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
const logListenerPort = () => {
|
|
470
|
+
this.log.debug('%s [%s].', platformLang.listenerPort, listenerServer.address().port)
|
|
471
|
+
}
|
|
472
|
+
|
|
457
473
|
// Start listening on the above created server
|
|
458
474
|
if (this.config.wemoClient.listen_interface) {
|
|
459
475
|
// User has defined a specific network interface to listen on
|
|
460
|
-
listenerServer.listen(this.config.wemoClient.port, this.getLocalInterfaceAddress(),
|
|
461
|
-
if (err) {
|
|
462
|
-
this.log.warn('%s: %s.', platformLang.listenerError, err)
|
|
463
|
-
} else if (this.config.debug) {
|
|
464
|
-
// Log the port of the listener server in debug mode
|
|
465
|
-
this.log('%s [%s].', platformLang.listenerPort, listenerServer.address().port)
|
|
466
|
-
}
|
|
467
|
-
})
|
|
476
|
+
listenerServer.listen(this.config.wemoClient.port, this.getLocalInterfaceAddress(), logListenerPort)
|
|
468
477
|
} else {
|
|
469
478
|
// User has not defined a specific network interface to listen on
|
|
470
|
-
listenerServer.listen(this.config.wemoClient.port,
|
|
471
|
-
if (err) {
|
|
472
|
-
this.log.warn('%s: %s', platformLang.listenerError, err)
|
|
473
|
-
} else if (this.config.debug) {
|
|
474
|
-
// Log the port of the listener server in debug mode
|
|
475
|
-
this.log('%s [%s].', platformLang.listenerPort, listenerServer.address().port)
|
|
476
|
-
}
|
|
477
|
-
})
|
|
479
|
+
listenerServer.listen(this.config.wemoClient.port, logListenerPort)
|
|
478
480
|
}
|
|
479
481
|
|
|
480
482
|
// Set up the SSDP client if the user has not specified manual devices only
|
|
481
483
|
if (this.config.mode !== 'manual') {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
// The discovery client's own chatter goes to the debug log, where
|
|
485
|
+
// Homebridge decides whether it is shown. It used to be handed the
|
|
486
|
+
// normal logger only when the plugin thought debug was on, which it
|
|
487
|
+
// could not tell in a child bridge. The wrapper matters: Homebridge's
|
|
488
|
+
// `debug` is not bound to its logger, so passing it bare would throw
|
|
489
|
+
// the first time the discovery client tried to log anything.
|
|
490
|
+
this.config.wemoClient.discover_opts.customLogger = (...args) => this.log.debug(...args)
|
|
485
491
|
ssdpClient = new ssdp(this.config.wemoClient.discover_opts)
|
|
486
492
|
}
|
|
487
493
|
|
|
@@ -511,19 +517,13 @@ export default class {
|
|
|
511
517
|
|
|
512
518
|
// Shutdown the listener server if it's running
|
|
513
519
|
if (listenerServer) {
|
|
514
|
-
listenerServer.close(() =>
|
|
515
|
-
if (this.config.debug) {
|
|
516
|
-
this.log('%s.', platformLang.listenerClosed)
|
|
517
|
-
}
|
|
518
|
-
})
|
|
520
|
+
listenerServer.close(() => this.log.debug('%s.', platformLang.listenerClosed))
|
|
519
521
|
}
|
|
520
522
|
|
|
521
523
|
// Stop the SSDP client if it's running
|
|
522
524
|
if (ssdpClient) {
|
|
523
525
|
ssdpClient.stop()
|
|
524
|
-
|
|
525
|
-
this.log('%s.', platformLang.ssdpStopped)
|
|
526
|
-
}
|
|
526
|
+
this.log.debug('%s.', platformLang.ssdpStopped)
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
// Close accessory subscriptions
|
|
@@ -778,22 +778,21 @@ export default class {
|
|
|
778
778
|
}
|
|
779
779
|
|
|
780
780
|
applyAccessoryLogging(accessory) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
793
|
-
}
|
|
794
|
-
accessory.logDebug = () => {}
|
|
795
|
-
accessory.logDebugWarn = () => {}
|
|
781
|
+
// Debug lines always go to Homebridge's debug logger, which prints them only
|
|
782
|
+
// when debug is on - so there is nothing to decide here. Working it out from
|
|
783
|
+
// the process arguments instead is what lost debug logging entirely for
|
|
784
|
+
// anyone running the plugin in a child bridge.
|
|
785
|
+
accessory.logDebug = msg => this.log.debug('[%s] %s.', accessory.displayName, msg)
|
|
786
|
+
accessory.logDebugWarn = accessory.logDebug
|
|
787
|
+
|
|
788
|
+
if (this.config.disableDeviceLogging) {
|
|
789
|
+
accessory.log = () => {}
|
|
790
|
+
accessory.logWarn = () => {}
|
|
791
|
+
return
|
|
796
792
|
}
|
|
793
|
+
|
|
794
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
795
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
797
796
|
}
|
|
798
797
|
|
|
799
798
|
async initialiseDevice(device) {
|
package/lib/utils/lang-en.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
accNotFound: 'accessory not found',
|
|
3
3
|
accNotReady: 'has not been discovered yet so commands will fail',
|
|
4
|
-
alDisabled: 'adaptive lighting
|
|
4
|
+
alDisabled: 'adaptive lighting turned off, as the colour was changed outside HomeKit',
|
|
5
5
|
awaiting: 'The following devices have still not been initially found',
|
|
6
6
|
brand: 'Belkin Wemo',
|
|
7
7
|
brightnessFail: 'could not obtain updated brightness as',
|
package/package.json
CHANGED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { decodeXML, generateRandomString, hasProperty, parseError, parseSerialNumber } from './functions.js'
|
|
4
|
-
|
|
5
|
-
describe('decodeXML', () => {
|
|
6
|
-
it('decodes the entities wemo devices put in their soap responses', () => {
|
|
7
|
-
expect(decodeXML('<tag>')).toBe('<tag>')
|
|
8
|
-
expect(decodeXML('Tom & Jerry')).toBe('Tom & Jerry')
|
|
9
|
-
expect(decodeXML('"quoted"')).toBe('"quoted"')
|
|
10
|
-
expect(decodeXML('it's')).toBe('it\'s')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('decodes a realistic nested payload', () => {
|
|
14
|
-
// Wemo wraps its state XML inside another XML document, so the inner
|
|
15
|
-
// document arrives entity-encoded and has to be decoded before parsing.
|
|
16
|
-
expect(decodeXML('<BinaryState>1</BinaryState>'))
|
|
17
|
-
.toBe('<BinaryState>1</BinaryState>')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('leaves text with no entities untouched', () => {
|
|
21
|
-
expect(decodeXML('plain text')).toBe('plain text')
|
|
22
|
-
expect(decodeXML('')).toBe('')
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('decodes an escaped ampersand entity twice, because & is replaced first', () => {
|
|
26
|
-
// ⚠️ Documenting real behaviour, not endorsing it. The replacements run in
|
|
27
|
-
// order with & first, so "&lt;" becomes "<" and is then decoded
|
|
28
|
-
// again to "<". Anything relying on a literal "<" surviving would be
|
|
29
|
-
// disappointed - worth knowing if this is ever reused elsewhere.
|
|
30
|
-
expect(decodeXML('&lt;')).toBe('<')
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe('parseSerialNumber', () => {
|
|
35
|
-
it('uppercases and strips the spacing users paste in', () => {
|
|
36
|
-
expect(parseSerialNumber('abc 123')).toBe('ABC123')
|
|
37
|
-
expect(parseSerialNumber(' abc123 ')).toBe('ABC123')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('strips quotes, which get added when copying from a log', () => {
|
|
41
|
-
expect(parseSerialNumber('"ABC123"')).toBe('ABC123')
|
|
42
|
-
expect(parseSerialNumber('\'ABC123\'')).toBe('ABC123')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('accepts a non-string value without throwing', () => {
|
|
46
|
-
expect(parseSerialNumber(123456)).toBe('123456')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('leaves an already-clean serial alone', () => {
|
|
50
|
-
expect(parseSerialNumber('ABC123')).toBe('ABC123')
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('generateRandomString', () => {
|
|
55
|
-
it('returns the requested length', () => {
|
|
56
|
-
expect(generateRandomString(16)).toHaveLength(16)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('uses only lowercase letters and digits', () => {
|
|
60
|
-
expect(generateRandomString(200)).toMatch(/^[a-z0-9]+$/)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('does not return the same value twice in a row', () => {
|
|
64
|
-
expect(generateRandomString(32)).not.toBe(generateRandomString(32))
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('hasProperty', () => {
|
|
69
|
-
it('detects own properties only', () => {
|
|
70
|
-
expect(hasProperty({ a: 1 }, 'a')).toBe(true)
|
|
71
|
-
expect(hasProperty({ a: undefined }, 'a')).toBe(true)
|
|
72
|
-
expect(hasProperty({}, 'a')).toBe(false)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('ignores inherited properties', () => {
|
|
76
|
-
expect(hasProperty({}, 'toString')).toBe(false)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('parseError', () => {
|
|
81
|
-
it('appends the first stack frame to the message', () => {
|
|
82
|
-
const err = new Error('boom')
|
|
83
|
-
err.stack = 'Error: boom\n at thing (/a.js:1:1)'
|
|
84
|
-
expect(parseError(err)).toBe('boom at thing (/a.js:1:1)')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('hides the stack for errors the caller expects', () => {
|
|
88
|
-
const err = new Error('EHOSTUNREACH')
|
|
89
|
-
err.stack = 'Error: EHOSTUNREACH\n at thing (/a.js:1:1)'
|
|
90
|
-
expect(parseError(err, ['EHOSTUNREACH'])).toBe('EHOSTUNREACH')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('returns the message when there is no stack', () => {
|
|
94
|
-
const err = new Error('nostack')
|
|
95
|
-
err.stack = ''
|
|
96
|
-
expect(parseError(err)).toBe('nostack')
|
|
97
|
-
})
|
|
98
|
-
})
|