A small web app with a basic login that shows each person only their own lights and air conditioning, and lets them control them. It talks to your Home Assistant instance through a small Python backend that keeps your Home Assistant token private (the browser never sees it).
This repo is a Home Assistant add-on repository; the add-on lives in the
control_center/ subfolder:
repository.yaml Marks this repo as an HA add-on repository
control_center/ The add-on
config.yaml Add-on manifest
Dockerfile Builds the add-on image
app.py Flask backend (login + talks to Home Assistant)
requirements.txt Python dependencies
users.json Seed accounts + which entities each may control
static/ The web UI (React vendored locally, no build step)
The frontend is React, vendored locally and transformed in the browser by Babel, so there's no npm and no build step.
On Home Assistant OS / Supervised:
- Settings → Add-ons → Add-on Store → ⋮ (top-right) → Repositories.
- Paste
https://github.com/Suds-Lab/Home-Assistant-Frontendand Add. - The Control Center add-on appears in the store. Open it → Install.
- Configuration tab: optionally set
jwt_secret(leave it blank to auto-generate one). Start the add-on. (Users are not configured here; see step 5.) - Open Control Center in the sidebar. A default admin
alice/changemeis created on first run; log in, change its password, and add everyone else from the Manage users screen. Point household members athttp://<your-home-assistant>:8099for their dashboard.
Updates then arrive as a normal Update button when you push new commits.
- Open Home Assistant in your browser.
- Click your profile (bottom-left), open the Security tab.
- Under Long-lived access tokens, click Create Token, name it (e.g. "Control Center App"), and copy the token.
Copy .env.example to .env and fill in:
HA_URL: your Home Assistant address, e.g.http://homeassistant.local:8123orhttp://192.168.1.50:8123(no trailing slash).HA_TOKEN: the token you just created.JWT_SECRET: optional; leave blank to auto-generate and persist a random one.
The easiest way is the built-in Manage users screen (see below), but the
first admin account has to be seeded in a file. Edit users.json:
each user has a username, password, display name, an optional admin flag, and
the list of entity IDs they're allowed to control:
{
"username": "alice",
"password": "changeme",
"displayName": "Alice",
"admin": true,
"entities": ["light.bedroom", "climate.bedroom_ac"]
}"admin": true lets that user open the Manage users screen. Entity IDs are
found in HA under Settings → Devices & Services → Entities or Developer
Tools → States.
Assign any entity to a user; the dashboard groups devices by type and shows controls tailored to each:
| Type | Controls |
|---|---|
light |
on/off toggle + brightness slider |
switch, input_boolean |
on/off toggle |
fan |
on/off + speed slider |
climate |
mode buttons + target-temperature stepper |
cover |
open / stop / close + position slider |
lock |
lock / unlock |
media_player |
prev / play-pause / next + volume |
scene, script |
activate / run |
automation |
trigger + enable toggle |
button, input_button |
press |
vacuum |
start / pause / dock |
anything else (e.g. sensor) |
read-only state display |
Every card has an ⓘ button that opens a detail panel showing the entity's full state, last-changed time, and every attribute.
Control is enforced server-side: the backend only calls a fixed allow-list of
services (ALLOWED_SERVICES in app.py), always scoped to the entity's own
domain and only for entities the user owns.
The app serves two different experiences, decided by which port a request arrives on:
| Management | User dashboard | |
|---|---|---|
| Who | You, the admin/owner | Each household member |
| Where | HA sidebar tab (Ingress, port 4000) | http://<home-assistant>:8099 |
| Auth | HA's own login (admin-only, like Terminal) | the app account you created |
| Does | add/edit/delete users, assign devices | control only their devices |
The management port (4000) is never published; it's reachable only through Home Assistant's Ingress, so a request there is trusted as an authenticated HA admin (no separate password). The user port (8099) is published for household members and is protected by the app's per-user login; the admin endpoints are not available on it at all.
Open the Control Center tab in the HA sidebar, which goes straight to the
management screen. There you add, edit, and delete users and tick each person's
devices from a searchable, grouped list of all your real Home Assistant
entities (no typing entity IDs). Accounts are saved to a persistent store
(/data/users.json in the add-on, users.json standalone) and survive
restarts. Passwords left blank on edit are kept, and the system won't let you
remove the last admin.
users.json is only the initial seed (it bootstraps a default admin on
first run). Once the store exists, the Manage users screen is the single
source of truth; users are not set in the add-on's Configuration tab.
The user dashboard can authenticate household members with Google (or any OpenID Connect provider) instead of, or alongside, a local password. The OAuth credentials live in the add-on Configuration; you then choose the sign-in methods in Settings → Sign-in methods (Local / Google / Both).
The dashboard must be reachable over HTTPS at a public URL (e.g. a reverse proxy or Cloudflare Tunnel). Your redirect URI is that URL plus
/api/oauth/callback, e.g.https://home.example.com/api/oauth/callback.
1. Create Google credentials in the Google Cloud Console:
- APIs & Services → OAuth consent screen: pick Internal (Workspace) or
External, set the app name + support email, add the scopes
openid,.../auth/userinfo.email,.../auth/userinfo.profile. For External, add test users or Publish. - Credentials → Create credentials → OAuth client ID → Web application: under Authorised redirect URIs add your redirect URI exactly (the provider only redirects to URLs you register here). Copy the Client ID and secret.
2. Configure the add-on (Configuration tab):
oauth_client_id: "1234….apps.googleusercontent.com"
oauth_client_secret: "GOCSPX-…"
oauth_redirect_url: "https://home.example.com" # public base URL (no trailing slash needed)
oauth_allowed_domains: ["my.domain"] # restrict to these domains (optional)oauth_allowed_domains restricts sign-in to those email domains (only
*@my.domain). For personal Gmail (or anyone without a domain of their own),
list the individual addresses instead: oauth_allowed_emails: ["alice@gmail.com", "bob@gmail.com"]. If you set neither, sign-in is refused (fail-closed). Set
oauth_allow_any: true only if you deliberately want anyone with a verified
email to sign in (they still land on the onboarding screen with no devices until
an admin assigns some). Restart the add-on, then enable Google or Both in
Settings → Sign-in methods. (For non-Google providers, oauth_logo_url sets
the button's logo.)
Behind Cloudflare Tunnel / a reverse proxy: point the tunnel's public
hostname at the add-on's user-dashboard port 8099; the /api/oauth/* routes
ride along on that same app. The add-on builds the redirect from
oauth_redirect_url (not the proxied request), so Google sees the real HTTPS
URL even though Cloudflare reaches the add-on over plain HTTP. If you also run
Cloudflare Access on that hostname, drop it here or bypass /api/oauth/*,
or its login page will intercept the callback.
First sign-in & onboarding: a Google user who signs in for the first time is auto-created with no devices and sees a "reach out to your administrator" screen until an admin assigns them devices in Manage users. Only users with at least one device see the dashboard. (You can also pre-create a user whose username is their email and assign devices first; an OAuth login for that email then adopts that account, so the same person can use a password or OAuth.)
Other providers: override oauth_authorize_url, oauth_token_url,
oauth_userinfo_url, oauth_scopes and oauth_provider_name for any OIDC
provider. The provider must return a verified email (email_verified: true).
(The full walkthrough is also in the add-on's Documentation tab /
control_center/DOCS.md.)
From the repo root:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r control_center/requirements.txt
copy control_center\.env.example .env # then fill in HA_URL + HA_TOKEN
python control_center/app.pyThis serves both ports from one process:
- http://localhost:4000: the management screen (opens directly, no login, since this port stands in for HA's Ingress).
- http://localhost:8099: the user dashboard; log in with a user from
users.json.
The management port (4000) binds to 127.0.0.1 by default, so the no-login
admin screen isn't exposed on your LAN. To reach it from another machine, set
INGRESS_BIND=0.0.0.0, but firewall it, since anything that reaches port 4000 is
trusted as admin.
Instead of adding the repository URL (see Install it above), you can drop the add-on onto the machine directly:
- Copy the
control_center/folder into/addons/control_center/on your HA machine. Easiest ways to get files there: the Samba share, Studio Code Server, or Advanced SSH & Web Terminal add-ons. - In Home Assistant go to Settings → Add-ons → Add-on Store, open the ⋮ menu (top-right) → Check for updates. "Control Center" appears under Local add-ons.
- Click it → Install (this builds the image; takes a few minutes).
- Open the Configuration tab (optionally set
jwt_secret; blank auto-generates one). Start the add-on. - Thanks to Ingress, Control Center appears as its own admin tab in the Home Assistant sidebar (like the Terminal / Mosquitto add-ons); click it to open the management screen right inside HA. No separate password: HA already authenticated you as an admin.
- A default admin (
alice/changeme) is seeded on first run. Use Manage users to change it and add household members + their devices. - Tell each member to open
http://<your-home-assistant>:8099and log in with the account you created; that's their personal device dashboard. (Make sure port 8099 is enabled in the add-on's Network section.)
The add-on also gets a proper Documentation tab (from DOCS.md) and an
icon/logo in the store and sidebar (icon.png / logo.png).
Notes:
- Requires an install with the Supervisor (HA OS or Supervised). Plain Docker
(HA Container) or pip (Core) installs don't have add-ons; use the standalone
run above and a
panel_iframe:sidebar link instead. homeassistant_api: trueinconfig.yamlgrants the proxied API access; the app talks tohttp://supervisor/core/apiwith the injectedSUPERVISOR_TOKEN.- HA's own login gates the page (Ingress); the app's per-user login then scopes each person to their own entities.
The user dashboard is a Progressive Web App. On a first visit it shows an Install banner; installing adds a "Control Center" icon to the phone's home screen and runs full-screen with no browser chrome; it feels like a native app, and the app shell is cached so it loads instantly (and offline).
- Android / desktop Chrome: tap Install in the banner (or the browser's install button).
- iOS Safari: the banner says to use Share → Add to Home Screen.
- Taps give subtle haptic feedback on phones that support it (iOS and Android).
- Every screen has a theme toggle (top corner) for System / Light / Dark, remembered per device.
⚠️ Installing (and the service worker / offline cache) needs a secure context, i.e.https://orlocalhost. Over plainhttp://<ip>:8099on a LAN, Android won't offer install and offline won't work, though iOS "Add to Home Screen" still gives a full-screen shortcut. To get the full experience, serve it over HTTPS (e.g. a reverse proxy, or Home Assistant's own TLS / Nabu Casa in front of it).
- Real-time updates. The backend holds one WebSocket to Home Assistant and
relays state changes to each browser over a WebSocket (
/api/ws), so the UI reflects any change (the app, a physical switch, an automation) within a fraction of a second, with no polling. The browser reconnects and re-syncs over REST automatically, so it can't go stale; if the link drops it shows a "Connection lost. Reconnecting…" notice with a Retry now button. When a new version is deployed, open dashboards reload themselves to pick it up.- Behind a reverse proxy, allow the WebSocket upgrade on
/api/ws(nginx:proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;). Cloudflare passes WebSockets through automatically. If you can't proxy WebSockets, setSTREAM=0to fall back to polling.
- Behind a reverse proxy, allow the WebSocket upgrade on
- The browser only ever talks to this backend. Your Home Assistant token lives
only in
.env(standalone) or never exists at all (add-on, via Supervisor). - Login issues a 7-day session token (JWT). Each request is checked against the user's allowed entity list, so users can't control devices that aren't theirs.
- Passwords are stored hashed (PBKDF2-HMAC-SHA256 with a per-password salt);
the store never holds plaintext. Any legacy plaintext from an older version is
upgraded to a hash automatically (on upgrade and on first login). Keep
users.jsonprivate regardless, and put the dashboard behind HTTPS for any internet exposure. - Login is rate-limited per username to slow brute-force attempts (it ignores
the client
X-Forwarded-For, which can be spoofed), with constant-time password checks that don't reveal whether a username exists. - The session-signing secret (
jwt_secret) is auto-generated and persisted on first run when you don't set one, so sessions are never signed with a guessable default. Rotate it anytime from Settings → Session security (which signs everyone out). - Device IDs and time ranges are validated before any call to Home Assistant, so a logged-in user can't smuggle a path into the HA API.
- Responses send
X-Content-Type-Options: nosniff. Setblock_iframe_embedding: trueto also sendX-Frame-Options: DENYon the user dashboard (anti-clickjacking); it's off by default so it never breaks apanel_iframeembed, and the management UI is always exempt.