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.
| theme | Home Assistant | RemoteCompose |
|---|---|---|
| light | ![]() | ![]() |
| dark | ![]() | ![]() |
tile — light theme
| variant | Home Assistant | RemoteCompose |
|---|---|---|
| temperature sensor | ![]() | ![]() |
| light on | ![]() | ![]() |
| light off | ![]() | ![]() |
| lock locked | ![]() | ![]() |
tile — dark theme
| variant | Home Assistant | RemoteCompose |
|---|---|---|
| temperature sensor | ![]() | ![]() |
| light on | ![]() | ![]() |
other cards (light)
| card | Home Assistant | RemoteCompose |
|---|---|---|
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
| Module | Target | Role |
|---|---|---|
ha-model | KMP (Android + JVM) | Dashboard / view / section / card config + HA state snapshot types, kotlinx-serialization. |
ha-client | KMP (Android + JVM) | Ktor WebSocket client for lovelace/config + state fetch. |
rc-components | Android | Tier-1 RemoteHa* composables — RemoteCompose-native cards (RemoteString / RemoteColor / actions). |
rc-components-ui | Android | Tier-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-converter | Android | CardConverter strategy + one impl per card type. Emits RemoteCompose content. |
previews | Android | @Preview fixtures per card type, rendered by the ee.schimke.composeai.preview plugin for pixel-parity iteration. |
demo-app | Android | Sample 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-alpha02— Wear-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@Previewcomposables to PNG outside Android Studio. Drives the pixel-parity loop; see thecompose-previewskill.
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:
- Fetch a reference screenshot for every card config we care about
(use
hass-lovelace-screenshotteror a Puppeteer script) — these go underreferences/<card-type>/<name>.png. - Write a
CardConverterand a@Previewfixture with the same config- snapshot.
- Render with
compose-preview show --json— output lands underpreviews/build/compose-previews/renders/. - Diff against the reference; iterate on the converter until pixel
delta is under threshold. A
diffimg/resemblejs-style check in CI gates regressions. - 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/config→Dashboard(resolved JSON — strategy dashboards are resolved client-side today; we do not).get_states→Map<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
Renderbodies inrc-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 readRemoteMaterialTheme.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
.rcbytes over HTTP) and an Android-side deployment (fetches config + renders locally) are both viable; keep the converter pure so both work.























