homebridge-bluos 1.0.2 → 1.1.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 +14 -0
- package/DEVELOPMENT.md +8 -6
- package/README.md +54 -152
- package/SECURITY.md +8 -8
- package/config.schema.json +27 -5
- package/dist/api/client.d.ts +50 -2
- package/dist/api/client.js +67 -7
- package/dist/api/discovery.d.ts +1 -1
- package/dist/api/discovery.js +1 -1
- package/dist/api/http.d.ts +17 -8
- package/dist/api/http.js +47 -7
- package/dist/api/identity.d.ts +1 -1
- package/dist/api/identity.js +1 -1
- package/dist/devices/host.d.ts +32 -0
- package/dist/devices/index.d.ts +2 -0
- package/dist/devices/index.js +2 -0
- package/dist/devices/reboot-accessory.d.ts +58 -0
- package/dist/devices/reboot-accessory.js +113 -0
- package/dist/devices/reboot-all-accessory.d.ts +64 -0
- package/dist/devices/reboot-all-accessory.js +133 -0
- package/dist/platform.d.ts +32 -1
- package/dist/platform.js +101 -7
- package/dist/settings.d.ts +40 -1
- package/dist/settings.js +41 -2
- package/dist/types/index.d.ts +23 -2
- package/dist/types/index.js +12 -2
- package/dist/utils/errors.d.ts +12 -1
- package/dist/utils/errors.js +12 -1
- package/dist/utils/validators.d.ts +19 -1
- package/dist/utils/validators.js +52 -10
- package/docs/FEATURES.md +20 -11
- package/docs/PROTOCOL.md +102 -22
- package/docs/README-DETAILED.md +262 -0
- package/homebridge-ui/public/index.html +22 -0
- package/homebridge-ui/public/index.js +61 -4
- package/homebridge-ui/server.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0](https://github.com/tbaur/homebridge-bluos/compare/v1.0.3...v1.1.0) (2026-08-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add per-player and whole-install reboot switches ([#21](https://github.com/tbaur/homebridge-bluos/issues/21)) ([55270a4](https://github.com/tbaur/homebridge-bluos/commit/55270a428ca8dc02d9af04814c8496dc6d8bdda2))
|
|
9
|
+
|
|
10
|
+
## [1.0.3](https://github.com/tbaur/homebridge-bluos/compare/v1.0.2...v1.0.3) (2026-08-18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Miscellaneous Chores
|
|
14
|
+
|
|
15
|
+
* release 1.0.3 ([c069e0f](https://github.com/tbaur/homebridge-bluos/commit/c069e0fab32b2d4615d70d0651c348026db479b0))
|
|
16
|
+
|
|
3
17
|
## [1.0.2](https://github.com/tbaur/homebridge-bluos/compare/v1.0.1...v1.0.2) (2026-08-18)
|
|
4
18
|
|
|
5
19
|
|
package/DEVELOPMENT.md
CHANGED
|
@@ -23,17 +23,19 @@ Break these and you break someone's rooms, or you guess the protocol.
|
|
|
23
23
|
|
|
24
24
|
- **Identity is `MAC:port:kind` (plus preset level), never the address.** A DHCP change must not mint new accessories. Adopt a cached accessory that matches identity; do not replace it. Changing a preset's level *is* a new accessory.
|
|
25
25
|
- **Bad config disables the platform. It does not unregister anything.** Rooms and automations survive a typo. One bad `devices[]` entry is skipped; only a wholly unusable config is fatal.
|
|
26
|
-
- **Unknown is No Response**, not zero. Until a real `/SyncStatus` arrives, and again when the player stops answering, characteristics report `SERVICE_COMMUNICATION_FAILURE`.
|
|
26
|
+
- **Unknown is No Response**, not zero. Until a real `/SyncStatus` arrives, and again when the player stops answering, characteristics report `SERVICE_COMMUNICATION_FAILURE`. *One exception, in the reboot switches:* they always read `false` and stay pressable. The rule exists so automations cannot fire against invented **readings**, and a button reports no reading. `false` honestly means "not pressed", whether or not the player is answering. A No Response tile cannot be pressed in the Home app, which would grey out the reboot button in exactly the situation it is for. Adding a second exception needs the same standard of argument.
|
|
27
27
|
- **Mute is inferred from `muteVolume` / `muteDb`.** `/SyncStatus` never sends `mute`. `volume="0" db="-100"` is the same for mute and for level zero. See PROTOCOL.md.
|
|
28
28
|
- **`tell_slaves` is decided in one place** (`BaseAccessory.writeScope`): a group leader carries the group, everyone else writes locally. Do not special-case it per accessory.
|
|
29
|
-
- **Rate rules live in `api/client.ts`.** One second between same-resource calls, 100 ms between control calls, writes serialised per chassis. Call through the client; do not open your own HTTP.
|
|
30
|
-
|
|
29
|
+
- **Rate rules live in `api/client.ts`.** One second between same-resource calls, 100 ms between control calls, writes serialised per chassis. Call through the client; do not open your own HTTP. *The per-chassis lock has one exemption:* reboot skips it. That lock protects one address from rapid, repeated volume and mute traffic; reboot is one request per box per press, and holding the lock would only make a box that dies mid-response delay whatever queued behind it by a full timeout. The one-second same-resource gap still applies, keyed on the host.
|
|
30
|
+
|
|
31
|
+
- **Reboot is addressed by host, not by endpoint**, and is the only call that is. It is served on port 80, the box's own web server, and answers 404 on the control ports, so `BluOSClient.reboot` takes a bare host and there is no way to restart one zone of a multi-zone chassis. Anything resolving reboot targets de-duplicates on host for that reason, not for tidiness: a second request would land on a box that is already going down. See [docs/PROTOCOL.md](docs/PROTOCOL.md) for the measurements.
|
|
32
|
+
- **Hardware wins the spec.** Record the measurement in PROTOCOL.md and pin a fixture. Never commit a raw capture. [scripts/README.md](scripts/README.md) is the pseudonymise path, and `tests/unit/fixtures.test.ts` allowlists every address and MAC in the tree.
|
|
31
33
|
|
|
32
34
|
A capability ships if HomeKit can express it as a tile, a scene, an automation or a spoken command. A library, a queue and artwork cannot, so they stay in the BluOS app.
|
|
33
35
|
|
|
34
36
|
## Commands
|
|
35
37
|
|
|
36
|
-
Node 20+, matching `engines`. CI runs 20 / 22 / 24, a Homebridge 1.6 floor job, and a runtime `npm audit`. `@types/node` stays on the 20 line for the same reason: newer types let the compiler accept API that the oldest supported runtime does not have. Dependabot is told not to raise it
|
|
38
|
+
Node 20+, matching `engines`. CI runs 20 / 22 / 24, a Homebridge 1.6 floor job, and a runtime `npm audit`. `@types/node` stays on the 20 line for the same reason: newer types let the compiler accept API that the oldest supported runtime does not have. Dependabot is told not to raise it, because the floor and the types are meant to move together.
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
41
|
npm install
|
|
@@ -51,7 +53,7 @@ node scripts/smoke.js
|
|
|
51
53
|
node scripts/grouping.js
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
Tests inject fakes. `tests/helpers/hap.ts` is the HAP stand-in. `api/http.ts` is the one module tested against a loopback socket. Coverage includes `homebridge-ui/` and is gated at 80%. Fake timers, not real sleeps. Module mocks are plain classes behind a getter
|
|
56
|
+
Tests inject fakes. `tests/helpers/hap.ts` is the HAP stand-in. `api/http.ts` is the one module tested against a loopback socket. Coverage includes `homebridge-ui/` and is gated at 80%. Fake timers, not real sleeps. Module mocks are plain classes behind a getter, because `resetMocks` strips `mockImplementation` on a factory.
|
|
55
57
|
|
|
56
58
|
## Adding a capability
|
|
57
59
|
|
|
@@ -59,7 +61,7 @@ Tests inject fakes. `tests/helpers/hap.ts` is the HAP stand-in. `api/http.ts` is
|
|
|
59
61
|
2. Parse in `src/api/sync-status.ts`, call through `BluOSClient`.
|
|
60
62
|
3. Add a `BaseAccessory` subclass under `src/devices/`.
|
|
61
63
|
4. Extend `config.schema.json`, the config types, and `validateConfig` / `resolveAccessories`. Constrain the schema to what the plugin will accept. `required` must be an array of property names on the object (draft-07); a boolean on a field fails Homebridge verification CI.
|
|
62
|
-
5. Wire `attachHandler` in `platform.ts`. Extend the identity key only if the new accessory needs more than `kind`.
|
|
64
|
+
5. Wire `attachHandler` in `platform.ts`. Extend the identity key only if the new accessory needs more than `kind`. An accessory that belongs to the install instead of to a player uses `PLATFORM_DEVICE_ID` and gets no poller, so it must resolve whatever it needs at the moment it is used. `RebootAllAccessory` is the one example.
|
|
63
65
|
6. Tests for the parser, the client method, the accessory and the config path. A new response shape needs a recorded fixture.
|
|
64
66
|
7. Update PROTOCOL.md and [docs/FEATURES.md](docs/FEATURES.md).
|
|
65
67
|
8. `npm run build` and commit `dist/`.
|
package/README.md
CHANGED
|
@@ -1,43 +1,44 @@
|
|
|
1
1
|
# homebridge-bluos
|
|
2
2
|
|
|
3
|
-
[](https://github.com/tbaur/homebridge-bluos/actions/workflows/test.yml) [](https://www.npmjs.com/package/homebridge-bluos) [](https://www.npmjs.com/package/homebridge-bluos) [](https://nodejs.org) [](https://homebridge.io) [](LICENSE)
|
|
3
|
+
[](https://github.com/tbaur/homebridge-bluos/actions/workflows/test.yml) [](https://www.npmjs.com/package/homebridge-bluos) [](https://www.npmjs.com/package/homebridge-bluos) [](https://nodejs.org) [](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [](https://homebridge.io) [](LICENSE)
|
|
4
4
|
|
|
5
|
-
**BluOS players in Apple HomeKit, over your LAN.** No cloud, no accounts, no polling loops hammering your speakers. Verified against NAD and Bluesound hardware
|
|
5
|
+
**BluOS players in Apple HomeKit, over your LAN.** No cloud, no accounts, no polling loops hammering your speakers. Verified against NAD and Bluesound hardware. Other BluOS brands (DALI, Monitor Audio, Roksan) run the same firmware and are expected to work, but are untested here.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
What stays in the BluOS app is the part HomeKit has no vocabulary for: browsing, search, building a queue, artwork, and setting a player up in the first place. HomeKit has no way to render a library and no way to say "play the third album by that artist", so anything this plugin exposed there would be a worse version of an app you already have. Where HomeKit is genuinely better is the tile, the scene, the automation and the spoken command, and that is the line this plugin draws.
|
|
7
|
+
Browsing, search, queues and artwork stay in the BluOS app, which HomeKit cannot render anyway. What this plugin adds is the tile, the scene, the automation and the spoken command.
|
|
10
8
|
|
|
11
9
|
## Features
|
|
12
10
|
|
|
13
|
-
###
|
|
11
|
+
### Per zone
|
|
12
|
+
|
|
13
|
+
- **Volume slider:** 0–100, the same scale the BluOS app uses. A fan by default, a lightbulb if you prefer
|
|
14
|
+
- **Mute switch:** unmuting restores the level the player remembered, not a guess
|
|
15
|
+
- **Volume presets:** one switch per exact level, addressable by name with Siri
|
|
16
|
+
- **Battery sensor:** level, charging state and low-battery warning, for players with a battery pack
|
|
17
|
+
- **Reboot switch:** momentary, so a scene cannot restart your stereo, and still pressable when the player has stopped answering
|
|
18
|
+
- **Grouping-aware:** a zone leading a BluOS group moves the whole group, exactly as it does in the BluOS app
|
|
19
|
+
- **Multi-zone chassis:** each zone of a NAD CI S2 or CI 580 is its own player, on its own port
|
|
20
|
+
|
|
21
|
+
### For the whole install
|
|
14
22
|
|
|
15
|
-
- **
|
|
16
|
-
- **Mute switch** — On when the player is muted; unmuting restores the level the player remembered, not a guess
|
|
17
|
-
- **Grouping-aware** — A zone that is leading a group moves the whole group, exactly as its slider does in the BluOS app. Every other zone moves alone
|
|
18
|
-
- **Volume preset switches** — One switch per level, e.g. "Study Evening" at 15. Addressable by name with Siri, and far safer than a slider inside an automation
|
|
19
|
-
- **Battery sensor** — Charge level, charging state and low-battery warning for players with a battery pack fitted (PULSE FLEX with BP100, PULSE M)
|
|
20
|
-
- **Multi-zone chassis** — Each zone of a NAD CI-S2 or CI 580 is a separate player with its own accessories, discovered on its own port
|
|
21
|
-
- **Accessory Information** — Brand and model as the player reports them, the plugin version as firmware revision, and an opaque, stable serial number
|
|
23
|
+
- **Reboot all:** off by default. One switch that restarts every BluOS player it can find on the network, naming each one in the log first
|
|
22
24
|
|
|
23
25
|
### Reliability
|
|
24
26
|
|
|
25
|
-
- **Discovery in the settings page
|
|
26
|
-
- **Long-polling, not polling
|
|
27
|
-
- **Follows the API's rate rules
|
|
28
|
-
- **Survives a DHCP lease change
|
|
29
|
-
- **Backs off when a player is off
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **Honest state** — An accessory reports No Response until the player has actually been read, and again once it stops answering, rather than showing a value it cannot confirm
|
|
33
|
-
- **Never loses your rooms** — A broken configuration disables the platform and leaves every accessory registered and showing No Response. Accessories are adopted by identity, never replaced
|
|
27
|
+
- **Discovery in the settings page,** with manual entry for networks that filter multicast
|
|
28
|
+
- **Long-polling, not polling,** so a change at the front panel reaches HomeKit in about a second
|
|
29
|
+
- **Follows the API's rate rules,** with writes serialised per chassis and separate chassis kept parallel
|
|
30
|
+
- **Survives a DHCP lease change:** identity is the player's MAC and port, never its address
|
|
31
|
+
- **Backs off when a player is off,** to a one-minute ceiling, instead of dialling it every second
|
|
32
|
+
- **Honest state:** No Response until the player has actually been read, never a value it cannot confirm
|
|
33
|
+
- **Never loses your rooms:** a broken config disables the platform without unregistering anything
|
|
34
34
|
|
|
35
35
|
### Quality
|
|
36
36
|
|
|
37
|
-
- **Strict TypeScript
|
|
38
|
-
- **Tested
|
|
39
|
-
- **
|
|
40
|
-
|
|
37
|
+
- **Strict TypeScript,** with `noUncheckedIndexedAccess` and type-aware lint
|
|
38
|
+
- **Tested:** a behavioural Jest suite over 95% of statements, against XML fixtures recorded from real hardware
|
|
39
|
+
- **No analytics:** no tracking, no cloud, no accounts
|
|
40
|
+
|
|
41
|
+
Every accessory, field and log line is documented in [Detailed documentation](docs/README-DETAILED.md).
|
|
41
42
|
|
|
42
43
|
## Quick Start
|
|
43
44
|
|
|
@@ -51,12 +52,11 @@ npm install -g homebridge-bluos
|
|
|
51
52
|
|
|
52
53
|
### 2. Prepare your players
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
2. Nothing needs enabling on the player: the BluOS LAN API is always on
|
|
55
|
+
Nothing needs enabling on the player, because the BluOS LAN API is always on. A static IP or DHCP reservation is not required either, since the plugin re-resolves addresses on its own, but it makes logs easier to read.
|
|
56
56
|
|
|
57
57
|
### 3. Configure
|
|
58
58
|
|
|
59
|
-
**Homebridge UI** (recommended): open the plugin settings and press **Discover Players**. Every zone that answers is listed with a suggested set of accessories
|
|
59
|
+
**Homebridge UI** (recommended): open the plugin settings and press **Discover Players**. Every zone that answers is listed with a suggested set of accessories. Tick what you want and save.
|
|
60
60
|
|
|
61
61
|
Or in `config.json`:
|
|
62
62
|
|
|
@@ -74,85 +74,20 @@ Or in `config.json`:
|
|
|
74
74
|
"port": 11000,
|
|
75
75
|
"volumeSlider": true,
|
|
76
76
|
"mute": true,
|
|
77
|
-
"volumePresets": [
|
|
78
|
-
{ "name": "Study Quiet", "volume": 15 },
|
|
79
|
-
{ "name": "Study Loud", "volume": 70 }
|
|
80
|
-
]
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"id": "90:56:82:0A:00:02:11010",
|
|
84
|
-
"name": "Library",
|
|
85
|
-
"host": "192.168.4.11",
|
|
86
|
-
"port": 11010,
|
|
87
|
-
"volumeSlider": true
|
|
77
|
+
"volumePresets": [{ "name": "Study Quiet", "volume": 15 }]
|
|
88
78
|
}
|
|
89
|
-
]
|
|
90
|
-
"options": {
|
|
91
|
-
"sliderService": "fan",
|
|
92
|
-
"discoveryTimeoutSec": 5
|
|
93
|
-
}
|
|
79
|
+
]
|
|
94
80
|
}
|
|
95
81
|
]
|
|
96
82
|
}
|
|
97
83
|
```
|
|
98
84
|
|
|
85
|
+
See the [full configuration reference](docs/README-DETAILED.md#full-configuration-reference) for every option and a multi-player example.
|
|
86
|
+
|
|
99
87
|
### 4. Restart Homebridge
|
|
100
88
|
|
|
101
89
|
Accessories appear in the Home app after restart, showing No Response for the second or two before the first read completes.
|
|
102
90
|
|
|
103
|
-
### Example: Apple Shortcuts
|
|
104
|
-
|
|
105
|
-
A "settle in" shortcut, using preset switches rather than the slider so the levels are exact:
|
|
106
|
-
|
|
107
|
-
1. Turn **Study Quiet** On
|
|
108
|
-
2. Turn **Library Quiet** On
|
|
109
|
-
3. Start your playlist in the BluOS app or with AirPlay
|
|
110
|
-
|
|
111
|
-
For "quiet, now", one **Mute** switch is faster than any slider.
|
|
112
|
-
|
|
113
|
-
### Example logs
|
|
114
|
-
|
|
115
|
-
Startup:
|
|
116
|
-
|
|
117
|
-
```text
|
|
118
|
-
[BluOS] Initializing BluOS platform
|
|
119
|
-
[BluOS] adding Study Volume
|
|
120
|
-
[BluOS] adding Study Mute
|
|
121
|
-
[BluOS] adding Study Quiet
|
|
122
|
-
[BluOS] adding Library Volume
|
|
123
|
-
[BluOS] BluOS is watching 2 zone(s) with 4 accessory(s)
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
A player that moved to a new address:
|
|
127
|
-
|
|
128
|
-
```text
|
|
129
|
-
[BluOS] Library Volume [90:56:82:0A:00:02:11010] is not responding: connect ETIMEDOUT 192.168.4.11:11010
|
|
130
|
-
[BluOS] Library moved to 192.168.4.23:11010
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
The identity in brackets is the player's `id`, so two rooms that happen to share a name are still tellable apart.
|
|
134
|
-
|
|
135
|
-
A HomeKit write that reached the player:
|
|
136
|
-
|
|
137
|
-
```text
|
|
138
|
-
[BluOS] Study Volume: SET 35
|
|
139
|
-
[BluOS] Study Mute: ON
|
|
140
|
-
[BluOS] Study Quiet: SET 15
|
|
141
|
-
[BluOS] Study Volume: SET 40 (group)
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Mute `ON` means muted. `(group)` means that zone is leading a BluOS group, so the write carried the followers.
|
|
145
|
-
|
|
146
|
-
A configuration the plugin will not act on:
|
|
147
|
-
|
|
148
|
-
```text
|
|
149
|
-
[BluOS] devices[0] has no usable id (re-run discovery in the plugin settings); skipping devices[0]
|
|
150
|
-
[BluOS] all 1 configured device(s) were rejected; see the warnings above
|
|
151
|
-
[BluOS] BluOS is disabled until its configuration is fixed. Cached accessories are kept and will show as No Response, so rooms and automations are not lost.
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
The platform only disables itself when *every* player was rejected. One bad entry among several is skipped with the first line and nothing else changes.
|
|
155
|
-
|
|
156
91
|
## Supported Devices
|
|
157
92
|
|
|
158
93
|
Any BluOS player running the Custom Integration API, which is all of them from BluOS 3.x onwards.
|
|
@@ -160,75 +95,41 @@ Any BluOS player running the Custom Integration API, which is all of them from B
|
|
|
160
95
|
| Verified against | Notes |
|
|
161
96
|
|---|---|
|
|
162
97
|
| **NAD C658** | Streamer/preamp, firmware 4.16.6 |
|
|
163
|
-
| **NAD CI
|
|
98
|
+
| **NAD CI S2** | Two independent zones on one chassis, ports 11000 and 11010 |
|
|
164
99
|
| **Bluesound P430** | Soundbar |
|
|
165
100
|
| **Bluesound PULSE-class portable** | Battery pack reporting |
|
|
166
101
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
## Roadmap
|
|
170
|
-
|
|
171
|
-
Ordered by how well HomeKit expresses the thing, not by how easy it is to build. Each one ships only after it has been verified against real hardware, with the protocol behaviour recorded in [docs/PROTOCOL.md](docs/PROTOCOL.md).
|
|
172
|
-
|
|
173
|
-
**Next** — group scenes (one switch that forms or breaks a named group, which the BluOS app can do by hand but cannot put into an automation), a chime (`/Doorbell`, so a HomeKit doorbell or door sensor can sound on your speakers), transport (play and pause as a switch a scene or a spoken command can drive, with skip and back), and station preset switches (recall a saved BluOS preset by name, addressable by Siri).
|
|
174
|
-
|
|
175
|
-
**Likely** — input switches for the physical inputs on players that have them, a playback sensor so "when music starts here" can trigger other accessories, relative volume nudges in dB for physical-button automations, and shuffle and repeat.
|
|
176
|
-
|
|
177
|
-
**Needs verification first** — a sleep timer. `/Status` reports the minutes remaining, but v1.7 documents no way to set it, so the endpoint has to be confirmed against hardware before anything is built on it.
|
|
178
|
-
|
|
179
|
-
**Being weighed** — a single media tile per player using HomeKit's `SmartSpeaker` or `Television` service instead of separate switches. It reads better in the Home app when it works, but its rendering varies by iOS version in ways that are worth confirming before anyone's rooms depend on it.
|
|
180
|
-
|
|
181
|
-
**Not planned** — browsing, search, queue editing, artwork and now-playing metadata, and player setup. HomeKit has no way to render a library or a queue, and no vocabulary for "play the third album by that artist". The BluOS app does these properly and this plugin will not pretend to.
|
|
102
|
+
Fixed-output players are detected and get no slider, because writing a level to them does nothing.
|
|
182
103
|
|
|
183
104
|
## Configuration Options
|
|
184
105
|
|
|
185
|
-
Only one `BluOS` platform block is supported
|
|
106
|
+
Only one `BluOS` platform block is supported. It can hold as many players as you like.
|
|
186
107
|
|
|
187
108
|
| Option | Required | Description |
|
|
188
109
|
|---|:-:|---|
|
|
189
|
-
| `name` | ✓ (UI) | Plugin instance name in the Homebridge log
|
|
190
|
-
| `devices` | ✓ | List of players
|
|
110
|
+
| `name` | ✓ (UI) | Plugin instance name in the Homebridge log |
|
|
111
|
+
| `devices` | ✓ | List of players |
|
|
191
112
|
| `options.sliderService` | | `fan` (default) or `lightbulb`, for every slider |
|
|
192
113
|
| `options.discoveryTimeoutSec` | | mDNS listening window, 1–30 seconds (default 5) |
|
|
114
|
+
| `options.rebootAll` | | Expose one switch that restarts **every BluOS player on the network** (default false) |
|
|
115
|
+
| `options.rebootAllName` | | What that switch is called in the Home app |
|
|
193
116
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
| Field | Required | Description |
|
|
197
|
-
|---|:-:|---|
|
|
198
|
-
| `id` | ✓ | Stable identity, normally `MAC:port`. Written by discovery. **Changing it detaches the accessories from their HomeKit rooms and automations** |
|
|
199
|
-
| `name` | ✓ | The room's name. Every accessory is named from it: `Study Volume`, `Study Mute`, `Study Battery`, and each preset's own name |
|
|
200
|
-
| `host` | ✓ | IP address or hostname |
|
|
201
|
-
| `port` | | Control port (default 11000). Extra zones of a multi-zone chassis use 11010, 11020, 11030 |
|
|
202
|
-
| `volumeSlider` | | Expose the slider (default true) |
|
|
203
|
-
| `sliderService` | | Override the platform slider style for this player. Empty means "use the platform setting". Changing it removes the old control from the accessory rather than leaving both |
|
|
204
|
-
| `mute` | | Expose a mute switch (default false) |
|
|
205
|
-
| `battery` | | Expose a battery sensor (default false; only meaningful with a battery pack) |
|
|
206
|
-
| `volumePresets[]` | | `{ "name": "...", "volume": 0-100 }`. Duplicate levels on one player are skipped with a warning |
|
|
207
|
-
|
|
208
|
-
An accessory's identity is the player's `id` plus its kind (and, for a preset, its level). It deliberately does **not** include the address, so changing `host` or `port` keeps your existing accessories intact. Renaming a player is applied in place. Changing a preset's `volume`, however, creates a **new** accessory and removes the old one, which loses its room assignment, scenes and automations — rename freely, but change preset levels only when you are ready to re-add them in the Home app.
|
|
209
|
-
|
|
210
|
-
Serial numbers in Accessory Information are opaque values generated once per accessory and kept in the Homebridge accessory cache. Clearing that cache issues new ones.
|
|
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.
|
|
211
118
|
|
|
212
119
|
## Not Working?
|
|
213
120
|
|
|
214
|
-
1. **Nothing found by Discover Players
|
|
215
|
-
2. **Everything shows No Response
|
|
216
|
-
3. **
|
|
217
|
-
4. **A
|
|
218
|
-
5. **The
|
|
219
|
-
6.
|
|
220
|
-
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
|
|
221
|
-
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
|
|
222
|
-
9. **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: the player maps 0–100 onto whatever range it is configured for and clamps anything above it, so a limit set there 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
|
|
223
|
-
10. Restart Homebridge after editing `config.json` by hand
|
|
224
|
-
|
|
225
|
-
### Why is my volume a fan?
|
|
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
|
+
2. **Everything shows No Response and stays that way.** Check the log for `BluOS is disabled until its configuration is fixed`
|
|
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 restarted the room next door.** Expected on a multi-zone chassis. BluOS serves reboot per box, not per zone
|
|
125
|
+
5. **The reboot switch turns itself off.** By design. It is a button, not a state
|
|
126
|
+
6. Restart Homebridge after editing `config.json` by hand
|
|
226
127
|
|
|
227
|
-
|
|
128
|
+
The [full troubleshooting list](docs/README-DETAILED.md#troubleshooting) covers thirteen cases, including why the volume slider is a fan.
|
|
228
129
|
|
|
229
130
|
## Security
|
|
230
131
|
|
|
231
|
-
The plugin talks only to the addresses in your configuration, on your LAN.
|
|
132
|
+
The plugin talks only to the addresses in your configuration, on your LAN. There is no cloud, no account and no credential to store. That last point cuts both ways: the BluOS API has no authentication at all, so anyone who can reach a player on your network can already control it. Secure the network, not the plugin. See [SECURITY.md](SECURITY.md).
|
|
232
133
|
|
|
233
134
|
## Requirements
|
|
234
135
|
|
|
@@ -238,11 +139,12 @@ The plugin talks only to the addresses in your configuration, on your LAN. No cl
|
|
|
238
139
|
|
|
239
140
|
## More Info
|
|
240
141
|
|
|
241
|
-
- [
|
|
242
|
-
- [
|
|
243
|
-
- [
|
|
142
|
+
- [Detailed documentation](docs/README-DETAILED.md)
|
|
143
|
+
- [Features](docs/FEATURES.md)
|
|
144
|
+
- [Protocol reference](docs/PROTOCOL.md): hardware-verified BluOS behaviour
|
|
145
|
+
- [Development](DEVELOPMENT.md)
|
|
244
146
|
- [Report Issues](https://github.com/tbaur/homebridge-bluos/issues)
|
|
245
|
-
- [Changelog](
|
|
147
|
+
- [Changelog](CHANGELOG.md)
|
|
246
148
|
|
|
247
149
|
## License
|
|
248
150
|
|
package/SECURITY.md
CHANGED
|
@@ -12,19 +12,19 @@ Do not open a public issue. Use GitHub's [private vulnerability reporting](https
|
|
|
12
12
|
|
|
13
13
|
## Security Measures
|
|
14
14
|
|
|
15
|
-
- **LAN only
|
|
16
|
-
- **Input validation
|
|
17
|
-
- **Log safety
|
|
18
|
-
- **Bounded I/O
|
|
19
|
-
- **Discovery
|
|
20
|
-
- **Settings probe
|
|
21
|
-
- **Dependencies
|
|
15
|
+
- **LAN only:** Plain HTTP to the addresses in your configuration. The BluOS API has no authentication, and this plugin stores no credentials.
|
|
16
|
+
- **Input validation:** Config is checked at startup. A missing or non-list `devices` value disables the platform without unregistering accessories. A bad player or preset is skipped. Hosts, ports and timeouts are rejected or clamped.
|
|
17
|
+
- **Log safety:** Values written to logs have control characters stripped and are length-limited.
|
|
18
|
+
- **Bounded I/O:** Connect and total timeouts on every request, responses capped at 128 KiB, and XML parsed with size, depth and element caps.
|
|
19
|
+
- **Discovery:** mDNS errors are caught so they cannot take Homebridge down. Browse results are capped.
|
|
20
|
+
- **Settings probe:** Accepts only a private or local address and a documented BluOS port.
|
|
21
|
+
- **Dependencies:** CI runs `npm audit` on the runtime tree and OSV-Scanner on the full tree.
|
|
22
22
|
|
|
23
23
|
## Best Practices for Users
|
|
24
24
|
|
|
25
25
|
1. Keep Homebridge and this plugin updated.
|
|
26
26
|
2. Run Homebridge with minimal privileges, and do not expose it or player port 11000 to the internet.
|
|
27
|
-
3. The BluOS LAN API is unauthenticated
|
|
27
|
+
3. The BluOS LAN API is unauthenticated, so anyone who can reach a player can already control it.
|
|
28
28
|
4. Set volume limits in the BluOS app. The plugin has none of its own, so a limit there covers every controller.
|
|
29
29
|
5. A group leader's volume change applies to the whole group.
|
|
30
30
|
|
package/config.schema.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"singular": true,
|
|
5
5
|
"customUi": true,
|
|
6
6
|
"customUiPath": "./homebridge-ui",
|
|
7
|
-
"headerDisplay": "Exposes BluOS players to HomeKit as a volume slider, a mute switch, volume-preset switches and a
|
|
7
|
+
"headerDisplay": "Exposes BluOS players to HomeKit as a volume slider, a mute switch, volume-preset switches, a battery sensor and a reboot switch. Verified on NAD and Bluesound hardware; other BluOS brands run the same firmware and are expected to work. Use **Discover Players** to find zones on your network; every field below can also be edited by hand.",
|
|
8
8
|
"footerDisplay": "HomeKit has no speaker volume control that the Home app renders, so the volume slider is exposed as a fan. See the [documentation](https://github.com/tbaur/homebridge-bluos#readme) for what this plugin does and does not do.",
|
|
9
9
|
"schema": {
|
|
10
10
|
"type": "object",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"type": "string",
|
|
26
26
|
"default": "fan",
|
|
27
27
|
"oneOf": [
|
|
28
|
-
{ "title": "Fan
|
|
29
|
-
{ "title": "Lightbulb
|
|
28
|
+
{ "title": "Fan (recommended)", "enum": ["fan"] },
|
|
29
|
+
{ "title": "Lightbulb (affected by \"turn off all the lights\")", "enum": ["lightbulb"] }
|
|
30
30
|
],
|
|
31
31
|
"description": "Both render an identical slider. A lightbulb is swept up by Siri commands aimed at lights, which can silence or max out your speakers."
|
|
32
32
|
},
|
|
@@ -37,6 +37,22 @@
|
|
|
37
37
|
"minimum": 1,
|
|
38
38
|
"maximum": 30,
|
|
39
39
|
"description": "How long to listen for players. Raise it on a busy or slow network."
|
|
40
|
+
},
|
|
41
|
+
"rebootAll": {
|
|
42
|
+
"title": "Reboot All Switch",
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": false,
|
|
45
|
+
"description": "One switch that restarts every BluOS player it can find. Its reach is the whole network, not just the players listed below: pressing it also restarts players you have deliberately left out of this configuration. Every player is named in the Homebridge log before anything is sent."
|
|
46
|
+
},
|
|
47
|
+
"rebootAllName": {
|
|
48
|
+
"title": "Reboot All Name",
|
|
49
|
+
"type": "string",
|
|
50
|
+
"maxLength": 64,
|
|
51
|
+
"placeholder": "BluOS Reboot All",
|
|
52
|
+
"description": "What the switch is called in the Home app, so you can keep it in the room you want. Leave it empty to use the plugin name followed by \"Reboot All\".",
|
|
53
|
+
"condition": {
|
|
54
|
+
"functionBody": "return model.options && model.options.rebootAll === true;"
|
|
55
|
+
}
|
|
40
56
|
}
|
|
41
57
|
}
|
|
42
58
|
},
|
|
@@ -60,7 +76,7 @@
|
|
|
60
76
|
"title": "Name",
|
|
61
77
|
"type": "string",
|
|
62
78
|
"maxLength": 64,
|
|
63
|
-
"description": "The room's name. Accessories are named from it: \"Study\" gives \"Study Volume\", \"Study Mute\" and \"Study
|
|
79
|
+
"description": "The room's name. Accessories are named from it: \"Study\" gives \"Study Volume\", \"Study Mute\", \"Study Battery\" and \"Study Reboot\"."
|
|
64
80
|
},
|
|
65
81
|
"host": {
|
|
66
82
|
"title": "Address",
|
|
@@ -76,7 +92,7 @@
|
|
|
76
92
|
"default": 11000,
|
|
77
93
|
"minimum": 1,
|
|
78
94
|
"maximum": 65535,
|
|
79
|
-
"description": "11000 for a normal player. Multi-zone models such as the NAD CI
|
|
95
|
+
"description": "11000 for a normal player. Multi-zone models such as the NAD CI S2 and CI 580 use 11010, 11020 and 11030 for their extra zones."
|
|
80
96
|
},
|
|
81
97
|
"volumeSlider": {
|
|
82
98
|
"title": "Volume Slider",
|
|
@@ -105,6 +121,12 @@
|
|
|
105
121
|
"default": false,
|
|
106
122
|
"description": "State of charge. Only for players with a battery pack fitted, such as a PULSE FLEX with the BP100."
|
|
107
123
|
},
|
|
124
|
+
"reboot": {
|
|
125
|
+
"title": "Reboot Switch",
|
|
126
|
+
"type": "boolean",
|
|
127
|
+
"default": false,
|
|
128
|
+
"description": "A switch that restarts 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. Restarting interrupts playback. On a multi-zone chassis such as a CI S2 it restarts every zone on that box: BluOS reboots the box, not the zone, and there is no way to restart just this room."
|
|
129
|
+
},
|
|
108
130
|
"volumePresets": {
|
|
109
131
|
"title": "Volume Presets",
|
|
110
132
|
"type": "array",
|
package/dist/api/client.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* clients, and phrases it as a requirement rather than advice.
|
|
15
15
|
* 2. At least 100 ms between control calls to one endpoint, so a HomeKit scene
|
|
16
16
|
* that touches several tiles at once cannot burst a player.
|
|
17
|
-
* 3. Writes to one chassis are serialised. A NAD CI
|
|
17
|
+
* 3. Writes to one chassis are serialised. A NAD CI S2 or CI 580 exposes several
|
|
18
18
|
* zones on one IP; concurrent writes to `:11000` and `:11010` are writes to
|
|
19
19
|
* the same box. Different chassis still run in parallel.
|
|
20
20
|
*
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* is expressed per write through {@link WriteScope}.
|
|
25
25
|
*/
|
|
26
26
|
import type { PlayerObservation, PluginLogger } from '../types';
|
|
27
|
-
import { type HttpGet } from './http';
|
|
27
|
+
import { type HttpGet, type HttpPost } from './http';
|
|
28
28
|
import { type VolumeResult } from './sync-status';
|
|
29
29
|
/** Where to reach one player zone. */
|
|
30
30
|
export interface Endpoint {
|
|
@@ -44,10 +44,23 @@ export interface Endpoint {
|
|
|
44
44
|
export interface WriteScope {
|
|
45
45
|
tellSlaves: boolean;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* What a reboot request produced.
|
|
49
|
+
*
|
|
50
|
+
* `acknowledged` is false when the player took the request and then stopped
|
|
51
|
+
* answering, which is a success rather than a failure — see
|
|
52
|
+
* {@link BluOSClient.reboot}. It is carried so the log can say which happened,
|
|
53
|
+
* because "sent, no answer" and "sent, answered" look identical to the user and
|
|
54
|
+
* only one of them is worth investigating if the player never comes back.
|
|
55
|
+
*/
|
|
56
|
+
export interface RebootResult {
|
|
57
|
+
acknowledged: boolean;
|
|
58
|
+
}
|
|
47
59
|
/** Injectable collaborators, so tests need neither sockets nor real clocks. */
|
|
48
60
|
export interface BluOSClientOptions {
|
|
49
61
|
log: PluginLogger;
|
|
50
62
|
httpGet?: HttpGet;
|
|
63
|
+
httpPost?: HttpPost;
|
|
51
64
|
now?: () => number;
|
|
52
65
|
sleep?: (ms: number) => Promise<void>;
|
|
53
66
|
}
|
|
@@ -60,6 +73,7 @@ export interface BluOSClientOptions {
|
|
|
60
73
|
export declare class BluOSClient {
|
|
61
74
|
private readonly log;
|
|
62
75
|
private readonly httpGet;
|
|
76
|
+
private readonly httpPost;
|
|
63
77
|
private readonly now;
|
|
64
78
|
private readonly sleep;
|
|
65
79
|
/** Last request time, keyed by `endpoint|resource`, for the one-second rule. */
|
|
@@ -112,6 +126,30 @@ export declare class BluOSClient {
|
|
|
112
126
|
* `mute=1` produced `mute="1" muteVolume="72"` on a real player.
|
|
113
127
|
*/
|
|
114
128
|
setMute(endpoint: Endpoint, muted: boolean, scope?: WriteScope): Promise<VolumeResult>;
|
|
129
|
+
/**
|
|
130
|
+
* Restart the box at an address.
|
|
131
|
+
*
|
|
132
|
+
* Takes a host and no port, which is the whole story about this call. `/reboot`
|
|
133
|
+
* is served on port 80 alongside `/diagnostics`, and the control ports answer
|
|
134
|
+
* 404 for it. Port 80 is one server per chassis, so this restarts every zone
|
|
135
|
+
* behind the address and cannot be aimed at one zone of a CI S2, however much
|
|
136
|
+
* the rest of the API is per zone. See docs/PROTOCOL.md.
|
|
137
|
+
*
|
|
138
|
+
* Deliberately outside {@link withChassisLock}, unlike every other write. That
|
|
139
|
+
* lock protects one address from concurrent volume and mute traffic, which is
|
|
140
|
+
* rapid and repeated; a reboot is one request per address per press. Holding
|
|
141
|
+
* the lock would only mean that a box which dies mid-response makes anything
|
|
142
|
+
* queued behind it wait out the whole timeout. `respectResourceGap` still
|
|
143
|
+
* paces repeat presses, keyed on the same address.
|
|
144
|
+
*
|
|
145
|
+
* A lost connection counts as success once the request reached the player.
|
|
146
|
+
* This is the one call where that is right rather than reckless: a player that
|
|
147
|
+
* is restarting cannot finish answering, so insisting on a clean response would
|
|
148
|
+
* report failure precisely when the command worked. A failure to connect at all
|
|
149
|
+
* is still a failure, which is the distinction {@link ConnectionError.delivered}
|
|
150
|
+
* exists to draw.
|
|
151
|
+
*/
|
|
152
|
+
reboot(host: string): Promise<RebootResult>;
|
|
115
153
|
private control;
|
|
116
154
|
/**
|
|
117
155
|
* Queue work behind anything already writing to this chassis.
|
|
@@ -127,5 +165,15 @@ export declare class BluOSClient {
|
|
|
127
165
|
* the same resource on the same endpoint.
|
|
128
166
|
*/
|
|
129
167
|
private respectResourceGap;
|
|
168
|
+
/**
|
|
169
|
+
* Validate the host and wait out the same-resource gap, then name the target.
|
|
170
|
+
*
|
|
171
|
+
* Shared by every request whatever its method, so a new call path cannot
|
|
172
|
+
* forget either. The host check especially: two copies of the one defence
|
|
173
|
+
* against a configuration or cache value altering a request URL would
|
|
174
|
+
* eventually disagree, and the disagreement would be the security regression.
|
|
175
|
+
* It is the same check the settings page and the configuration validator apply.
|
|
176
|
+
*/
|
|
177
|
+
private prepare;
|
|
130
178
|
private get;
|
|
131
179
|
}
|