homebridge-bluos 1.1.0 → 1.1.2
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 +14 -0
- package/README.md +4 -4
- package/config.schema.json +2 -2
- package/dist/devices/host.d.ts +8 -0
- package/dist/devices/reboot-accessory.js +4 -1
- package/dist/devices/reboot-all-accessory.d.ts +5 -4
- package/dist/devices/reboot-all-accessory.js +21 -11
- package/dist/platform.d.ts +12 -0
- package/dist/platform.js +31 -0
- package/dist/poller.d.ts +8 -0
- package/dist/poller.js +11 -0
- package/dist/settings.d.ts +9 -0
- package/dist/settings.js +10 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/reboot-grace.d.ts +51 -0
- package/dist/utils/reboot-grace.js +87 -0
- package/docs/FEATURES.md +3 -3
- package/docs/README-DETAILED.md +32 -9
- package/homebridge-ui/public/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.2](https://github.com/tbaur/homebridge-bluos/compare/v1.1.1...v1.1.2) (2026-08-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* do not end the reboot quiet window on a last-gasp poll ([#26](https://github.com/tbaur/homebridge-bluos/issues/26)) ([91651a4](https://github.com/tbaur/homebridge-bluos/commit/91651a4c4b114c8a8e7bfba0cc7c4d6148375b7d))
|
|
9
|
+
|
|
10
|
+
## [1.1.1](https://github.com/tbaur/homebridge-bluos/compare/v1.1.0...v1.1.1) (2026-08-30)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* keep accessories quiet while a rebooted player comes back ([#24](https://github.com/tbaur/homebridge-bluos/issues/24)) ([dc4f792](https://github.com/tbaur/homebridge-bluos/commit/dc4f7921cb584697e5e49f92ec0207642d88a541))
|
|
16
|
+
|
|
3
17
|
## [1.1.0](https://github.com/tbaur/homebridge-bluos/compare/v1.0.3...v1.1.0) (2026-08-30)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -14,13 +14,13 @@ Browsing, search, queues and artwork stay in the BluOS app, which HomeKit cannot
|
|
|
14
14
|
- **Mute switch:** unmuting restores the level the player remembered, not a guess
|
|
15
15
|
- **Volume presets:** one switch per exact level, addressable by name with Siri
|
|
16
16
|
- **Battery sensor:** level, charging state and low-battery warning, for players with a battery pack
|
|
17
|
-
- **Reboot switch:** momentary, so a scene cannot
|
|
17
|
+
- **Reboot switch:** momentary, so a scene cannot reboot your stereo, and still pressable when the player has stopped answering
|
|
18
18
|
- **Grouping-aware:** a zone leading a BluOS group moves the whole group, exactly as it does in the BluOS app
|
|
19
19
|
- **Multi-zone chassis:** each zone of a NAD CI S2 or CI 580 is its own player, on its own port
|
|
20
20
|
|
|
21
21
|
### For the whole install
|
|
22
22
|
|
|
23
|
-
- **Reboot all:** off by default. One switch that
|
|
23
|
+
- **Reboot all:** off by default. One switch that reboots every BluOS player it can find on the network. The info log is a count (`found 2 device(s), 3 player(s)`); the debug log names each box
|
|
24
24
|
|
|
25
25
|
### Reliability
|
|
26
26
|
|
|
@@ -111,7 +111,7 @@ Only one `BluOS` platform block is supported. It can hold as many players as you
|
|
|
111
111
|
| `devices` | ✓ | List of players |
|
|
112
112
|
| `options.sliderService` | | `fan` (default) or `lightbulb`, for every slider |
|
|
113
113
|
| `options.discoveryTimeoutSec` | | mDNS listening window, 1–30 seconds (default 5) |
|
|
114
|
-
| `options.rebootAll` | | Expose one switch that
|
|
114
|
+
| `options.rebootAll` | | Expose one switch that reboots **every BluOS player on the network** (default false) |
|
|
115
115
|
| `options.rebootAllName` | | What that switch is called in the Home app |
|
|
116
116
|
|
|
117
117
|
Each entry in `devices[]` takes `id`, `name` and `host`, plus an optional `port`, `volumeSlider`, `sliderService`, `mute`, `battery`, `reboot` and `volumePresets[]`. The [detailed documentation](docs/README-DETAILED.md#devices-entries) describes each one, and explains which changes are safe to make to a working install.
|
|
@@ -121,7 +121,7 @@ Each entry in `devices[]` takes `id`, `name` and `host`, plus an optional `port`
|
|
|
121
121
|
1. **Nothing found by Discover Players.** mDNS is often filtered across VLANs and by some access points. Use manual address entry, or add the player by hand
|
|
122
122
|
2. **Everything shows No Response and stays that way.** Check the log for `BluOS is disabled until its configuration is fixed`
|
|
123
123
|
3. **A zone on a multi-zone chassis is missing.** Check the port. Zone two is 11010, not 11000
|
|
124
|
-
4. **A reboot switch
|
|
124
|
+
4. **A reboot switch rebooted the room next door.** Expected on a multi-zone chassis. BluOS serves reboot per box, not per zone
|
|
125
125
|
5. **The reboot switch turns itself off.** By design. It is a button, not a state
|
|
126
126
|
6. Restart Homebridge after editing `config.json` by hand
|
|
127
127
|
|
package/config.schema.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"title": "Reboot All Switch",
|
|
43
43
|
"type": "boolean",
|
|
44
44
|
"default": false,
|
|
45
|
-
"description": "One switch that
|
|
45
|
+
"description": "One switch that reboots every BluOS player it can find. Its reach is the whole network, not just the players listed below: pressing it also reboots players you have deliberately left out of this configuration. The info log is a count of devices and players; the debug log names each box before anything is sent."
|
|
46
46
|
},
|
|
47
47
|
"rebootAllName": {
|
|
48
48
|
"title": "Reboot All Name",
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"title": "Reboot Switch",
|
|
126
126
|
"type": "boolean",
|
|
127
127
|
"default": false,
|
|
128
|
-
"description": "A switch that
|
|
128
|
+
"description": "A switch that reboots this player. It springs back to off on its own and does nothing when switched off, so a scene or \"turn everything off\" cannot trigger it. Rebooting interrupts playback. On a multi-zone chassis such as a CI S2 it reboots every zone on that box: BluOS reboots the box, not the zone, and there is no way to reboot just this room."
|
|
129
129
|
},
|
|
130
130
|
"volumePresets": {
|
|
131
131
|
"title": "Volume Presets",
|
package/dist/devices/host.d.ts
CHANGED
|
@@ -74,4 +74,12 @@ export interface AccessoryHost {
|
|
|
74
74
|
* loud rather than letting someone discover it.
|
|
75
75
|
*/
|
|
76
76
|
playersSharingAddress(deviceId: string): readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* A reboot request has reached this address, so silence from it is expected.
|
|
79
|
+
*
|
|
80
|
+
* The poller will fail while the box is down. Calling this is what stops every
|
|
81
|
+
* accessory on that address from warning, and what makes the next successful
|
|
82
|
+
* poll the source of truth for HomeKit instead of the pre-reboot reading.
|
|
83
|
+
*/
|
|
84
|
+
expectReboot(host: string): void;
|
|
77
85
|
}
|
|
@@ -51,7 +51,7 @@ class RebootAccessory extends base_accessory_1.BaseAccessory {
|
|
|
51
51
|
.onSet(async (value) => this.writeOn(value));
|
|
52
52
|
const shared = this.host.playersSharingAddress(this.deviceId);
|
|
53
53
|
if (shared.length > 0) {
|
|
54
|
-
this.host.log.warn(`${(0, utils_1.forLog)(this.displayName)} will also
|
|
54
|
+
this.host.log.warn(`${(0, utils_1.forLog)(this.displayName)}: will also reboot ${shared.map(utils_1.forLog).join(', ')}: `
|
|
55
55
|
+ 'they are zones of one chassis, and BluOS reboots the whole box');
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -67,6 +67,9 @@ class RebootAccessory extends base_accessory_1.BaseAccessory {
|
|
|
67
67
|
}
|
|
68
68
|
// The host only: reboot lives on port 80, not on the zone's control port.
|
|
69
69
|
const result = await this.host.client.reboot(endpoint.host);
|
|
70
|
+
// The box is going down. Tell the platform so the other accessories
|
|
71
|
+
// stay quiet, and so the next poll is what HomeKit shows.
|
|
72
|
+
this.host.expectReboot(endpoint.host);
|
|
70
73
|
// Never a group operation. Grouping decides where a *volume* change
|
|
71
74
|
// reaches; a reboot restarts a box and has no notion of followers.
|
|
72
75
|
this.logAction(result.acknowledged ? 'REBOOT' : 'REBOOT (sent; the player stopped answering, as expected)', { tellSlaves: false });
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Its reach is wider than the plugin's configuration: it restarts every player
|
|
15
15
|
* mDNS answers for, including ones deliberately left out of `devices[]`. That is
|
|
16
|
-
* what it is for, and it is why the option is off by default
|
|
17
|
-
*
|
|
18
|
-
* authentication, so anything on the
|
|
16
|
+
* what it is for, and it is why the option is off by default. The info log is a
|
|
17
|
+
* count of devices and players; the debug log names every box before a single
|
|
18
|
+
* request goes out. The BluOS API has no authentication, so anything on the
|
|
19
|
+
* segment will comply.
|
|
19
20
|
*
|
|
20
21
|
* It works in addresses rather than players, because reboot is served on port 80
|
|
21
22
|
* and port 80 is one server per chassis. A CI S2 carrying two zones is one
|
|
@@ -34,7 +35,7 @@ export declare class RebootAllAccessory extends BaseAccessory {
|
|
|
34
35
|
private resetTimer;
|
|
35
36
|
constructor(init: AccessoryInit);
|
|
36
37
|
private writeOn;
|
|
37
|
-
/**
|
|
38
|
+
/** Count at info, name every box at debug, before any request goes out. */
|
|
38
39
|
private announce;
|
|
39
40
|
/**
|
|
40
41
|
* Restart every target, letting each succeed or fail on its own.
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Its reach is wider than the plugin's configuration: it restarts every player
|
|
16
16
|
* mDNS answers for, including ones deliberately left out of `devices[]`. That is
|
|
17
|
-
* what it is for, and it is why the option is off by default
|
|
18
|
-
*
|
|
19
|
-
* authentication, so anything on the
|
|
17
|
+
* what it is for, and it is why the option is off by default. The info log is a
|
|
18
|
+
* count of devices and players; the debug log names every box before a single
|
|
19
|
+
* request goes out. The BluOS API has no authentication, so anything on the
|
|
20
|
+
* segment will comply.
|
|
20
21
|
*
|
|
21
22
|
* It works in addresses rather than players, because reboot is served on port 80
|
|
22
23
|
* and port 80 is one server per chassis. A CI S2 carrying two zones is one
|
|
@@ -54,7 +55,7 @@ class RebootAllAccessory extends base_accessory_1.BaseAccessory {
|
|
|
54
55
|
await this.completeWithinBudget('reboot all', async () => {
|
|
55
56
|
const targets = await this.host.rebootTargets();
|
|
56
57
|
if (targets.length === 0) {
|
|
57
|
-
this.host.log.warn(`${(0, utils_1.forLog)(this.displayName)} found nothing to
|
|
58
|
+
this.host.log.warn(`${(0, utils_1.forLog)(this.displayName)}: found nothing to reboot. `
|
|
58
59
|
+ 'Multicast may be filtered on this network and no players are configured');
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
@@ -66,13 +67,14 @@ class RebootAllAccessory extends base_accessory_1.BaseAccessory {
|
|
|
66
67
|
this.scheduleReset();
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
/**
|
|
70
|
+
/** Count at info, name every box at debug, before any request goes out. */
|
|
70
71
|
announce(targets) {
|
|
72
|
+
const players = playerCount(targets);
|
|
73
|
+
this.host.log.info(`${(0, utils_1.forLog)(this.displayName)}: found ${targets.length} device(s), ${players} player(s)`);
|
|
71
74
|
const listed = targets
|
|
72
75
|
.map((target) => `${target.host} (${target.names.map(utils_1.forLog).join(', ')})`)
|
|
73
76
|
.join('; ');
|
|
74
|
-
|
|
75
|
-
this.host.log.info(`${(0, utils_1.forLog)(this.displayName)}: rebooting ${targets.length} box(es) carrying `
|
|
77
|
+
this.host.log.debug(`${(0, utils_1.forLog)(this.displayName)}: rebooting ${targets.length} box(es) carrying `
|
|
76
78
|
+ `${players} player(s): ${listed}`);
|
|
77
79
|
}
|
|
78
80
|
/**
|
|
@@ -89,15 +91,19 @@ class RebootAllAccessory extends base_accessory_1.BaseAccessory {
|
|
|
89
91
|
let failed = 0;
|
|
90
92
|
outcomes.forEach((outcome, index) => {
|
|
91
93
|
const target = targets[index];
|
|
92
|
-
if (
|
|
94
|
+
if (target === undefined) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (outcome.status === 'fulfilled') {
|
|
98
|
+
this.host.expectReboot(target.host);
|
|
93
99
|
return;
|
|
94
100
|
}
|
|
95
101
|
failed += 1;
|
|
96
|
-
this.host.log.warn(`${(0, utils_1.forLog)(this.displayName)} could not reboot ${target.host} `
|
|
102
|
+
this.host.log.warn(`${(0, utils_1.forLog)(this.displayName)}: could not reboot ${target.host} `
|
|
97
103
|
+ `(${target.names.map(utils_1.forLog).join(', ')}): ${(0, utils_1.describeError)(outcome.reason)}`);
|
|
98
104
|
});
|
|
99
|
-
const
|
|
100
|
-
this.host.log.info(`${(0, utils_1.forLog)(this.displayName)}: ${
|
|
105
|
+
const rebooted = targets.length - failed;
|
|
106
|
+
this.host.log.info(`${(0, utils_1.forLog)(this.displayName)}: ${rebooted} of ${targets.length} device(s) rebooted`);
|
|
101
107
|
}
|
|
102
108
|
/** Spring the tile back to off, the way a real button returns. */
|
|
103
109
|
scheduleReset() {
|
|
@@ -131,3 +137,7 @@ class RebootAllAccessory extends base_accessory_1.BaseAccessory {
|
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
exports.RebootAllAccessory = RebootAllAccessory;
|
|
140
|
+
/** How many player names sit behind the given boxes. */
|
|
141
|
+
function playerCount(targets) {
|
|
142
|
+
return targets.reduce((total, target) => total + target.names.length, 0);
|
|
143
|
+
}
|
package/dist/platform.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export declare class BluOSPlatform implements DynamicPlatformPlugin, AccessoryHo
|
|
|
51
51
|
private readonly staggerTimers;
|
|
52
52
|
/** The launch address sweep, tracked so a shutdown can wait for it to end. */
|
|
53
53
|
private launchSweep;
|
|
54
|
+
/** Addresses we have just asked to reboot, while silence from them is expected. */
|
|
55
|
+
private readonly rebootGrace;
|
|
54
56
|
constructor(log: Logging, config: PlatformConfig, api: API);
|
|
55
57
|
get hap(): API['hap'];
|
|
56
58
|
/** Homebridge hands back every accessory it restored from disk. */
|
|
@@ -98,6 +100,16 @@ export declare class BluOSPlatform implements DynamicPlatformPlugin, AccessoryHo
|
|
|
98
100
|
* not asked to manage.
|
|
99
101
|
*/
|
|
100
102
|
playersSharingAddress(deviceId: string): readonly string[];
|
|
103
|
+
/**
|
|
104
|
+
* A reboot request has reached this address.
|
|
105
|
+
*
|
|
106
|
+
* In-flight long-polls are dropped so the next reading is of the player after
|
|
107
|
+
* it comes back, not of the request that died with the box. Failures during
|
|
108
|
+
* the grace window do not mark accessories unreachable.
|
|
109
|
+
*/
|
|
110
|
+
expectReboot(host: string): void;
|
|
111
|
+
/** Current address of a player, preferring the poller when it has one. */
|
|
112
|
+
private hostOf;
|
|
101
113
|
private start;
|
|
102
114
|
private stop;
|
|
103
115
|
/**
|
package/dist/platform.js
CHANGED
|
@@ -58,6 +58,8 @@ class BluOSPlatform {
|
|
|
58
58
|
staggerTimers = new Set();
|
|
59
59
|
/** The launch address sweep, tracked so a shutdown can wait for it to end. */
|
|
60
60
|
launchSweep;
|
|
61
|
+
/** Addresses we have just asked to reboot, while silence from them is expected. */
|
|
62
|
+
rebootGrace = new utils_1.RebootGrace(settings_1.REBOOT_GRACE_MS);
|
|
61
63
|
constructor(log, config, api) {
|
|
62
64
|
this.log = log;
|
|
63
65
|
this.config = config;
|
|
@@ -198,6 +200,26 @@ class BluOSPlatform {
|
|
|
198
200
|
&& (this.endpointFor(device.id)?.host ?? device.host) === host)
|
|
199
201
|
.map((device) => device.name);
|
|
200
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* A reboot request has reached this address.
|
|
205
|
+
*
|
|
206
|
+
* In-flight long-polls are dropped so the next reading is of the player after
|
|
207
|
+
* it comes back, not of the request that died with the box. Failures during
|
|
208
|
+
* the grace window do not mark accessories unreachable.
|
|
209
|
+
*/
|
|
210
|
+
expectReboot(host) {
|
|
211
|
+
this.rebootGrace.expect(host);
|
|
212
|
+
for (const [deviceId, poller] of this.pollers) {
|
|
213
|
+
if (this.hostOf(deviceId) === host) {
|
|
214
|
+
poller.refreshNow();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Current address of a player, preferring the poller when it has one. */
|
|
219
|
+
hostOf(deviceId) {
|
|
220
|
+
return this.endpointFor(deviceId)?.host
|
|
221
|
+
?? this.devices.find((device) => device.id === deviceId)?.host;
|
|
222
|
+
}
|
|
201
223
|
// --- Lifecycle ------------------------------------------------------------
|
|
202
224
|
start() {
|
|
203
225
|
const result = (0, utils_1.validateConfig)(this.config);
|
|
@@ -507,6 +529,10 @@ class BluOSPlatform {
|
|
|
507
529
|
}
|
|
508
530
|
}
|
|
509
531
|
publish(deviceId, observation, reason) {
|
|
532
|
+
// Ends the window only after the box has already gone quiet. A last-gasp
|
|
533
|
+
// reading while the ports are still up must not cancel it, or the real
|
|
534
|
+
// outage that follows is logged as a surprise.
|
|
535
|
+
this.rebootGrace.clearIfRecovered(this.hostOf(deviceId));
|
|
510
536
|
for (const handler of this.handlers.get(deviceId) ?? []) {
|
|
511
537
|
try {
|
|
512
538
|
handler.applyObservation(observation, reason);
|
|
@@ -517,6 +543,11 @@ class BluOSPlatform {
|
|
|
517
543
|
}
|
|
518
544
|
}
|
|
519
545
|
reportUnavailable(deviceId, error) {
|
|
546
|
+
const host = this.hostOf(deviceId);
|
|
547
|
+
if (this.rebootGrace.isExpected(host)) {
|
|
548
|
+
this.rebootGrace.noteSilence(host);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
520
551
|
for (const handler of this.handlers.get(deviceId) ?? []) {
|
|
521
552
|
// Guarded exactly like publish. This runs from inside the poll loop's catch
|
|
522
553
|
// block, so a throw here — a characteristic missing from a hand-edited
|
package/dist/poller.d.ts
CHANGED
|
@@ -79,6 +79,14 @@ export declare class DevicePoller {
|
|
|
79
79
|
* update available.
|
|
80
80
|
*/
|
|
81
81
|
adoptWriteResult(result: VolumeResult): void;
|
|
82
|
+
/**
|
|
83
|
+
* Drop the request in flight and start a plain read.
|
|
84
|
+
*
|
|
85
|
+
* Used after a reboot: the long-poll is aimed at a box that is already going
|
|
86
|
+
* down, and waiting it out only delays the first reading of whatever state the
|
|
87
|
+
* player comes back in.
|
|
88
|
+
*/
|
|
89
|
+
refreshNow(): void;
|
|
82
90
|
/** Cancel the request in flight and wake any backoff sleep. */
|
|
83
91
|
private interrupt;
|
|
84
92
|
private run;
|
package/dist/poller.js
CHANGED
|
@@ -145,6 +145,17 @@ class DevicePoller {
|
|
|
145
145
|
this.options.onObservation(merged, 'post-set');
|
|
146
146
|
this.interrupt();
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Drop the request in flight and start a plain read.
|
|
150
|
+
*
|
|
151
|
+
* Used after a reboot: the long-poll is aimed at a box that is already going
|
|
152
|
+
* down, and waiting it out only delays the first reading of whatever state the
|
|
153
|
+
* player comes back in.
|
|
154
|
+
*/
|
|
155
|
+
refreshNow() {
|
|
156
|
+
this.etag = undefined;
|
|
157
|
+
this.interrupt();
|
|
158
|
+
}
|
|
148
159
|
/** Cancel the request in flight and wake any backoff sleep. */
|
|
149
160
|
interrupt() {
|
|
150
161
|
this.abort?.abort();
|
package/dist/settings.d.ts
CHANGED
|
@@ -99,6 +99,15 @@ export declare const REBOOT_TIMEOUT_MS = 3000;
|
|
|
99
99
|
export declare const REBOOT_RESOURCE = "reboot";
|
|
100
100
|
/** @see REBOOT_RESOURCE */
|
|
101
101
|
export declare const REBOOT_FORM: Readonly<Record<string, string>>;
|
|
102
|
+
/**
|
|
103
|
+
* How long after a reboot request we treat silence as the player coming back.
|
|
104
|
+
*
|
|
105
|
+
* A reboot takes the control ports down with the box. Polls fail, and without
|
|
106
|
+
* this window every accessory on that box would warn "is not responding" and
|
|
107
|
+
* then info "is responding again" — which is exactly what a reboot looks like.
|
|
108
|
+
* After this window a still-silent player is logged the usual way.
|
|
109
|
+
*/
|
|
110
|
+
export declare const REBOOT_GRACE_MS = 90000;
|
|
102
111
|
/** Minimum spacing between control calls to one endpoint. */
|
|
103
112
|
export declare const CONTROL_RATE_LIMIT_MS = 100;
|
|
104
113
|
/** First reconnect delay after a failed poll. Doubles up to the ceiling. */
|
package/dist/settings.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* can tell a vendor requirement apart from a judgement call.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.MAX_NAME_LENGTH = exports.MAX_LOG_FIELD_LENGTH = exports.DISCOVERY_VERIFY_CONCURRENCY = exports.MAX_DISCOVERY_CANDIDATES = exports.MAX_DISCOVERY_RECORDS = exports.REDISCOVERY_MIN_INTERVAL_MS = exports.FAILURES_BEFORE_REDISCOVERY = exports.MAX_DISCOVERY_TIMEOUT_SEC = exports.MIN_DISCOVERY_TIMEOUT_SEC = exports.DEFAULT_DISCOVERY_TIMEOUT_SEC = exports.MDNS_SERVICE_SECONDARY = exports.MDNS_SERVICE_PRIMARY = exports.MAX_XML_ATTRIBUTES = exports.MAX_XML_ELEMENTS = exports.MAX_XML_DEPTH = exports.MAX_XML_BYTES = exports.MUTED_DB_SENTINEL = exports.FIXED_VOLUME_SENTINEL = exports.VOLUME_MAX = exports.VOLUME_MIN = exports.MOMENTARY_RESET_MS = exports.SLIDER_COALESCE_MS = exports.DEFAULT_RESTORE_VOLUME = exports.HOMEKIT_WRITE_BUDGET_MS = exports.POLL_FAILURE_REWARN_MS = exports.POLL_FAILURES_BEFORE_UNKNOWN = exports.POLL_BACKOFF_MAX_MS = exports.POLL_BACKOFF_BASE_MS = exports.CONTROL_RATE_LIMIT_MS = exports.REBOOT_FORM = exports.REBOOT_RESOURCE = exports.REBOOT_TIMEOUT_MS = exports.STATUS_TIMEOUT_MS = exports.CONTROL_TIMEOUT_MS = exports.CONNECT_TIMEOUT_MS = exports.SAME_RESOURCE_MIN_GAP_MS = exports.LONG_POLL_READ_SLACK_MS = exports.LONG_POLL_SEC = exports.MAX_PORT = exports.MIN_PORT = exports.DOCUMENTED_BLUOS_PORTS = exports.DEFAULT_BLUOS_PORT = exports.DEFAULT_MODEL = exports.DEFAULT_BRAND = exports.UNKNOWN_PLUGIN_VERSION = exports.PLATFORM_DEVICE_ID = exports.UUID_PREFIX = exports.PLATFORM_NAME = exports.PLUGIN_NAME = void 0;
|
|
15
|
+
exports.MAX_NAME_LENGTH = exports.MAX_LOG_FIELD_LENGTH = exports.DISCOVERY_VERIFY_CONCURRENCY = exports.MAX_DISCOVERY_CANDIDATES = exports.MAX_DISCOVERY_RECORDS = exports.REDISCOVERY_MIN_INTERVAL_MS = exports.FAILURES_BEFORE_REDISCOVERY = exports.MAX_DISCOVERY_TIMEOUT_SEC = exports.MIN_DISCOVERY_TIMEOUT_SEC = exports.DEFAULT_DISCOVERY_TIMEOUT_SEC = exports.MDNS_SERVICE_SECONDARY = exports.MDNS_SERVICE_PRIMARY = exports.MAX_XML_ATTRIBUTES = exports.MAX_XML_ELEMENTS = exports.MAX_XML_DEPTH = exports.MAX_XML_BYTES = exports.MUTED_DB_SENTINEL = exports.FIXED_VOLUME_SENTINEL = exports.VOLUME_MAX = exports.VOLUME_MIN = exports.MOMENTARY_RESET_MS = exports.SLIDER_COALESCE_MS = exports.DEFAULT_RESTORE_VOLUME = exports.HOMEKIT_WRITE_BUDGET_MS = exports.POLL_FAILURE_REWARN_MS = exports.POLL_FAILURES_BEFORE_UNKNOWN = exports.POLL_BACKOFF_MAX_MS = exports.POLL_BACKOFF_BASE_MS = exports.CONTROL_RATE_LIMIT_MS = exports.REBOOT_GRACE_MS = exports.REBOOT_FORM = exports.REBOOT_RESOURCE = exports.REBOOT_TIMEOUT_MS = exports.STATUS_TIMEOUT_MS = exports.CONTROL_TIMEOUT_MS = exports.CONNECT_TIMEOUT_MS = exports.SAME_RESOURCE_MIN_GAP_MS = exports.LONG_POLL_READ_SLACK_MS = exports.LONG_POLL_SEC = exports.MAX_PORT = exports.MIN_PORT = exports.DOCUMENTED_BLUOS_PORTS = exports.DEFAULT_BLUOS_PORT = exports.DEFAULT_MODEL = exports.DEFAULT_BRAND = exports.UNKNOWN_PLUGIN_VERSION = exports.PLATFORM_DEVICE_ID = exports.UUID_PREFIX = exports.PLATFORM_NAME = exports.PLUGIN_NAME = void 0;
|
|
16
16
|
exports.readPluginVersion = readPluginVersion;
|
|
17
17
|
/** npm package name. Must match `package.json` `name` for Homebridge to load us. */
|
|
18
18
|
exports.PLUGIN_NAME = 'homebridge-bluos';
|
|
@@ -104,6 +104,15 @@ exports.REBOOT_TIMEOUT_MS = 3_000;
|
|
|
104
104
|
exports.REBOOT_RESOURCE = 'reboot';
|
|
105
105
|
/** @see REBOOT_RESOURCE */
|
|
106
106
|
exports.REBOOT_FORM = { noheader: '0', yes: '1' };
|
|
107
|
+
/**
|
|
108
|
+
* How long after a reboot request we treat silence as the player coming back.
|
|
109
|
+
*
|
|
110
|
+
* A reboot takes the control ports down with the box. Polls fail, and without
|
|
111
|
+
* this window every accessory on that box would warn "is not responding" and
|
|
112
|
+
* then info "is responding again" — which is exactly what a reboot looks like.
|
|
113
|
+
* After this window a still-silent player is logged the usual way.
|
|
114
|
+
*/
|
|
115
|
+
exports.REBOOT_GRACE_MS = 90_000;
|
|
107
116
|
/** Minimum spacing between control calls to one endpoint. */
|
|
108
117
|
exports.CONTROL_RATE_LIMIT_MS = 100;
|
|
109
118
|
/** First reconnect delay after a failed poll. Doubles up to the ceiling. */
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -24,6 +24,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
__exportStar(require("./context"), exports);
|
|
26
26
|
__exportStar(require("./errors"), exports);
|
|
27
|
+
__exportStar(require("./reboot-grace"), exports);
|
|
27
28
|
__exportStar(require("./serial"), exports);
|
|
28
29
|
__exportStar(require("./timing"), exports);
|
|
29
30
|
__exportStar(require("./validators"), exports);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 tbaur
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0
|
|
5
|
+
* See LICENSE file for full license text
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview A short window after a reboot request in which silence is expected.
|
|
8
|
+
*
|
|
9
|
+
* Reboot takes the box down. The poller then fails, and every accessory on that
|
|
10
|
+
* address would otherwise warn and later claim to have recovered. That is noise:
|
|
11
|
+
* we asked it to go down. This clock is what lets the platform stay quiet until
|
|
12
|
+
* the player answers again, or until the window ends and a real outage remains.
|
|
13
|
+
*
|
|
14
|
+
* A reading that arrives before the box actually drops is not recovery. The
|
|
15
|
+
* control ports often answer one more time after POST /reboot. That last gasp
|
|
16
|
+
* must not end the window, or the real outage that follows is logged as usual.
|
|
17
|
+
* Recovery is a successful poll after we have already seen the expected silence.
|
|
18
|
+
*/
|
|
19
|
+
/** Hosts we have just asked to reboot, and when that expectation expires. */
|
|
20
|
+
export declare class RebootGrace {
|
|
21
|
+
private readonly graceMs;
|
|
22
|
+
private readonly now;
|
|
23
|
+
private readonly expiresAt;
|
|
24
|
+
/** Addresses that have gone quiet at least once inside their window. */
|
|
25
|
+
private readonly sawSilence;
|
|
26
|
+
constructor(graceMs: number, now?: () => number);
|
|
27
|
+
/** Start (or refresh) the quiet window for this address. */
|
|
28
|
+
expect(host: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Record that this address failed while a reboot was still expected.
|
|
31
|
+
*
|
|
32
|
+
* The next successful poll is then treated as the player coming back, not as
|
|
33
|
+
* a last gasp from a box that has not gone down yet.
|
|
34
|
+
*/
|
|
35
|
+
noteSilence(host: string | undefined): void;
|
|
36
|
+
/**
|
|
37
|
+
* End the window if this address already went quiet and has now answered.
|
|
38
|
+
*
|
|
39
|
+
* A success with no prior silence is ignored: the box has not dropped yet.
|
|
40
|
+
*/
|
|
41
|
+
clearIfRecovered(host: string | undefined): void;
|
|
42
|
+
/**
|
|
43
|
+
* End the window now.
|
|
44
|
+
*
|
|
45
|
+
* Used when the clock runs out, and by tests. Recovery goes through
|
|
46
|
+
* {@link clearIfRecovered} so a last-gasp reading cannot cancel the window.
|
|
47
|
+
*/
|
|
48
|
+
clear(host: string | undefined): void;
|
|
49
|
+
/** True while a reboot of this address is still expected to look like an outage. */
|
|
50
|
+
isExpected(host: string | undefined): boolean;
|
|
51
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 tbaur
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
* See LICENSE file for full license text
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview A short window after a reboot request in which silence is expected.
|
|
9
|
+
*
|
|
10
|
+
* Reboot takes the box down. The poller then fails, and every accessory on that
|
|
11
|
+
* address would otherwise warn and later claim to have recovered. That is noise:
|
|
12
|
+
* we asked it to go down. This clock is what lets the platform stay quiet until
|
|
13
|
+
* the player answers again, or until the window ends and a real outage remains.
|
|
14
|
+
*
|
|
15
|
+
* A reading that arrives before the box actually drops is not recovery. The
|
|
16
|
+
* control ports often answer one more time after POST /reboot. That last gasp
|
|
17
|
+
* must not end the window, or the real outage that follows is logged as usual.
|
|
18
|
+
* Recovery is a successful poll after we have already seen the expected silence.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.RebootGrace = void 0;
|
|
22
|
+
/** Hosts we have just asked to reboot, and when that expectation expires. */
|
|
23
|
+
class RebootGrace {
|
|
24
|
+
graceMs;
|
|
25
|
+
now;
|
|
26
|
+
expiresAt = new Map();
|
|
27
|
+
/** Addresses that have gone quiet at least once inside their window. */
|
|
28
|
+
sawSilence = new Set();
|
|
29
|
+
constructor(graceMs, now = Date.now) {
|
|
30
|
+
this.graceMs = graceMs;
|
|
31
|
+
this.now = now;
|
|
32
|
+
}
|
|
33
|
+
/** Start (or refresh) the quiet window for this address. */
|
|
34
|
+
expect(host) {
|
|
35
|
+
this.expiresAt.set(host, this.now() + this.graceMs);
|
|
36
|
+
this.sawSilence.delete(host);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Record that this address failed while a reboot was still expected.
|
|
40
|
+
*
|
|
41
|
+
* The next successful poll is then treated as the player coming back, not as
|
|
42
|
+
* a last gasp from a box that has not gone down yet.
|
|
43
|
+
*/
|
|
44
|
+
noteSilence(host) {
|
|
45
|
+
if (host !== undefined && this.isExpected(host)) {
|
|
46
|
+
this.sawSilence.add(host);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* End the window if this address already went quiet and has now answered.
|
|
51
|
+
*
|
|
52
|
+
* A success with no prior silence is ignored: the box has not dropped yet.
|
|
53
|
+
*/
|
|
54
|
+
clearIfRecovered(host) {
|
|
55
|
+
if (host !== undefined && this.sawSilence.has(host)) {
|
|
56
|
+
this.clear(host);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* End the window now.
|
|
61
|
+
*
|
|
62
|
+
* Used when the clock runs out, and by tests. Recovery goes through
|
|
63
|
+
* {@link clearIfRecovered} so a last-gasp reading cannot cancel the window.
|
|
64
|
+
*/
|
|
65
|
+
clear(host) {
|
|
66
|
+
if (host !== undefined) {
|
|
67
|
+
this.expiresAt.delete(host);
|
|
68
|
+
this.sawSilence.delete(host);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** True while a reboot of this address is still expected to look like an outage. */
|
|
72
|
+
isExpected(host) {
|
|
73
|
+
if (host === undefined) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const expires = this.expiresAt.get(host);
|
|
77
|
+
if (expires === undefined) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (this.now() >= expires) {
|
|
81
|
+
this.clear(host);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.RebootGrace = RebootGrace;
|
package/docs/FEATURES.md
CHANGED
|
@@ -12,8 +12,8 @@ A checklist of what is built. The plugin aims to cover everything about a BluOS
|
|
|
12
12
|
- ✅ Mute switch, with unmute restoring the level the player remembered instead of a guess
|
|
13
13
|
- ✅ Volume preset switches: one exact level per switch, addressable by name with Siri; set On sets the level, set Off is a no-op
|
|
14
14
|
- ✅ Battery sensor (level, charging state, low-battery) for players with a battery pack fitted
|
|
15
|
-
- ✅ Reboot switch per player, momentary so a scene or "turn everything off" cannot
|
|
16
|
-
- ✅ Optional "reboot all" switch that
|
|
15
|
+
- ✅ Reboot switch per player, momentary so a scene or "turn everything off" cannot reboot the stereo, and still pressable when the player has stopped answering. Reboots the whole box on a multi-zone chassis, which BluOS gives no way to avoid, and warns at startup when that means other configured rooms
|
|
16
|
+
- ✅ Optional "reboot all" switch that reboots every BluOS player on the network, sending once per box instead of once per zone. The info log is a count (`found … device(s), … player(s)`), then `N of N device(s) rebooted`. The debug log names each box before anything is sent. Accessories on a box that was just rebooted stay quiet until it answers again, then take its live state. You choose its name with `options.rebootAllName`, so it can sit in whichever Home app room suits you
|
|
17
17
|
- ✅ Fixed-output players detected from `volume="-1"` and given no slider, with one explanatory log line
|
|
18
18
|
- ✅ Multi-zone chassis support (NAD CI S2, CI 580): each zone is a separate player on its own port
|
|
19
19
|
- ✅ mDNS discovery in the settings page, with manual address entry for networks that filter multicast
|
|
@@ -89,7 +89,7 @@ HomeKit has no way to render a library or a queue, and no vocabulary for "play t
|
|
|
89
89
|
|
|
90
90
|
| Configuration | HomeKit service |
|
|
91
91
|
| --- | --- |
|
|
92
|
-
| `options.rebootAll` | Switch, momentary. One for the whole install,
|
|
92
|
+
| `options.rebootAll` | Switch, momentary. One for the whole install, rebooting every box it can find. Named by `options.rebootAllName` |
|
|
93
93
|
|
|
94
94
|
## Protocol surface
|
|
95
95
|
|
package/docs/README-DETAILED.md
CHANGED
|
@@ -38,9 +38,9 @@ Where HomeKit is genuinely better is the tile, the scene, the automation and the
|
|
|
38
38
|
|
|
39
39
|
**Battery sensor.** Charge level, charging state and low-battery warning, for players with a battery pack fitted (PULSE FLEX with BP100, PULSE M).
|
|
40
40
|
|
|
41
|
-
**Reboot switch.**
|
|
41
|
+
**Reboot switch.** Reboots the player. It springs back to off and ignores being switched off, so a scene or "turn everything off" cannot reboot your stereo. It stays pressable while a player is showing No Response, which is exactly when you want it.
|
|
42
42
|
|
|
43
|
-
On a multi-zone chassis it
|
|
43
|
+
On a multi-zone chassis it reboots both zones. BluOS serves reboot on the box's own web server, not on a zone's control port, so "Study Reboot" on a CI S2 also takes down the other room. There is no way to avoid this. At startup the plugin warns, naming the other affected rooms.
|
|
44
44
|
|
|
45
45
|
**Grouping awareness.** A zone that is leading a BluOS group moves the whole group, exactly as its slider does in the BluOS app. Every other zone moves alone. The scope is stated explicitly on every write, never left to the firmware default.
|
|
46
46
|
|
|
@@ -48,9 +48,9 @@ On a multi-zone chassis it restarts both zones. BluOS serves reboot on the box's
|
|
|
48
48
|
|
|
49
49
|
### For the whole install
|
|
50
50
|
|
|
51
|
-
**Reboot all.** Off by default. One switch that
|
|
51
|
+
**Reboot all.** Off by default. One switch that reboots **every BluOS player it can find on the network**, not only the ones listed in your configuration. It sweeps with mDNS and adds your configured players, so it still works where multicast is filtered.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
The info log is a count (`found 2 device(s), 3 player(s)`), then `2 of 2 device(s) rebooted`. The debug log names every box and the players on it before a request goes out. One press sends one reboot per box, so a multi-zone chassis reboots once and takes all of its zones with it. A box that cannot be reached is a warning (`could not reboot`), and the rest still go.
|
|
54
54
|
|
|
55
55
|
Name it with `options.rebootAllName`. It is the one accessory with no room of its own, so you choose where it lives in the Home app.
|
|
56
56
|
|
|
@@ -88,7 +88,7 @@ Only one `BluOS` platform block is supported (`singular` in the schema). It can
|
|
|
88
88
|
| `devices` | ✓ | List of players. An empty list is allowed and warns. A missing or non-list value is an error |
|
|
89
89
|
| `options.sliderService` | | `fan` (default) or `lightbulb`, for every slider |
|
|
90
90
|
| `options.discoveryTimeoutSec` | | mDNS listening window, 1–30 seconds (default 5) |
|
|
91
|
-
| `options.rebootAll` | | Expose one switch that
|
|
91
|
+
| `options.rebootAll` | | Expose one switch that reboots **every BluOS player on the network** (default false). Its reach is not limited to `devices[]` |
|
|
92
92
|
| `options.rebootAllName` | | What that switch is called in the Home app, so you can keep it in the room you want. Defaults to the plugin name followed by `Reboot All` |
|
|
93
93
|
|
|
94
94
|
### `devices[]` entries
|
|
@@ -103,7 +103,7 @@ Only one `BluOS` platform block is supported (`singular` in the schema). It can
|
|
|
103
103
|
| `sliderService` | | Override the platform slider style for this player. Empty means "use the platform setting". Changing it removes the old control from the accessory instead of leaving both |
|
|
104
104
|
| `mute` | | Expose a mute switch (default false) |
|
|
105
105
|
| `battery` | | Expose a battery sensor (default false, and only meaningful with a battery pack) |
|
|
106
|
-
| `reboot` | | Expose a switch that
|
|
106
|
+
| `reboot` | | Expose a switch that reboots this player (default false). Rebooting interrupts playback, and on a multi-zone chassis it reboots every zone on that box |
|
|
107
107
|
| `volumePresets[]` | | `{ "name": "...", "volume": 0-100 }`. Duplicate levels on one player are skipped with a warning |
|
|
108
108
|
|
|
109
109
|
### A complete example
|
|
@@ -188,6 +188,29 @@ A HomeKit write that reached the player:
|
|
|
188
188
|
|
|
189
189
|
Mute `ON` means muted. `(group)` means that zone is leading a BluOS group, so the write carried the followers.
|
|
190
190
|
|
|
191
|
+
A Reboot All press. Info is the count and the result. Debug names every box.
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
[BluOS] Downstairs Reboot: found 2 device(s), 3 player(s)
|
|
195
|
+
[BluOS] Downstairs Reboot: rebooting 2 box(es) carrying 3 player(s): 192.168.4.11 (Zone One, Zone Two); 192.168.4.12 (Kitchen)
|
|
196
|
+
[BluOS] Downstairs Reboot: 2 of 2 device(s) rebooted
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The middle line is debug. A box that did not take the request is a warning:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
[BluOS] Downstairs Reboot: could not reboot 192.168.4.11 (Zone One, Zone Two): connect EHOSTUNREACH
|
|
203
|
+
[BluOS] Downstairs Reboot: 1 of 2 device(s) rebooted
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A per-player reboot switch on a shared chassis, at startup:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
[BluOS] Zone One Reboot: will also reboot Zone Two: they are zones of one chassis, and BluOS reboots the whole box
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
After a reboot the other accessories stay quiet. The player is expected to stop answering, so they do not log `is not responding` or `is responding again`. A reading that arrives before the box actually drops is ignored for that purpose: the control ports often answer once more after the reboot is sent. The first successful poll after the silence writes whatever the player is actually doing into HomeKit. A player that is still silent after that window is logged as not responding, the usual way.
|
|
213
|
+
|
|
191
214
|
A configuration the plugin will not act on:
|
|
192
215
|
|
|
193
216
|
```text
|
|
@@ -232,9 +255,9 @@ Ordered by how well HomeKit expresses the thing, not by how easy it is to build.
|
|
|
232
255
|
6. **A zone on a multi-zone chassis is missing.** Check the port. Zone two is 11010, not 11000
|
|
233
256
|
7. **The volume moved but the slider did not, for a second.** A change made on the player takes one long-poll round trip to arrive. A change made from HomeKit is immediate
|
|
234
257
|
8. **One zone's slider moved several rooms.** That zone is currently leading a BluOS group, so it carries its followers, the same as its slider in the BluOS app. Ungroup in the BluOS app and it goes back to moving alone
|
|
235
|
-
9. **The reboot switch turns itself off.** This is by design. It is a button, not a state: it fires when switched on, then springs back. Switching it off does nothing, which is what stops a scene or "turn everything off" from
|
|
236
|
-
10. **Reboot All
|
|
237
|
-
11. **A reboot switch
|
|
258
|
+
9. **The reboot switch turns itself off.** This is by design. It is a button, not a state: it fires when switched on, then springs back. Switching it off does nothing, which is what stops a scene or "turn everything off" from rebooting your stereo
|
|
259
|
+
10. **Reboot All rebooted a player you did not configure.** Also by design, and the reason it is off by default. It sweeps the network instead of reading `devices[]`. The info log is a count (`found … device(s), … player(s)`); the debug log lists every player by name and address
|
|
260
|
+
11. **A reboot switch rebooted the room next door.** Expect this on a multi-zone chassis such as a CI S2. BluOS serves `/reboot` on the box's own web server, not on a zone's control port, so there is no way to reboot one zone of a shared box. At startup the plugin warns, naming the other rooms
|
|
238
261
|
12. **100 is louder than you ever want.** Set the limit on the player, in the BluOS app's settings for it. The wording varies by model: a volume limit on Bluesound players, a maximum volume on NAD amplifiers. The plugin deliberately has no ceiling of its own, so a limit set on the player is enforced by the hardware for every controller. No HomeKit automation or misheard Siri phrase can exceed it, and a second limit here could only disagree with the first
|
|
239
262
|
13. Restart Homebridge after editing `config.json` by hand
|
|
240
263
|
|
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
(value) => { player.battery = value },
|
|
379
379
|
))
|
|
380
380
|
options.append(checkbox(
|
|
381
|
-
'Reboot switch (
|
|
381
|
+
'Reboot switch (reboots this player, and any zone sharing its box)',
|
|
382
382
|
player.reboot,
|
|
383
383
|
false,
|
|
384
384
|
(value) => { player.reboot = value },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-bluos",
|
|
3
3
|
"displayName": "Homebridge BluOS",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.2",
|
|
5
5
|
"description": "Homebridge plugin for BluOS players — per-zone volume, mute, volume-preset and battery accessories over the LAN Custom Integration API. Verified on NAD and Bluesound hardware",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|