homebridge-bluos 0.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 +7 -0
- package/DEVELOPMENT.md +65 -0
- package/LICENSE +202 -0
- package/README.md +251 -0
- package/SECURITY.md +43 -0
- package/config.schema.json +135 -0
- package/dist/api/client.d.ts +131 -0
- package/dist/api/client.js +226 -0
- package/dist/api/discovery.d.ts +136 -0
- package/dist/api/discovery.js +402 -0
- package/dist/api/http.d.ts +52 -0
- package/dist/api/http.js +136 -0
- package/dist/api/identity.d.ts +73 -0
- package/dist/api/identity.js +120 -0
- package/dist/api/index.d.ts +14 -0
- package/dist/api/index.js +30 -0
- package/dist/api/sync-status.d.ts +50 -0
- package/dist/api/sync-status.js +191 -0
- package/dist/api/xml.d.ts +76 -0
- package/dist/api/xml.js +365 -0
- package/dist/devices/base-accessory.d.ts +131 -0
- package/dist/devices/base-accessory.js +236 -0
- package/dist/devices/battery-accessory.d.ts +28 -0
- package/dist/devices/battery-accessory.js +85 -0
- package/dist/devices/host.d.ts +45 -0
- package/dist/devices/host.js +14 -0
- package/dist/devices/index.d.ts +14 -0
- package/dist/devices/index.js +30 -0
- package/dist/devices/mute-accessory.d.ts +35 -0
- package/dist/devices/mute-accessory.js +71 -0
- package/dist/devices/volume-accessory.d.ts +66 -0
- package/dist/devices/volume-accessory.js +218 -0
- package/dist/devices/volume-preset-accessory.d.ts +32 -0
- package/dist/devices/volume-preset-accessory.js +89 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +19 -0
- package/dist/platform.d.ts +124 -0
- package/dist/platform.js +489 -0
- package/dist/poller.d.ts +109 -0
- package/dist/poller.js +300 -0
- package/dist/settings.d.ts +184 -0
- package/dist/settings.js +210 -0
- package/dist/types/index.d.ts +218 -0
- package/dist/types/index.js +38 -0
- package/dist/ui-api.d.ts +20 -0
- package/dist/ui-api.js +32 -0
- package/dist/utils/context.d.ts +18 -0
- package/dist/utils/context.js +56 -0
- package/dist/utils/errors.d.ts +37 -0
- package/dist/utils/errors.js +92 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.js +29 -0
- package/dist/utils/serial.d.ts +22 -0
- package/dist/utils/serial.js +37 -0
- package/dist/utils/timing.d.ts +52 -0
- package/dist/utils/timing.js +74 -0
- package/dist/utils/validators.d.ts +99 -0
- package/dist/utils/validators.js +461 -0
- package/docs/FEATURES.md +91 -0
- package/docs/PROTOCOL.md +194 -0
- package/homebridge-ui/public/index.html +87 -0
- package/homebridge-ui/public/index.js +475 -0
- package/homebridge-ui/server.js +189 -0
- package/package.json +91 -0
package/dist/poller.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 tbaur
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
* See LICENSE file for full license text
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview One long-poll loop per player zone.
|
|
9
|
+
*
|
|
10
|
+
* The loop holds a `/SyncStatus?timeout=100&etag=…` request open and is woken by
|
|
11
|
+
* the player the moment that zone's state changes. Verified against firmware
|
|
12
|
+
* 4.16.6: with a current etag the request holds for exactly the requested window,
|
|
13
|
+
* with a stale etag it answers in 44 ms, and the etag is per-zone — a sibling zone
|
|
14
|
+
* on the same chassis changing volume does not wake this poll. That last point is
|
|
15
|
+
* why one loop per zone is correct rather than wasteful.
|
|
16
|
+
*
|
|
17
|
+
* The set/poll race is handled with a generation counter. A response that was
|
|
18
|
+
* computed before a local write cannot be distinguished from a fresh one by its
|
|
19
|
+
* contents, so any response that arrives across a write is discarded and replaced
|
|
20
|
+
* by an immediate re-read. That costs one cheap request — a stale etag answers
|
|
21
|
+
* immediately — and removes the class of bug where a HomeKit slider springs back
|
|
22
|
+
* to its old position a moment after being moved.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.DevicePoller = void 0;
|
|
26
|
+
const settings_1 = require("./settings");
|
|
27
|
+
const utils_1 = require("./utils");
|
|
28
|
+
/**
|
|
29
|
+
* Marks a read that this plugin cancelled on purpose.
|
|
30
|
+
*
|
|
31
|
+
* A write, a refresh request or an address change drops the poll in flight, and
|
|
32
|
+
* the resulting rejection is indistinguishable from a network failure by its
|
|
33
|
+
* type. Counting it as one would be actively harmful: three writes in a row
|
|
34
|
+
* would trip the unreachable threshold and show every accessory for that player
|
|
35
|
+
* as No Response, and each write would pay a backoff delay before the state it
|
|
36
|
+
* just changed could be read back.
|
|
37
|
+
*/
|
|
38
|
+
class PollInterrupted extends Error {
|
|
39
|
+
constructor() {
|
|
40
|
+
super('poll interrupted');
|
|
41
|
+
this.name = 'PollInterrupted';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Drives one player zone. */
|
|
45
|
+
class DevicePoller {
|
|
46
|
+
options;
|
|
47
|
+
currentEndpoint;
|
|
48
|
+
observation;
|
|
49
|
+
/** Opaque long-poll token. Cleared to force a plain read next time round. */
|
|
50
|
+
etag;
|
|
51
|
+
/** Incremented by every local write, to invalidate responses that predate it. */
|
|
52
|
+
generation = 0;
|
|
53
|
+
consecutiveFailures = 0;
|
|
54
|
+
lastRediscoveryAt = 0;
|
|
55
|
+
stopped = false;
|
|
56
|
+
loop;
|
|
57
|
+
abort;
|
|
58
|
+
/** Resolves the current backoff sleep early when a refresh is requested. */
|
|
59
|
+
wake;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.options = options;
|
|
62
|
+
this.currentEndpoint = options.endpoint;
|
|
63
|
+
}
|
|
64
|
+
get endpoint() {
|
|
65
|
+
return this.currentEndpoint;
|
|
66
|
+
}
|
|
67
|
+
get lastObservation() {
|
|
68
|
+
return this.observation;
|
|
69
|
+
}
|
|
70
|
+
/** Point this poller at a new address, dropping any request in flight. */
|
|
71
|
+
setEndpoint(endpoint) {
|
|
72
|
+
if (endpoint.host === this.currentEndpoint.host && endpoint.port === this.currentEndpoint.port) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.options.log.info(`${(0, utils_1.forLog)(this.options.displayName)} moved to ${endpoint.host}:${endpoint.port}`);
|
|
76
|
+
this.currentEndpoint = endpoint;
|
|
77
|
+
this.etag = undefined;
|
|
78
|
+
this.options.onEndpointChanged?.(endpoint);
|
|
79
|
+
this.interrupt();
|
|
80
|
+
}
|
|
81
|
+
/** Begin polling. Safe to call more than once. */
|
|
82
|
+
start() {
|
|
83
|
+
if (this.loop !== undefined) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.stopped = false;
|
|
87
|
+
// The loop is deliberately not awaited by its caller, so its promise needs a
|
|
88
|
+
// rejection handler of its own. Without one, an unexpected throw anywhere in
|
|
89
|
+
// the loop becomes an unhandled rejection, which on Node 20+ terminates the
|
|
90
|
+
// process — taking Homebridge and every other plugin down with it.
|
|
91
|
+
this.loop = this.run().catch((error) => {
|
|
92
|
+
this.options.log.error(`${this.label()} polling stopped after an unexpected error, so its state will `
|
|
93
|
+
+ `no longer update until Homebridge restarts: ${(0, utils_1.describeError)(error)}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Stop polling and drop any request in flight. */
|
|
97
|
+
async stop() {
|
|
98
|
+
this.stopped = true;
|
|
99
|
+
this.interrupt();
|
|
100
|
+
const loop = this.loop;
|
|
101
|
+
this.loop = undefined;
|
|
102
|
+
if (loop !== undefined) {
|
|
103
|
+
await loop;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Adopt the state a write reported, and invalidate anything in flight.
|
|
108
|
+
*
|
|
109
|
+
* The player's answer to a write is authoritative — it clamps the level into
|
|
110
|
+
* its own configured range — so this is both the fastest and the most accurate
|
|
111
|
+
* update available.
|
|
112
|
+
*/
|
|
113
|
+
adoptWriteResult(result) {
|
|
114
|
+
this.generation += 1;
|
|
115
|
+
const previous = this.observation;
|
|
116
|
+
const merged = {
|
|
117
|
+
...(previous ?? {
|
|
118
|
+
name: this.options.displayName,
|
|
119
|
+
fixedVolume: false,
|
|
120
|
+
muted: false,
|
|
121
|
+
syncRole: 'standalone',
|
|
122
|
+
}),
|
|
123
|
+
fixedVolume: result.fixedVolume,
|
|
124
|
+
muted: result.muted,
|
|
125
|
+
};
|
|
126
|
+
if (result.level !== undefined) {
|
|
127
|
+
merged.volume = result.level;
|
|
128
|
+
}
|
|
129
|
+
if (result.muteVolume !== undefined) {
|
|
130
|
+
merged.muteVolume = result.muteVolume;
|
|
131
|
+
}
|
|
132
|
+
else if (!result.muted) {
|
|
133
|
+
// The firmware only publishes muteVolume while muted, so carrying the old
|
|
134
|
+
// one forward past an unmute would let a stale pre-mute level win the
|
|
135
|
+
// restore decision in restoreLevelFrom and jump the room loud.
|
|
136
|
+
delete merged.muteVolume;
|
|
137
|
+
}
|
|
138
|
+
if (result.db !== undefined) {
|
|
139
|
+
merged.db = result.db;
|
|
140
|
+
}
|
|
141
|
+
// The etag from a /Volume response is not a /SyncStatus etag, so the next
|
|
142
|
+
// poll starts from a plain read rather than a token from the wrong resource.
|
|
143
|
+
this.etag = undefined;
|
|
144
|
+
this.observation = merged;
|
|
145
|
+
this.options.onObservation(merged, 'post-set');
|
|
146
|
+
this.interrupt();
|
|
147
|
+
}
|
|
148
|
+
/** Cancel the request in flight and wake any backoff sleep. */
|
|
149
|
+
interrupt() {
|
|
150
|
+
this.abort?.abort();
|
|
151
|
+
this.abort = undefined;
|
|
152
|
+
this.wake?.();
|
|
153
|
+
this.wake = undefined;
|
|
154
|
+
}
|
|
155
|
+
async run() {
|
|
156
|
+
let first = true;
|
|
157
|
+
while (!this.stopped) {
|
|
158
|
+
const generation = this.generation;
|
|
159
|
+
try {
|
|
160
|
+
const observation = await this.readOnce();
|
|
161
|
+
if (this.stopped) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (this.generation !== generation) {
|
|
165
|
+
// A write landed while this was in flight, so the response may describe
|
|
166
|
+
// the state before it. Discard and re-read rather than risk showing the
|
|
167
|
+
// user their change being undone.
|
|
168
|
+
this.options.log.debug(`${(0, utils_1.forLog)(this.options.displayName)} discarding a poll response that crossed a write`);
|
|
169
|
+
this.etag = undefined;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
this.consecutiveFailures = 0;
|
|
173
|
+
this.etag = observation.etag;
|
|
174
|
+
this.observation = observation;
|
|
175
|
+
this.options.onObservation(observation, first ? 'startup' : 'poll');
|
|
176
|
+
first = false;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (this.stopped) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (error instanceof PollInterrupted) {
|
|
183
|
+
// Our own doing, so it is not a failure: start a fresh read at once.
|
|
184
|
+
this.etag = undefined;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
await this.handleFailure(error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async readOnce() {
|
|
192
|
+
const etag = this.etag;
|
|
193
|
+
if (etag === undefined) {
|
|
194
|
+
return this.options.client.readSyncStatus(this.currentEndpoint);
|
|
195
|
+
}
|
|
196
|
+
const abort = new AbortController();
|
|
197
|
+
this.abort = abort;
|
|
198
|
+
try {
|
|
199
|
+
return await this.options.client.pollSyncStatus(this.currentEndpoint, etag, abort.signal);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw abort.signal.aborted ? new PollInterrupted() : error;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
if (this.abort === abort) {
|
|
206
|
+
this.abort = undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async handleFailure(error) {
|
|
211
|
+
this.consecutiveFailures += 1;
|
|
212
|
+
// A failed long-poll leaves the token untrustworthy; the next attempt starts
|
|
213
|
+
// from a plain read so a stale etag cannot mask a recovered player.
|
|
214
|
+
this.etag = undefined;
|
|
215
|
+
if (this.consecutiveFailures === 1) {
|
|
216
|
+
this.options.log.debug(`${this.label()} poll failed: ${(0, utils_1.describeError)(error)}`);
|
|
217
|
+
}
|
|
218
|
+
// Two separate policies, deliberately read from two constants: how long
|
|
219
|
+
// before HomeKit is told the truth, and how long before we go looking for a
|
|
220
|
+
// new address.
|
|
221
|
+
if (this.consecutiveFailures >= settings_1.POLL_FAILURES_BEFORE_UNKNOWN) {
|
|
222
|
+
this.options.onUnreachable(error);
|
|
223
|
+
}
|
|
224
|
+
if (this.consecutiveFailures >= settings_1.FAILURES_BEFORE_REDISCOVERY) {
|
|
225
|
+
const moved = await this.tryRediscovery();
|
|
226
|
+
if (moved) {
|
|
227
|
+
// The address is known to have changed, so there is nothing to wait for:
|
|
228
|
+
// sleeping out a backoff that may already be at its one-minute ceiling
|
|
229
|
+
// would delay recovery for no reason.
|
|
230
|
+
this.consecutiveFailures = 0;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (this.stopped) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const exponent = Math.min(this.consecutiveFailures - 1, 10);
|
|
238
|
+
const ceiling = Math.min(settings_1.POLL_BACKOFF_MAX_MS, settings_1.POLL_BACKOFF_BASE_MS * 2 ** exponent);
|
|
239
|
+
// Full jitter, so a rebooting fleet does not synchronise its retries into a
|
|
240
|
+
// burst against the network.
|
|
241
|
+
const delay = Math.round(ceiling * (0.5 + Math.random() * 0.5));
|
|
242
|
+
await this.sleepInterruptibly(delay);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Look for a new address for this player. True when the address changed.
|
|
246
|
+
*
|
|
247
|
+
* Rate-limited: a player that is switched off would otherwise trigger a
|
|
248
|
+
* multicast sweep on every backoff cycle.
|
|
249
|
+
*/
|
|
250
|
+
async tryRediscovery() {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
if (this.stopped || now - this.lastRediscoveryAt < settings_1.REDISCOVERY_MIN_INTERVAL_MS) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
this.lastRediscoveryAt = now;
|
|
256
|
+
try {
|
|
257
|
+
const found = await this.options.resolveEndpoint(this.options.deviceId);
|
|
258
|
+
// A sweep can outlive a shutdown request, and re-pointing a stopping poller
|
|
259
|
+
// would persist an address change nobody asked for.
|
|
260
|
+
if (this.stopped || found === undefined) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const moved = found.host !== this.currentEndpoint.host
|
|
264
|
+
|| found.port !== this.currentEndpoint.port;
|
|
265
|
+
this.setEndpoint(found);
|
|
266
|
+
return moved;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
this.options.log.debug(`${this.label()} address lookup failed: ${(0, utils_1.describeError)(error)}`);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Name plus stable id, for a log line someone has to act on.
|
|
275
|
+
*
|
|
276
|
+
* Two players can share a display name — the configuration validator warns
|
|
277
|
+
* about it rather than refusing it — so a failure line naming only the name
|
|
278
|
+
* cannot be traced back to a player.
|
|
279
|
+
*/
|
|
280
|
+
label() {
|
|
281
|
+
return `${(0, utils_1.forLog)(this.options.displayName)} [${(0, utils_1.forLog)(this.options.deviceId)}]`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Sleep, but return early if a write, refresh or shutdown arrives.
|
|
285
|
+
*
|
|
286
|
+
* The timer is cleared rather than left to expire, so stopping the plugin does
|
|
287
|
+
* not have to wait out a backoff delay that no longer matters.
|
|
288
|
+
*/
|
|
289
|
+
async sleepInterruptibly(ms) {
|
|
290
|
+
const pending = (0, utils_1.interruptibleSleep)(ms);
|
|
291
|
+
this.wake = () => pending.interrupt();
|
|
292
|
+
try {
|
|
293
|
+
await pending.promise;
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
this.wake = undefined;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
exports.DevicePoller = DevicePoller;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 tbaur
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0
|
|
5
|
+
* See LICENSE file for full license text
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Plugin-wide constants and the plugin version reader.
|
|
8
|
+
*
|
|
9
|
+
* Numbers that came from the BluOS Custom Integration API v1.7 specification or
|
|
10
|
+
* from measurements against real players cite their source, so a future reader
|
|
11
|
+
* can tell a vendor requirement apart from a judgement call.
|
|
12
|
+
*/
|
|
13
|
+
import type { PluginLogger } from './types';
|
|
14
|
+
/** npm package name. Must match `package.json` `name` for Homebridge to load us. */
|
|
15
|
+
export declare const PLUGIN_NAME = "homebridge-bluos";
|
|
16
|
+
/** Platform alias used in `config.json` and `config.schema.json`. */
|
|
17
|
+
export declare const PLATFORM_NAME = "BluOS";
|
|
18
|
+
/**
|
|
19
|
+
* Namespace for generated accessory UUIDs.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately does not include a host or port: seeding accessory identity with
|
|
22
|
+
* an address means a DHCP lease change re-creates every accessory and destroys
|
|
23
|
+
* the rooms, scenes and automations the user built on top of them.
|
|
24
|
+
*/
|
|
25
|
+
export declare const UUID_PREFIX = "homebridge-bluos:";
|
|
26
|
+
/** Reported when `package.json` cannot be read. */
|
|
27
|
+
export declare const UNKNOWN_PLUGIN_VERSION = "0.0.0";
|
|
28
|
+
/** Manufacturer shown when `/SyncStatus` does not report a brand. */
|
|
29
|
+
export declare const DEFAULT_BRAND = "BluOS";
|
|
30
|
+
/** Model shown when `/SyncStatus` does not report one. */
|
|
31
|
+
export declare const DEFAULT_MODEL = "BluOS Player";
|
|
32
|
+
/** Control port for a primary player (API v1.7 section 1). */
|
|
33
|
+
export declare const DEFAULT_BLUOS_PORT = 11000;
|
|
34
|
+
/**
|
|
35
|
+
* Ports the specification documents for multi-zone chassis.
|
|
36
|
+
*
|
|
37
|
+
* API v1.7 section 1: the CI 580 exposes four streamer nodes on one IP, using
|
|
38
|
+
* 11000, 11010, 11020 and 11030. The CI-S2 uses 11000 and 11010. Ports outside
|
|
39
|
+
* this set are accepted but warned about, because mDNS SRV records are the
|
|
40
|
+
* authority on which port a zone actually listens to.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DOCUMENTED_BLUOS_PORTS: readonly number[];
|
|
43
|
+
/** Lowest port number accepted anywhere in configuration. */
|
|
44
|
+
export declare const MIN_PORT = 1;
|
|
45
|
+
/** Highest port number accepted anywhere in configuration. */
|
|
46
|
+
export declare const MAX_PORT = 65535;
|
|
47
|
+
/**
|
|
48
|
+
* Duration passed as `/SyncStatus?timeout=`.
|
|
49
|
+
*
|
|
50
|
+
* The specification recommends 180 s for `/SyncStatus`, and forbids anything
|
|
51
|
+
* faster than 10 s. 100 s is inside that envelope and halves the worst-case
|
|
52
|
+
* delay before an unplugged player's socket read times out, which is how a
|
|
53
|
+
* silent disappearance (as opposed to a connection reset) gets noticed.
|
|
54
|
+
*/
|
|
55
|
+
export declare const LONG_POLL_SEC = 100;
|
|
56
|
+
/** Read headroom beyond the player's own long-poll timeout. */
|
|
57
|
+
export declare const LONG_POLL_READ_SLACK_MS = 5000;
|
|
58
|
+
/**
|
|
59
|
+
* Minimum gap between two consecutive requests for the same resource.
|
|
60
|
+
*
|
|
61
|
+
* API v1.7 section 2 makes this a requirement, not a suggestion: "a client must
|
|
62
|
+
* not make two consecutive requests for the same resource less than one second
|
|
63
|
+
* apart, even if the first request returns in less than one second".
|
|
64
|
+
*/
|
|
65
|
+
export declare const SAME_RESOURCE_MIN_GAP_MS = 1000;
|
|
66
|
+
/** Connect timeout. Short so a powered-off player cannot stall startup. */
|
|
67
|
+
export declare const CONNECT_TIMEOUT_MS = 2500;
|
|
68
|
+
/** Total timeout for a control call (`/Volume?level=`, `/Volume?mute=`). */
|
|
69
|
+
export declare const CONTROL_TIMEOUT_MS = 5000;
|
|
70
|
+
/** Total timeout for a plain, non-long-poll status read. */
|
|
71
|
+
export declare const STATUS_TIMEOUT_MS = 6000;
|
|
72
|
+
/** Minimum spacing between control calls to one endpoint. */
|
|
73
|
+
export declare const CONTROL_RATE_LIMIT_MS = 100;
|
|
74
|
+
/** First reconnect delay after a failed poll. Doubles up to the ceiling. */
|
|
75
|
+
export declare const POLL_BACKOFF_BASE_MS = 2000;
|
|
76
|
+
/** Ceiling for poll backoff. */
|
|
77
|
+
export declare const POLL_BACKOFF_MAX_MS = 60000;
|
|
78
|
+
/** Consecutive poll failures before an accessory reports No Response. */
|
|
79
|
+
export declare const POLL_FAILURES_BEFORE_UNKNOWN = 3;
|
|
80
|
+
/** How long before a still-failing player is warned about again. */
|
|
81
|
+
export declare const POLL_FAILURE_REWARN_MS = 3600000;
|
|
82
|
+
/**
|
|
83
|
+
* How long a HomeKit write may block before HAP gives up on it.
|
|
84
|
+
*
|
|
85
|
+
* HAP-NodeJS warns at `Accessory.TIMEOUT_WARNING` (3 s) and abandons the write
|
|
86
|
+
* at 9 s total, returning `OPERATION_TIMED_OUT` and discarding whatever the
|
|
87
|
+
* handler eventually returns. A set therefore has to answer well inside that
|
|
88
|
+
* window and finish any slower work in the background.
|
|
89
|
+
*/
|
|
90
|
+
export declare const HOMEKIT_WRITE_BUDGET_MS = 2500;
|
|
91
|
+
/** Level restored when the slider is switched on and no previous level is known. */
|
|
92
|
+
export declare const DEFAULT_RESTORE_VOLUME = 20;
|
|
93
|
+
/**
|
|
94
|
+
* Window for coalescing the pair of writes HomeKit sends when a slider moves
|
|
95
|
+
* from off.
|
|
96
|
+
*
|
|
97
|
+
* Dragging a Fanv2 up from zero produces an `Active` write immediately followed
|
|
98
|
+
* by a `RotationSpeed` write. Acting on both would set the restore level and then
|
|
99
|
+
* the requested level, which the user hears as a jump. Waiting briefly lets the
|
|
100
|
+
* second write supersede the first, so one value reaches the player. Short enough
|
|
101
|
+
* to be imperceptible next to the LAN round trip.
|
|
102
|
+
*/
|
|
103
|
+
export declare const SLIDER_COALESCE_MS = 150;
|
|
104
|
+
/** Lowest BluOS volume level. */
|
|
105
|
+
export declare const VOLUME_MIN = 0;
|
|
106
|
+
/** Highest BluOS volume level. */
|
|
107
|
+
export declare const VOLUME_MAX = 100;
|
|
108
|
+
/**
|
|
109
|
+
* `volume="-1"` means the player's output level is fixed.
|
|
110
|
+
*
|
|
111
|
+
* API v1.7 section 2.2: "-1 means fixed volume". Such a player must not be
|
|
112
|
+
* given a volume slider; writing a level to it is meaningless.
|
|
113
|
+
*/
|
|
114
|
+
export declare const FIXED_VOLUME_SENTINEL = -1;
|
|
115
|
+
/**
|
|
116
|
+
* `db="-100"` means silence, and is not a real output level.
|
|
117
|
+
*
|
|
118
|
+
* Measured on firmware 4.16.6: muting reports `volume="0" db="-100"` alongside
|
|
119
|
+
* `muteVolume` and `muteDb` carrying the pre-mute values. Note that writing
|
|
120
|
+
* `level=0` reports the same `db="-100"` with no `muteVolume`, so this value
|
|
121
|
+
* cannot be used to detect mute — see `readMuted` in `api/sync-status.ts`.
|
|
122
|
+
*/
|
|
123
|
+
export declare const MUTED_DB_SENTINEL = -100;
|
|
124
|
+
/**
|
|
125
|
+
* Largest `/SyncStatus` body accepted.
|
|
126
|
+
*
|
|
127
|
+
* Real responses measure a few hundred bytes; a fully grouped CI 580 is still
|
|
128
|
+
* far under a kilobyte. 128 KiB is generous while keeping a hostile or
|
|
129
|
+
* malfunctioning endpoint from growing the heap.
|
|
130
|
+
*/
|
|
131
|
+
export declare const MAX_XML_BYTES = 131072;
|
|
132
|
+
/** Deepest element nesting accepted. */
|
|
133
|
+
export declare const MAX_XML_DEPTH = 16;
|
|
134
|
+
/** Most elements accepted in one document. */
|
|
135
|
+
export declare const MAX_XML_ELEMENTS = 2000;
|
|
136
|
+
/** Most attributes accepted on one element. */
|
|
137
|
+
export declare const MAX_XML_ATTRIBUTES = 64;
|
|
138
|
+
/** mDNS service type advertised by primary players. */
|
|
139
|
+
export declare const MDNS_SERVICE_PRIMARY = "_musc._tcp.local";
|
|
140
|
+
/**
|
|
141
|
+
* mDNS service type advertised by secondary zones of a multi-zone chassis.
|
|
142
|
+
*
|
|
143
|
+
* API v1.7 appendix 13.1 maps this to LSDP class 0x0003, "BluOS Player
|
|
144
|
+
* (secondary in multi-zone players such as the CI580)".
|
|
145
|
+
*/
|
|
146
|
+
export declare const MDNS_SERVICE_SECONDARY = "_musp._tcp.local";
|
|
147
|
+
/** Default discovery window, in seconds. */
|
|
148
|
+
export declare const DEFAULT_DISCOVERY_TIMEOUT_SEC = 5;
|
|
149
|
+
/** Shortest configurable discovery window. */
|
|
150
|
+
export declare const MIN_DISCOVERY_TIMEOUT_SEC = 1;
|
|
151
|
+
/** Longest configurable discovery window. */
|
|
152
|
+
export declare const MAX_DISCOVERY_TIMEOUT_SEC = 30;
|
|
153
|
+
/** Consecutive poll failures before the platform tries to re-resolve an address. */
|
|
154
|
+
export declare const FAILURES_BEFORE_REDISCOVERY = 3;
|
|
155
|
+
/** Minimum gap between address re-resolution attempts. */
|
|
156
|
+
export declare const REDISCOVERY_MIN_INTERVAL_MS = 60000;
|
|
157
|
+
/**
|
|
158
|
+
* Most records of one kind kept from a browse window.
|
|
159
|
+
*
|
|
160
|
+
* Anything on the segment can answer a multicast query, and a browse window can
|
|
161
|
+
* be as long as 30 s, so the maps that accumulate advertisements need a ceiling
|
|
162
|
+
* they cannot be talked past. A household fleet uses a handful of entries.
|
|
163
|
+
*/
|
|
164
|
+
export declare const MAX_DISCOVERY_RECORDS = 256;
|
|
165
|
+
/**
|
|
166
|
+
* Most candidate endpoints verified from one browse.
|
|
167
|
+
*
|
|
168
|
+
* Verification opens a real connection per candidate, so the count of
|
|
169
|
+
* advertisements must not decide how many sockets this plugin opens.
|
|
170
|
+
*/
|
|
171
|
+
export declare const MAX_DISCOVERY_CANDIDATES = 64;
|
|
172
|
+
/** Candidate endpoints verified at once. Bounds concurrent sockets during a sweep. */
|
|
173
|
+
export declare const DISCOVERY_VERIFY_CONCURRENCY = 6;
|
|
174
|
+
/** Longest untrusted string interpolated into a log line. */
|
|
175
|
+
export declare const MAX_LOG_FIELD_LENGTH = 100;
|
|
176
|
+
/** Longest accepted HomeKit display name. */
|
|
177
|
+
export declare const MAX_NAME_LENGTH = 64;
|
|
178
|
+
/**
|
|
179
|
+
* Read this plugin's version from `package.json`.
|
|
180
|
+
*
|
|
181
|
+
* Reported to HomeKit as FirmwareRevision, which makes the version visible in
|
|
182
|
+
* the Home app and therefore in bug reports.
|
|
183
|
+
*/
|
|
184
|
+
export declare function readPluginVersion(log?: PluginLogger): string;
|