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 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
- - **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.
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 the floor and the types move together, deliberately.
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 `resetMocks` strips `mockImplementation` on a factory.
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
- [![Tests](https://github.com/tbaur/homebridge-bluos/actions/workflows/test.yml/badge.svg)](https://github.com/tbaur/homebridge-bluos/actions/workflows/test.yml) [![npm version](https://img.shields.io/npm/v/homebridge-bluos?style=flat-square)](https://www.npmjs.com/package/homebridge-bluos) [![npm downloads](https://img.shields.io/npm/dt/homebridge-bluos?label=downloads&style=flat-square)](https://www.npmjs.com/package/homebridge-bluos) [![Node.js](https://img.shields.io/badge/node-%3E%3D20-green)](https://nodejs.org) [![Homebridge](https://img.shields.io/badge/homebridge-%3E%3D1.6.0%20%7C%7C%202.x-purple)](https://homebridge.io) [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
3
+ [![Tests](https://github.com/tbaur/homebridge-bluos/actions/workflows/test.yml/badge.svg)](https://github.com/tbaur/homebridge-bluos/actions/workflows/test.yml) [![npm version](https://img.shields.io/npm/v/homebridge-bluos?style=flat-square)](https://www.npmjs.com/package/homebridge-bluos) [![npm downloads](https://img.shields.io/npm/dt/homebridge-bluos?label=downloads&style=flat-square)](https://www.npmjs.com/package/homebridge-bluos) [![Node.js](https://img.shields.io/badge/node-%3E%3D20-green)](https://nodejs.org) [![verified-by-homebridge](https://img.shields.io/badge/homebridge-verified-blueviolet?color=%23491F59&style=flat)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [![Homebridge](https://img.shields.io/badge/homebridge-%3E%3D1.6.0%20%7C%7C%202.x-purple)](https://homebridge.io) [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](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; other BluOS brands (DALI, Monitor Audio, Roksan) run the same firmware and are expected to work, but are untested here.
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
- The goal is a complete integration: everything about a BluOS player that HomeKit can express well, built one accessory at a time and verified against real hardware before it ships. Volume, mute, volume presets and battery are what exist today — the first set, not the ceiling. See the [roadmap](#roadmap) for what comes next.
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
- ### Accessories today, per zone
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
- - **Volume slider** 0–100, the same scale the BluOS app uses. Exposed as a fan by default (see [why](#why-is-my-volume-a-fan))
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** Finds zones over mDNS and writes the configuration for you; manual address entry for networks where multicast is filtered
26
- - **Long-polling, not polling** — `/SyncStatus?timeout=100` with an `etag`, so a change made on the front panel, the remote or the BluOS app reaches HomeKit in about a second, without hammering the player
27
- - **Follows the API's rate rules** One second minimum between two requests for the same resource, per the BluOS specification; writes to one chassis are serialised, and separate chassis stay parallel
28
- - **Survives a DHCP lease change** — Accessory identity is the player's MAC and zone port, never its address. If a player moves, the plugin re-resolves it and remembers the new address
29
- - **Backs off when a player is off** — Exponential delay to a one-minute ceiling instead of dialling an absent player every second. A single failure is a debug line; the warning comes when the player has missed three polls and HomeKit is told No Response, then at most hourly while it stays away, with one line when it answers again
30
- - **Answers HomeKit promptly** A write returns inside HAP's patience window and finishes anything slower in the background, where the result still reaches HomeKit through the next poll
31
- - **No volume jumps** The pair of writes HomeKit sends when a slider leaves zero is coalesced into one command
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** `strict`, plus `noUncheckedIndexedAccess` and type-aware lint
38
- - **Tested** A behavioural Jest suite over 95% of statements, including XML fixtures recorded from real hardware (NAD C658, CI-S2, Bluesound P430, portable player) and the settings page that writes your configuration
39
- - **CI** Build, lint (warnings are failures), type-check and test on Node 20/22/24, a job against the oldest supported Homebridge, committed-`dist` drift check, dependency audit and OSV scanning
40
- - **No analytics** — No tracking, no cloud, no accounts
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
- 1. Give each player a static IP or a DHCP reservation. Not required the plugin re-resolves addresses but it makes logs easier to read
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; tick what you want and save.
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-S2** | Two independent zones on one chassis, ports 11000 and 11010 |
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
- Multi-zone chassis (CI-S2, CI 580) are supported by treating each zone as its own player. Fixed-output players are detected and get no slider, because writing a level to them does nothing.
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 (`singular` in the schema); it can hold as many players as you like.
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. Homebridge itself falls back to the platform alias, `BluOS`, if a hand-edited `config.json` omits it |
190
- | `devices` | ✓ | List of players. An empty list is allowed and warns; a missing or non-list value is an error |
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
- ### `devices[]` entries
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?** mDNS is often filtered across VLANs and by some access points. Use the manual address entry, or add the player by hand in `config.json`
215
- 2. **Everything shows No Response right after a restart** normal until the first read completes; the plugin reports unknown state rather than guessing
216
- 3. **Everything shows No Response and stays that way** check the log for `BluOS is disabled until its configuration is fixed`. The plugin stays inert, without deleting anything, until the reported problem is fixed
217
- 4. **A player has no slider** it reports a fixed output level (`volume="-1"`), so a level cannot be written to it. The log says so once
218
- 5. **The mute switch does not follow volume zero** — by design. Muting and setting level zero are different things to BluOS, and only mute remembers the level to come back to
219
- 6. **A zone on a multi-zone chassis is missing** — check the port. Zone two is 11010, not 11000
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
- HomeKit has no speaker volume characteristic that the Home app renders, so every plugin borrows another accessory type. This one uses a **fan** and its rotation speed: it looks like a slider, and it is not swept up by "Hey Siri, turn off all the lights" — which, with a lightbulb, would silence your speakers, or worse, set them to maximum. A lightbulb is available if you prefer it.
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. No cloud, no accounts, no credentials which also means the BluOS API has no authentication: anyone who can reach a player on your network can already control it, so secure the network rather than the plugin. Responses are parsed by a size-, depth- and element-capped XML reader rather than a general-purpose parser, everything interpolated into a log line is sanitised, and manual probe targets are validated before a request is made. See [`SECURITY.md`](SECURITY.md).
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
- - [Features](https://github.com/tbaur/homebridge-bluos/blob/main/docs/FEATURES.md)
242
- - [Protocol reference](https://github.com/tbaur/homebridge-bluos/blob/main/docs/PROTOCOL.md) — hardware-verified BluOS behaviour
243
- - [Development](https://github.com/tbaur/homebridge-bluos/blob/main/DEVELOPMENT.md)
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](https://github.com/tbaur/homebridge-bluos/blob/main/CHANGELOG.md)
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** Plain HTTP to the addresses in your configuration. The BluOS API has no authentication; 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; 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.
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 anyone who can reach a player can already control it.
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
 
@@ -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 battery sensor. 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.",
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 recommended", "enum": ["fan"] },
29
- { "title": "Lightbulb affected by \"turn off all the lights\"", "enum": ["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 Battery\"."
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-S2 and CI 580 use 11010, 11020 and 11030 for their extra zones."
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",
@@ -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-S2 or CI 580 exposes several
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
  }