homebridge-bluos 1.1.2 → 1.1.3
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/README.md +2 -2
- package/config.schema.json +1 -1
- package/dist/devices/battery-accessory.d.ts +7 -13
- package/dist/devices/battery-accessory.js +18 -63
- package/dist/devices/mute-accessory.d.ts +2 -0
- package/dist/devices/mute-accessory.js +13 -0
- package/dist/devices/player-battery.d.ts +48 -0
- package/dist/devices/player-battery.js +109 -0
- package/dist/devices/volume-accessory.d.ts +2 -0
- package/dist/devices/volume-accessory.js +14 -0
- package/dist/platform.js +3 -0
- package/dist/types/index.d.ts +15 -4
- package/dist/types/index.js +5 -4
- package/dist/utils/context.js +3 -0
- package/dist/utils/validators.js +6 -1
- package/docs/FEATURES.md +2 -2
- package/docs/README-DETAILED.md +1 -1
- package/homebridge-ui/public/index.html +4 -4
- package/homebridge-ui/public/index.js +58 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.3](https://github.com/tbaur/homebridge-bluos/compare/v1.1.2...v1.1.3) (2026-08-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* host battery on the volume or mute tile ([#29](https://github.com/tbaur/homebridge-bluos/issues/29)) ([beee853](https://github.com/tbaur/homebridge-bluos/commit/beee8532ca39af8c2b97a7c7412b4c5b82c66621))
|
|
9
|
+
|
|
3
10
|
## [1.1.2](https://github.com/tbaur/homebridge-bluos/compare/v1.1.1...v1.1.2) (2026-08-30)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Browsing, search, queues and artwork stay in the BluOS app, which HomeKit cannot
|
|
|
13
13
|
- **Volume slider:** 0–100, the same scale the BluOS app uses. A fan by default, a lightbulb if you prefer
|
|
14
14
|
- **Mute switch:** unmuting restores the level the player remembered, not a guess
|
|
15
15
|
- **Volume presets:** one switch per exact level, addressable by name with Siri
|
|
16
|
-
- **Battery
|
|
16
|
+
- **Battery:** level, charging and low-battery, on the volume tile (or mute if there is no slider). A battery-only player still gets its own tile; the Home app will not render that one
|
|
17
17
|
- **Reboot switch:** momentary, so a scene cannot reboot your stereo, and still pressable when the player has stopped answering
|
|
18
18
|
- **Grouping-aware:** a zone leading a BluOS group moves the whole group, exactly as it does in the BluOS app
|
|
19
19
|
- **Multi-zone chassis:** each zone of a NAD CI S2 or CI 580 is its own player, on its own port
|
|
@@ -56,7 +56,7 @@ Nothing needs enabling on the player, because the BluOS LAN API is always on. A
|
|
|
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
|
|
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, then press the Homebridge Save button.
|
|
60
60
|
|
|
61
61
|
Or in `config.json`:
|
|
62
62
|
|
package/config.schema.json
CHANGED
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"title": "Battery Sensor",
|
|
120
120
|
"type": "boolean",
|
|
121
121
|
"default": false,
|
|
122
|
-
"description": "State of charge
|
|
122
|
+
"description": "State of charge for a player with a battery pack (PULSE FLEX with the BP100, PULSE M). Appears on the volume tile, or on mute if there is no slider. A battery-only player gets its own tile, which the Home app cannot render."
|
|
123
123
|
},
|
|
124
124
|
"reboot": {
|
|
125
125
|
"title": "Reboot Switch",
|
|
@@ -4,25 +4,19 @@
|
|
|
4
4
|
* Licensed under the Apache License, Version 2.0
|
|
5
5
|
* See LICENSE file for full license text
|
|
6
6
|
*
|
|
7
|
-
* @fileoverview
|
|
7
|
+
* @fileoverview Standalone battery accessory, used only when no other tile exists.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* HomeKit will not render this in the Home app. Prefer hosting the Battery
|
|
10
|
+
* service on the volume or mute accessory, which resolveAccessories does
|
|
11
|
+
* whenever one of those is enabled. This class remains for a player that
|
|
12
|
+
* exposes battery and nothing else.
|
|
13
13
|
*/
|
|
14
14
|
import type { PlayerObservation, RefreshReason } from '../types';
|
|
15
15
|
import { BaseAccessory, type AccessoryInit } from './base-accessory';
|
|
16
|
-
/** A battery sensor for one player. */
|
|
16
|
+
/** A battery sensor for one player, with no other HomeKit service beside it. */
|
|
17
17
|
export declare class BatteryAccessory extends BaseAccessory {
|
|
18
|
-
private readonly
|
|
19
|
-
private level;
|
|
20
|
-
private charging;
|
|
18
|
+
private readonly battery;
|
|
21
19
|
constructor(init: AccessoryInit);
|
|
22
|
-
private readLevel;
|
|
23
|
-
private readLowBattery;
|
|
24
|
-
private readChargingState;
|
|
25
|
-
private requireBattery;
|
|
26
20
|
protected updateFromObservation(observation: PlayerObservation, _reason: RefreshReason): void;
|
|
27
21
|
protected markUnavailable(): void;
|
|
28
22
|
}
|
|
@@ -5,81 +5,36 @@
|
|
|
5
5
|
* Licensed under the Apache License, Version 2.0
|
|
6
6
|
* See LICENSE file for full license text
|
|
7
7
|
*
|
|
8
|
-
* @fileoverview
|
|
8
|
+
* @fileoverview Standalone battery accessory, used only when no other tile exists.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* HomeKit will not render this in the Home app. Prefer hosting the Battery
|
|
11
|
+
* service on the volume or mute accessory, which resolveAccessories does
|
|
12
|
+
* whenever one of those is enabled. This class remains for a player that
|
|
13
|
+
* exposes battery and nothing else.
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.BatteryAccessory = void 0;
|
|
17
|
-
const utils_1 = require("../utils");
|
|
18
17
|
const base_accessory_1 = require("./base-accessory");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/** A battery sensor for one player. */
|
|
18
|
+
const player_battery_1 = require("./player-battery");
|
|
19
|
+
/** A battery sensor for one player, with no other HomeKit service beside it. */
|
|
22
20
|
class BatteryAccessory extends base_accessory_1.BaseAccessory {
|
|
23
|
-
|
|
24
|
-
level;
|
|
25
|
-
charging = false;
|
|
21
|
+
battery;
|
|
26
22
|
constructor(init) {
|
|
27
23
|
super(init);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.requireBattery();
|
|
37
|
-
return this.level ?? 0;
|
|
38
|
-
}
|
|
39
|
-
readLowBattery() {
|
|
40
|
-
this.requireBattery();
|
|
41
|
-
const { Characteristic: Char } = this.host.hap;
|
|
42
|
-
return (this.level ?? 100) <= LOW_BATTERY_THRESHOLD
|
|
43
|
-
? Char.StatusLowBattery.BATTERY_LEVEL_LOW
|
|
44
|
-
: Char.StatusLowBattery.BATTERY_LEVEL_NORMAL;
|
|
45
|
-
}
|
|
46
|
-
readChargingState() {
|
|
47
|
-
this.requireBattery();
|
|
48
|
-
const { Characteristic: Char } = this.host.hap;
|
|
49
|
-
return this.charging ? Char.ChargingState.CHARGING : Char.ChargingState.NOT_CHARGING;
|
|
50
|
-
}
|
|
51
|
-
requireBattery() {
|
|
52
|
-
this.requireObservedState();
|
|
53
|
-
if (this.level === undefined) {
|
|
54
|
-
throw this.communicationFailure();
|
|
55
|
-
}
|
|
24
|
+
this.battery = new player_battery_1.PlayerBattery({
|
|
25
|
+
hap: this.host.hap,
|
|
26
|
+
accessory: this.accessory,
|
|
27
|
+
displayName: this.displayName,
|
|
28
|
+
warnOnce: (key, message) => this.warnOnce(key, message),
|
|
29
|
+
communicationFailure: () => this.communicationFailure(),
|
|
30
|
+
hasObservedState: () => this.hasObservedState(),
|
|
31
|
+
});
|
|
56
32
|
}
|
|
57
33
|
updateFromObservation(observation, _reason) {
|
|
58
|
-
|
|
59
|
-
if (battery === undefined) {
|
|
60
|
-
if (this.level !== undefined) {
|
|
61
|
-
this.level = undefined;
|
|
62
|
-
}
|
|
63
|
-
this.warnOnce('no-battery', `${(0, utils_1.forLog)(this.displayName)} reports no battery pack; disable the battery sensor `
|
|
64
|
-
+ 'for this player in the plugin settings');
|
|
65
|
-
this.markUnavailable();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
this.level = battery.level;
|
|
69
|
-
this.charging = battery.charging;
|
|
70
|
-
const { Characteristic: Char } = this.host.hap;
|
|
71
|
-
this.service.updateCharacteristic(Char.BatteryLevel, battery.level);
|
|
72
|
-
this.service.updateCharacteristic(Char.StatusLowBattery, battery.level <= LOW_BATTERY_THRESHOLD
|
|
73
|
-
? Char.StatusLowBattery.BATTERY_LEVEL_LOW
|
|
74
|
-
: Char.StatusLowBattery.BATTERY_LEVEL_NORMAL);
|
|
75
|
-
this.service.updateCharacteristic(Char.ChargingState, battery.charging ? Char.ChargingState.CHARGING : Char.ChargingState.NOT_CHARGING);
|
|
34
|
+
this.battery.apply(observation);
|
|
76
35
|
}
|
|
77
36
|
markUnavailable() {
|
|
78
|
-
|
|
79
|
-
const error = this.communicationFailure();
|
|
80
|
-
this.service.updateCharacteristic(Char.BatteryLevel, error);
|
|
81
|
-
this.service.updateCharacteristic(Char.StatusLowBattery, error);
|
|
82
|
-
this.service.updateCharacteristic(Char.ChargingState, error);
|
|
37
|
+
this.battery.markUnavailable();
|
|
83
38
|
}
|
|
84
39
|
}
|
|
85
40
|
exports.BatteryAccessory = BatteryAccessory;
|
|
@@ -27,6 +27,8 @@ export declare class MuteAccessory extends BaseAccessory {
|
|
|
27
27
|
private readonly service;
|
|
28
28
|
/** Last mute state read from the player. */
|
|
29
29
|
private muted;
|
|
30
|
+
/** Present when this switch also carries the player's battery. */
|
|
31
|
+
private readonly battery;
|
|
30
32
|
constructor(init: AccessoryInit);
|
|
31
33
|
private readOn;
|
|
32
34
|
private writeOn;
|
|
@@ -25,11 +25,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
25
25
|
exports.MuteAccessory = void 0;
|
|
26
26
|
const utils_1 = require("../utils");
|
|
27
27
|
const base_accessory_1 = require("./base-accessory");
|
|
28
|
+
const player_battery_1 = require("./player-battery");
|
|
28
29
|
/** A mute switch for one player. */
|
|
29
30
|
class MuteAccessory extends base_accessory_1.BaseAccessory {
|
|
30
31
|
service;
|
|
31
32
|
/** Last mute state read from the player. */
|
|
32
33
|
muted;
|
|
34
|
+
/** Present when this switch also carries the player's battery. */
|
|
35
|
+
battery;
|
|
33
36
|
constructor(init) {
|
|
34
37
|
super(init);
|
|
35
38
|
const { Characteristic: Char, Service: HapService } = this.host.hap;
|
|
@@ -39,6 +42,14 @@ class MuteAccessory extends base_accessory_1.BaseAccessory {
|
|
|
39
42
|
.getCharacteristic(Char.On)
|
|
40
43
|
.onGet(() => this.readOn())
|
|
41
44
|
.onSet(async (value) => this.writeOn(value));
|
|
45
|
+
this.battery = (0, player_battery_1.attachHostedBattery)({
|
|
46
|
+
hap: this.host.hap,
|
|
47
|
+
accessory: this.accessory,
|
|
48
|
+
displayName: this.displayName,
|
|
49
|
+
warnOnce: (key, message) => this.warnOnce(key, message),
|
|
50
|
+
communicationFailure: () => this.communicationFailure(),
|
|
51
|
+
hasObservedState: () => this.hasObservedState(),
|
|
52
|
+
}, this.context.hostsBattery === true);
|
|
42
53
|
}
|
|
43
54
|
readOn() {
|
|
44
55
|
this.requireObservedState();
|
|
@@ -63,9 +74,11 @@ class MuteAccessory extends base_accessory_1.BaseAccessory {
|
|
|
63
74
|
updateFromObservation(observation, _reason) {
|
|
64
75
|
this.muted = observation.muted;
|
|
65
76
|
this.service.updateCharacteristic(this.host.hap.Characteristic.On, observation.muted);
|
|
77
|
+
this.battery?.apply(observation);
|
|
66
78
|
}
|
|
67
79
|
markUnavailable() {
|
|
68
80
|
this.service.updateCharacteristic(this.host.hap.Characteristic.On, this.communicationFailure());
|
|
81
|
+
this.battery?.markUnavailable();
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
exports.MuteAccessory = MuteAccessory;
|
|
@@ -0,0 +1,48 @@
|
|
|
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 The HAP Battery service for one player.
|
|
8
|
+
*
|
|
9
|
+
* HomeKit will not render a Battery accessory on its own: the Home app parks
|
|
10
|
+
* that tile under Other and says Not Supported. The service has to sit on a
|
|
11
|
+
* tile Home already understands, which for this plugin is the volume slider or
|
|
12
|
+
* the mute switch. A player that exposes neither still gets a standalone
|
|
13
|
+
* Battery accessory, and Home will keep saying Not Supported for that one.
|
|
14
|
+
*
|
|
15
|
+
* `/SyncStatus` already carries `<battery level charging/>` when a pack is
|
|
16
|
+
* fitted, so this costs no extra traffic. A player without a pack never reports
|
|
17
|
+
* the element, and the service then reports No Response rather than inventing
|
|
18
|
+
* a charge level.
|
|
19
|
+
*/
|
|
20
|
+
import type { API, PlatformAccessory } from 'homebridge';
|
|
21
|
+
import type { PlayerObservation } from '../types';
|
|
22
|
+
/** Collaborators for one Battery service. */
|
|
23
|
+
export interface PlayerBatteryInit {
|
|
24
|
+
hap: API['hap'];
|
|
25
|
+
accessory: PlatformAccessory;
|
|
26
|
+
displayName: string;
|
|
27
|
+
warnOnce: (key: string, message: string) => void;
|
|
28
|
+
communicationFailure: () => Error;
|
|
29
|
+
hasObservedState: () => boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Charge level, charging state and low-battery on one HAP Battery service. */
|
|
32
|
+
export declare class PlayerBattery {
|
|
33
|
+
private readonly init;
|
|
34
|
+
private readonly service;
|
|
35
|
+
private level;
|
|
36
|
+
private charging;
|
|
37
|
+
constructor(init: PlayerBatteryInit);
|
|
38
|
+
apply(observation: PlayerObservation): void;
|
|
39
|
+
markUnavailable(): void;
|
|
40
|
+
private readLevel;
|
|
41
|
+
private readLowBattery;
|
|
42
|
+
private readChargingState;
|
|
43
|
+
private requireBattery;
|
|
44
|
+
}
|
|
45
|
+
/** Bind a Battery service, or drop a leftover one when this tile no longer hosts it. */
|
|
46
|
+
export declare function attachHostedBattery(init: PlayerBatteryInit, hosts: boolean): PlayerBattery | undefined;
|
|
47
|
+
/** Drop a leftover Battery service when this accessory no longer hosts one. */
|
|
48
|
+
export declare function dropPlayerBattery(accessory: PlatformAccessory, hap: API['hap']): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
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 The HAP Battery service for one player.
|
|
9
|
+
*
|
|
10
|
+
* HomeKit will not render a Battery accessory on its own: the Home app parks
|
|
11
|
+
* that tile under Other and says Not Supported. The service has to sit on a
|
|
12
|
+
* tile Home already understands, which for this plugin is the volume slider or
|
|
13
|
+
* the mute switch. A player that exposes neither still gets a standalone
|
|
14
|
+
* Battery accessory, and Home will keep saying Not Supported for that one.
|
|
15
|
+
*
|
|
16
|
+
* `/SyncStatus` already carries `<battery level charging/>` when a pack is
|
|
17
|
+
* fitted, so this costs no extra traffic. A player without a pack never reports
|
|
18
|
+
* the element, and the service then reports No Response rather than inventing
|
|
19
|
+
* a charge level.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.PlayerBattery = void 0;
|
|
23
|
+
exports.attachHostedBattery = attachHostedBattery;
|
|
24
|
+
exports.dropPlayerBattery = dropPlayerBattery;
|
|
25
|
+
const utils_1 = require("../utils");
|
|
26
|
+
/** Below this percentage HomeKit is told the battery is low. */
|
|
27
|
+
const LOW_BATTERY_THRESHOLD = 20;
|
|
28
|
+
/** Charge level, charging state and low-battery on one HAP Battery service. */
|
|
29
|
+
class PlayerBattery {
|
|
30
|
+
init;
|
|
31
|
+
service;
|
|
32
|
+
level;
|
|
33
|
+
charging = false;
|
|
34
|
+
constructor(init) {
|
|
35
|
+
this.init = init;
|
|
36
|
+
const { Characteristic: Char, Service: HapService } = init.hap;
|
|
37
|
+
this.service = requireService(init.accessory, HapService.Battery, init.displayName);
|
|
38
|
+
this.service.setCharacteristic(Char.Name, init.displayName);
|
|
39
|
+
this.service.getCharacteristic(Char.BatteryLevel).onGet(() => this.readLevel());
|
|
40
|
+
this.service.getCharacteristic(Char.StatusLowBattery).onGet(() => this.readLowBattery());
|
|
41
|
+
this.service.getCharacteristic(Char.ChargingState).onGet(() => this.readChargingState());
|
|
42
|
+
}
|
|
43
|
+
apply(observation) {
|
|
44
|
+
const battery = observation.battery;
|
|
45
|
+
if (battery === undefined) {
|
|
46
|
+
this.level = undefined;
|
|
47
|
+
this.init.warnOnce('no-battery', `${(0, utils_1.forLog)(this.init.displayName)} reports no battery pack; disable the battery sensor `
|
|
48
|
+
+ 'for this player in the plugin settings');
|
|
49
|
+
this.markUnavailable();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.level = battery.level;
|
|
53
|
+
this.charging = battery.charging;
|
|
54
|
+
const { Characteristic: Char } = this.init.hap;
|
|
55
|
+
this.service.updateCharacteristic(Char.BatteryLevel, battery.level);
|
|
56
|
+
this.service.updateCharacteristic(Char.StatusLowBattery, battery.level <= LOW_BATTERY_THRESHOLD
|
|
57
|
+
? Char.StatusLowBattery.BATTERY_LEVEL_LOW
|
|
58
|
+
: Char.StatusLowBattery.BATTERY_LEVEL_NORMAL);
|
|
59
|
+
this.service.updateCharacteristic(Char.ChargingState, battery.charging ? Char.ChargingState.CHARGING : Char.ChargingState.NOT_CHARGING);
|
|
60
|
+
}
|
|
61
|
+
markUnavailable() {
|
|
62
|
+
const { Characteristic: Char } = this.init.hap;
|
|
63
|
+
const error = this.init.communicationFailure();
|
|
64
|
+
this.service.updateCharacteristic(Char.BatteryLevel, error);
|
|
65
|
+
this.service.updateCharacteristic(Char.StatusLowBattery, error);
|
|
66
|
+
this.service.updateCharacteristic(Char.ChargingState, error);
|
|
67
|
+
}
|
|
68
|
+
readLevel() {
|
|
69
|
+
this.requireBattery();
|
|
70
|
+
return this.level ?? 0;
|
|
71
|
+
}
|
|
72
|
+
readLowBattery() {
|
|
73
|
+
this.requireBattery();
|
|
74
|
+
const { Characteristic: Char } = this.init.hap;
|
|
75
|
+
return (this.level ?? 100) <= LOW_BATTERY_THRESHOLD
|
|
76
|
+
? Char.StatusLowBattery.BATTERY_LEVEL_LOW
|
|
77
|
+
: Char.StatusLowBattery.BATTERY_LEVEL_NORMAL;
|
|
78
|
+
}
|
|
79
|
+
readChargingState() {
|
|
80
|
+
this.requireBattery();
|
|
81
|
+
const { Characteristic: Char } = this.init.hap;
|
|
82
|
+
return this.charging ? Char.ChargingState.CHARGING : Char.ChargingState.NOT_CHARGING;
|
|
83
|
+
}
|
|
84
|
+
requireBattery() {
|
|
85
|
+
if (!this.init.hasObservedState() || this.level === undefined) {
|
|
86
|
+
throw this.init.communicationFailure();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.PlayerBattery = PlayerBattery;
|
|
91
|
+
/** Bind a Battery service, or drop a leftover one when this tile no longer hosts it. */
|
|
92
|
+
function attachHostedBattery(init, hosts) {
|
|
93
|
+
if (!hosts) {
|
|
94
|
+
dropPlayerBattery(init.accessory, init.hap);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return new PlayerBattery(init);
|
|
98
|
+
}
|
|
99
|
+
/** Drop a leftover Battery service when this accessory no longer hosts one. */
|
|
100
|
+
function dropPlayerBattery(accessory, hap) {
|
|
101
|
+
const stale = accessory.services.find((service) => service.UUID === hap.Service.Battery.UUID);
|
|
102
|
+
if (stale !== undefined) {
|
|
103
|
+
accessory.removeService(stale);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function requireService(accessory, ServiceType, displayName) {
|
|
107
|
+
const existing = accessory.services.find((service) => service.UUID === ServiceType.UUID);
|
|
108
|
+
return existing ?? accessory.addService(new ServiceType(displayName));
|
|
109
|
+
}
|
|
@@ -36,6 +36,8 @@ export declare class VolumeAccessory extends BaseAccessory {
|
|
|
36
36
|
private fixedVolume;
|
|
37
37
|
/** Level awaiting a coalesced write. */
|
|
38
38
|
private pendingLevel;
|
|
39
|
+
/** Present when this slider also carries the player's battery. */
|
|
40
|
+
private readonly battery;
|
|
39
41
|
/** In-flight coalesced write, shared by every handler that queued into it. */
|
|
40
42
|
private flush;
|
|
41
43
|
constructor(init: AccessoryInit);
|
|
@@ -32,6 +32,7 @@ const sync_status_1 = require("../api/sync-status");
|
|
|
32
32
|
const settings_1 = require("../settings");
|
|
33
33
|
const utils_1 = require("../utils");
|
|
34
34
|
const base_accessory_1 = require("./base-accessory");
|
|
35
|
+
const player_battery_1 = require("./player-battery");
|
|
35
36
|
/** A volume slider for one player. */
|
|
36
37
|
class VolumeAccessory extends base_accessory_1.BaseAccessory {
|
|
37
38
|
surface;
|
|
@@ -41,12 +42,22 @@ class VolumeAccessory extends base_accessory_1.BaseAccessory {
|
|
|
41
42
|
fixedVolume = false;
|
|
42
43
|
/** Level awaiting a coalesced write. */
|
|
43
44
|
pendingLevel;
|
|
45
|
+
/** Present when this slider also carries the player's battery. */
|
|
46
|
+
battery;
|
|
44
47
|
/** In-flight coalesced write, shared by every handler that queued into it. */
|
|
45
48
|
flush;
|
|
46
49
|
constructor(init) {
|
|
47
50
|
super(init);
|
|
48
51
|
this.surface = this.buildSurface();
|
|
49
52
|
this.bindHandlers();
|
|
53
|
+
this.battery = (0, player_battery_1.attachHostedBattery)({
|
|
54
|
+
hap: this.host.hap,
|
|
55
|
+
accessory: this.accessory,
|
|
56
|
+
displayName: this.displayName,
|
|
57
|
+
warnOnce: (key, message) => this.warnOnce(key, message),
|
|
58
|
+
communicationFailure: () => this.communicationFailure(),
|
|
59
|
+
hasObservedState: () => this.hasObservedState(),
|
|
60
|
+
}, this.context.hostsBattery === true);
|
|
50
61
|
}
|
|
51
62
|
buildSurface() {
|
|
52
63
|
const { Characteristic: Char, Service: HapService } = this.host.hap;
|
|
@@ -190,6 +201,7 @@ class VolumeAccessory extends base_accessory_1.BaseAccessory {
|
|
|
190
201
|
+ 'cannot do anything; disable the slider for this player in the plugin settings');
|
|
191
202
|
this.markUnavailable();
|
|
192
203
|
}
|
|
204
|
+
this.battery?.apply(observation);
|
|
193
205
|
return;
|
|
194
206
|
}
|
|
195
207
|
this.fixedVolume = false;
|
|
@@ -207,12 +219,14 @@ class VolumeAccessory extends base_accessory_1.BaseAccessory {
|
|
|
207
219
|
const { Characteristic: Char } = this.host.hap;
|
|
208
220
|
this.surface.service.updateCharacteristic(this.context.sliderService === 'lightbulb' ? Char.On : Char.Active, this.surface.toActiveValue(level > settings_1.VOLUME_MIN));
|
|
209
221
|
this.surface.service.updateCharacteristic(this.context.sliderService === 'lightbulb' ? Char.Brightness : Char.RotationSpeed, level);
|
|
222
|
+
this.battery?.apply(observation);
|
|
210
223
|
}
|
|
211
224
|
markUnavailable() {
|
|
212
225
|
const { Characteristic: Char } = this.host.hap;
|
|
213
226
|
const error = this.communicationFailure();
|
|
214
227
|
this.surface.service.updateCharacteristic(this.context.sliderService === 'lightbulb' ? Char.On : Char.Active, error);
|
|
215
228
|
this.surface.service.updateCharacteristic(this.context.sliderService === 'lightbulb' ? Char.Brightness : Char.RotationSpeed, error);
|
|
229
|
+
this.battery?.markUnavailable();
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
exports.VolumeAccessory = VolumeAccessory;
|
package/dist/platform.js
CHANGED
|
@@ -404,6 +404,9 @@ class BluOSPlatform {
|
|
|
404
404
|
if (Number.isInteger(previous?.lastNonZeroVolume)) {
|
|
405
405
|
context.lastNonZeroVolume = previous?.lastNonZeroVolume;
|
|
406
406
|
}
|
|
407
|
+
if (accessory.hostsBattery === true) {
|
|
408
|
+
context.hostsBattery = true;
|
|
409
|
+
}
|
|
407
410
|
return context;
|
|
408
411
|
}
|
|
409
412
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -18,10 +18,11 @@ export interface PluginLogger {
|
|
|
18
18
|
* What an accessory does. Part of its identity, and therefore of its UUID.
|
|
19
19
|
*
|
|
20
20
|
* `volume` is the fake slider, `mute` the mute switch, `volumePreset` a
|
|
21
|
-
* one-level switch, `battery`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* player it
|
|
21
|
+
* one-level switch, `battery` a standalone charge sensor used only when the
|
|
22
|
+
* player has no volume or mute tile to host it, and `reboot` a momentary
|
|
23
|
+
* switch that restarts one player. `rebootAll` is the only kind with no
|
|
24
|
+
* player behind it: it belongs to the platform and restarts every player it
|
|
25
|
+
* can find.
|
|
25
26
|
*/
|
|
26
27
|
export declare const ACCESSORY_KINDS: readonly ["volume", "mute", "volumePreset", "battery", "reboot", "rebootAll"];
|
|
27
28
|
/** @see ACCESSORY_KINDS */
|
|
@@ -121,6 +122,14 @@ export interface ResolvedAccessory {
|
|
|
121
122
|
sliderService: SliderService;
|
|
122
123
|
/** Target level, for `volumePreset` only. */
|
|
123
124
|
volume?: number;
|
|
125
|
+
/**
|
|
126
|
+
* True when this volume or mute accessory also carries the Battery service.
|
|
127
|
+
*
|
|
128
|
+
* HomeKit will not render a Battery accessory on its own. The service sits
|
|
129
|
+
* on the volume tile when that exists, otherwise on mute. A player that
|
|
130
|
+
* exposes neither still gets a standalone `battery` accessory.
|
|
131
|
+
*/
|
|
132
|
+
hostsBattery?: boolean;
|
|
124
133
|
}
|
|
125
134
|
/**
|
|
126
135
|
* What gets persisted in `PlatformAccessory.context`.
|
|
@@ -150,6 +159,8 @@ export interface AccessoryContext {
|
|
|
150
159
|
* authoritative and preferred.
|
|
151
160
|
*/
|
|
152
161
|
lastNonZeroVolume?: number;
|
|
162
|
+
/** True when this volume or mute accessory also carries the Battery service. */
|
|
163
|
+
hostsBattery?: boolean;
|
|
153
164
|
}
|
|
154
165
|
/**
|
|
155
166
|
* How a player relates to a runtime sync group.
|
package/dist/types/index.js
CHANGED
|
@@ -16,10 +16,11 @@ exports.isSliderService = isSliderService;
|
|
|
16
16
|
* What an accessory does. Part of its identity, and therefore of its UUID.
|
|
17
17
|
*
|
|
18
18
|
* `volume` is the fake slider, `mute` the mute switch, `volumePreset` a
|
|
19
|
-
* one-level switch, `battery`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* player it
|
|
19
|
+
* one-level switch, `battery` a standalone charge sensor used only when the
|
|
20
|
+
* player has no volume or mute tile to host it, and `reboot` a momentary
|
|
21
|
+
* switch that restarts one player. `rebootAll` is the only kind with no
|
|
22
|
+
* player behind it: it belongs to the platform and restarts every player it
|
|
23
|
+
* can find.
|
|
23
24
|
*/
|
|
24
25
|
exports.ACCESSORY_KINDS = [
|
|
25
26
|
'volume',
|
package/dist/utils/context.js
CHANGED
package/dist/utils/validators.js
CHANGED
|
@@ -432,6 +432,9 @@ function resolveAccessories(devices, warnings, platform) {
|
|
|
432
432
|
// ambiguous both on screen and to Siri.
|
|
433
433
|
name: suffixName(device.name, 'Volume'),
|
|
434
434
|
sliderService: device.sliderService,
|
|
435
|
+
// Home will not render a Battery accessory on its own. The service sits
|
|
436
|
+
// on this tile when a slider exists, so the charge icon appears there.
|
|
437
|
+
...(device.battery ? { hostsBattery: true } : {}),
|
|
435
438
|
});
|
|
436
439
|
}
|
|
437
440
|
if (device.mute) {
|
|
@@ -440,9 +443,11 @@ function resolveAccessories(devices, warnings, platform) {
|
|
|
440
443
|
deviceId: device.id,
|
|
441
444
|
name: suffixName(device.name, 'Mute'),
|
|
442
445
|
sliderService: device.sliderService,
|
|
446
|
+
// Mute hosts the battery only when there is no slider to put it on.
|
|
447
|
+
...(device.battery && !device.volumeSlider ? { hostsBattery: true } : {}),
|
|
443
448
|
});
|
|
444
449
|
}
|
|
445
|
-
if (device.battery) {
|
|
450
|
+
if (device.battery && !device.volumeSlider && !device.mute) {
|
|
446
451
|
accessories.push({
|
|
447
452
|
kind: 'battery',
|
|
448
453
|
deviceId: device.id,
|
package/docs/FEATURES.md
CHANGED
|
@@ -11,7 +11,7 @@ A checklist of what is built. The plugin aims to cover everything about a BluOS
|
|
|
11
11
|
- ✅ Slider exposed as a fan (default) or a lightbulb. Siri commands aimed at lights do not sweep up the fan
|
|
12
12
|
- ✅ Mute switch, with unmute restoring the level the player remembered instead of a guess
|
|
13
13
|
- ✅ Volume preset switches: one exact level per switch, addressable by name with Siri; set On sets the level, set Off is a no-op
|
|
14
|
-
- ✅ Battery
|
|
14
|
+
- ✅ Battery (level, charging state, low-battery) on the volume tile, or on mute if there is no slider, for players with a pack fitted. A battery-only player still gets a standalone tile; the Home app will not render that one
|
|
15
15
|
- ✅ Reboot switch per player, momentary so a scene or "turn everything off" cannot reboot the stereo, and still pressable when the player has stopped answering. Reboots the whole box on a multi-zone chassis, which BluOS gives no way to avoid, and warns at startup when that means other configured rooms
|
|
16
16
|
- ✅ Optional "reboot all" switch that reboots every BluOS player on the network, sending once per box instead of once per zone. The info log is a count (`found … device(s), … player(s)`), then `N of N device(s) rebooted`. The debug log names each box before anything is sent. Accessories on a box that was just rebooted stay quiet until it answers again, then take its live state. You choose its name with `options.rebootAllName`, so it can sit in whichever Home app room suits you
|
|
17
17
|
- ✅ Fixed-output players detected from `volume="-1"` and given no slider, with one explanatory log line
|
|
@@ -82,7 +82,7 @@ HomeKit has no way to render a library or a queue, and no vocabulary for "play t
|
|
|
82
82
|
| `volumeSlider` | Fanv2 (default) or Lightbulb, as a 0–100 slider |
|
|
83
83
|
| `mute` | Switch |
|
|
84
84
|
| `volumePresets[]` | Switch, one per level |
|
|
85
|
-
| `battery` | Battery |
|
|
85
|
+
| `battery` | Battery service on the volume tile, or on mute if there is no slider. Standalone Battery accessory only when both are off |
|
|
86
86
|
| `reboot` | Switch, momentary |
|
|
87
87
|
|
|
88
88
|
## Accessories per platform
|
package/docs/README-DETAILED.md
CHANGED
|
@@ -36,7 +36,7 @@ Where HomeKit is genuinely better is the tile, the scene, the automation and the
|
|
|
36
36
|
|
|
37
37
|
**Volume preset switches.** One switch per level, for example "Study Evening" at 15. Addressable by name with Siri, and far safer than a slider inside an automation. Setting one On sets the level. Setting it Off does nothing.
|
|
38
38
|
|
|
39
|
-
**Battery
|
|
39
|
+
**Battery.** Charge level, charging state and low-battery warning, for players with a battery pack fitted (PULSE FLEX with BP100, PULSE M). The Home app will not render a Battery accessory on its own, so the service sits on the volume tile when that exists, otherwise on mute. A player that exposes battery and nothing else still gets a standalone tile, and Home will keep saying Not Supported for that one.
|
|
40
40
|
|
|
41
41
|
**Reboot switch.** Reboots the player. It springs back to off and ignores being switched off, so a scene or "turn everything off" cannot reboot your stereo. It stays pressable while a player is showing No Response, which is exactly when you want it.
|
|
42
42
|
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
.bluos-badge { font-size: .7rem; text-transform: uppercase; letter-spacing: .03em; padding: .12rem .4rem; border-radius: .25rem; background: rgba(128, 128, 128, .2); }
|
|
21
21
|
.bluos-badge.warn { background: rgba(255, 193, 7, .28); }
|
|
22
22
|
.bluos-options { display: flex; gap: 1.1rem; flex-wrap: wrap; margin-top: .6rem; }
|
|
23
|
+
.bluos-check { display: inline-flex; align-items: center; gap: .4rem; margin: 0; }
|
|
24
|
+
.bluos-check input { margin: 0; }
|
|
25
|
+
.bluos-check label { margin: 0; }
|
|
23
26
|
.bluos-presets { margin-top: .6rem; }
|
|
24
27
|
.bluos-preset-row { display: flex; gap: .4rem; align-items: center; margin-bottom: .35rem; }
|
|
25
28
|
.bluos-preset-row input[type="text"] { flex: 1 1 8rem; }
|
|
@@ -83,7 +86,7 @@
|
|
|
83
86
|
<div class="bluos-card-title">Whole install</div>
|
|
84
87
|
</div>
|
|
85
88
|
<div class="bluos-options">
|
|
86
|
-
<label class="
|
|
89
|
+
<label class="bluos-check">
|
|
87
90
|
<input type="checkbox" id="reboot-all" />
|
|
88
91
|
Reboot All switch
|
|
89
92
|
</label>
|
|
@@ -96,13 +99,10 @@
|
|
|
96
99
|
<label for="reboot-all-name" style="font-size: .8rem;">Name in the Home app</label>
|
|
97
100
|
<input type="text" class="form-control form-control-sm" id="reboot-all-name"
|
|
98
101
|
maxlength="64" placeholder="BluOS Reboot All" />
|
|
99
|
-
<div class="bluos-meta">Name it for the room you want to keep it in.</div>
|
|
100
102
|
</div>
|
|
101
103
|
</div>
|
|
102
104
|
|
|
103
105
|
<div class="bluos-actions">
|
|
104
|
-
<button class="btn btn-success" id="save" disabled>Save</button>
|
|
105
|
-
<span class="spacer"></span>
|
|
106
106
|
<span id="summary" class="text-muted" style="font-size: .85rem;"></span>
|
|
107
107
|
</div>
|
|
108
108
|
|
|
@@ -80,9 +80,8 @@
|
|
|
80
80
|
platformConfig.devices = []
|
|
81
81
|
}
|
|
82
82
|
// Copied rather than aliased: `getPluginConfig` hands back the live objects,
|
|
83
|
-
// and editing a control
|
|
84
|
-
//
|
|
85
|
-
// this closes the same gap for the install-wide settings.
|
|
83
|
+
// and editing a control must not change them. The page pushes a fresh copy
|
|
84
|
+
// into Homebridge; the footer Save is what writes that copy to disk.
|
|
86
85
|
platformConfig.options = typeof platformConfig.options === 'object' && platformConfig.options !== null
|
|
87
86
|
? Object.assign({}, platformConfig.options)
|
|
88
87
|
: {}
|
|
@@ -115,9 +114,9 @@
|
|
|
115
114
|
? device.volumePresets.filter((preset) => preset && typeof preset.name === 'string')
|
|
116
115
|
.map((preset) => ({ name: preset.name, volume: Number(preset.volume) || 0 }))
|
|
117
116
|
: [],
|
|
118
|
-
// Kept so that
|
|
119
|
-
// as a per-device sliderService written in the advanced editor.
|
|
120
|
-
// it, opening this page and
|
|
117
|
+
// Kept so that a later push preserves settings this page does not model,
|
|
118
|
+
// such as a per-device sliderService written in the advanced editor.
|
|
119
|
+
// Without it, opening this page and saving would silently discard them.
|
|
121
120
|
saved: device,
|
|
122
121
|
})
|
|
123
122
|
}
|
|
@@ -140,6 +139,9 @@
|
|
|
140
139
|
if (existing.fixedVolume) {
|
|
141
140
|
existing.volumeSlider = false
|
|
142
141
|
}
|
|
142
|
+
if (!existing.hasBattery) {
|
|
143
|
+
existing.battery = false
|
|
144
|
+
}
|
|
143
145
|
continue
|
|
144
146
|
}
|
|
145
147
|
players.set(player.id, {
|
|
@@ -178,7 +180,7 @@
|
|
|
178
180
|
port: player.port,
|
|
179
181
|
volumeSlider: player.volumeSlider === true,
|
|
180
182
|
mute: player.mute === true,
|
|
181
|
-
battery: player.battery === true,
|
|
183
|
+
battery: player.battery === true && player.hasBattery === true,
|
|
182
184
|
reboot: player.reboot === true,
|
|
183
185
|
})
|
|
184
186
|
if (player.brand) {
|
|
@@ -239,22 +241,33 @@
|
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Copy the form into Homebridge's in-memory plugin config.
|
|
246
|
+
*
|
|
247
|
+
* The purple footer Save writes that copy to disk. This page does not save on
|
|
248
|
+
* its own: a second Save is easy to miss, and `savePluginConfig` is the same
|
|
249
|
+
* action as that footer button.
|
|
250
|
+
*/
|
|
251
|
+
async function pushConfig() {
|
|
252
|
+
platformConfig.devices = toDevices()
|
|
245
253
|
applyPlatformOptions()
|
|
246
254
|
try {
|
|
247
255
|
await homebridge.updatePluginConfig([platformConfig])
|
|
248
|
-
await homebridge.savePluginConfig()
|
|
249
|
-
homebridge.toast.success(
|
|
250
|
-
devices.length === 1 ? '1 player saved.' : `${devices.length} players saved.`,
|
|
251
|
-
'Saved',
|
|
252
|
-
)
|
|
253
256
|
} catch (error) {
|
|
254
|
-
homebridge.toast.error(describeError(error), 'Could not
|
|
257
|
+
homebridge.toast.error(describeError(error), 'Could not update the configuration')
|
|
255
258
|
}
|
|
256
259
|
}
|
|
257
260
|
|
|
261
|
+
function changed() {
|
|
262
|
+
refreshSummary()
|
|
263
|
+
void pushConfig()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderChanged() {
|
|
267
|
+
render()
|
|
268
|
+
void pushConfig()
|
|
269
|
+
}
|
|
270
|
+
|
|
258
271
|
// --- Rendering ----------------------------------------------------------
|
|
259
272
|
|
|
260
273
|
function renderPresets(player) {
|
|
@@ -270,7 +283,7 @@
|
|
|
270
283
|
name.value = preset.name
|
|
271
284
|
name.addEventListener('input', () => {
|
|
272
285
|
preset.name = name.value
|
|
273
|
-
|
|
286
|
+
changed()
|
|
274
287
|
})
|
|
275
288
|
|
|
276
289
|
const volume = el('input', {
|
|
@@ -279,12 +292,13 @@
|
|
|
279
292
|
volume.value = String(preset.volume)
|
|
280
293
|
volume.addEventListener('input', () => {
|
|
281
294
|
preset.volume = Number(volume.value)
|
|
295
|
+
changed()
|
|
282
296
|
})
|
|
283
297
|
|
|
284
298
|
const remove = el('button', { class: 'btn btn-outline-danger btn-sm', type: 'button' }, 'Remove')
|
|
285
299
|
remove.addEventListener('click', () => {
|
|
286
300
|
player.presets.splice(index, 1)
|
|
287
|
-
|
|
301
|
+
renderChanged()
|
|
288
302
|
})
|
|
289
303
|
|
|
290
304
|
row.append(name, volume, remove)
|
|
@@ -294,20 +308,23 @@
|
|
|
294
308
|
const add = el('button', { class: 'btn btn-outline-secondary btn-sm', type: 'button' }, 'Add volume preset')
|
|
295
309
|
add.addEventListener('click', () => {
|
|
296
310
|
player.presets.push({ name: '', volume: 30 })
|
|
297
|
-
|
|
311
|
+
renderChanged()
|
|
298
312
|
})
|
|
299
313
|
wrapper.append(add)
|
|
300
314
|
return wrapper
|
|
301
315
|
}
|
|
302
316
|
|
|
303
317
|
function checkbox(label, checked, disabled, onChange) {
|
|
304
|
-
const wrapper = el('div', { class: '
|
|
318
|
+
const wrapper = el('div', { class: 'bluos-check' })
|
|
305
319
|
const id = `opt-${Math.random().toString(36).slice(2)}`
|
|
306
|
-
const input = el('input', { type: 'checkbox',
|
|
320
|
+
const input = el('input', { type: 'checkbox', id })
|
|
307
321
|
input.checked = checked
|
|
308
322
|
input.disabled = disabled === true
|
|
309
|
-
input.addEventListener('change', () =>
|
|
310
|
-
|
|
323
|
+
input.addEventListener('change', () => {
|
|
324
|
+
onChange(input.checked)
|
|
325
|
+
changed()
|
|
326
|
+
})
|
|
327
|
+
const text = el('label', { for: id }, label)
|
|
311
328
|
wrapper.append(input, text)
|
|
312
329
|
return wrapper
|
|
313
330
|
}
|
|
@@ -321,7 +338,7 @@
|
|
|
321
338
|
include.setAttribute('aria-label', 'Expose this player in HomeKit')
|
|
322
339
|
include.addEventListener('change', () => {
|
|
323
340
|
player.selected = include.checked
|
|
324
|
-
|
|
341
|
+
renderChanged()
|
|
325
342
|
})
|
|
326
343
|
|
|
327
344
|
const title = el('div', { class: 'bluos-card-title' })
|
|
@@ -329,7 +346,7 @@
|
|
|
329
346
|
nameInput.value = player.name
|
|
330
347
|
nameInput.addEventListener('input', () => {
|
|
331
348
|
player.name = nameInput.value
|
|
332
|
-
|
|
349
|
+
changed()
|
|
333
350
|
})
|
|
334
351
|
title.append(nameInput)
|
|
335
352
|
|
|
@@ -371,12 +388,14 @@
|
|
|
371
388
|
(value) => { player.volumeSlider = value },
|
|
372
389
|
))
|
|
373
390
|
options.append(checkbox('Mute switch', player.mute, false, (value) => { player.mute = value }))
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
391
|
+
if (player.hasBattery) {
|
|
392
|
+
options.append(checkbox(
|
|
393
|
+
'Battery (on the volume or mute tile; its own tile only if those are off)',
|
|
394
|
+
player.battery,
|
|
395
|
+
false,
|
|
396
|
+
(value) => { player.battery = value },
|
|
397
|
+
))
|
|
398
|
+
}
|
|
380
399
|
options.append(checkbox(
|
|
381
400
|
'Reboot switch (reboots this player, and any zone sharing its box)',
|
|
382
401
|
player.reboot,
|
|
@@ -397,7 +416,7 @@
|
|
|
397
416
|
for (const device of devices) {
|
|
398
417
|
tiles += device.volumeSlider ? 1 : 0
|
|
399
418
|
tiles += device.mute ? 1 : 0
|
|
400
|
-
tiles += device.battery ? 1 : 0
|
|
419
|
+
tiles += device.battery && !device.volumeSlider && !device.mute ? 1 : 0
|
|
401
420
|
tiles += device.reboot ? 1 : 0
|
|
402
421
|
tiles += Array.isArray(device.volumePresets) ? device.volumePresets.length : 0
|
|
403
422
|
}
|
|
@@ -407,7 +426,6 @@
|
|
|
407
426
|
byId('summary').textContent = devices.length === 0 && tiles === 0
|
|
408
427
|
? 'Nothing selected.'
|
|
409
428
|
: `${devices.length} player(s), ${tiles} HomeKit accessory(s).`
|
|
410
|
-
byId('save').disabled = false
|
|
411
429
|
}
|
|
412
430
|
|
|
413
431
|
function render() {
|
|
@@ -449,7 +467,7 @@
|
|
|
449
467
|
}
|
|
450
468
|
}
|
|
451
469
|
mergeDiscovered(found)
|
|
452
|
-
|
|
470
|
+
renderChanged()
|
|
453
471
|
if (found.length === 0) {
|
|
454
472
|
homebridge.toast.warning(
|
|
455
473
|
'No players answered. If multicast is filtered on your network, add a player by address.',
|
|
@@ -487,7 +505,7 @@
|
|
|
487
505
|
player.selected = true
|
|
488
506
|
}
|
|
489
507
|
}
|
|
490
|
-
|
|
508
|
+
renderChanged()
|
|
491
509
|
homebridge.toast.success(`Found ${found.length} zone(s) at ${host}.`, 'Player added')
|
|
492
510
|
} catch (error) {
|
|
493
511
|
homebridge.toast.error(describeError(error), 'Probe failed')
|
|
@@ -498,10 +516,12 @@
|
|
|
498
516
|
|
|
499
517
|
byId('discover').addEventListener('click', () => void discover())
|
|
500
518
|
byId('manual-probe').addEventListener('click', () => void probe())
|
|
501
|
-
byId('save').addEventListener('click', () => void save())
|
|
502
519
|
byId('reboot-all').addEventListener('change', () => {
|
|
503
520
|
refreshRebootAllName()
|
|
504
|
-
|
|
521
|
+
changed()
|
|
522
|
+
})
|
|
523
|
+
byId('reboot-all-name').addEventListener('input', () => {
|
|
524
|
+
changed()
|
|
505
525
|
})
|
|
506
526
|
byId('toggle-manual').addEventListener('click', () => {
|
|
507
527
|
const panel = byId('manual')
|
|
@@ -524,6 +544,7 @@
|
|
|
524
544
|
loadConfig()
|
|
525
545
|
.then(() => {
|
|
526
546
|
render()
|
|
547
|
+
return pushConfig()
|
|
527
548
|
})
|
|
528
549
|
.catch((error) => {
|
|
529
550
|
homebridge.toast.error(describeError(error), 'Could not read the configuration')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-bluos",
|
|
3
3
|
"displayName": "Homebridge BluOS",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.3",
|
|
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",
|