LightDash
A lightweight, self-contained dashboard renderer for Home Assistant. Instead of running HA's full Lovelace frontend, which is a struggle for low-power devices such as NSPanels and older Android tablets, LightDash is a focused alternative: support for tiles and built-in entities, intelligent mapping of some common custom cards to lightweight alternatives, and plain HTML + CSS with much of the interactivity shifted to the addon itself.
I orignally built LightDash to run on the NSPanel Pro in-wall touchscreens I have around my house, which are getting increasingly slow as the HA team add more dashboard capabilities. Wonderful for iPads, desktop browsing and recent smartphones, but almost unusable on the devices that sit in the gap between ESPHome and modern browsers.
LightDash is designed to handle copy-and-pasted YAML from existing dashboards with minimal (not quite zero) adjustment - there's an edit-and-preview web UI accessible from the addon control panel, where you can tweak the YAML and see the results immediately before saving.
Caveat 1: I've focused on the cards I use in my own small-screen dashboards. I'd love for contributors to add support for their own layouts!
Caveat 2: Yep, I used OpenCode to build a lot of this. I'm a 25+ year software architect and developer, but this is a one-day project. I'm pretty happy it's not filled with slop - I've reviewed it and it's passable - but I make no warranties about code quality this early in its life.
Installation
Add the Repository
- Go to Settings → Apps → Install app
- Click the ⋮ menu (top-right) and select Repositories
- Paste
https://github.com/richkershaw/HA-LightDash - Click Add
Install LightDash
- The LightDash add-on appears in the store
- Click Install and wait for the download to complete
- Go to the Info tab and click Start
- Toggle Show in sidebar to add the LightDash item
- Go to LightDash in the sidebar to start setting up dashboards
No manual configuration needed. Dashboards are managed through the in-app editor (see In-App Editor below).
Accessing Dashboards
LightDash serves dashboards via two methods. Both work simultaneously.
Via the HA Sidebar (Ingress)
After starting the add-on, click the Open Web UI button or the LightDash sidebar entry. This opens the dashboard index page within the HA interface. This is fine for devices you're happy to login on regularly.
Via Direct Port (HTTP, No Auth)
The add-on also exposes a raw HTTP port (8001 by default). Any device on
your LAN can reach it without Home Assistant authentication:
http://[your HA server]]:8001/
This is useful for:
- Wall-mounted tablets that shouldn't show a login screen
- Guest devices that shouldn't have HA credentials
- kiosk-mode browsers or screens that auto-launch a URL
The hostname defaults to your HA instance's hostname (auto-detected from the Supervisor API). You can override it in the add-on Configuration tab:
| Option | Default | Description |
|---|---|---|
public_host |
auto-detected | Hostname for direct-port URLs |
public_port |
8001 |
Port mapped to 8000/tcp inside the container |
Security note: The direct port has no authentication. Anyone on the
network can view dashboards. Use firewall rules or a reverse proxy if you
need to restrict access. Disable the port mapping in the add-on Info tab
(change 8000/tcp: 8001 to 8000/tcp: null) if you only want ingress access.
Dashboard URLs
Each dashboard is available at:
{base}/d/{name}
Where {base} depends on the access method:
- Ingress:
https://[your HA server]/api/hassio_ingress/{token}/d/{dashboard name} - Direct port:
http://[your HA server]:8001/d/{dashboard name}
The exact URLs are logged in the add-on logs at startup and listed at the
/dashboards endpoint. Use the Public URL button in the config editor
to copy the external URL for the current dashboard.
In-App Editor
Dashboards are managed entirely through the in-app editor.
- Open the LightDash sidebar entry (or navigate to the dashboard index)
- Click ⚙ Config at the bottom of the page
- Click + Add Dashboard and enter a URL-safe name (e.g.
living-room) - Edit the YAML in the left pane
- Click Save — the preview pane updates automatically
- Click Preview to refresh the preview without saving
- Click Public URL to copy the externally-available URL to add to your kiosk devices' config
The config page shows a split view:
┌──────────────────────────────────────────────────────────┐
│ Dashboard list CodeMirror YAML Preview │
│ editor (iframe) │
│ living-room ──active── ┌─────────────────┐ │
│ kitchen │ views: │ [rendered │
│ │ - title: Home │ view] │
│ [+ Add Dashboard] │ path: home │ │
│ [Delete] │ sections:...│ │
│ └─────────────────┘ │
│ [Preview] [Save] │
└──────────────────────────────────────────────────────────┘
- Add Dashboard: Creates a new YAML file with a starter template
- Delete: Removes the dashboard file entirely
- Rename: Renames the dashboard (and its YAML file on disk)
- Save: Writes YAML to disk and reloads the dashboard
- Preview: Renders the current editor content in the right pane
- Public URL: Copies the external dashboard URL to the clipboard
Dashboards are stored as individual YAML files in the add-on data directory
(/data/dashboards/), which is included in HA snapshots.
YAML Dashboard Format
A dashboard is a YAML file with a top-level views key:
title: Living Room
lightdash:
container_width: 480px
container_height: 480px
views:
- title: Home
path: home
icon: mdi:home
bg_image: /api/image/serve/abc123/original
type: sections
max_columns: 4
sections:
- type: grid
cards:
- type: tile
entity: light.porch
features:
- type: light-brightness
features_position: inline
Top-level fields:
| Field | Description |
|---|---|
title |
Display title |
lightdash |
Container sizing (see below) |
views |
List of views |
lightdash config
You can optionally fix the container size - useful for small-screen devices, and previewing rendering.
lightdash:
container_width: 480px # fixed container width (e.g. 480px, 100%)
container_height: 480px # fixed container height
View fields
| Field | Description |
|---|---|
title |
Display title (also used in <title>) |
path |
URL path segment (defaults to slug of title) |
icon |
MDI icon (shown in view index) |
bg_color |
CSS background-color |
bg_image |
Background image URL (/api/image/serve/...) |
type |
View layout type (sections or custom:layout-card) |
max_columns |
Column count for max-width grid |
When type: custom:layout-card is used, the parser groups cards into grid
sections split by custom:layout-break card entries. The layout.max_cols
value determines section column count.
Section fields
| Field | Description |
|---|---|
type |
Section type (grid) |
columns |
Number of grid columns |
Grid options on cards
grid_options:
columns: 6 # span this many columns
rows: auto # span this many rows
Supported Card Types
tile
A rich card showing entity icon, name, state, and optional controls.
type: tile
entity: light.living_room
name: Living Room
icon: mdi:lamp
color: yellow # tint icon (yellow/orange/red/pink/purple/blue/green/teal)
vertical: true # stack icon above info
hide_state: true # hide entity state & toggle
features_position: inline # or "bottom" (default)
features:
- type: light-brightness
- type: light-color-temp
- type: numeric-input
Features:
| Feature | Description |
|---|---|
light-brightness |
Range slider (0–100%), posts light.turn_on |
light-color-temp |
Range slider (153–500 mired), posts light.turn_on |
numeric-input |
Decrement/increment buttons, posts input_number.decrement/increment |
Binary-domain entities (light, switch, fan, input_boolean) get a
toggle switch. Non-binary entities show state text. Cover entities show
open/stop/close buttons instead of a toggle.
entities
A grouped list of entity rows, each with icon, name, state, and controls.
type: entities
title: Lights
entities:
- entity: light.dining_room
- entity: light.kitchen
name: Kitchen
icon: mdi:counter
- entity: cover.kitchen_roof
icon: mdi:window-closed
- type: divider # horizontal rule
- type: section # section header
name: Other
Cover entities automatically get open/stop/close buttons. Binary non-cover entities get a toggle switch.
button
A compact action button. Icon and name are on one line. Supports tap_action.
type: button
name: Other Rooms
icon: mdi:arrow-right-bold
tap_action:
action: navigate
navigation_path: other-rooms
glance
A grid of entity icons with names and state, organised in columns.
type: glance
title: Sensors
columns: 3
entities:
- sensor.temperature
- entity: sensor.humidity
icon: mdi:water-percent
tap_action:
action: toggle
entity
A single-row entity card.
type: entity
entity: sensor.temperature
name: Temp
icon: mdi:thermometer
heading
type: heading
heading: Living Room
icon: mdi:sofa
markdown
Simple markdown rendering with bold, italic, code, links, lists, and headers.
HA Jinja2 template syntax ({{, {%) is not supported. Use a clock
card for time display instead.
type: markdown
content: |
# Hello
**bold** and *italic*
clock
Digital clock card with timezone and format support. Updates every 30 seconds
via JS Intl.DateTimeFormat.
type: clock
time_zone: Europe/London
time_format: "24" # or "12"
show_seconds: false
clock_size: large # small / medium / large
no_background: true
sensor
type: sensor
entity: sensor.temperature
name: Outside
graph: line # or leave unset
hours_to_show: 24
gauge
type: gauge
entity: sensor.battery
min: 0
max: 100
severity:
green: 40
yellow: 20
red: 0
history-graph / statistics-graph
type: history-graph
title: Temperature
entities:
- sensor.outdoor_temp
hours_to_show: 24
Requires uPlot (loaded from CDN).
light
A legacy light card with toggle + brightness slider (all-in-one).
type: light
entity: light.living_room
name: Ceiling
grid / horizontal-stack / vertical-stack
Nested card layouts:
type: grid
columns: 2
cards:
- type: entity
entity: sensor.a
- type: entity
entity: sensor.b
conditional
Shows/hides a child card based on entity state conditions:
type: conditional
conditions:
- entity: light.test
state: "on"
card:
type: entity
entity: sensor.a
iframe
type: iframe
url: https://example.com
aspect_ratio: "50%"
placeholder
Rendered when a card type is unknown. Displays a ? placeholder.
Tap Actions
Cards can define a tap_action configuration:
| Action | Effect |
|---|---|
toggle |
Posts entity toggle to HA |
call-service |
Calls an arbitrary HA service |
navigate |
Navigates to another view within the same dashboard |
url |
Opens a URL in a new tab |
tap_action:
action: call-service
service: light.turn_on
target:
entity_id: light.living_room
data:
brightness_pct: 100
Auto-Mapped HA Custom Cards
These card types are automatically translated at parse time. The renderer never sees the original type.
| Source card | Target | Notes |
|---|---|---|
custom:mushroom-light-card |
tile |
brightness/color-temp features, inline layout |
custom:mushroom-cover-card |
entities |
single entity row with open/stop/close buttons |
custom:mushroom-number-card |
tile |
numeric-input feature |
custom:layout-card (view type) |
sections | grouped by custom:layout-break into sections |
Unsupported card types (not mapped, rendered as placeholder):
custom:mushroom-template-card— usebuttonwithtap_action.navigateinsteadshortcut— usebuttoninstead- Any other
custom:*card type
Compatibility Checker
The compatibility module (app/compat.py) scans dashboards at startup for
known limitations and logs warnings:
- Custom card types not in the mapping table — rendered as
placeholder - HA Jinja2 template syntax in markdown cards — unsupported
card_modstyling — not supported- Mapped custom cards — the mapping may not capture every nuance of the original Mushroom/Layout card configuration
Updating
When a new version is released, the add-on shows an Update button on the
Info tab. Click it and then Restart. Dashboards persist across updates
in /data/dashboards/.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ LightDash (FastAPI) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ parser │◄──│ config │◄──│ *yaml files │ or inline │
│ │ .py │ │ .py │ │ config/ │ add-on config │
│ └────┬─────┘ └──────────┘ └──────────────┘ │
│ │ Dashboard / View / Card models │
│ ▼ │
│ ┌──────────┐ │
│ │ renderer │────► HTML + CSS + JS (htmx + SSE) │
│ │ .py │ │
│ └──────────��� │
│ │ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ compat │ │ ha_client │◄── HTTP POST /api/services/... │
│ │ .py │ │ .py │◄── GET /api/states/... │
│ └──────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────────┐ │ │
│ │ sse │◄─────────┘ WebSocket /api/websocket │
│ │ manager │──► SSE /_sse (entity state events) │
│ └──────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
│ Ingress (add-on) or direct HTTP (local dev)
▼
┌──────────────────┐ ┌────────────────────┐
│ Browser (client)│◄─────►│ Home Assistant │
│ - htmx v2 │ SSE │ - REST API │
│ - htmx-sse ext │ │ - WebSocket │
│ - live updates │ └────────────────────┘
└──────────────────┘
Data flow:
Startup — YAML dashboard files are parsed into
Dashboard/View/Cardmodel objects. Custom HA card types (custom:mushroom-*,custom:layout-card) are mapped to native LightDash equivalents.Navigation —
GET /d/{name}redirects (302) to the first view.GET /d/{name}/view/{path}renders a full HTML page.Rendering — The renderer walks view cards/sections, generates HTML with htmx attributes for live interactions, SSE event attributes for live state updates, and inline CSS/JS for sliders, toggles, and clock.
Live updates — The SSE manager connects to the HA WebSocket API, subscribes to
state_changedevents, and relays entity state changes to connected browser clients via Server-Sent Events. htmx'ssse-swapattribute updates entity state spans in-place.Actions — Toggle switches, sliders, and tap actions POST to
/action, which forwards service calls to the HA REST API.Toggle sync — A JS function (
st()) runs after every htmx swap and SSE message, synchronising toggle switch positions and dimming classes with the rendered entity state text.Slider sync — A JS function (
ss()) runs after SSE messages for views with brightness/color-temp features. It fetches the live entity state and updates slider values when the state changes externally.
Local Development
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in HA_URL and HA_TOKEN
uvicorn app.main:app --reload # → http://localhost:8000
Create dashboards by dropping YAML files into config/.
Each file becomes a dashboard at /d/{filename_without_ext}.
Run tests:
python3 -m pytest tests/test_pipeline.py -v
Comments