HA Integration Architecture
LCARdS ships as a HACS Integration (custom_components/lcards/). This page covers the Python-side architecture — how the integration boots, what it registers in Home Assistant, and how it hands off to the JavaScript frontend.
Two-Layer Architecture
Python Component Files
| File | Responsibility |
|---|---|
__init__.py | Entry point — wires up static paths, JS injection, sidebar panel, storage init, WS commands, services, log level, and options update listener |
frontend.py | Registers static HTTP paths and injects lcards.js (with ?log= param) into every HA frontend session |
config_flow.py | Initial setup flow (single-instance, no user input) + options flow (panel, log level, sidebar customisation) |
websocket_api.py | Registers lcards/info and all lcards/storage/* WebSocket commands |
storage.py | LCARdSStorage — HA Store-backed flat key/value persistence (.storage/lcards) |
services.py | Registers the lcards.* HA action namespace — 9 services covering alert modes and frontend control |
services.yaml | Action descriptions and field selectors shown in Developer Tools → Actions |
const.py | Shared constants: DOMAIN, DOMAIN_VERSION, option keys, _LOG_LEVEL_MAP |
manifest.json | HACS/HA integration manifest — domain, version (HA CalVer), dependencies |
strings.json + translations/en.json | UI strings for the config and options dialog |
Boot Sequence
HA calls the integration in two phases:
Phase 1 — async_setup() (HA start, before config entry)
Runs at HA startup before any config entry is loaded. Registers infrastructure that must be available immediately:
- Static paths (via
frontend.py) — three paths registered:/lcards/lcards.js→custom_components/lcards/lcards.js/lcards/lcards.js.map→custom_components/lcards/lcards.js.map/hacsfiles/lcards/→custom_components/lcards/(alias for asset URLs)
- WebSocket commands —
lcards/inforegistered so the JS probe works even before setup
Phase 2 — async_setup_entry() (config entry active)
Runs when the integration is configured (after initial setup or on restart):
- Log level — maps the
log_leveloption to a Pythonlogginglevel and callssetLevel()on thecustom_components.lcardsparent logger, cascading to all child loggers - Storage init — creates
LCARdSStorage, loads.storage/lcardsfrom disk, stores the instance athass.data["lcards"]["storage"] - Services —
async_setup_services(hass)registers the fulllcards.*action namespace (9 services) - JS injection —
add_extra_js_urlloadslcards.js?v=...&log=<level>on every HA page; the?log=param letslcards.jsread the configured level at module load time viaimport.meta.url - Lovelace resource — registers the script for Cast / kiosk support
- Sidebar panel —
async_register_built_in_panelwith the configured title and icon, ifshow_paneloption isTrue - Options listener —
entry.add_update_listener()triggers an entry reload when the user saves new options, applying changes without an HA restart
Unload — async_unload_entry()
Called on HA restart, explicit reload (triggered by options change), or removal:
- Services —
async_remove_services(hass)deregisters alllcards.*services - Removes
add_extra_js_urlinjection - Removes the Lovelace resource
- Removes the sidebar panel
async_remove_entry() is a no-op — async_unload_entry handles all cleanup.
Static Paths
Three paths are served from the same custom_components/lcards/ directory:
| URL path | Serves | Purpose |
|---|---|---|
/lcards/lcards.js | lcards.js | Main JS bundle — loaded by add_extra_js_url |
/lcards/lcards.js.map | lcards.js.map | Source map for browser devtools stack traces |
/hacsfiles/lcards/* | Whole custom_components/lcards/ dir | Asset alias — all hardcoded font/SVG/sound URLs in the JS bundle reference this prefix |
The /hacsfiles/lcards/ alias means the JS bundle's asset URLs (/hacsfiles/lcards/fonts/..., /hacsfiles/lcards/msd/..., /hacsfiles/lcards/sounds/...) resolve without any JS changes, regardless of whether HACS itself is installed.
Config & Options Flow
LCARdS enforces a single-instance constraint (async_set_unique_id(DOMAIN)). The initial setup form requires no user input — clicking through is sufficient.
After setup, users configure options via Settings → Integrations → LCARdS → Configure:
| Option key | const.py constant | Default | Effect |
|---|---|---|---|
show_panel | CONF_SHOW_PANEL | True | Register or remove the sidebar panel |
sidebar_title | CONF_SIDEBAR_TITLE | "LCARdS Config" | Sidebar label text |
sidebar_icon | CONF_SIDEBAR_ICON | "mdi:space-invaders" | Sidebar icon (MDI name) |
log_level | CONF_LOG_LEVEL | "warn" | Frontend + backend verbosity — see Logging below |
All changes applied immediately via entry reload — no HA restart required.
WebSocket API
The integration registers WebSocket commands under the lcards/* namespace via websocket_api.py. Commands registered in async_setup() (before the config entry) are available immediately after HA start.
lcards/info
Backend probe — registered in async_setup() so it is always available.
| Command | Registered | Response |
|---|---|---|
lcards/info | async_setup() | { available: true, version: "..." } |
Used by IntegrationService on the JS side to detect backend presence. → See Integration Service.
lcards/storage/*
Persistent key/value store — registered in async_setup(), but requires the storage instance (initialised in async_setup_entry()) to respond.
| Command | Parameters | Response |
|---|---|---|
lcards/storage/get | key?: string | { key, value } — value is null for missing key |
lcards/storage/set | data: { [key]: value } | { ok: true, keys: [...] } |
lcards/storage/delete | key: string | { ok: true, existed: bool } |
lcards/storage/reset | — | { ok: true } |
lcards/storage/dump | — | { version: 1, data: { ... } } |
→ Full reference including browser console test snippets: Persistent Storage.
Logging
All integration Python files use logging.getLogger(__name__) — the logger hierarchy is custom_components.lcards.*.
The log_level option controls both frontend and backend verbosity:
- Frontend —
log_levelis appended as?log=<level>to theadd_extra_js_urlscript URL.lcards.jsreads it fromimport.meta.urlat module load time (before the banner). The page URL parameter?lcards_log_level=overrides it for the current session. - Backend —
async_setup_entry()maps the level string to a Pythonlogginglevel via_LOG_LEVEL_MAPand callssetLevel()oncustom_components.lcards, cascading to all child loggers.
| lcards level | Python level |
|---|---|
off | CRITICAL + 1 (effectively silent) |
error | ERROR |
warn | WARNING |
info | INFO |
debug | DEBUG |
trace | DEBUG |
You can also override the Python log level independently via configuration.yaml:
logger:
logs:
custom_components.lcards: debugHA Services (Actions)
The integration registers a lcards.* action namespace in async_setup_entry(), handled by services.py.
| Service | Parameters | Effect |
|---|---|---|
lcards.set_alert_mode | mode: string | Sets input_select.lcards_alert_mode to the supplied mode |
lcards.red_alert | — | Sets alert mode to red_alert |
lcards.yellow_alert | — | Sets alert mode to yellow_alert |
lcards.blue_alert | — | Sets alert mode to blue_alert |
lcards.gray_alert | — | Sets alert mode to gray_alert |
lcards.black_alert | — | Sets alert mode to black_alert |
lcards.clear_alert | — | Sets alert mode to green_alert (normal) |
lcards.reload | — | Fires lcards_event {action: reload} to all browser tabs |
lcards.set_log_level | level: string | Updates Python loggers + fires lcards_event {action: set_log_level} |
Alert mode services delegate to input_select.select_option on input_select.lcards_alert_mode. This means the full LCARdS pipeline (ThemeManager, SoundManager, alert overlays) fires via the existing HelperManager subscriptions in JS — no separate JS wiring required.
Schema validation is handled by voluptuous; invalid values are rejected before the handler fires. If input_select.lcards_alert_mode doesn't exist, a WARNING is logged and the service exits without raising.
→ Full reference including automation examples: HA Services
Python → JS Push Channel
Two services (lcards.reload and lcards.set_log_level) push instructions directly to connected browser tabs without requiring a page reload or WS request/response cycle.
How it works
Event payload shapes
action | Additional fields | JS handler |
|---|---|---|
reload | — | window.location.reload() |
set_log_level | level: string | window.lcards.setGlobalLogLevel(level) |
Subscription lifecycle
IntegrationService subscribes to the lcards_event HA event after the lcards/info probe succeeds — so the push channel is only open when the backend is confirmed active:
async_setup_entry → backend online → IntegrationService.initialize() succeeds
→ _startEventListener() → hass.connection.subscribeEvents(handler, 'lcards_event')
→ stores unsub fn as _eventUnsubscribe
async_unload_entry → integration unloaded (tab navigates away / WS closes)
→ WebSocket disconnect → subscription cleaned up automaticallyThe channel is a broadcast — all open browser tabs subscribed to lcards_event receive every event simultaneously.
→ JS implementation details: Integration Service — Push Channel
Build & Dev Workflow
The integration build outputs directly into custom_components/lcards/:
npm run build:integration
# = vite build --mode integration && node scripts/copy-assets.js
# Outputs:
# custom_components/lcards/lcards.js
# custom_components/lcards/lcards.js.map
# custom_components/lcards/fonts/ (from src/assets/fonts/)
# custom_components/lcards/msd/ (from src/assets/msd/)
# custom_components/lcards/sounds/ (from src/assets/sounds/)In the devcontainer, custom_components/lcards/ is bind-mounted into the HA core workspace, so a build is picked up immediately. A browser hard-refresh (Ctrl+Shift+R) applies JS changes; Python changes require a full HA restart.
CI / Release Pipeline
The release.yml GitHub Actions workflow handles versioning and packaging:
- Tag push or
workflow_dispatchwith a version string triggers the workflow - Version is normalised to HA CalVer (no leading zeros, no pre-release suffix) and stamped into
manifest.json; the full tag is kept inconst.py npm run build:integrationproduces the complete integration directorycustom_components/lcards/is zipped aslcards.zip(excluding__pycache__,.pyc)- A GitHub release is created with the zip attached
HACS downloads this zip and extracts it into custom_components/lcards/ on the user's HA instance.