homeassistant-remotecompose

August 15, 2026 · View on GitHub

Kotlin Multiplatform library that converts Home Assistant Lovelace dashboard cards into RemoteCompose documents. Android target today; the shared data/client layers are structured as KMP so more targets are possible as RemoteCompose publishes more klibs.

Rendered previews

Full gallery (every card type, light + dark): gist.github.com/yschimke/09c6fbeb83f71b4de3442a410010ee3f.

HA reference on the left, RemoteCompose on the right.

End-to-end dashboard

A realistic vertical stack — heading + two tiles + entities (with toggles) + glance. Each card is its own .rc document played in its own RemotePreview host; the "dashboard" is a regular Compose Column stacking those players. This mirrors the end-goal app where each Lovelace card becomes an independent, live-updating Glance widget.

themeHome AssistantRemoteCompose
light
dark

tile — light theme

variantHome AssistantRemoteCompose
temperature sensor
light on
light off
lock locked

tile — dark theme

variantHome AssistantRemoteCompose
temperature sensor
light on

other cards (light)

cardHome AssistantRemoteCompose
button
entities
glance
markdown

Why this exists

HA's frontend is lit-element web components that read a live hass object and call services. Every public "render a Lovelace card to PNG/other" project runs the real frontend inside a headless Chromium (Puppeteer) — nobody has re-implemented the card set natively. That is the gap this library fills: take a card config + an HA state snapshot and emit a RemoteCompose byte stream that any RC player can render (Android apps, Glance widgets, Wear launcher tiles, ESP32 devices, etc.).

Module layout

ModuleTargetRole
ha-modelKMP (Android + JVM)Dashboard / view / section / card config + HA state snapshot types, kotlinx-serialization.
ha-clientKMP (Android + JVM)Ktor WebSocket client for lovelace/config + state fetch.
rc-componentsAndroidTier-1 RemoteHa* composables — RemoteCompose-native cards (RemoteString / RemoteColor / actions).
rc-components-uiAndroidTier-2 plain Compose-UI wrappers (HaTile, HaButton, …) embedding the Tier-1 composables via RemotePreview, so apps can drop them into a normal Compose tree without seeing RemoteCompose types.
rc-converterAndroidCardConverter strategy + one impl per card type. Emits RemoteCompose content.
previewsAndroid@Preview fixtures per card type, rendered by the ee.schimke.composeai.preview plugin for pixel-parity iteration.
demo-appAndroidSample app: fetch a real dashboard, render via remote-player-compose.

Key library references

  • androidx.compose.remote:remote-creation-compose:1.0.0-alpha08 — the authoring DSL. Alpha, API shifts between releases.
  • androidx.compose.remote:remote-player-compose:1.0.0-alpha08 — playback inside a Compose app.
  • androidx.wear.compose.remote:remote-material3:1.0.0-alpha02Wear-only mirror of Wear Material3. Not used for phone cards; for phone-sized HA cards we build on core RC primitives + hand-written Material3-flavoured wrappers.
  • ee.schimke.composeai.preview — the Gradle plugin that renders @Preview composables to PNG outside Android Studio. Drives the pixel-parity loop; see the compose-preview skill.

Rendering strategy: why 1-by-1, not automatic

Short answer: automatic conversion isn't feasible from a JVM/Android process. Cards don't expose a shared render IR we can intercept — each is a self-contained LitElement that pulls from hass.states, subscribes to history/statistics via WebSocket, and uses per-type CSS. The only way to "run" them is in a browser.

So the approach is hand-written converters verified against real output:

  1. Fetch a reference screenshot for every card config we care about (use hass-lovelace-screenshotter or a Puppeteer script) — these go under references/<card-type>/<name>.png.
  2. Write a CardConverter and a @Preview fixture with the same config
    • snapshot.
  3. Render with compose-preview show --json — output lands under previews/build/compose-previews/renders/.
  4. Diff against the reference; iterate on the converter until pixel delta is under threshold. A diffimg / resemblejs-style check in CI gates regressions.
  5. Move to the next card type.

Start with tile (scaffolded), then entities / glance / gauge / button — these cover the bulk of real dashboards. picture-elements, map, and the graph cards are hard; iframe and custom cards are explicitly out of scope.

Local setup

One-time bootstrap to wire the repo's .githooks/ (ktfmt pre-commit check) into your clone:

./gradlew installGitHooks

Running the previews

compose-preview doctor             # verifies Java 21 toolchain
compose-preview list               # lists all @Preview entries
compose-preview show --json        # renders and prints PNG paths
compose-preview render --filter tile

Fetching HA config

HaClient (in ha-client) wraps the WebSocket commands the converter needs:

  • lovelace/configDashboard (resolved JSON — strategy dashboards are resolved client-side today; we do not).
  • get_statesMap<String, EntityState> for the current snapshot.

Auth is a long-lived access token (HA Profile → Security → create token).

Open questions / TODOs

  • Pin the RemoteCompose alpha once API churn slows. The Render bodies in rc-converter/cards/* are stubbed at the draw-call boundary for this reason.
  • Custom cards (type: custom:…) have no shared schema — each needs a hand-written converter. Start with the HACS top-10 (Mushroom, button-card, mini-graph-card, ApexCharts).
  • Strategy-based dashboards need client-side resolution; not implemented.
  • Theming: HA uses CSS custom properties via --ha-<name>. Remote components read RemoteMaterialTheme.colorScheme; the current HA-role projection covers shared card chrome, while richer HA-specific CSS role mapping remains future work.
  • Hosting: this library is agnostic about where it runs. A HA-side deployment (serves .rc bytes over HTTP) and an Android-side deployment (fetches config + renders locally) are both viable; keep the converter pure so both work.