homebridge-sonos-scenes 0.1.7 → 0.1.9
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/README.md
CHANGED
|
@@ -20,7 +20,7 @@ The goal is not general Sonos control. The goal is a clean way to trigger multi-
|
|
|
20
20
|
|
|
21
21
|
## What Is Implemented
|
|
22
22
|
|
|
23
|
-
- A singular Homebridge dynamic platform plugin with switch accessories for each configured scene.
|
|
23
|
+
- A singular Homebridge dynamic platform plugin with switch accessories for each configured scene plus companion volume controls for quick level adjustments in Apple Home.
|
|
24
24
|
- A normalized scene model that stores stable Sonos IDs instead of room-name strings.
|
|
25
25
|
- A local-first `SonosTransport` abstraction with live discovery through the `sonos` package and fixture fallback for UI/testing.
|
|
26
26
|
- A `SceneRunner` that validates scenes, serializes execution per coordinator, retries transient failures, and emits structured logs.
|
|
@@ -7,12 +7,13 @@ export declare class SceneSpeakerAccessory {
|
|
|
7
7
|
private service;
|
|
8
8
|
private scene;
|
|
9
9
|
private lastKnownVolume;
|
|
10
|
+
private lastKnownActiveVolume;
|
|
10
11
|
private lastKnownMuted;
|
|
11
12
|
constructor(platform: SonosScenesPlatform, accessory: PlatformAccessory, scene: SceneDefinition);
|
|
12
13
|
updateScene(scene: SceneDefinition): void;
|
|
13
14
|
private displayNameFor;
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
-
private
|
|
15
|
+
private handleBrightnessGet;
|
|
16
|
+
private handleBrightnessSet;
|
|
17
|
+
private handleOnGet;
|
|
18
|
+
private handleOnSet;
|
|
18
19
|
}
|
|
@@ -7,76 +7,122 @@ class SceneSpeakerAccessory {
|
|
|
7
7
|
service;
|
|
8
8
|
scene;
|
|
9
9
|
lastKnownVolume;
|
|
10
|
+
lastKnownActiveVolume;
|
|
10
11
|
lastKnownMuted = false;
|
|
11
12
|
constructor(platform, accessory, scene) {
|
|
12
13
|
this.platform = platform;
|
|
13
14
|
this.accessory = accessory;
|
|
14
15
|
this.scene = scene;
|
|
15
|
-
this.lastKnownVolume = scene.coordinatorVolume ??
|
|
16
|
+
this.lastKnownVolume = scene.coordinatorVolume ?? 30;
|
|
17
|
+
this.lastKnownActiveVolume = this.lastKnownVolume > 0 ? this.lastKnownVolume : 30;
|
|
16
18
|
this.accessory.context.sceneId = scene.id;
|
|
17
|
-
this.accessory.context.kind = "
|
|
19
|
+
this.accessory.context.kind = "volume";
|
|
18
20
|
this.accessory.displayName = this.displayNameFor(scene);
|
|
19
|
-
this.accessory.category =
|
|
21
|
+
this.accessory.category = 5 /* this.platform.api.hap.Categories.LIGHTBULB */;
|
|
22
|
+
const legacySpeakerService = this.accessory.getService(this.platform.Service.Speaker);
|
|
23
|
+
if (legacySpeakerService) {
|
|
24
|
+
this.accessory.removeService(legacySpeakerService);
|
|
25
|
+
}
|
|
20
26
|
this.service =
|
|
21
|
-
this.accessory.getService(this.platform.Service.
|
|
22
|
-
?? this.accessory.addService(this.platform.Service.
|
|
23
|
-
if (!this.service.testCharacteristic(this.platform.Characteristic.
|
|
24
|
-
this.service.addCharacteristic(this.platform.Characteristic.
|
|
27
|
+
this.accessory.getService(this.platform.Service.Lightbulb)
|
|
28
|
+
?? this.accessory.addService(this.platform.Service.Lightbulb);
|
|
29
|
+
if (!this.service.testCharacteristic(this.platform.Characteristic.Brightness)) {
|
|
30
|
+
this.service.addCharacteristic(this.platform.Characteristic.Brightness);
|
|
25
31
|
}
|
|
26
32
|
this.service.setCharacteristic(this.platform.Characteristic.Name, this.displayNameFor(scene));
|
|
27
|
-
this.service.getCharacteristic(this.platform.Characteristic.
|
|
28
|
-
.onGet(this.
|
|
29
|
-
.onSet(this.
|
|
30
|
-
this.service.getCharacteristic(this.platform.Characteristic.
|
|
31
|
-
.onGet(this.
|
|
32
|
-
.onSet(this.
|
|
33
|
+
this.service.getCharacteristic(this.platform.Characteristic.On)
|
|
34
|
+
.onGet(this.handleOnGet.bind(this))
|
|
35
|
+
.onSet(this.handleOnSet.bind(this));
|
|
36
|
+
this.service.getCharacteristic(this.platform.Characteristic.Brightness)
|
|
37
|
+
.onGet(this.handleBrightnessGet.bind(this))
|
|
38
|
+
.onSet(this.handleBrightnessSet.bind(this));
|
|
33
39
|
const accessoryInformation = this.accessory.getService(this.platform.Service.AccessoryInformation)
|
|
34
40
|
?? this.accessory.addService(this.platform.Service.AccessoryInformation);
|
|
35
41
|
accessoryInformation
|
|
36
42
|
.setCharacteristic(this.platform.Characteristic.Manufacturer, "homebridge-sonos-scenes")
|
|
37
|
-
.setCharacteristic(this.platform.Characteristic.Model, "Sonos Scene
|
|
38
|
-
.setCharacteristic(this.platform.Characteristic.SerialNumber, `${scene.id}:
|
|
43
|
+
.setCharacteristic(this.platform.Characteristic.Model, "Sonos Scene Volume Dimmer")
|
|
44
|
+
.setCharacteristic(this.platform.Characteristic.SerialNumber, `${scene.id}:volume`);
|
|
39
45
|
}
|
|
40
46
|
updateScene(scene) {
|
|
41
47
|
this.scene = scene;
|
|
42
|
-
|
|
48
|
+
const configuredVolume = scene.coordinatorVolume;
|
|
49
|
+
if (configuredVolume !== undefined) {
|
|
50
|
+
this.lastKnownVolume = configuredVolume;
|
|
51
|
+
if (configuredVolume > 0) {
|
|
52
|
+
this.lastKnownActiveVolume = configuredVolume;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
43
55
|
this.accessory.context.sceneId = scene.id;
|
|
44
|
-
this.accessory.context.kind = "
|
|
56
|
+
this.accessory.context.kind = "volume";
|
|
45
57
|
this.accessory.displayName = this.displayNameFor(scene);
|
|
46
58
|
this.service.setCharacteristic(this.platform.Characteristic.Name, this.displayNameFor(scene));
|
|
47
59
|
}
|
|
48
60
|
displayNameFor(scene) {
|
|
49
61
|
return `${scene.name} Volume`;
|
|
50
62
|
}
|
|
51
|
-
async
|
|
63
|
+
async handleBrightnessGet() {
|
|
52
64
|
try {
|
|
53
65
|
this.lastKnownVolume = await this.platform.getSceneVolume(this.scene.id);
|
|
66
|
+
if (this.lastKnownVolume > 0) {
|
|
67
|
+
this.lastKnownActiveVolume = this.lastKnownVolume;
|
|
68
|
+
}
|
|
54
69
|
}
|
|
55
70
|
catch {
|
|
56
71
|
void 0;
|
|
57
72
|
}
|
|
58
73
|
return this.lastKnownVolume;
|
|
59
74
|
}
|
|
60
|
-
async
|
|
75
|
+
async handleBrightnessSet(value) {
|
|
61
76
|
const nextVolume = Math.max(0, Math.min(100, Math.round(Number(value))));
|
|
62
77
|
await this.platform.setSceneVolume(this.scene.id, nextVolume);
|
|
63
78
|
this.lastKnownVolume = nextVolume;
|
|
64
|
-
this.service.updateCharacteristic(this.platform.Characteristic.
|
|
79
|
+
this.service.updateCharacteristic(this.platform.Characteristic.Brightness, nextVolume);
|
|
80
|
+
if (nextVolume > 0) {
|
|
81
|
+
this.lastKnownActiveVolume = nextVolume;
|
|
82
|
+
if (this.lastKnownMuted) {
|
|
83
|
+
await this.platform.setSceneMuted(this.scene.id, false);
|
|
84
|
+
this.lastKnownMuted = false;
|
|
85
|
+
}
|
|
86
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await this.platform.setSceneMuted(this.scene.id, true);
|
|
90
|
+
this.lastKnownMuted = true;
|
|
91
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, false);
|
|
65
92
|
}
|
|
66
|
-
async
|
|
93
|
+
async handleOnGet() {
|
|
67
94
|
try {
|
|
68
95
|
this.lastKnownMuted = await this.platform.getSceneMuted(this.scene.id);
|
|
96
|
+
this.lastKnownVolume = await this.platform.getSceneVolume(this.scene.id);
|
|
97
|
+
if (this.lastKnownVolume > 0) {
|
|
98
|
+
this.lastKnownActiveVolume = this.lastKnownVolume;
|
|
99
|
+
}
|
|
69
100
|
}
|
|
70
101
|
catch {
|
|
71
102
|
void 0;
|
|
72
103
|
}
|
|
73
|
-
return this.lastKnownMuted;
|
|
104
|
+
return !this.lastKnownMuted && this.lastKnownVolume > 0;
|
|
74
105
|
}
|
|
75
|
-
async
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
106
|
+
async handleOnSet(value) {
|
|
107
|
+
const nextOn = value === true;
|
|
108
|
+
if (nextOn) {
|
|
109
|
+
const restoredVolume = this.lastKnownVolume > 0 ? this.lastKnownVolume : this.lastKnownActiveVolume;
|
|
110
|
+
if (this.lastKnownVolume <= 0 && restoredVolume > 0) {
|
|
111
|
+
await this.platform.setSceneVolume(this.scene.id, restoredVolume);
|
|
112
|
+
this.lastKnownVolume = restoredVolume;
|
|
113
|
+
this.service.updateCharacteristic(this.platform.Characteristic.Brightness, restoredVolume);
|
|
114
|
+
}
|
|
115
|
+
await this.platform.setSceneMuted(this.scene.id, false);
|
|
116
|
+
this.lastKnownMuted = false;
|
|
117
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, true);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (this.lastKnownVolume > 0) {
|
|
121
|
+
this.lastKnownActiveVolume = this.lastKnownVolume;
|
|
122
|
+
}
|
|
123
|
+
await this.platform.setSceneMuted(this.scene.id, true);
|
|
124
|
+
this.lastKnownMuted = true;
|
|
125
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, false);
|
|
80
126
|
}
|
|
81
127
|
}
|
|
82
128
|
exports.SceneSpeakerAccessory = SceneSpeakerAccessory;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneSpeaker.js","sourceRoot":"","sources":["../../../src/accessories/sceneSpeaker.ts"],"names":[],"mappings":";;;AAIA,MAAa,qBAAqB;
|
|
1
|
+
{"version":3,"file":"sceneSpeaker.js","sourceRoot":"","sources":["../../../src/accessories/sceneSpeaker.ts"],"names":[],"mappings":";;;AAIA,MAAa,qBAAqB;IAQb;IACA;IARX,OAAO,CAAU;IACjB,KAAK,CAAkB;IACvB,eAAe,CAAS;IACxB,qBAAqB,CAAS;IAC9B,cAAc,GAAG,KAAK,CAAC;IAE/B,YACmB,QAA6B,EAC7B,SAA4B,EAC7C,KAAsB;QAFL,aAAQ,GAAR,QAAQ,CAAqB;QAC7B,cAAS,GAAT,SAAS,CAAmB;QAG7C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,iBAAiB,IAAI,EAAE,CAAC;QACrD,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QACxD,IAAI,CAAC,SAAS,CAAC,QAAQ,qDAA6C,CAAC;QAErE,MAAM,oBAAoB,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACtF,IAAI,oBAAoB,EAAE,CAAC;YACzB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC;QACrD,CAAC;QAED,IAAI,CAAC,OAAO;YACV,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC;mBACvD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAEhE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9E,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QAC1E,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9F,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;aAC5D,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aAClC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC;aACpE,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aAC1C,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAE9C,MAAM,oBAAoB,GACxB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oBAAoB,CAAC;eAClE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAE3E,oBAAoB;aACjB,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,yBAAyB,CAAC;aACvF,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,2BAA2B,CAAC;aAClF,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,GAAG,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC;IACxF,CAAC;IAED,WAAW,CAAC,KAAsB;QAChC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,MAAM,gBAAgB,GAAG,KAAK,CAAC,iBAAiB,CAAC;QACjD,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC,eAAe,GAAG,gBAAgB,CAAC;YACxC,IAAI,gBAAgB,GAAG,CAAC,EAAE,CAAC;gBACzB,IAAI,CAAC,qBAAqB,GAAG,gBAAgB,CAAC;YAChD,CAAC;QACH,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QACxD,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;IAChG,CAAC;IAEO,cAAc,CAAC,KAAsB;QAC3C,OAAO,GAAG,KAAK,CAAC,IAAI,SAAS,CAAC;IAChC,CAAC;IAEO,KAAK,CAAC,mBAAmB;QAC/B,IAAI,CAAC;YACH,IAAI,CAAC,eAAe,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzE,IAAI,IAAI,CAAC,eAAe,GAAG,CAAC,EAAE,CAAC;gBAC7B,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,eAAe,CAAC;YACpD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,CAAC,CAAC;QACT,CAAC;QAED,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAAC,KAA0B;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACzE,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;QAC9D,IAAI,CAAC,eAAe,GAAG,UAAU,CAAC;QAClC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAEvF,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,CAAC,qBAAqB,GAAG,UAAU,CAAC;YACxC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;gBACxB,MAAM,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;gBACxD,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;YAC9B,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACzE,OAAO;QACT,CAAC;QAED,MAAM,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACvD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC5E,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,IAAI,CAAC;YACH,IAAI,CAAC,cAAc,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACvE,IAAI,CAAC,eAAe,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzE,IAAI,IAAI,CAAC,eAAe,GAAG,CAAC,EAAE,CAAC;gBAC7B,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,eAAe,CAAC;YACpD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,CAAC,CAAC;QACT,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;IAC1D,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,KAA0B;QAClD,MAAM,MAAM,GAAG,KAAK,KAAK,IAAI,CAAC;QAC9B,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC;YACpG,IAAI,IAAI,CAAC,eAAe,IAAI,CAAC,IAAI,cAAc,GAAG,CAAC,EAAE,CAAC;gBACpD,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC;gBAClE,IAAI,CAAC,eAAe,GAAG,cAAc,CAAC;gBACtC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;YAC7F,CAAC;YACD,MAAM,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YACxD,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;YAC5B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACzE,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,eAAe,GAAG,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,eAAe,CAAC;QACpD,CAAC;QACD,MAAM,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACvD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC5E,CAAC;CACF;AA5ID,sDA4IC"}
|
|
@@ -365,6 +365,33 @@
|
|
|
365
365
|
max-width: 132px;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
.scene-member-flags {
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-wrap: wrap;
|
|
371
|
+
gap: 0.35rem;
|
|
372
|
+
margin-top: 0.2rem;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.scene-member-flag {
|
|
376
|
+
display: inline-flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
padding: 0.15rem 0.45rem;
|
|
379
|
+
border-radius: 999px;
|
|
380
|
+
font-size: 0.72rem;
|
|
381
|
+
font-weight: 600;
|
|
382
|
+
line-height: 1.2;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.scene-member-flag.source {
|
|
386
|
+
background: rgba(10, 132, 255, 0.12);
|
|
387
|
+
color: #0a84ff;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.scene-member-flag.primary {
|
|
391
|
+
background: rgba(168, 85, 247, 0.16);
|
|
392
|
+
color: #7c3aed;
|
|
393
|
+
}
|
|
394
|
+
|
|
368
395
|
.scene-log {
|
|
369
396
|
max-height: 280px;
|
|
370
397
|
overflow: auto;
|
|
@@ -592,7 +619,7 @@
|
|
|
592
619
|
<div class="card-header scene-section-header">
|
|
593
620
|
<div class="scene-section-header-copy">
|
|
594
621
|
<strong>Scenes</strong>
|
|
595
|
-
<div class="scene-help">Each saved scene becomes a HomeKit switch plus a companion volume
|
|
622
|
+
<div class="scene-help">Each saved scene becomes a HomeKit switch plus a companion volume control.</div>
|
|
596
623
|
<div class="scene-inline-note mt-2">Save scene changes in the editor, then use Homebridge's footer Save button when you're ready to write everything to `config.json`.</div>
|
|
597
624
|
</div>
|
|
598
625
|
<div class="scene-section-actions">
|
|
@@ -624,14 +651,14 @@
|
|
|
624
651
|
<span>Scene Name</span>
|
|
625
652
|
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Scene Name">
|
|
626
653
|
?
|
|
627
|
-
<span class="scene-tooltip">The friendly name shown for this scene in HomeKit. The companion volume
|
|
654
|
+
<span class="scene-tooltip">The friendly name shown for this scene in HomeKit. The companion volume control uses the same name with "Volume" appended.</span>
|
|
628
655
|
</span>
|
|
629
656
|
</span>
|
|
630
657
|
</label>
|
|
631
658
|
<input class="form-control" id="scene-name" type="text">
|
|
632
659
|
</div>
|
|
633
660
|
<input id="scene-id" type="hidden">
|
|
634
|
-
<div class="col-
|
|
661
|
+
<div class="col-12">
|
|
635
662
|
<label class="form-label" for="household-select">
|
|
636
663
|
<span class="scene-label-row">
|
|
637
664
|
<span>Household</span>
|
|
@@ -643,25 +670,13 @@
|
|
|
643
670
|
</label>
|
|
644
671
|
<select class="form-select" id="household-select"></select>
|
|
645
672
|
</div>
|
|
646
|
-
<div class="col-md-6">
|
|
647
|
-
<label class="form-label" for="coordinator-select">
|
|
648
|
-
<span class="scene-label-row">
|
|
649
|
-
<span>Coordinator Room</span>
|
|
650
|
-
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Coordinator Room">
|
|
651
|
-
?
|
|
652
|
-
<span class="scene-tooltip">The main room that anchors the group and receives the source change first. Choose the room that should lead the scene.</span>
|
|
653
|
-
</span>
|
|
654
|
-
</span>
|
|
655
|
-
</label>
|
|
656
|
-
<select class="form-select" id="coordinator-select"></select>
|
|
657
|
-
</div>
|
|
658
673
|
<div class="col-12">
|
|
659
674
|
<label class="form-label">
|
|
660
675
|
<span class="scene-label-row">
|
|
661
|
-
<span>
|
|
676
|
+
<span>Scene Rooms</span>
|
|
662
677
|
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Group Members">
|
|
663
678
|
?
|
|
664
|
-
<span class="scene-tooltip">
|
|
679
|
+
<span class="scene-tooltip">Pick the rooms that should be part of this scene. For line-in and TV scenes, the source room is included automatically. One selected room becomes the internal lead room behind the scenes.</span>
|
|
665
680
|
</span>
|
|
666
681
|
</span>
|
|
667
682
|
</label>
|
|
@@ -673,7 +688,7 @@
|
|
|
673
688
|
<span>Source Kind</span>
|
|
674
689
|
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Source Kind">
|
|
675
690
|
?
|
|
676
|
-
<span class="scene-tooltip">The type of thing this scene should load. The list
|
|
691
|
+
<span class="scene-tooltip">The type of thing this scene should load. The list is based on what is available in this household, such as favorites, line-in, or TV.</span>
|
|
677
692
|
</span>
|
|
678
693
|
</span>
|
|
679
694
|
</label>
|
|
@@ -691,19 +706,7 @@
|
|
|
691
706
|
</label>
|
|
692
707
|
<select class="form-select" id="source-target"></select>
|
|
693
708
|
</div>
|
|
694
|
-
<div class="col-md-
|
|
695
|
-
<label class="form-label" for="coordinator-volume">
|
|
696
|
-
<span class="scene-label-row">
|
|
697
|
-
<span>Coordinator Volume</span>
|
|
698
|
-
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Coordinator Volume">
|
|
699
|
-
?
|
|
700
|
-
<span class="scene-tooltip">Optional starting volume for the coordinator room. Leave this blank to keep whatever volume the room already has.</span>
|
|
701
|
-
</span>
|
|
702
|
-
</span>
|
|
703
|
-
</label>
|
|
704
|
-
<input class="form-control" id="coordinator-volume" type="number" min="0" max="100">
|
|
705
|
-
</div>
|
|
706
|
-
<div class="col-md-3">
|
|
709
|
+
<div class="col-md-4">
|
|
707
710
|
<label class="form-label" for="settle-ms">
|
|
708
711
|
<span class="scene-label-row">
|
|
709
712
|
<span>Settle Delay (ms)</span>
|
|
@@ -715,7 +718,7 @@
|
|
|
715
718
|
</label>
|
|
716
719
|
<input class="form-control" id="settle-ms" type="number" min="0">
|
|
717
720
|
</div>
|
|
718
|
-
<div class="col-md-
|
|
721
|
+
<div class="col-md-4">
|
|
719
722
|
<label class="form-label" for="retry-count">
|
|
720
723
|
<span class="scene-label-row">
|
|
721
724
|
<span>Retry Count</span>
|
|
@@ -727,7 +730,7 @@
|
|
|
727
730
|
</label>
|
|
728
731
|
<input class="form-control" id="retry-count" type="number" min="0">
|
|
729
732
|
</div>
|
|
730
|
-
<div class="col-md-
|
|
733
|
+
<div class="col-md-4">
|
|
731
734
|
<label class="form-label" for="retry-delay-ms">
|
|
732
735
|
<span class="scene-label-row">
|
|
733
736
|
<span>Retry Delay (ms)</span>
|
|
@@ -739,7 +742,7 @@
|
|
|
739
742
|
</label>
|
|
740
743
|
<input class="form-control" id="retry-delay-ms" type="number" min="0">
|
|
741
744
|
</div>
|
|
742
|
-
<div class="col-md-
|
|
745
|
+
<div class="col-md-6">
|
|
743
746
|
<label class="form-label" for="auto-reset-ms">
|
|
744
747
|
<span class="scene-label-row">
|
|
745
748
|
<span>Auto Reset (ms)</span>
|
|
@@ -751,7 +754,7 @@
|
|
|
751
754
|
</label>
|
|
752
755
|
<input class="form-control" id="auto-reset-ms" type="number" min="0">
|
|
753
756
|
</div>
|
|
754
|
-
<div class="col-md-
|
|
757
|
+
<div class="col-md-6">
|
|
755
758
|
<label class="form-label" for="off-behavior">
|
|
756
759
|
<span class="scene-label-row">
|
|
757
760
|
<span>Off Behavior</span>
|
|
@@ -811,11 +814,9 @@
|
|
|
811
814
|
sceneName: document.getElementById("scene-name"),
|
|
812
815
|
sceneId: document.getElementById("scene-id"),
|
|
813
816
|
householdSelect: document.getElementById("household-select"),
|
|
814
|
-
coordinatorSelect: document.getElementById("coordinator-select"),
|
|
815
817
|
memberList: document.getElementById("member-list"),
|
|
816
818
|
sourceKind: document.getElementById("source-kind"),
|
|
817
819
|
sourceTarget: document.getElementById("source-target"),
|
|
818
|
-
coordinatorVolume: document.getElementById("coordinator-volume"),
|
|
819
820
|
settleMs: document.getElementById("settle-ms"),
|
|
820
821
|
retryCount: document.getElementById("retry-count"),
|
|
821
822
|
retryDelayMs: document.getElementById("retry-delay-ms"),
|
|
@@ -958,15 +959,45 @@
|
|
|
958
959
|
return (household?.favorites || []).filter((favorite) => favorite.playable !== false);
|
|
959
960
|
}
|
|
960
961
|
|
|
962
|
+
function getSelectedSceneRoomIdsFromDraft() {
|
|
963
|
+
const selected = Array.from(new Set([state.draft.coordinatorPlayerId, ...(state.draft.memberPlayerIds || [])].filter(Boolean)));
|
|
964
|
+
const source = state.draft.source;
|
|
965
|
+
if ((source?.kind === "line_in" || source?.kind === "tv") && source.deviceId) {
|
|
966
|
+
if (!selected.includes(source.deviceId)) {
|
|
967
|
+
selected.unshift(source.deviceId);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return selected;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function resolveCoordinatorPlayerId(selectedRoomIds, source) {
|
|
974
|
+
const uniqueRooms = Array.from(new Set((selectedRoomIds || []).filter(Boolean)));
|
|
975
|
+
|
|
976
|
+
if ((source?.kind === "line_in" || source?.kind === "tv") && source.deviceId) {
|
|
977
|
+
return source.deviceId;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (state.draft?.coordinatorPlayerId && uniqueRooms.includes(state.draft.coordinatorPlayerId)) {
|
|
981
|
+
return state.draft.coordinatorPlayerId;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return uniqueRooms[0] || "";
|
|
985
|
+
}
|
|
986
|
+
|
|
961
987
|
function serializeDraft() {
|
|
962
988
|
serializeCloudConfig();
|
|
963
989
|
const draft = clone(state.draft);
|
|
964
990
|
draft.name = elements.sceneName.value.trim() || "New Scene";
|
|
965
991
|
draft.householdId = elements.householdSelect.value;
|
|
966
|
-
draft.coordinatorPlayerId = elements.coordinatorSelect.value;
|
|
967
|
-
draft.memberPlayerIds = Array.from(elements.memberList.querySelectorAll("input[type='checkbox']:checked")).map((checkbox) => checkbox.value);
|
|
968
992
|
draft.source = buildSourcePayload();
|
|
969
|
-
|
|
993
|
+
const selectedRoomIds = Array.from(elements.memberList.querySelectorAll("input[type='checkbox']:checked")).map((checkbox) => checkbox.value);
|
|
994
|
+
draft.coordinatorPlayerId = resolveCoordinatorPlayerId(selectedRoomIds, draft.source);
|
|
995
|
+
if ((draft.source?.kind === "line_in" || draft.source?.kind === "tv") && draft.source.deviceId && !selectedRoomIds.includes(draft.source.deviceId)) {
|
|
996
|
+
selectedRoomIds.unshift(draft.source.deviceId);
|
|
997
|
+
}
|
|
998
|
+
draft.memberPlayerIds = selectedRoomIds.filter((playerId) => playerId !== draft.coordinatorPlayerId);
|
|
999
|
+
const leadVolumeInput = elements.memberList.querySelector("input[data-coordinator-volume]");
|
|
1000
|
+
draft.coordinatorVolume = leadVolumeInput && leadVolumeInput.value !== "" ? Number(leadVolumeInput.value) : undefined;
|
|
970
1001
|
draft.settleMs = Number(elements.settleMs.value || 0);
|
|
971
1002
|
draft.retryCount = Number(elements.retryCount.value || 0);
|
|
972
1003
|
draft.retryDelayMs = Number(elements.retryDelayMs.value || 0);
|
|
@@ -974,6 +1005,7 @@
|
|
|
974
1005
|
draft.offBehavior = { kind: elements.offBehavior.value };
|
|
975
1006
|
draft.playerVolumes = Array.from(elements.memberList.querySelectorAll("input[data-player-id]"))
|
|
976
1007
|
.filter((input) => input.value !== "")
|
|
1008
|
+
.filter((input) => selectedRoomIds.includes(input.dataset.playerId) && input.dataset.playerId !== draft.coordinatorPlayerId)
|
|
977
1009
|
.map((input) => ({
|
|
978
1010
|
playerId: input.dataset.playerId,
|
|
979
1011
|
volume: Number(input.value),
|
|
@@ -1127,40 +1159,54 @@
|
|
|
1127
1159
|
.join("");
|
|
1128
1160
|
}
|
|
1129
1161
|
|
|
1130
|
-
function renderCoordinatorOptions() {
|
|
1131
|
-
const household = getActiveHousehold();
|
|
1132
|
-
const players = household?.players || [];
|
|
1133
|
-
const currentCoordinator = players.some((player) => player.id === state.draft.coordinatorPlayerId)
|
|
1134
|
-
? state.draft.coordinatorPlayerId
|
|
1135
|
-
: players[0]?.id || "";
|
|
1136
|
-
state.draft.coordinatorPlayerId = currentCoordinator;
|
|
1137
|
-
elements.coordinatorSelect.innerHTML = players
|
|
1138
|
-
.map((player) => `<option value="${player.id}" ${player.id === currentCoordinator ? "selected" : ""}>${player.name}</option>`)
|
|
1139
|
-
.join("");
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
1162
|
function renderMemberOptions() {
|
|
1143
1163
|
const household = getActiveHousehold();
|
|
1144
|
-
const
|
|
1145
|
-
const selected = new Set(
|
|
1164
|
+
const selectedRoomIds = getSelectedSceneRoomIdsFromDraft();
|
|
1165
|
+
const selected = new Set(selectedRoomIds);
|
|
1166
|
+
const coordinatorId = resolveCoordinatorPlayerId(selectedRoomIds, state.draft.source);
|
|
1167
|
+
state.draft.coordinatorPlayerId = coordinatorId;
|
|
1168
|
+
state.draft.memberPlayerIds = selectedRoomIds.filter((playerId) => playerId !== coordinatorId);
|
|
1146
1169
|
const values = new Map((state.draft.playerVolumes || []).map((entry) => [entry.playerId, entry.volume]));
|
|
1147
|
-
const
|
|
1170
|
+
const forcedSourceId = (state.draft.source?.kind === "line_in" || state.draft.source?.kind === "tv")
|
|
1171
|
+
? state.draft.source.deviceId
|
|
1172
|
+
: "";
|
|
1173
|
+
const players = household?.players || [];
|
|
1148
1174
|
elements.memberList.innerHTML = players.length === 0
|
|
1149
|
-
? `<div class="scene-help">No
|
|
1175
|
+
? `<div class="scene-help">No rooms are available in this household.</div>`
|
|
1150
1176
|
: players
|
|
1151
1177
|
.map(
|
|
1152
1178
|
(player) => {
|
|
1153
|
-
const
|
|
1179
|
+
const isForcedSource = forcedSourceId === player.id;
|
|
1180
|
+
const isSelected = isForcedSource || selected.has(player.id);
|
|
1181
|
+
const flags = [
|
|
1182
|
+
isForcedSource ? `<span class="scene-member-flag source">Source Room</span>` : "",
|
|
1183
|
+
isSelected && coordinatorId === player.id ? `<span class="scene-member-flag primary">Lead Room</span>` : "",
|
|
1184
|
+
].filter(Boolean).join("");
|
|
1154
1185
|
return `
|
|
1155
1186
|
<div class="scene-member-pill ${isSelected ? "selected" : ""}">
|
|
1156
1187
|
<label class="scene-member-row scene-member-toggle">
|
|
1157
|
-
<input class="form-check-input" data-member-checkbox type="checkbox" value="${player.id}" ${isSelected ? "checked" : ""}>
|
|
1188
|
+
<input class="form-check-input" data-member-checkbox type="checkbox" value="${player.id}" ${isSelected ? "checked" : ""} ${isForcedSource ? "disabled" : ""}>
|
|
1158
1189
|
<span class="scene-member-copy">
|
|
1159
1190
|
<span class="scene-member-title">${player.name}</span>
|
|
1160
1191
|
<span class="scene-member-meta">${player.model || "Unknown model"} - ${((player.sourceOptions || []).join(", ") || "favorite").replaceAll("_", " ")}</span>
|
|
1192
|
+
${flags ? `<span class="scene-member-flags">${flags}</span>` : ""}
|
|
1161
1193
|
</span>
|
|
1162
1194
|
</label>
|
|
1163
|
-
${isSelected ? `
|
|
1195
|
+
${isSelected && coordinatorId === player.id ? `
|
|
1196
|
+
<div class="scene-member-volume">
|
|
1197
|
+
<label class="scene-member-volume-label" for="lead-volume-${player.id}">Lead Room Volume</label>
|
|
1198
|
+
<input
|
|
1199
|
+
class="form-control form-control-sm scene-member-volume-input"
|
|
1200
|
+
id="lead-volume-${player.id}"
|
|
1201
|
+
data-coordinator-volume="true"
|
|
1202
|
+
type="number"
|
|
1203
|
+
min="0"
|
|
1204
|
+
max="100"
|
|
1205
|
+
placeholder="Keep current"
|
|
1206
|
+
value="${state.draft.coordinatorVolume ?? ""}">
|
|
1207
|
+
</div>
|
|
1208
|
+
` : ""}
|
|
1209
|
+
${isSelected && coordinatorId !== player.id ? `
|
|
1164
1210
|
<div class="scene-member-volume">
|
|
1165
1211
|
<label class="scene-member-volume-label" for="member-volume-${player.id}">Volume Override</label>
|
|
1166
1212
|
<input
|
|
@@ -1183,8 +1229,8 @@
|
|
|
1183
1229
|
|
|
1184
1230
|
function renderSourceControls() {
|
|
1185
1231
|
const household = getActiveHousehold();
|
|
1186
|
-
const
|
|
1187
|
-
const supportedKinds = ["favorite", ...new Set((
|
|
1232
|
+
const players = household?.players || [];
|
|
1233
|
+
const supportedKinds = ["favorite", ...new Set(players.flatMap((player) => player.sourceOptions || []).filter((kind) => kind !== "favorite"))];
|
|
1188
1234
|
const sourceKind = supportedKinds.includes(state.draft.source?.kind) ? state.draft.source.kind : supportedKinds[0] || "favorite";
|
|
1189
1235
|
state.draft.source = state.draft.source || { kind: sourceKind, favoriteId: "" };
|
|
1190
1236
|
state.draft.source.kind = sourceKind;
|
|
@@ -1351,10 +1397,8 @@
|
|
|
1351
1397
|
elements.sceneName.value = state.draft.name || "";
|
|
1352
1398
|
elements.sceneId.value = state.draft.id || "";
|
|
1353
1399
|
renderHouseholdOptions();
|
|
1354
|
-
renderCoordinatorOptions();
|
|
1355
|
-
renderMemberOptions();
|
|
1356
1400
|
renderSourceControls();
|
|
1357
|
-
|
|
1401
|
+
renderMemberOptions();
|
|
1358
1402
|
elements.settleMs.value = state.draft.settleMs ?? 750;
|
|
1359
1403
|
elements.retryCount.value = state.draft.retryCount ?? 3;
|
|
1360
1404
|
elements.retryDelayMs.value = state.draft.retryDelayMs ?? 750;
|
|
@@ -1552,12 +1596,6 @@
|
|
|
1552
1596
|
render();
|
|
1553
1597
|
});
|
|
1554
1598
|
|
|
1555
|
-
elements.coordinatorSelect.addEventListener("change", () => {
|
|
1556
|
-
serializeDraft();
|
|
1557
|
-
state.draft.memberPlayerIds = state.draft.memberPlayerIds.filter((playerId) => playerId !== state.draft.coordinatorPlayerId);
|
|
1558
|
-
render();
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
1599
|
elements.sourceKind.addEventListener("change", () => {
|
|
1562
1600
|
serializeDraft();
|
|
1563
1601
|
state.draft.source = elements.sourceKind.value === "favorite"
|
|
@@ -1566,6 +1604,11 @@
|
|
|
1566
1604
|
render();
|
|
1567
1605
|
});
|
|
1568
1606
|
|
|
1607
|
+
elements.sourceTarget.addEventListener("change", () => {
|
|
1608
|
+
serializeDraft();
|
|
1609
|
+
render();
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1569
1612
|
homebridge.addEventListener("scene-test-result", (event) => {
|
|
1570
1613
|
state.lastRun = event.data;
|
|
1571
1614
|
render();
|
package/package.json
CHANGED