homebridge-sonos-scenes 0.1.7 → 0.1.8

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.
@@ -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;
@@ -631,7 +658,7 @@
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>
@@ -694,10 +709,10 @@
694
709
  <div class="col-md-3">
695
710
  <label class="form-label" for="coordinator-volume">
696
711
  <span class="scene-label-row">
697
- <span>Coordinator Volume</span>
712
+ <span>Lead Room Volume</span>
698
713
  <span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Coordinator Volume">
699
714
  ?
700
- <span class="scene-tooltip">Optional starting volume for the coordinator room. Leave this blank to keep whatever volume the room already has.</span>
715
+ <span class="scene-tooltip">Optional starting volume for the scene's lead room. Leave this blank to keep whatever volume that room already has.</span>
701
716
  </span>
702
717
  </span>
703
718
  </label>
@@ -811,7 +826,6 @@
811
826
  sceneName: document.getElementById("scene-name"),
812
827
  sceneId: document.getElementById("scene-id"),
813
828
  householdSelect: document.getElementById("household-select"),
814
- coordinatorSelect: document.getElementById("coordinator-select"),
815
829
  memberList: document.getElementById("member-list"),
816
830
  sourceKind: document.getElementById("source-kind"),
817
831
  sourceTarget: document.getElementById("source-target"),
@@ -958,14 +972,43 @@
958
972
  return (household?.favorites || []).filter((favorite) => favorite.playable !== false);
959
973
  }
960
974
 
