Skip to content

Commit 4a78920

Browse files
committed
Merge card id state isolation for 5.9 local
2 parents 7dad749 + d86ee89 commit 4a78920

29 files changed

Lines changed: 662 additions & 222 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 5.9.0-local - Unreleased
4+
5+
Local development baseline for the next 5.9.0 release.
6+
7+
Added:
8+
9+
- Optional `card_id` YAML key. When set, HOMEii Flow namespaces browser-storage keys by the chosen id so multiple dashboards in the same browser can keep separate player selection, theme, layout, pinned/excluded players, screensaver settings, and other in-card customizations.
10+
- `src/core/state/card-id.js` foundation helpers with validation and storage-key scoping coverage.
11+
- `card_id` validator and visual-editor field in the Connection And Behavior section.
12+
13+
Migration note:
14+
15+
- Adding `card_id` to a card that previously had no `card_id` will appear to reset that card's in-card customizations once. The old global values stay in localStorage under their original keys; the card just starts reading from card-scoped keys.
16+
317
## 5.8.2-beta.8 - 2026-06-03
418

519
Focused beta follow-up for issue #28 after beta 7 exposed the next Home Assistant service-call layer.

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,32 @@ condition:
474474
value_template: "{{ states('input_text.homeii_flow_active_player') | regex_match('^media_player\\.') }}"
475475
```
476476

477+
## Running Multiple Dashboards In One Browser
478+
479+
By default, every HOMEii Flow card in the same browser shares its in-card customizations (theme, layout, excluded players, pinned players, screensaver settings, and the rest) via a single set of browser-storage keys. That keeps phones, tablets, and the desktop dashboard in sync when you only run one HOMEii Flow card.
480+
481+
If you want **separate** dashboards — for example one card per kid's bedroom, or a kitchen wall tablet showing one player and a living-room phone view showing another — give each card its own `card_id`:
482+
483+
```yaml
484+
type: custom:homeii-music-flow
485+
card_id: ida-music
486+
entity: media_player.ida_vaerelse
487+
```
488+
489+
```yaml
490+
type: custom:homeii-music-flow
491+
card_id: toke-music
492+
entity: media_player.toke_vaerelse
493+
```
494+
495+
Rules:
496+
497+
- `card_id` is optional. Cards without it behave exactly as before (shared state).
498+
- `card_id` must be 1-64 characters of letters, digits, `-`, or `_`.
499+
- Two cards that share the same `card_id` will share state — useful when you want a phone and a wall tablet to stay in sync within one dashboard.
500+
- Adding `card_id` to a card that previously had no `card_id` will appear to reset its in-card customizations once. The old global values still live in localStorage under the original keys; the card now reads from the new card-scoped keys. Reconfigure once via the in-card Settings panel and you are done.
501+
- This only affects browser-local UI state. Music Assistant playback, your `media_player` entities, and Home Assistant configuration are unaffected.
502+
477503
## Sendspin Browser Player
478504

479505
HOMEii Flow includes a local browser player flow powered by Sendspin. In the card this appears as **This device**.

dist/config/editor-forms.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function homeiiRadioBrowserCountrySelectorOptions(translateFn = null, language =
4141

4242
export function getBaseCardConfigForm() {
4343
const labels = {
44+
card_id: homeiiEditorI18n("ui.card_id", {}, "Card ID"),
4445
config_entry_id: "Config Entry ID",
4546
ma_url: homeiiEditorI18n("ui.music_assistant_url"),
4647
music_assistant_external_url: "Music Assistant external URL",
@@ -65,6 +66,7 @@ export function getBaseCardConfigForm() {
6566
popup_opacity: homeiiEditorI18n("ui.popup_opacity"),
6667
};
6768
const helpers = {
69+
card_id: homeiiEditorI18n("ui.card_id_helper", {}, "Unique slug (letters, digits, '-', '_'). Set this when running multiple HOMEii Flow dashboards in the same browser so each dashboard keeps its own player picker, theme, layout, and other in-card settings. Leave blank to share state with every other HOMEii Flow card in this browser (the original behaviour)."),
6870
config_entry_id: homeiiEditorI18n("ui.music_assistant_config_entry_id_if_you_want_direct_integration_lookup_th"),
6971
ma_url: homeiiEditorI18n("ui.leave_empty_if_the_card_should_use_home_assistant_only"),
7072
music_assistant_external_url: "Optional HTTPS Music Assistant URL for HTTPS dashboards, Nabu Casa, and Companion App browser-player connections.",
@@ -126,6 +128,7 @@ export function getBaseCardConfigForm() {
126128
flatten: true,
127129
column_min_width: "220px",
128130
schema: [
131+
{ name: "card_id", selector: { text: {} } },
129132
{ name: "config_entry_id", selector: { text: {} } },
130133
{ name: "ma_url", selector: { text: { type: "url" } } },
131134
{ name: "music_assistant_external_url", selector: { text: { type: "url" } } },
@@ -289,6 +292,7 @@ export function getMobileEditorTexts() {
289292
player_sort_mode: homeiiEditorI18n("ui.player_sort"),
290293
player_order_entities: homeiiEditorI18n("ui.player_order"),
291294
...playerOrderSlotLabels,
295+
card_id: homeiiEditorI18n("ui.card_id", {}, "Card ID"),
292296
config_entry_id: "Config Entry ID",
293297
ma_url: homeiiEditorI18n("ui.music_assistant_url"),
294298
music_assistant_external_url: "Music Assistant external URL",
@@ -305,6 +309,7 @@ export function getMobileEditorTexts() {
305309
use_mass_queue_send_command: homeiiEditorI18n("ui.mass_queue_send_command_fallback"),
306310
},
307311
helpers: {
312+
card_id: homeiiEditorI18n("ui.card_id_helper", {}, "Unique slug (letters, digits, '-', '_'). Set this when running multiple HOMEii Flow dashboards in the same browser so each dashboard keeps its own player picker, theme, layout, and other in-card settings. Leave blank to share state with every other HOMEii Flow card in this browser (the original behaviour)."),
308313
settings_source: homeiiEditorI18n("ui.choose_whether_settings_are_controlled_from_the_in_card_ui_or_from_the_c"),
309314
layout_mode: homeiiEditorI18n("ui.auto_chooses_mobile_or_tablet_based_on_actual_width"),
310315
height: homeiiEditorI18n("ui.card_height_in_pixels"),
@@ -673,6 +678,7 @@ export function getMobileCardConfigForm() {
673678
flatten: true,
674679
column_min_width: "220px",
675680
schema: [
681+
{ name: "card_id", selector: { text: {} } },
676682
{ name: "config_entry_id", selector: { text: {} } },
677683
{ name: "ma_url", selector: { text: { type: "url" } } },
678684
{ name: "music_assistant_external_url", selector: { text: { type: "url" } } },

dist/config/validators.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,26 @@ export function assertStringArrayValuesIfDefined(value, key, allowedValues) {
3838
}
3939
}
4040

41+
const CARD_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
42+
43+
export function assertCardIdIfDefined(value, key) {
44+
if (value == null) return;
45+
if (typeof value !== "string") throw new Error(`${key} must be a string`);
46+
const trimmed = value.trim();
47+
if (trimmed === "") return;
48+
if (!CARD_ID_PATTERN.test(trimmed)) {
49+
throw new Error(
50+
`${key} must be 1-64 characters of letters, digits, '-' or '_'`
51+
);
52+
}
53+
}
54+
4155
export function validateBaseCardEditorConfig(config) {
4256
if (!config || typeof config !== "object" || Array.isArray(config)) {
4357
throw new Error("Card config must be an object");
4458
}
4559

60+
assertCardIdIfDefined(config.card_id, "card_id");
4661
assertStringIfDefined(config.config_entry_id, "config_entry_id");
4762
assertStringIfDefined(config.ma_url, "ma_url");
4863
assertStringIfDefined(config.music_assistant_external_url, "music_assistant_external_url");

dist/core/base-music-card.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export function createHomeiiBaseMusicCard({
33
HOMEII_VISIBLE_LANGUAGE_OPTIONS,
44
HomeiiStateFoundation,
55
HomeiiConfigValidators,
6+
HomeiiCardIdFoundation,
67
HomeiiResponsiveFoundation,
78
HomeiiMediaQueueFoundation,
89
HomeiiMediaPresentationFoundation,
@@ -151,7 +152,7 @@ export function createHomeiiBaseMusicCard({
151152

152153
try {
153154
const configuredLanguage = this._config.language || "en";
154-
const storedLanguage = localStorage.getItem("homeii_music_flow_lang");
155+
const storedLanguage = localStorage.getItem(this._lsKey("homeii_music_flow_lang"));
155156
const configControlsLanguage = typeof this._usesVisualSettings === "function" && this._usesVisualSettings();
156157
const configuredLanguageBase = String(configuredLanguage || "")
157158
.trim()
@@ -167,12 +168,12 @@ export function createHomeiiBaseMusicCard({
167168
}
168169

169170
try {
170-
this._state.cardTheme = localStorage.getItem("homeii_music_flow_theme") || this._config.theme_mode || "auto";
171+
this._state.cardTheme = localStorage.getItem(this._lsKey("homeii_music_flow_theme")) || this._config.theme_mode || "auto";
171172
} catch (_) {
172173
this._state.cardTheme = this._config.theme_mode || "auto";
173174
}
174175
try {
175-
this._state.tracksLayout = localStorage.getItem("homeii_music_flow_tracks_layout") || "list";
176+
this._state.tracksLayout = localStorage.getItem(this._lsKey("homeii_music_flow_tracks_layout")) || "list";
176177
} catch (_) {
177178
this._state.tracksLayout = "list";
178179
}
@@ -3758,7 +3759,7 @@ export function createHomeiiBaseMusicCard({
37583759

37593760
_toggleLanguage() {
37603761
this._state.lang = this._nextLanguageCode();
3761-
try { localStorage.setItem("homeii_music_flow_lang", this._state.lang); } catch (_) {}
3762+
try { localStorage.setItem(this._lsKey("homeii_music_flow_lang"), this._state.lang); } catch (_) {}
37623763

37633764
const currentTheme = this._state.cardTheme;
37643765
const currentPlayer = this._state.selectedPlayer;
@@ -3813,7 +3814,7 @@ export function createHomeiiBaseMusicCard({
38133814
_toggleCardTheme() {
38143815
const effective = this._effectiveTheme();
38153816
this._state.cardTheme = effective === "dark" ? "light" : "dark";
3816-
try { localStorage.setItem("homeii_music_flow_theme", this._state.cardTheme); } catch (_) {}
3817+
try { localStorage.setItem(this._lsKey("homeii_music_flow_theme"), this._state.cardTheme); } catch (_) {}
38173818
const card = this.shadowRoot.querySelector(".card");
38183819
if (card) {
38193820
card.classList.remove("theme-dark", "theme-light");
@@ -3835,6 +3836,12 @@ export function createHomeiiBaseMusicCard({
38353836
return player.state === "playing" || player.state === "paused" || !!attrs.media_title || !!attrs.active_queue;
38363837
}
38373838

3839+
_lsKey(baseKey) {
3840+
return HomeiiCardIdFoundation
3841+
? HomeiiCardIdFoundation.scopeStorageKey(baseKey, this._config?.card_id)
3842+
: baseKey;
3843+
}
3844+
38383845
_thisDeviceStorageKey() {
38393846
return `homeii-this-device-player::${this._maBrowserUrl() || this._config?.ma_url || location.origin || "default"}`;
38403847
}
@@ -5189,7 +5196,7 @@ export function createHomeiiBaseMusicCard({
51895196

51905197
_saveMobileRecentHistory() {
51915198
try {
5192-
localStorage.setItem("homeii_music_flow_mobile_recent_history", JSON.stringify((this._state.mobileRecentHistory || []).slice(0, 10)));
5199+
localStorage.setItem(this._lsKey("homeii_music_flow_mobile_recent_history"), JSON.stringify((this._state.mobileRecentHistory || []).slice(0, 10)));
51935200
} catch (_) {}
51945201
}
51955202

@@ -6393,7 +6400,7 @@ export function createHomeiiBaseMusicCard({
63936400
}
63946401

63956402
_controlRoomScenesStorageKey() {
6396-
return "homeii_music_flow_control_room_scenes_v1";
6403+
return this._lsKey("homeii_music_flow_control_room_scenes_v1");
63976404
}
63986405

63996406
_normalizeControlRoomScene(scene = {}, index = 0) {
@@ -7924,11 +7931,11 @@ export function createHomeiiBaseMusicCard({
79247931
}
79257932

79267933
_likedStorageKey() {
7927-
return "homeii_music_flow_likes_v2";
7934+
return this._lsKey("homeii_music_flow_likes_v2");
79287935
}
79297936

79307937
_likedMetaStorageKey() {
7931-
return "homeii_music_flow_like_meta_v2";
7938+
return this._lsKey("homeii_music_flow_like_meta_v2");
79327939
}
79337940

79347941
_loadLikedUris() {
@@ -8920,7 +8927,7 @@ export function createHomeiiBaseMusicCard({
89208927

89218928
_setTracksLayout(layout) {
89228929
this._state.tracksLayout = layout === "grid" ? "grid" : "list";
8923-
try { localStorage.setItem("homeii_music_flow_tracks_layout", this._state.tracksLayout); } catch (_) {}
8930+
try { localStorage.setItem(this._lsKey("homeii_music_flow_tracks_layout"), this._state.tracksLayout); } catch (_) {}
89248931
if (this._state.view === "tracks") this._renderTracks();
89258932
}
89268933

dist/core/state/card-id.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const VALID_CARD_ID = /^[A-Za-z0-9_-]{1,64}$/;
2+
const SCOPE_SEPARATOR = "__";
3+
4+
export function normalizeCardId(value) {
5+
const id = String(value ?? "").trim();
6+
if (!id) return "";
7+
return VALID_CARD_ID.test(id) ? id : "";
8+
}
9+
10+
export function isValidCardId(value) {
11+
return normalizeCardId(value) !== "";
12+
}
13+
14+
export function scopeStorageKey(baseKey, cardId) {
15+
const id = normalizeCardId(cardId);
16+
return id ? `${baseKey}${SCOPE_SEPARATOR}${id}` : baseKey;
17+
}

dist/localization/en.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ const messages = Object.freeze({
926926
"ui.hotel_mode": "Hotel Mode",
927927
"ui.hotel_mode_helper": "Minimal hotel-safe UI: player controls, volume, search, artwork browsing, and player selection only.",
928928
"ui.style_and_genres": "Style and genres",
929+
"ui.card_id": "Card ID",
930+
"ui.card_id_helper": "Unique slug (letters, digits, '-', '_'). Set this when running multiple HOMEii Flow dashboards in the same browser so each dashboard keeps its own player picker, theme, layout, and other in-card settings. Leave blank to share state with every other HOMEii Flow card in this browser (the original behaviour).",
929931
});
930932

931933
export default messages;

dist/localization/es.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ const messages = Object.freeze({
926926
"ui.hotel_mode": "Modo hotel",
927927
"ui.hotel_mode_helper": "Interfaz mínima segura para hotel: solo controles del reproductor, volumen, búsqueda, navegación por carátulas y selección de reproductor.",
928928
"ui.style_and_genres": "Estilos y géneros",
929+
"ui.card_id": "Card ID",
930+
"ui.card_id_helper": "Unique slug (letters, digits, '-', '_'). Set this when running multiple HOMEii Flow dashboards in the same browser so each dashboard keeps its own player picker, theme, layout, and other in-card settings. Leave blank to share state with every other HOMEii Flow card in this browser (the original behaviour).",
929931
});
930932

931933
export default messages;

dist/localization/fr.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ const messages = Object.freeze({
926926
"ui.hotel_mode": "Mode hôtel",
927927
"ui.hotel_mode_helper": "Interface minimale sécurisée pour l'hôtel : commandes du lecteur, volume, recherche, navigation dans les pochettes et sélection du lecteur uniquement.",
928928
"ui.style_and_genres": "Styles et genres",
929+
"ui.card_id": "Card ID",
930+
"ui.card_id_helper": "Unique slug (letters, digits, '-', '_'). Set this when running multiple HOMEii Flow dashboards in the same browser so each dashboard keeps its own player picker, theme, layout, and other in-card settings. Leave blank to share state with every other HOMEii Flow card in this browser (the original behaviour).",
929931
});
930932

931933
export default messages;

dist/localization/he.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ const messages = Object.freeze({
926926
"ui.hotel_mode": "מצב מלון",
927927
"ui.hotel_mode_helper": "ממשק מצב מלון מינימלי: פקדי נגן, ווליום, חיפוש, דפדוף בעטיפות ובחירת נגן בלבד.",
928928
"ui.style_and_genres": "סגנונות וז'אנרים",
929+
"ui.card_id": "Card ID",
930+
"ui.card_id_helper": "Unique slug (letters, digits, '-', '_'). Set this when running multiple HOMEii Flow dashboards in the same browser so each dashboard keeps its own player picker, theme, layout, and other in-card settings. Leave blank to share state with every other HOMEii Flow card in this browser (the original behaviour).",
929931
});
930932

931933
export default messages;

0 commit comments

Comments
 (0)