Skip to content

Scoped Settings Service

Subsystem: window.lcards.core.scopedSettingsServiceClass: ScopedSettingsService (src/core/services/ScopedSettingsService.js) Requires: LCARdS Integration v1.12+ with scoped_storage capability


Overview

ScopedSettingsService implements a three-tier waterfall for any LCARdS setting:

Card YAML config
      ↓  (highest priority)
Device scope  — localStorage UUID → _device_<uuid> in HA backend

User scope    — HA user ID (server-side) → _user_<userid> in HA backend

Global tier   — HA input helpers / legacy flat storage key
      ↓  (lowest priority / fallback)
globalFallback() function — provided by caller

Settings are resolved at read time by walking the scope list and returning the first non-null hit. Writes target a single explicit scope.


Capability Guard

All scoped operations are no-ops (silent pass-through to the global fallback) if the backend does not advertise scoped_storage in its capabilities. This ensures graceful degradation when running against an older LCARdS integration version.

javascript
const integration = window.lcards.core.integrationService;
const supported = integration.capabilities.has('scoped_storage');

API Reference

read(key, scopes, globalFallback)

Async waterfall read. Stops at the first scope that returns a non-null value; if all scopes miss, calls globalFallback().

ParameterTypeDefaultDescription
keystringStorage key (use a constant from ScopedSettingsConstants.js)
scopesstring[]['device','user','global']Ordered list of scopes to probe
globalFallbackfunction|nullnullSynchronous function returning the global value
javascript
const sss = window.lcards.core.scopedSettingsService;
const hm  = window.lcards.core.helperManager;

const volume = await sss.read(
  STORAGE_KEY_SOUND_VOLUME,
  ['device', 'user', 'global'],
  () => hm.getHelperValue('sound_volume')
);

write(key, value, scope)

Writes a value to exactly one scope. Auto-downgrades to the global tier if scoped storage is unavailable.

ParameterTypeDescription
keystringStorage key
valueanyValue to store
scope'user'|'device'|'global'Target scope
javascript
await sss.write(STORAGE_KEY_SOUND_VOLUME, 0.85, 'user');
await sss.write(STORAGE_KEY_SOUND_SCHEME, 'enterprise', 'device');

clear(key, scope)

Removes a scoped override, allowing the setting to fall through to the parent scope.

javascript
await sss.clear(STORAGE_KEY_SOUND_VOLUME, 'device');

readAllScopes(key, globalFallback)

Reads the setting value at all three tiers simultaneously and returns a diagnostic object. Useful for admin UIs and debugging.

javascript
const result = await sss.readAllScopes(STORAGE_KEY_SOUND_VOLUME, fallback);
// result = { device: 0.9, user: null, global: 0.5, resolved: 0.9 }
FieldDescription
deviceValue stored at device scope (null = not set)
userValue stored at user scope (null = not set)
globalValue from the global tier / fallback
resolvedEffective value after waterfall resolution

Constants

All storage key strings live in src/core/services/ScopedSettingsConstants.js to prevent magic-string drift between JS and the Python backend.

ConstantValueUsed for
STORAGE_KEY_SOUND_OVERRIDES'sound_overrides'Per-event sound override map
STORAGE_KEY_SOUND_VOLUME'sound_volume'Master volume (0.0–1.0)
STORAGE_KEY_SOUND_ENABLED'sound_enabled'Master on/off flag
STORAGE_KEY_SOUND_SCHEME'sound_scheme'Active sound scheme name
STORAGE_KEY_SOUND_CATEGORY_ENABLED'sound_category_enabled'Per-category toggles
STORAGE_NS_USER'_user'Backend namespace prefix for user entries
STORAGE_NS_DEVICE'_device'Backend namespace prefix for device entries
DEVICE_KEY_DISPLAY_NAME'display_name'Device display name field
DEVICE_KEY_LAST_SEEN'last_seen'Device last-seen ISO timestamp
DEVICE_KEY_USER_AGENT'user_agent'Device user-agent string
URL_PARAM_DEVICE_NAME'lcards_device'URL param to set device display name
LOCALSTORAGE_DEVICE_ID'lcards_device_id'localStorage key for device UUID

Backend Storage Layout

Settings are stored under the existing LCARdS integration key in HA's .storage/lcards. The flat key format is:

_user_<ha-user-id>       → { sound_volume: 0.8, sound_scheme: "enterprise" }
_device_<uuid>           → { sound_volume: 0.6, display_name: "Kitchen Tablet" }
sound_overrides          → { tap: "beep.wav" }   ← legacy global flat key

User IDs are always derived server-side from the authenticated WebSocket connection — the browser never sends a user ID for per-user operations.


WebSocket Commands

CommandDirectionAuthDescription
lcards/storage/user/get→ backendany userRead caller's user-scoped data
lcards/storage/user/set→ backendany userDeep-merge into caller's user data
lcards/storage/user/delete→ backendany userDelete a sub-key from caller's user data
lcards/storage/device/get→ backendany userRead a device's scoped data
lcards/storage/device/set→ backendany userDeep-merge into a device's data
lcards/storage/users/list→ backendadminList all user IDs with stored data
lcards/storage/users/get→ backendadminRead any user's data
lcards/storage/users/set→ backendadminWrite any user's data
lcards/storage/users/clear→ backendadminDelete all data for a user
lcards/storage/devices/list→ backendadminList all registered devices
lcards/storage/devices/get→ backendadminRead any device's data
lcards/storage/devices/set→ backendadminWrite any device's data
lcards/storage/devices/clear→ backendadminDelete a device's entire record

Adding a New Scoped Setting

  1. Add a constant to ScopedSettingsConstants.js:

    javascript
    export const STORAGE_KEY_MY_SETTING = 'my_setting';
  2. Read in your component with the three-tier waterfall:

    javascript
    const value = await sss.read(
      STORAGE_KEY_MY_SETTING,
      ['device', 'user', 'global'],
      () => getGlobalFallback()
    );
  3. Write via the service (not via integration.writeStorage or setHelperValue directly):

    javascript
    await sss.write(STORAGE_KEY_MY_SETTING, newValue, 'user');

See Also