975
+ function getSelectedSceneRoomIdsFromDraft() {
976
+ const selected = Array.from(new Set([state.draft.coordinatorPlayerId, ...(state.draft.memberPlayerIds || [])].filter(Boolean)));
977
+ const source = state.draft.source;
978
+ if ((source?.kind === "line_in" || source?.kind === "tv") && source.deviceId) {
979
+ if (!selected.includes(source.deviceId)) {
980
+ selected.unshift(source.deviceId);
981
+ }
982
+ }
983
+ return selected;
984
+ }
985
+
986
+ function resolveCoordinatorPlayerId(selectedRoomIds, source) {
987
+ const uniqueRooms = Array.from(new Set((selectedRoomIds || []).filter(Boolean)));
988
+
989
+ if ((source?.kind === "line_in" || source?.kind === "tv") && source.deviceId) {
990
+ return source.deviceId;
991
+ }
992
+
993
+ if (state.draft?.coordinatorPlayerId && uniqueRooms.includes(state.draft.coordinatorPlayerId)) {
994
+ return state.draft.coordinatorPlayerId;
995
+ }
996
+
997
+ return uniqueRooms[0] || "";
998
+ }
999
+
961
1000
  function serializeDraft() {
962
1001
  serializeCloudConfig();
963
1002
  const draft = clone(state.draft);
964
1003
  draft.name = elements.sceneName.value.trim() || "New Scene";
965
1004
  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
1005
  draft.source = buildSourcePayload();
1006
+ const selectedRoomIds = Array.from(elements.memberList.querySelectorAll("input[type='checkbox']:checked")).map((checkbox) => checkbox.value);
1007
+ draft.coordinatorPlayerId = resolveCoordinatorPlayerId(selectedRoomIds, draft.source);
1008
+ if ((draft.source?.kind === "line_in" || draft.source?.kind === "tv") && draft.source.deviceId && !selectedRoomIds.includes(draft.source.deviceId)) {
1009
+ selectedRoomIds.unshift(draft.source.deviceId);
1010
+ }
1011
+ draft.memberPlayerIds = selectedRoomIds.filter((playerId) => playerId !== draft.coordinatorPlayerId);
969
1012
  draft.coordinatorVolume = elements.coordinatorVolume.value === "" ? undefined : Number(elements.coordinatorVolume.value);
970
1013
  draft.settleMs = Number(elements.settleMs.value || 0);
971
1014
  draft.retryCount = Number(elements.retryCount.value || 0);
@@ -974,6 +1017,7 @@
974
1017
  draft.offBehavior = { kind: elements.offBehavior.value };
975
1018
  draft.playerVolumes = Array.from(elements.memberList.querySelectorAll("input[data-player-id]"))
976
1019
  .filter((input) => input.value !== "")
1020
+ .filter((input) => selectedRoomIds.includes(input.dataset.playerId) && input.dataset.playerId !== draft.coordinatorPlayerId)
977
1021
  .map((input) => ({
978
1022
  playerId: input.dataset.playerId,
979
1023
  volume: Number(input.value),
@@ -1127,40 +1171,40 @@
1127
1171
  .join("");
1128
1172
  }
1129
1173
 
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
1174
  function renderMemberOptions() {
1143
1175
  const household = getActiveHousehold();
1144
- const coordinatorId = state.draft.coordinatorPlayerId;
1145
- const selected = new Set(state.draft.memberPlayerIds.filter((playerId) => playerId !== coordinatorId));
1176
+ const selectedRoomIds = getSelectedSceneRoomIdsFromDraft();
1177
+ const selected = new Set(selectedRoomIds);
1178
+ const coordinatorId = resolveCoordinatorPlayerId(selectedRoomIds, state.draft.source);
1179
+ state.draft.coordinatorPlayerId = coordinatorId;
1180
+ state.draft.memberPlayerIds = selectedRoomIds.filter((playerId) => playerId !== coordinatorId);
1146
1181
  const values = new Map((state.draft.playerVolumes || []).map((entry) => [entry.playerId, entry.volume]));
1147
- const players = (household?.players || []).filter((player) => player.id !== coordinatorId);
1182
+ const forcedSourceId = (state.draft.source?.kind === "line_in" || state.draft.source?.kind === "tv")
1183
+ ? state.draft.source.deviceId
1184
+ : "";
1185
+ const players = household?.players || [];
1148
1186
  elements.memberList.innerHTML = players.length === 0
1149
- ? `<div class="scene-help">No additional players are available for grouping.</div>`
1187
+ ? `<div class="scene-help">No rooms are available in this household.</div>`
1150
1188
  : players
1151
1189
  .map(
1152
1190
  (player) => {
1153
- const isSelected = selected.has(player.id);
1191
+ const isForcedSource = forcedSourceId === player.id;
1192
+ const isSelected = isForcedSource || selected.has(player.id);
1193
+ const flags = [
1194
+ isForcedSource ? `<span class="scene-member-flag source">Source Room</span>` : "",
1195
+ isSelected && coordinatorId === player.id ? `<span class="scene-member-flag primary">Lead Room</span>` : "",
1196
+ ].filter(Boolean).join("");
1154
1197
  return `
1155
1198
  <div class="scene-member-pill ${isSelected ? "selected" : ""}">
1156
1199
  <label class="scene-member-row scene-member-toggle">
1157
- <input class="form-check-input" data-member-checkbox type="checkbox" value="${player.id}" ${isSelected ? "checked" : ""}>
1200
+ <input class="form-check-input" data-member-checkbox type="checkbox" value="${player.id}" ${isSelected ? "checked" : ""} ${isForcedSource ? "disabled" : ""}>
1158
1201
  <span class="scene-member-copy">
1159
1202
  <span class="scene-member-title">${player.name}</span>
1160
1203
  <span class="scene-member-meta">${player.model || "Unknown model"} - ${((player.sourceOptions || []).join(", ") || "favorite").replaceAll("_", " ")}</span>
1204
+ ${flags ? `<span class="scene-member-flags">${flags}</span>` : ""}
1161
1205
  </span>
1162
1206
  </label>
1163
- ${isSelected ? `
1207
+ ${isSelected && coordinatorId !== player.id ? `
1164
1208
  <div class="scene-member-volume">
1165
1209
  <label class="scene-member-volume-label" for="member-volume-${player.id}">Volume Override</label>
1166
1210
  <input
@@ -1183,8 +1227,8 @@
1183
1227
 
1184
1228
  function renderSourceControls() {
1185
1229
  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"))];
1230
+ const players = household?.players || [];
1231
+ const supportedKinds = ["favorite", ...new Set(players.flatMap((player) => player.sourceOptions || []).filter((kind) => kind !== "favorite"))];
1188
1232
  const sourceKind = supportedKinds.includes(state.draft.source?.kind) ? state.draft.source.kind : supportedKinds[0] || "favorite";
1189
1233
  state.draft.source = state.draft.source || { kind: sourceKind, favoriteId: "" };
1190
1234
  state.draft.source.kind = sourceKind;
@@ -1351,9 +1395,8 @@
1351
1395
  elements.sceneName.value = state.draft.name || "";
1352
1396
  elements.sceneId.value = state.draft.id || "";
1353
1397
  renderHouseholdOptions();
1354
- renderCoordinatorOptions();
1355
- renderMemberOptions();
1356
1398
  renderSourceControls();
1399
+ renderMemberOptions();
1357
1400
  elements.coordinatorVolume.value = state.draft.coordinatorVolume === "" ? "" : state.draft.coordinatorVolume ?? "";
1358
1401
  elements.settleMs.value = state.draft.settleMs ?? 750;
1359
1402
  elements.retryCount.value = state.draft.retryCount ?? 3;
@@ -1552,12 +1595,6 @@
1552
1595
  render();
1553
1596
  });
1554
1597
 
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
1598
  elements.sourceKind.addEventListener("change", () => {
1562
1599
  serializeDraft();
1563
1600
  state.draft.source = elements.sourceKind.value === "favorite"
@@ -1566,6 +1603,11 @@
1566
1603
  render();
1567
1604
  });
1568
1605
 
1606
+ elements.sourceTarget.addEventListener("change", () => {
1607
+ serializeDraft();
1608
+ render();
1609
+ });
1610
+
1569
1611
  homebridge.addEventListener("scene-test-result", (event) => {
1570
1612
  state.lastRun = event.data;
1571
1613
  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.8",
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",