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 handleVolumeGet;
15
- private handleVolumeSet;
16
- private handleMuteGet;
17
- private handleMuteSet;
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 ?? 0;
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 = "speaker";
19
+ this.accessory.context.kind = "volume";
18
20
  this.accessory.displayName = this.displayNameFor(scene);
19
- this.accessory.category = 26 /* this.platform.api.hap.Categories.SPEAKER */;
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.Speaker)
22
- ?? this.accessory.addService(this.platform.Service.Speaker);
23
- if (!this.service.testCharacteristic(this.platform.Characteristic.Volume)) {
24
- this.service.addCharacteristic(this.platform.Characteristic.Volume);
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.Mute)
28
- .onGet(this.handleMuteGet.bind(this))
29
- .onSet(this.handleMuteSet.bind(this));
30
- this.service.getCharacteristic(this.platform.Characteristic.Volume)
31
- .onGet(this.handleVolumeGet.bind(this))
32
- .onSet(this.handleVolumeSet.bind(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 Speaker")
38
- .setCharacteristic(this.platform.Characteristic.SerialNumber, `${scene.id}:speaker`);
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
- this.lastKnownVolume = scene.coordinatorVolume ?? this.lastKnownVolume;
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 = "speaker";
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 handleVolumeGet() {
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 handleVolumeSet(value) {
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.Volume, nextVolume);
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 handleMuteGet() {
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 handleMuteSet(value) {
76
- const nextMuted = value === true;
77
- await this.platform.setSceneMuted(this.scene.id, nextMuted);
78
- this.lastKnownMuted = nextMuted;
79
- this.service.updateCharacteristic(this.platform.Characteristic.Mute, nextMuted);
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":"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"}
1
+ {"version":3,"file":"sceneSpeaker.js","sourceRoot":"","sources":["../../../src/accessories/sceneSpeaker.ts"],"names":[],"mappings":"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"}
@@ -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 speaker.</div>
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&apos;s footer Save button when you&apos;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 speaker uses the same name with "Volume" appended.</span>
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-md-6">
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>Group Members</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">Additional rooms to join the coordinator when the scene runs. Leave all unchecked for a single-room scene. Selected member tiles can also carry their own optional volume override.</span>
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 changes based on what the coordinator room supports, such as favorites, line-in, or TV.</span>
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-3">
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-3">
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-3">
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-4">
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-4">
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
- draft.coordinatorVolume = elements.coordinatorVolume.value === "" ? undefined : Number(elements.coordinatorVolume.value);
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 coordinatorId = state.draft.coordinatorPlayerId;
1145
- const selected = new Set(state.draft.memberPlayerIds.filter((playerId) => playerId !== coordinatorId));
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 players = (household?.players || []).filter((player) => player.id !== coordinatorId);
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 additional players are available for grouping.</div>`
1175
+ ? `<div class="scene-help">No rooms are available in this household.</div>`
1150
1176
  : players
1151
1177
  .map(
1152
1178
  (player) => {
1153
- const isSelected = selected.has(player.id);
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 coordinator = household?.players.find((player) => player.id === state.draft.coordinatorPlayerId);
1187
- const supportedKinds = ["favorite", ...new Set((coordinator?.sourceOptions || []).filter((kind) => kind !== "favorite"))];
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
- elements.coordinatorVolume.value = state.draft.coordinatorVolume === "" ? "" : state.draft.coordinatorVolume ?? "";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-sonos-scenes",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Homebridge plugin for Sonos workflow scenes and orchestration.",
5
5
  "author": "applemanj",
6
6
  "homepage": "https://github.com/applemanj/homebridge-sonos-scenes#readme",