homebridge-bluos 1.1.6 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/DEVELOPMENT.md +1 -1
- package/README.md +3 -3
- package/dist/api/client.d.ts +9 -2
- package/dist/api/client.js +10 -2
- package/dist/platform.d.ts +4 -0
- package/dist/platform.js +14 -3
- package/dist/poller.d.ts +7 -0
- package/dist/poller.js +10 -4
- package/dist/utils/context.d.ts +15 -0
- package/dist/utils/context.js +20 -0
- package/docs/FEATURES.md +2 -2
- package/docs/README-DETAILED.md +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0](https://github.com/tbaur/homebridge-bluos/compare/v1.1.6...v2.0.0) (2026-09-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* requires Homebridge 2.0 or newer and Node 22, 24 or 26. Homebridge 1.x and Node 20 are no longer supported.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* require Homebridge 2 and Node 22+, and fix cached-context persistence ([#39](https://github.com/tbaur/homebridge-bluos/issues/39)) ([72fdbf8](https://github.com/tbaur/homebridge-bluos/commit/72fdbf80de22183ca79b5da4c3cdda575e4ef8d1))
|
|
13
|
+
|
|
3
14
|
## [1.1.6](https://github.com/tbaur/homebridge-bluos/compare/v1.1.5...v1.1.6) (2026-08-31)
|
|
4
15
|
|
|
5
16
|
|
package/DEVELOPMENT.md
CHANGED
|
@@ -35,7 +35,7 @@ A capability ships if HomeKit can express it as a tile, a scene, an automation o
|
|
|
35
35
|
|
|
36
36
|
## Commands
|
|
37
37
|
|
|
38
|
-
Node
|
|
38
|
+
Node 22, 24 or 26, matching `engines`. CI runs 22 / 24 / 26 and a runtime `npm audit`. `@types/node` tracks the top of that range rather than its floor, so the Node 22 job in the matrix is what catches an API the oldest supported runtime does not have. Dependabot is told not to raise it on its own, because the range and the types are meant to move together.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
npm install
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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://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
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
|
|
|
@@ -133,8 +133,8 @@ The plugin talks only to the addresses in your configuration, on your LAN. There
|
|
|
133
133
|
|
|
134
134
|
## Requirements
|
|
135
135
|
|
|
136
|
-
- Homebridge
|
|
137
|
-
- Node.js
|
|
136
|
+
- Homebridge 2.x
|
|
137
|
+
- Node.js 22, 24 or 26, matching what Homebridge 2.x itself supports
|
|
138
138
|
- One or more BluOS players reachable on the same network
|
|
139
139
|
|
|
140
140
|
## More Info
|
package/dist/api/client.d.ts
CHANGED
|
@@ -83,8 +83,15 @@ export declare class BluOSClient {
|
|
|
83
83
|
/** Tail of the write queue for each chassis host. */
|
|
84
84
|
private readonly chassisWriteQueue;
|
|
85
85
|
constructor(options: BluOSClientOptions);
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Read `/SyncStatus` once, without long-polling.
|
|
88
|
+
*
|
|
89
|
+
* Takes a signal for the same reason the long poll does. This read is bounded
|
|
90
|
+
* by {@link STATUS_TIMEOUT_MS} rather than by a poll window, but a shutdown
|
|
91
|
+
* still has to be able to drop it: an unreachable player holds it for the full
|
|
92
|
+
* timeout, and a shutdown waits for every poller.
|
|
93
|
+
*/
|
|
94
|
+
readSyncStatus(endpoint: Endpoint, signal?: AbortSignal): Promise<PlayerObservation>;
|
|
88
95
|
/**
|
|
89
96
|
* Long-poll `/SyncStatus`, returning when the player's state changes or the
|
|
90
97
|
* poll window elapses.
|
package/dist/api/client.js
CHANGED
|
@@ -58,13 +58,21 @@ class BluOSClient {
|
|
|
58
58
|
this.now = options.now ?? Date.now;
|
|
59
59
|
this.sleep = options.sleep ?? timing_1.sleep;
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Read `/SyncStatus` once, without long-polling.
|
|
63
|
+
*
|
|
64
|
+
* Takes a signal for the same reason the long poll does. This read is bounded
|
|
65
|
+
* by {@link STATUS_TIMEOUT_MS} rather than by a poll window, but a shutdown
|
|
66
|
+
* still has to be able to drop it: an unreachable player holds it for the full
|
|
67
|
+
* timeout, and a shutdown waits for every poller.
|
|
68
|
+
*/
|
|
69
|
+
async readSyncStatus(endpoint, signal) {
|
|
63
70
|
const body = await this.get({
|
|
64
71
|
endpoint,
|
|
65
72
|
resource: 'SyncStatus',
|
|
66
73
|
query: {},
|
|
67
74
|
totalTimeoutMs: settings_1.STATUS_TIMEOUT_MS,
|
|
75
|
+
signal,
|
|
68
76
|
});
|
|
69
77
|
return (0, sync_status_1.parseSyncStatus)(body, (0, identity_1.formatEndpoint)(endpoint.host, endpoint.port));
|
|
70
78
|
}
|
package/dist/platform.d.ts
CHANGED
|
@@ -136,6 +136,10 @@ export declare class BluOSPlatform implements DynamicPlatformPlugin, AccessoryHo
|
|
|
136
136
|
*
|
|
137
137
|
* Driven by context rather than configuration so that the same path works when
|
|
138
138
|
* the platform is disabled and there is no valid configuration to consult.
|
|
139
|
+
*
|
|
140
|
+
* The validated context becomes the accessory's own, so the handler and the
|
|
141
|
+
* Homebridge cache hold one object. Anything a handler remembers is then in
|
|
142
|
+
* the object `persistContext` serialises. See {@link bindAccessoryContext}.
|
|
139
143
|
*/
|
|
140
144
|
private attachHandler;
|
|
141
145
|
private startPollers;
|
package/dist/platform.js
CHANGED
|
@@ -349,6 +349,9 @@ class BluOSPlatform {
|
|
|
349
349
|
device,
|
|
350
350
|
serialNumber: (0, utils_1.newAccessorySerialNumber)(),
|
|
351
351
|
adoptedLegacyUuid: false,
|
|
352
|
+
// Nothing to carry over: this path is taken only when no cached accessory
|
|
353
|
+
// answered to this identity.
|
|
354
|
+
previous: undefined,
|
|
352
355
|
});
|
|
353
356
|
this.attachHandler(platformAccessory);
|
|
354
357
|
this.active.set(uuid, platformAccessory);
|
|
@@ -372,6 +375,11 @@ class BluOSPlatform {
|
|
|
372
375
|
device,
|
|
373
376
|
serialNumber,
|
|
374
377
|
adoptedLegacyUuid: adopted || alreadyAdopted,
|
|
378
|
+
// Read off the accessory being adopted rather than looked up by the UUID
|
|
379
|
+
// this identity produces now. An adopted accessory is cached under its old
|
|
380
|
+
// UUID, so that lookup misses on exactly the path this branch exists for,
|
|
381
|
+
// and the level the user last set would be dropped by the migration.
|
|
382
|
+
previous: existing.context,
|
|
375
383
|
});
|
|
376
384
|
if (existing.displayName !== accessory.name) {
|
|
377
385
|
this.log.info(`${(0, utils_1.forLog)(existing.displayName)} is now named ${(0, utils_1.forLog)(accessory.name)}`);
|
|
@@ -382,8 +390,7 @@ class BluOSPlatform {
|
|
|
382
390
|
this.api.updatePlatformAccessories([existing]);
|
|
383
391
|
}
|
|
384
392
|
buildContext(input) {
|
|
385
|
-
const { accessory, device, serialNumber, adoptedLegacyUuid } = input;
|
|
386
|
-
const previous = this.restored.get(this.uuidFor(accessory))?.context;
|
|
393
|
+
const { accessory, device, serialNumber, adoptedLegacyUuid, previous } = input;
|
|
387
394
|
// The platform's own switch has no device, and so no address: its host and
|
|
388
395
|
// port are placeholders that nothing reads, because it resolves its targets
|
|
389
396
|
// when it is pressed rather than holding one endpoint.
|
|
@@ -414,11 +421,15 @@ class BluOSPlatform {
|
|
|
414
421
|
*
|
|
415
422
|
* Driven by context rather than configuration so that the same path works when
|
|
416
423
|
* the platform is disabled and there is no valid configuration to consult.
|
|
424
|
+
*
|
|
425
|
+
* The validated context becomes the accessory's own, so the handler and the
|
|
426
|
+
* Homebridge cache hold one object. Anything a handler remembers is then in
|
|
427
|
+
* the object `persistContext` serialises. See {@link bindAccessoryContext}.
|
|
417
428
|
*/
|
|
418
429
|
attachHandler(accessory) {
|
|
419
430
|
let context;
|
|
420
431
|
try {
|
|
421
|
-
context = (0, utils_1.
|
|
432
|
+
context = (0, utils_1.bindAccessoryContext)(accessory);
|
|
422
433
|
}
|
|
423
434
|
catch (error) {
|
|
424
435
|
this.log.warn(`${(0, utils_1.forLog)(accessory.displayName)} cannot be driven: ${(0, utils_1.describeError)(error)}. `
|
package/dist/poller.d.ts
CHANGED
|
@@ -90,6 +90,13 @@ export declare class DevicePoller {
|
|
|
90
90
|
/** Cancel the request in flight and wake any backoff sleep. */
|
|
91
91
|
private interrupt;
|
|
92
92
|
private run;
|
|
93
|
+
/**
|
|
94
|
+
* Read this zone once, long-polling when there is a token to poll with.
|
|
95
|
+
*
|
|
96
|
+
* Both reads are abortable, not just the long poll. A plain read is the path an
|
|
97
|
+
* unreachable player takes — a failure clears the etag — and it can sit for the
|
|
98
|
+
* whole status timeout, which a shutdown would otherwise wait out per player.
|
|
99
|
+
*/
|
|
93
100
|
private readOnce;
|
|
94
101
|
private handleFailure;
|
|
95
102
|
/**
|
package/dist/poller.js
CHANGED
|
@@ -199,15 +199,21 @@ class DevicePoller {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Read this zone once, long-polling when there is a token to poll with.
|
|
204
|
+
*
|
|
205
|
+
* Both reads are abortable, not just the long poll. A plain read is the path an
|
|
206
|
+
* unreachable player takes — a failure clears the etag — and it can sit for the
|
|
207
|
+
* whole status timeout, which a shutdown would otherwise wait out per player.
|
|
208
|
+
*/
|
|
202
209
|
async readOnce() {
|
|
203
210
|
const etag = this.etag;
|
|
204
|
-
if (etag === undefined) {
|
|
205
|
-
return this.options.client.readSyncStatus(this.currentEndpoint);
|
|
206
|
-
}
|
|
207
211
|
const abort = new AbortController();
|
|
208
212
|
this.abort = abort;
|
|
209
213
|
try {
|
|
210
|
-
return
|
|
214
|
+
return etag === undefined
|
|
215
|
+
? await this.options.client.readSyncStatus(this.currentEndpoint, abort.signal)
|
|
216
|
+
: await this.options.client.pollSyncStatus(this.currentEndpoint, etag, abort.signal);
|
|
211
217
|
}
|
|
212
218
|
catch (error) {
|
|
213
219
|
throw abort.signal.aborted ? new PollInterrupted() : error;
|
package/dist/utils/context.d.ts
CHANGED
|
@@ -16,3 +16,18 @@ import type { PlatformAccessory } from 'homebridge';
|
|
|
16
16
|
import { type AccessoryContext } from '../types';
|
|
17
17
|
/** Read and validate a restored accessory's context. */
|
|
18
18
|
export declare function parseAccessoryContext(accessory: PlatformAccessory): AccessoryContext;
|
|
19
|
+
/**
|
|
20
|
+
* Validate a restored accessory's context and make it the accessory's own.
|
|
21
|
+
*
|
|
22
|
+
* What a handler is given has to be the object Homebridge serialises, because
|
|
23
|
+
* handlers remember things in it: a slider's last non-zero level, a brand the
|
|
24
|
+
* player reported that configuration got wrong. {@link parseAccessoryContext}
|
|
25
|
+
* returns a new object by design, so a handler driven by that alone would write
|
|
26
|
+
* those values to a detached copy — thrown away at the next restart, after
|
|
27
|
+
* paying a full accessory-cache rewrite per volume step to save nothing.
|
|
28
|
+
*
|
|
29
|
+
* Kept separate from {@link parseAccessoryContext} so validating a context stays
|
|
30
|
+
* free of side effects for a caller that only wants to know whether it can be
|
|
31
|
+
* read.
|
|
32
|
+
*/
|
|
33
|
+
export declare function bindAccessoryContext(accessory: PlatformAccessory): AccessoryContext;
|
package/dist/utils/context.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.parseAccessoryContext = parseAccessoryContext;
|
|
18
|
+
exports.bindAccessoryContext = bindAccessoryContext;
|
|
18
19
|
const settings_1 = require("../settings");
|
|
19
20
|
const types_1 = require("../types");
|
|
20
21
|
const errors_1 = require("./errors");
|
|
@@ -57,3 +58,22 @@ function parseAccessoryContext(accessory) {
|
|
|
57
58
|
}
|
|
58
59
|
return context;
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate a restored accessory's context and make it the accessory's own.
|
|
63
|
+
*
|
|
64
|
+
* What a handler is given has to be the object Homebridge serialises, because
|
|
65
|
+
* handlers remember things in it: a slider's last non-zero level, a brand the
|
|
66
|
+
* player reported that configuration got wrong. {@link parseAccessoryContext}
|
|
67
|
+
* returns a new object by design, so a handler driven by that alone would write
|
|
68
|
+
* those values to a detached copy — thrown away at the next restart, after
|
|
69
|
+
* paying a full accessory-cache rewrite per volume step to save nothing.
|
|
70
|
+
*
|
|
71
|
+
* Kept separate from {@link parseAccessoryContext} so validating a context stays
|
|
72
|
+
* free of side effects for a caller that only wants to know whether it can be
|
|
73
|
+
* read.
|
|
74
|
+
*/
|
|
75
|
+
function bindAccessoryContext(accessory) {
|
|
76
|
+
const context = parseAccessoryContext(accessory);
|
|
77
|
+
accessory.context = context;
|
|
78
|
+
return context;
|
|
79
|
+
}
|
package/docs/FEATURES.md
CHANGED
|
@@ -45,8 +45,8 @@ A checklist of what is built. The plugin aims to cover everything about a BluOS
|
|
|
45
45
|
- ✅ Clean shutdown: poll loops, backoff delays and mDNS browses are cancelled, not left to run out
|
|
46
46
|
- ✅ A cached accessory that the plugin cannot drive reports No Response and says what to do about it. It never shows a stale value forever
|
|
47
47
|
- ✅ Custom Homebridge UI settings page, plus a plain `config.schema.json` form
|
|
48
|
-
- ✅ Homebridge
|
|
49
|
-
- ✅ Node.js
|
|
48
|
+
- ✅ Homebridge v2.0+ support
|
|
49
|
+
- ✅ Node.js 22, 24 and 26 support
|
|
50
50
|
|
|
51
51
|
## Not built yet
|
|
52
52
|
|
package/docs/README-DETAILED.md
CHANGED
|
@@ -275,7 +275,7 @@ Responses are parsed by a size-, depth- and element-capped XML reader instead of
|
|
|
275
275
|
|
|
276
276
|
- **Strict TypeScript:** `strict`, plus `noUncheckedIndexedAccess` and type-aware lint
|
|
277
277
|
- **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
|
|
278
|
-
- **CI:** build, lint (warnings are failures), type-check and test on Node
|
|
278
|
+
- **CI:** build, lint (warnings are failures), type-check and test on Node 22/24/26, a job against the oldest supported Homebridge, a committed-`dist` drift check, a dependency audit and OSV scanning
|
|
279
279
|
- **No analytics:** no tracking, no cloud, no accounts
|
|
280
280
|
|
|
281
281
|
## More
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-bluos",
|
|
3
3
|
"displayName": "Homebridge BluOS",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"homepage": "https://github.com/tbaur/homebridge-bluos#readme",
|
|
42
42
|
"overrides": {
|
|
43
|
-
"js-yaml": "^4.
|
|
43
|
+
"js-yaml": "^4.3.1",
|
|
44
44
|
"brace-expansion": "^5.0.9"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@types/dns-packet": "^5.6.5",
|
|
52
52
|
"@types/jest": "^30.0.0",
|
|
53
53
|
"@types/multicast-dns": "^7.2.4",
|
|
54
|
-
"@types/node": "^
|
|
54
|
+
"@types/node": "^26",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
56
56
|
"@typescript-eslint/parser": "^8.51.0",
|
|
57
57
|
"eslint": "^10.6.0",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"typescript": "^6.0.3"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
|
-
"homebridge": "^
|
|
66
|
-
"node": "
|
|
65
|
+
"homebridge": "^2.0.0",
|
|
66
|
+
"node": "^22 || ^24 || ^26"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsc",
|