- C# 48.8%
- TypeScript 41.7%
- SCSS 5.9%
- HTML 3.3%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Auto Build & Push Multi-Arch Docker Image / build-and-push (push) Successful in 3m12s
- Added ActionController to handle various actions on Home Assistant entities. - Introduced ActionRequest record to encapsulate action parameters. - Implemented Perform method to validate requests and call Home Assistant services. feat: Create DashboardController for rendering Home Assistant dashboard - Added DashboardController to serve the Home Assistant dashboard. - Implemented Get method to retrieve and translate dashboard views and screensavers. - Added States method to return the current states of all entities on the dashboard. - Implemented Calendar method to fetch calendar events for specified entities. feat: Introduce LocalController for serving local images - Added LocalController to serve images from Home Assistant's local directory. - Implemented Get method to validate requests and return local images. feat: Develop DashboardModel for managing dashboard configuration - Created DashboardModel to represent the Home Assistant dashboard structure. - Implemented methods to build and expand the dashboard configuration. - Added functionality to handle auto-entities and entity filtering. feat: Establish DashboardStore for managing dashboard state and entities - Introduced DashboardStore to manage the state of the dashboard and its entities. - Implemented methods to set and retrieve dashboard configuration, states, and translations. feat: Add Json utility methods for handling JSON nodes - Created Json static class with extension methods for easier JSON node manipulation. - Added methods to retrieve text, numbers, booleans, and lists of texts from JSON nodes. |
||
| .forgejo/workflows | ||
| client | ||
| server | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| InternalDashboard.slnx | ||
| LICENSE | ||
| README.md | ||
Internal Dashboard
A fullscreen wallboard for displaying Home Assistant sensor data, camera feeds, and weather forecasts. Designed to run on a wall-mounted tablet or monitor.
client/: Angular frontend: the wallboard itselfserver/InternalDashboard.Api/: ASP.NET Core (.NET 10): talks to Home Assistant (REST and WebSocket) and LibreTranslate, and serves the built Angular app. The Home Assistant token stays here: the browser never sees it.
Features
- Live clock and date
- Outdoor and indoor temperature and humidity (top left), each with a smooth temperature history sparkline (configurable time range)
- Click the indoor climate card to open a modal with the temperature and humidity of other rooms (also with sparklines)
- Click any temperature tile (outdoor, indoor, or a room) to open a large history chart with labelled X (time) and Y (value) axes; the indoor tile stays clickable for its own chart when no rooms are configured
- Or, with
HASS_DASHBOARD, a dashboard built in Home Assistant's own editor, with its wallpanel screensaver (see Home Assistant dashboard) - Configurable quick-access entities in the top-right bar
- Camera feed with a subtle blue-tint overlay and optional location dot
- Daily and hourly weather forecasts with precipitation charts (equal-height, side by side)
- Toggle support for switches, lights, automations, and scripts
- Real-time updates via the Home Assistant WebSocket API, relayed by the server (60 s polling fallback)
- Multi-language support (Dutch, English, German) — auto-detected from the browser, with a manual switcher; custom entity/room names are auto-translated too
- PWA support — installable on iPad/Android home screen
- Screen wake lock to keep the display active (Wake Lock API + silent video fallback for iOS 15.8)
- Subtle refresh button in the bottom-right corner
Compatibility
Tested with Home Assistant version 2026.6
Getting started
1. Configure environment
Copy .env.example to .env and fill in your values:
cp .env.example .env
2. Run with Docker
docker compose up -d --build
The dashboard is available at http://localhost:8080. docker-compose.yml also starts the libretranslate service used for the name auto-translation feature described under Translation.
Environment variables
Two variables are required; all others are optional.
General
| Variable | Required | Description |
|---|---|---|
APP_NAME |
— | Display name shown in the browser tab, home screen, and PWA manifest. Defaults to Dashboard. |
Home Assistant connection
| Variable | Required | Description |
|---|---|---|
HASS_URL |
✓ | Base URL of your Home Assistant instance, without trailing slash. Example: http://homeassistant.local:8123 |
HASS_TOKEN |
✓ | Long-lived access token. Create one via Profile → Security → Long-lived access tokens. |
Security note: The token stays on the server: the browser talks only to the dashboard, which talks to Home Assistant. Still, anyone who can open the dashboard can see the configured entities and use its switches — only expose it on a trusted local network, never on the public internet.
Home Assistant dashboard
| Variable | Required | Description |
|---|---|---|
HASS_DASHBOARD |
— | A dashboard from Home Assistant to show instead of the cards configured below: its url_path, optionally with a view (its number or path), e.g. dashboard-keuken or dashboard-keuken/0. The address from the browser (https://ha.example/dashboard-keuken/0) works too; lovelace is the default dashboard. With it, HASS_ENTITIES up to HASS_WEATHER_HOURLY are ignored. |
Without HASS_DASHBOARD the dashboard shows the cards from the settings below, as always. With it, you build the dashboard in Home Assistant's own editor and this app shows it in its own style: the server reads the dashboard over the WebSocket API and loads it again whenever it is saved, so changes appear on the wall display right away.
Shown are a sections view (sections side by side, as many as fit, each a grid of 12 columns) or the cards of an older view, with these cards:
- Home Assistant's
tile(with the featurestarget-temperature,climate-preset-modesandclimate-hvac-modes),heading(with badges),entities,sensor,button,picture-elements(a camera or picture with images, icons and states on it),grid,vertical-stack,horizontal-stack,conditionalandweather-forecast - the custom cards
auto-entities,date-time-card,week-planner-card,weather-forecast-cardandneerslag-card - the wallpanel screensaver of the dashboard: after its
idle_timeits cards cover the screen until the next touch
Cards follow their visibility conditions, names such as { type: area } show the area's name, and states, icons and colours are Home Assistant's own (in the visitor's language). card_mod styling is left out: it is CSS for Home Assistant's own pages. Other card types are left out as well.
Only what the dashboard's cards offer can be done through the app, so the wall display cannot switch everything in Home Assistant: a tap action toggle, the switch, run button or list of an entities row, and the features of a tile (only the presets and modes it lists). Likewise only the entities, cameras, calendars and /local/ images on the dashboard can be read.
Entities
| Variable | Required | Description |
|---|---|---|
HASS_ENTITIES |
— | Comma-separated list of entity IDs to display as cards on the dashboard. If omitted, no general entity cards are shown. |
HASS_ENTITY_NAMES |
— | Custom display names for HASS_ENTITIES, in the same order. |
HASS_OUTDOOR_TEMP |
— | Entity ID for outdoor temperature, shown prominently in the top left. |
HASS_OUTDOOR_HUMIDITY |
— | Entity ID for outdoor humidity, shown next to the outdoor temperature. |
HASS_INDOOR_TEMP |
— | Entity ID for indoor temperature, shown next to the outdoor climate card in the top left. |
HASS_INDOOR_HUMIDITY |
— | Entity ID for indoor humidity, shown next to the indoor temperature. |
HASS_ROOM_NAMES |
— | Comma-separated room labels for the rooms modal (opened by clicking the indoor climate card). |
HASS_ROOM_TEMPS |
— | Comma-separated temperature entity IDs for the rooms modal, in the same order as HASS_ROOM_NAMES. |
HASS_ROOM_HUMIDITY |
— | Comma-separated humidity entity IDs for the rooms modal, in the same order as HASS_ROOM_NAMES. |
HASS_HISTORY_HOURS |
24 |
Number of hours of history shown in the temperature sparklines (1–168). |
HASS_TOP_ENTITIES |
— | Comma-separated entity IDs shown in the fixed top-right bar. Supports sensors, switches, lights, scripts, and automations. Text is truncated with an ellipsis if too long. |
HASS_TOP_ENTITY_NAMES |
— | Custom display names for HASS_TOP_ENTITIES, in the same order. |
Only the entities configured here can be read or switched through the dashboard: a switch in HASS_ENTITIES or HASS_TOP_ENTITIES can be toggled, and the climate sensors have a history.
Camera
| Variable | Required | Description |
|---|---|---|
HASS_CAMERA |
— | Entity ID of a camera to display (e.g. camera.buienradar). The image is rendered square with a grayscale + blue-tint overlay. |
HASS_CAMERA_DOT |
— | Position of a small location dot on the camera image, as x,y in percentage (0–100). Example: 52,48 places the dot slightly left of centre. Useful for marking your location on a rain radar. |
Weather
| Variable | Required | Description |
|---|---|---|
HASS_WEATHER |
— | Entity ID of a weather integration for the daily forecast card (up to 8 days). |
HASS_WEATHER_HOURLY |
— | Entity ID of a weather integration for the hourly forecast card (up to 24 hours). |
The precipitation chart prefers the precipitation amount (mm) from the forecast. If the integration does not provide it (e.g. KNMI's daily forecast only exposes precipitation_probability), the chart falls back to showing the probability (%) instead. If neither field is present, no bars are drawn. Note that some integrations (e.g. Buienradar) do not support an hourly forecast at all — keep HASS_WEATHER_HOURLY on an entity that does.
Translation
| Variable | Required | Description |
|---|---|---|
HASS_NAMES_SOURCE_LANG |
nl |
Language the names above (HASS_ENTITY_NAMES, HASS_TOP_ENTITY_NAMES, HASS_ROOM_NAMES) are written in. Used as the source language when auto-translating them for visitors using another language. |
TRANSLATE_ENABLED |
true |
Set to false to disable auto-translation of the names above (they will then be shown as-is regardless of the selected language). |
TRANSLATE_URL |
— | URL of a LibreTranslate instance. Use http://libretranslate:5000, the libretranslate service in docker-compose.yml, unless you run your own instance elsewhere. |
TRANSLATE_API_KEY |
— | Optional API key, only needed if your LibreTranslate instance requires one. |
TRANSLATE_IGNORE_WORDS |
— | Comma-separated words that should never be translated (e.g. people's names used in room names, like Minna,Ida). Matched as whole words, case-insensitively. |
STORAGE_DIR |
/app/storage |
Where the translation cache is kept. |
For sensors (any entity outside the toggle domains switch, light, input_boolean, fan, automation, script), the state value itself is also auto-translated by default — useful for text-based sensors such as a waste collection calendar. Numbers, unavailable and unknown are never sent for translation, and toggle entities are never touched, since their raw on/off state drives the UI and must stay exact.
The translation cache is warmed automatically in the background every time the app starts, so a fresh or restarted container is never slow on its first non-default-language request — it briefly waits for LibreTranslate to become reachable first, in case it's still downloading its language models. Changes to .env take effect when the container is recreated (docker compose up -d), which warms the cache again. Without warming, translations still happen automatically on first use.
Language
The dashboard language is automatically detected from the browser, and can be changed manually via the globe button in the bottom-right corner (cycles through the supported languages and remembers the choice in a cookie). Without a choice and without a usable browser language (some kiosk browsers send none), the dashboard uses HASS_NAMES_SOURCE_LANG. Supported languages:
| Code | Language |
|---|---|
nl |
Dutch |
en |
English (default fallback) |
de |
German |
Translation files are located in client/public/i18n/. To add a language, create client/public/i18n/<code>.json following the structure of an existing file, add the code to LANGUAGES in i18n.ts and to Supported in Languages.cs, and load its model in the libretranslate service (LT_LOAD_ONLY in docker-compose.yml).
Custom entity and room names configured via .env (HASS_ENTITY_NAMES, HASS_TOP_ENTITY_NAMES, HASS_ROOM_NAMES) are written in a single language — see Translation above — and machine-translated on the fly for visitors using a different language, via the self-hosted libretranslate service. Results are cached on disk (translations-cache.json in STORAGE_DIR, kept in the storage volume) so each name is only translated once; a cache file from the PHP version can be copied in as is. If the translation service is unreachable or disabled, the original name is shown unchanged.
Real-time updates
The server keeps one connection to the Home Assistant WebSocket API and subscribes to state_changed events. When an entity shown on the dashboard changes, it tells every open dashboard through server-sent events (/api/events), and the dashboards update the cards in place without rebuilding the page, which avoids camera image flicker. A 60-second full refresh runs in parallel as a safety net. The server reconnects to Home Assistant after 5 seconds if the connection is lost; the browsers reconnect to the server on their own. With HASS_DASHBOARD the server also subscribes to lovelace_updated and the registry events, and the browsers load the dashboard again when it is saved in Home Assistant.
Troubleshooting
Inspecting the weather forecast
If a forecast card is empty or the precipitation chart shows no bars, you can inspect exactly what Home Assistant returns by adding ?debug to the forecast endpoint in your browser:
| URL | Forecast |
|---|---|
http://<host>:8080/api/forecast?debug |
Daily (HASS_WEATHER) |
http://<host>:8080/api/forecast?hourly&debug |
Hourly (HASS_WEATHER_HOURLY) |
The response highlights the most useful diagnostics at the top:
available_keys— the fields each forecast entry exposes (look forprecipitationand/orprecipitation_probability).has_precipitation— whether theprecipitation(mm) field is present.first_entry— the full first forecast entry.raw_service_response— the completeweather.get_forecastsresponse.
This is handy when switching between weather integrations, since each exposes a different set of fields.
PWA / tablet installation
The dashboard can be installed as a Progressive Web App on iPad or Android:
- Open the dashboard in Safari (iPad) or Chrome (Android).
- Tap the share icon → Add to Home Screen.
- The app opens fullscreen without browser chrome.
The fullscreen button is automatically hidden when running in standalone (PWA) mode or when the Fullscreen API is unavailable.
Keeping the screen awake
| Platform | Method |
|---|---|
| Modern browsers | Screen Wake Lock API |
| iOS 15.8 and older | Silent looping H.264 video (nosleep.mp4) activated on first touch |
On iPadOS 15 and older, also set Settings → Display & Brightness → Auto-Lock → Never for fully reliable always-on behaviour.
Local development
# API on http://localhost:5380; reads the settings from .env
cp .env.example .env
dotnet run --project server/InternalDashboard.Api
# Frontend in a second terminal; open http://localhost:4200
cd client
npm install
npm start
For translation during development, run LibreTranslate as well (docker compose up -d libretranslate) and set TRANSLATE_URL to an address your machine can reach, or set TRANSLATE_ENABLED=false.
Frontend tests: cd client && npm test. Server tests: dotnet test InternalDashboard.slnx.
API
| Method | Path | Description |
|---|---|---|
| GET | /api/config |
App name, the visitor's language, camera settings |
| GET | /api/states |
The entity cards (HASS_ENTITIES), names and sensor texts translated |
| GET | /api/states/top |
The top-right bar (HASS_TOP_ENTITIES) |
| GET | /api/states/outdoor, /api/states/indoor |
Temperature and humidity |
| GET | /api/states/rooms |
The rooms of the rooms modal |
| GET | /api/history?entity=…&hours=…&points=… |
History of a climate sensor, downsampled |
| GET | /api/forecast, /api/forecast?hourly |
Daily and hourly forecast (&debug, see above) |
| GET | /api/camera |
The current camera image |
| POST | /api/toggle |
Toggle a switch on the dashboard: { "entityId": "switch.…" } |
| GET | /api/events |
Server-sent events: the id of a dashboard entity whenever it changes; event: dashboard when the HASS_DASHBOARD dashboard was saved |
| GET | /api/dashboard |
With HASS_DASHBOARD: its view and screensaver, names translated |
| GET | /api/dashboard/states |
With HASS_DASHBOARD: the states of its entities, with Home Assistant's texts and icons |
| GET | /api/dashboard/calendar?entity=…&start=…&end=… |
With HASS_DASHBOARD: the events of a calendar on it |
| POST | /api/action |
With HASS_DASHBOARD: what a card offers: { "entityId": "climate.…", "service": "set_temperature", "value": 21 } |
| GET | /api/local/… |
With HASS_DASHBOARD: an image from Home Assistant's /local/ on it |
| GET | /manifest.webmanifest |
PWA manifest with APP_NAME |
The old addresses /index.php and /manifest.php redirect permanently to / and /manifest.webmanifest.
Docker
The Dockerfile builds the Angular app and the server into one image; the server serves the Angular app.
| Property | Value |
|---|---|
| Base image | mcr.microsoft.com/dotnet/aspnet:10.0 |
| Exposed port | 8080 |
| User | non-root ($APP_UID) |
| Volume | /app/storage (translation cache; optional) |
services:
internal-dashboard:
build: .
container_name: internal-dashboard
env_file:
- .env
ports:
- "8080:8080"
volumes:
- storage:/app/storage
The docker-compose.yml file adds the libretranslate service. It loads only the en, nl, and de models to keep resource usage down, and downloads them on first start — translation falls back to the untranslated name until that finishes.
The responses carry a strict Content-Security-Policy (no inline scripts or styles without the request's nonce; everything from the dashboard itself). Framing is allowed, so the dashboard can be shown inside Home Assistant.
CI/CD
The Forgejo workflow (.forgejo/workflows/docker-publish.yml) builds the image on every push to main (and monthly) and pushes it with a version tag and latest to the Forgejo container registry. This needs the repository secret REGISTRY_TOKEN.