ha-paneld is free, open-source, and exists to fix what's wrong with Home Assistant wall panels — the per-vendor fragmentation, the sluggish dashboards, and the clunky manufacturer software you're otherwise stuck with. It gives one consistent, Home-Assistant-first integration for the LEDs, buttons, sensors, screen and TTS across panels from different makers, plus the tooling to make a dashboard actually feel fast on cheap hardware.
It's a small Android agent that exposes panel-side hardware to Home Assistant over HTTP + MQTT auto-discovery + mDNS, so a panel pairs itself with HA when you sideload the APK — no per-device YAML.
It is built for panel-class Android (Sonoff NSPanelPro, Tuya TPA10, and similar), not personal phones. The official HA Companion app remains the HOME launcher and dashboard; ha-paneld runs as a headless foreground service alongside it and never takes the foreground.
Performance is a first-class concern. Panel-class hardware is cheap and frequently underpowered, so a Home Assistant dashboard that flies on a phone can crawl on a wall panel — and there's usually no visibility into why. A large part of ha-paneld is therefore about measuring and tuning: it reports on-device CPU / GPU / clock and thermal throttling, a dashboard responsiveness metric, the top CPU consumers, and a 1-click WebView DevTools relay — so you can find what's actually costing frames and tune the dashboard to the panel instead of guessing. See the performance comparison and docs/performance.md.
Note
v0.x preview — entity names and the API may still change before v1.0.0. It's an ordinary app install (no root-partition or firmware changes), so it uninstalls cleanly if you change your mind.
First enable network ADB on the panel (Developer options → "ADB debugging"). Then, from any machine
with adb on the same LAN, paste:
curl -fsSL https://raw.githubusercontent.com/maxlyth/ha-paneld/main/scripts/install.sh | bashNo checkout, no parameters: it checks your tools (with fix-it hints if adb/curl are missing),
prompts for the panel IP (and optional id / MQTT broker), downloads the latest signed release, and
provisions the panel. For scripted/fleet installs use scripts/provision.sh
directly (see Provisioning).
Note
ha-paneld is the headless agent — your dashboard launcher is the HA Companion app. On panels without Google Play, install its minimal release APK (the no-Google-Play build).
The one-liner uses network adb. If a panel doesn't expose adb over the network:
-
Bootstrap via USB (Tuya TPA10 / Smatek panels). adb — and
adb root— is often only available, or only rooted, on the USB port. Plug in, enable network adb, then install as normal:adb devices # accept the on-screen RSA prompt if shown adb root # if supported (needed for the sysfs-LED helper daemon) adb tcpip 5555 # expose adb on the network (resets on reboot) adb connect <panel-ip>:5555
then run the one-liner (or
scripts/provision.sh <panel-ip:5555>). -
No adb at all. With a browser or file manager on the panel, download the release APK, enable "install unknown apps" and tap to install — then grant permissions by hand (Settings → Apps → ha-paneld → Modify system settings; Accessibility → enable the service), which the app's setup screen guides. Not possible on locked-down panels with no browser/file manager.
Note
ha-paneld can only be sideloaded on a panel — it isn't on the Play Store (its accessibility use for buttons/nav doesn't fit Play policy), so even Play-capable panels (eg 2026 Android 14 models) should sideload it.
Important
First-run gotcha — update the panel's system WebView. Even modern panels ship with a WebView/Chromium far too old to render a current Home Assistant dashboard, so the HA Companion app might show a blank or broken UI. Fix it cleanly over adb — no F-Droid or third-party app store needed: see Updating the system WebView. This trips up almost everyone; do it before judging anything else.
The HA Companion app targets personal phones and tablets. Wall panels need different primitives: arbitrary-URL audio announcements, screen/LED/button control, hardware-button events back to HA, and turnkey mDNS pairing. ha-paneld covers those; Companion keeps doing what it does (and remains the dashboard host).
Fully Kiosk Browser is the usual answer for HA wall panels, and it's genuinely capable. But it sits awkwardly against Home Assistant's own values, and on a small mixed fleet its wins are narrow for the friction it adds:
- Not free, not open source. Fully Kiosk is closed-source commercial software. The free tier is limited and nags; the parts people actually want for a panel — the remote-admin REST/MQTT API, motion/screensaver controls, no watermark — need the paid Plus licence, per device. That cuts against HA's free, open, local-first ethos; ha-paneld is Apache-2.0 and the dashboard runs in the official, open Companion app.
- The Companion app already serves dashboards better. For day-to-day dashboard rendering, the Companion app is purpose-built for HA — native auth and sessions, push notifications, deep links, and it tracks the frontend. A general-purpose kiosk browser is a second rendering path to keep working; ha-paneld deliberately leaves dashboard hosting to HA Companion and only adds the panel hardware HA can't otherwise reach.
- Per-device config doesn't scale on a non-homogeneous fleet. Fully Kiosk is configured per
device (its settings UI / per-device cloud), so a mixed fleet of different panels drifts and each
unit is a bespoke setup. ha-paneld is config-as-code: MQTT auto-discovery, uniform entities across
every panel, and one
update-fleet.shto roll them together.
If Fully Kiosk's specific extras (e.g. its kiosk lockdown or its particular screensaver) are load-bearing for you, keep using it — ha-paneld doesn't try to replace a browser. It replaces the panel-hardware gap, openly, without a per-device licence.
| Cap | Surface |
|---|---|
| TTS / announce audio | POST /play + number.<panel>_volume (HA has no MQTT media_player platform) — server-side TTS recipe in docs/tts.md |
| Screen brightness | light.<panel>_screen brightness |
| Screen on/off (true backlight off, no lock/PIN) | light.<panel>_screen on/off |
| RGB LED | light.<panel>_led (per-panel HAL: rk3576 NDK /dev/ledjni, or sysfs via the root helper) |
| URL navigate | text.<panel>_navigate |
| Hardware-button events | event.<panel>_button (a11y key capture) |
| Ambient light / proximity | sensor.<panel>_illuminance, binary_sensor.<panel>_proximity |
| Reload dashboard / reboot | button.<panel>_reload, button.<panel>_reboot |
| Launcher / Home Assistant (bring a launcher or the HA dashboard forward) | button.<panel>_launcher, button.<panel>_home |
| Panel info + config web page | GET / (the device "Visit" link) |
Every panel publishes the same Home Assistant MQTT-discovery entities, regardless of underlying hardware (the per-panel HAL is hidden behind them). Configure an MQTT broker and they appear with no YAML:
| Entity | Capability | Notes |
|---|---|---|
light.<panel>_screen |
brightness + on/off | on = backlight on, off = true backlight-off (no keyguard/PIN); JSON schema, brightness 0–255 |
light.<panel>_led |
RGB | published only when a LED backend is present (NDK /dev/ledjni or the root helper) |
text.<panel>_navigate |
push a URL to the panel | depends on Companion intent handling; last URL restored on reconnect |
event.<panel>_button |
hardware button presses | published only when the a11y key-filter is enabled |
number.<panel>_volume |
TTS/announce volume | 0–100% → STREAM_MUSIC; playback is the HTTP /play contract below |
sensor.<panel>_illuminance |
ambient lux | standard SensorManager TYPE_LIGHT; published only if present |
binary_sensor.<panel>_proximity |
proximity (occupancy) | standard SensorManager TYPE_PROXIMITY; published only if present |
button.<panel>_reload |
reload dashboard | force-stop + relaunch the configured dashboard package (root helper, else su) |
button.<panel>_reboot |
reboot panel | root helper, else su |
button.<panel>_launcher |
bring a launcher to the foreground | fires CATEGORY_HOME at a non-default launcher (or configured launcher_package), leaving the boot/default home app unchanged |
button.<panel>_home |
bring the HA dashboard to the foreground | launches dashboard_package if set, else the default home app (the HA Companion) — the complement of the Launcher button |
The device's display name (configuration_url "Visit" link, friendly name) and the LED/screen
states are re-published on every (re)connect, and the MQTT client auto-reconnects, so HA stays in
sync after a panel reboot or broker blip.
GET / panel info + config page (versions, hardware, status; panel_id,
friendly name, MQTT broker/creds, dashboard package). This is the
device's configuration_url, so HA shows a "Visit" link.
GET /api interactive REST API explorer (renders the OpenAPI spec)
GET /openapi.json OpenAPI 3 spec of this API (import into Swagger / Postman)
POST /config form-encoded settings from the page; persists + live-reconfigures
GET /config full config as JSON (MQTT password redacted) for fleet tooling
GET /perf live performance JSON (CPU %/clock, GPU, RAM, temp, top procs,
responsiveness) — polled by the page; sampled only while viewed
POST /instrumentation enabled=true|false — turn the perf sampler on/off
GET /proximity live proximity raw + calibration (raw stays on the panel)
POST /proximity/{capture,threshold,sensitivity,reset} tune the cutoff
GET /inspect · POST /inspect/{start,stop} WebView DevTools relay (:9222)
GET /diag copy-paste diagnostics dump (build, SELinux, su probe, /dev +
/sys node listings, packages, capability assessment)
GET /health -> 200 "ha-paneld <version> panel=<id>"
POST /play body contains an audio URL (raw or {"url":"…"})
-> 200 "playing" (download + play happen in the background)
-> 400 "no-url" (no URL found in body)
The web page at / is how a user sets the MQTT broker without adb — find the panel's IP (mDNS
_ha-paneld._tcp, or the router), open http://<ip>:8888/, fill in the broker + credentials, Save.
The agent listens on :8888. Self-signed HTTPS sources are accepted (panels live on a trusted LAN). This is the same contract as the reference shell receiver it replaces, so HA-side automation needs no change when a panel migrates from the shell receiver to ha-paneld.
The agent advertises _ha-paneld._tcp.local. with TXT records (ver, caps, path). If an MQTT
broker is configured it publishes Home Assistant MQTT-discovery configs so panel entities appear
without YAML.
Zero-config: with no broker set, ha-paneld browses for Home Assistant's own _home-assistant._tcp
advert on the LAN and uses its MQTT broker on :1883 — so a fresh install pairs itself. If the broker
needs a login (e.g. the HA Mosquitto add-on), set the username/password on the config page; the MQTT
status reads auth rejected until they're right. Set the broker explicitly if it's elsewhere or your
LAN has more than one Home Assistant. With nothing found, the HTTP surface still works standalone.
All permissions are granted over adb — no per-device tap-through. Run the same script on every panel:
scripts/provision.sh <panel-ip:5555> \
[--id NAME] [--mqtt tcp://host:1883] [--latest] [--force]With no --apk and no local build it downloads the latest signed release from GitHub (--latest
forces that even when a local build exists). It connects, installs, grants the permissions below,
starts the agent, optionally sets the panel id / MQTT, and ends with a self-verify checklist. It is
idempotent — re-run the same command to finish after any interruption — and warns before
reinstalling the same or an older version (--force skips that). scripts/provision.sh <ip> --verify
re-checks a panel without changing anything.
Updating a whole fleet: use scripts/update-fleet.sh. The fleet script downloads the release once and runs provision.sh per panel, so every one
is installed and launched and verified:
scripts/update-fleet.sh --latest -- 192.168.1.10 192.168.1.11:5555
# or pipe a host list: printf '%s\n' 192.168.1.10 192.168.1.11 | scripts/update-fleet.sh --latestNon-root panels: use the in-app setup screen, which fires the standard system permission intents.
Permission → why:
| Permission | For | Grant |
|---|---|---|
POST_NOTIFICATIONS |
foreground-service notification | runtime / pm grant |
WRITE_SETTINGS |
screen brightness | appops set <pkg> WRITE_SETTINGS allow |
| Accessibility (key filter) | hardware-button events | settings put secure enabled_accessibility_services … |
Screen-off needs no device admin — ha-paneld powers the backlight off via the root helper daemon
or su (bl_power), falling back to brightness-0, so it never raises a keyguard/PIN and never blocks
its own uninstall. (Builds ≤ 0.5.0 shipped an optional device admin; 0.5.1 removed it — see the
signing note below if you're upgrading from one where you'd activated it.)
On the rk3576 panel (Electron WF1589T) the front RGB LED is reached via the char device
/dev/ledjni, which is world-rwx and labelled app-accessible (SELinux device domain), so a
normal app drives it without root or a helper. ha-paneld ships its own ~70-line NDK driver
(app/src/main/cpp/led_jni.c) doing the ioctls directly. The
protocol (request numbers, value range, open flags) was reverse-engineered clean-room from a
hardware sample so no vendor library is bundled or required. The LED entity is published only on panels where /dev/ledjni
is openable.
Other panels (e.g. Tuya TPA10) expose their LED only through root-only /sys/class/leds/*. A
sandboxed app cannot reach those (SELinux untrusted_app cannot exec su nor write sysfs_leds),
so those panels need a small root helper daemon that ha-paneld talks to over a localhost socket.
The daemon and its boot-persistent installer live in helper/ — build and install it with
helper/README.md.
ha-paneld needs no system-signed install. Standard-Android capabilities (brightness, sleep, navigate, TTS) work on any panel; LED/buttons depend on a per-panel HAL.
| Panel class | SoC | Android | ABI | Notes |
|---|---|---|---|---|
| Sonoff NSPanelPro / Pro120 | Rockchip PX30 | 8.1 (API 27) | arm64-v8a | toolbox su |
| Tuya TPA10 | Rockchip rk3566 | 11 (API 30) | armeabi-v7a | 32-bit userspace |
| Electron WF1589T | Rockchip rk3576 | userdebug (adb root) |
arm64-v8a | RGB LED via clean-room NDK ioctl on /dev/ledjni (no vendor lib) |
Other Android panels are welcome — contribute a HAL adapter for your hardware. Deep per-panel hardware references (SoC, sensors, LED path, radios) are in docs/hardware/.
minSdk is 26. API < 26 is unsupported (the MQTT client cannot connect below API 26).
Only Docker is required. The script builds a version-pinned image (JDK 17 + Android SDK 35 + NDK + CMake, matching CI) and runs Gradle inside it; the APK lands in your working tree.
./tools/build/build.sh # debug APK -> app/build/outputs/apk/debug/
./tools/build/build.sh :app:assembleRelease # any Gradle task(s) insteadThe image is built once and cached; Gradle caches persist in a named Docker volume, so repeat
builds are fast. See tools/build/ (and the HOST_WORKDIR note in build.sh if
you run from inside a container talking to an outer Docker daemon).
./gradlew :app:assembleDebug # debug APK -> app/build/outputs/apk/debug/
./gradlew :app:assembleRelease # release APK (unsigned unless signing configured)Requires JDK 17 and an Android SDK with NDK 27.0.12077973 + CMake 3.22.1 (for the native
/dev/ledjni LED driver). The Gradle wrapper pins the Gradle version; nothing else needs installing.
The build is pinned to a conservative AGP 8.7 / Kotlin 2.0 / Gradle 8.10 combo for reliable
first-run CI. Newer AGP/Kotlin is fine to adopt during the v0.x line — versions live in
gradle/libs.versions.toml.
You don't need to configure signing to build and run ha-paneld.
Signing details — dev vs official builds, the key-mismatch gotcha, legacy device-admin, signing your own fork
Two cases:
- Dev / fork builds are signed with the committed
debug.keystore(passwordandroid). It's in the repo on purpose — not a secret — so every build (yours, mine, CI's) shares one signature. That's what letsinstall -rupdate a panel in place without uninstalling. Just build and install. - Official releases are signed with a private key held in GitHub Actions secrets
(
ANDROID_KEYSTORE_BASE64,ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIAS,ANDROID_KEY_PASSWORD). A fork won't have those, so a tagged release in your fork falls back to a debug-signed APK — fine for personal use.
[!IMPORTANT] Android refuses to update an installed app with an APK signed by a different key. So you cannot install your own debug-signed build over an installed official (release-signed) build, or vice versa —
adb/the installer rejects it with a signature mismatch. Uninstall first (adb uninstall io.github.maxlyth.hapaneld), then install the other build. Uninstalling clears the panel's saved config, so re-run provisioning afterwards. This is the one thing that trips people up.Legacy device-admin (builds ≤ 0.5.0 only): 0.5.1 removed the device admin entirely, so fresh installs never hit this. But if you'd activated it on an older build, uninstall fails with
DELETE_FAILED_DEVICE_POLICY_MANAGERuntil you deactivate it — Settings → Security → Device admin apps → turn off "ha-paneld", thenadb uninstall io.github.maxlyth.hapaneld. (dpm remove-active-admindoes not work — Android refuses to remove a non-test admin via the CLI.)
Signing your own fork's releases (optional):
keytool -genkeypair -storetype PKCS12 -keystore release.jks -alias ha-paneld \
-keyalg RSA -keysize 2048 -validity 10000 -dname "CN=ha-paneld"
base64 -w0 release.jks # -> the ANDROID_KEYSTORE_BASE64 repo secretUse one password for both ANDROID_KEYSTORE_PASSWORD and ANDROID_KEY_PASSWORD, and ha-paneld
(your alias) for ANDROID_KEY_ALIAS. Back up release.jks and the password safely — losing them
means you can never publish an in-place update again. Never commit the keystore (*.jks is gitignored).
Validated across the panel fleet: Sonoff NSPanel Pro (PX30, Android 8.1), Tuya TPA10 (rk3566, Android 11), Electron WF1589T (rk3576, Android 14).
New in 0.8.2:
- Soft navigation bar (system overlay) — an on-screen Back · Launcher · Recents bar that, since these
panels have no physical keys, also carries Brightness ± and Volume ± (tap to step, hold to ramp).
For panels whose firmware hides the native navbar (e.g. NSPanel Pro). Modes Off / Always on / Swipe
reveal (
select.<panel>_navbar, Swipe reveal slides up from a bottom-edge strip and auto-hides); wide panels show the live brightness/volume percentage between each ± pair. Back/Recents use root key injection where available (no accessibility service needed), and Recents is omitted on panels whose firmware has no overview screen rather than showing a dead button.
Earlier releases (0.8.1 → 0.4.x) — full history in CHANGELOG.md
New in 0.8.1:
- LAN-only control surface — the HTTP API/UI (
:8888) refuses non-local sources (loopback / RFC1918 / link-local / IPv6 ULA), so a dual-stack panel isn't reachable from the internet over a routable IPv6 even if the router doesn't firewall inbound v6. - Screenshot-safe info page — Device ID, MQTT broker and globally-routable IP/IPv6 are blurred by default; click a value or the Reveal toggle to show them. LAN (RFC1918 / ULA) addresses stay visible.
- "Open in Home Assistant" — when the MQTT credentials are an HA user, the info page links straight to the panel's own HA device page (works for non-admin users and across reverse-proxy / tunnel setups).
- Screen diagonal on the info page (from resolution + dpi); click to toggle inches ↔ cm.
- Recommended display density per model — the "rec" button suggests 160 dpi (86P) / 250 dpi (120P).
New in 0.8.0:
- Auto-brightness (opt-in) —
switch.<panel>_auto_brightnessmaps a lux stream (the panel's own ambient-light sensor, or HA-fednumber.<panel>_ambient_lux) to the backlight, with an asymmetric response (snappy on lights-on, heavily smoothed on slow daylight drift) and a Dimmer↔Brighter bias. Off by default. - Controls card — an on-page software nav bar (Back / Recents / Launcher + Vol± / Reboot) for panels with no physical nav bar; each button disables itself when its capability (accessibility / root) is absent.
- NSPanel Pro Zigbee, hardened — detects both the stock vendor-native and NSPanelTools-managed
gateways (a stock 120P now shows Zigbee + a working
zigbee_routerswitch); the state persists across reboot, and the 120P vendor-guard CPU-spin is resolved. Relays are reported accurately — Gen1 has none (the kernel's phantomst_relaynodes are not mistaken for hardware). - Richer info page — split into Panel information / Networking / ha-paneld profile cards; shows model + firmware, total eMMC storage, and Light/Proximity technology · value-type · range; the page auto-reloads when the app is updated. Native CSS multi-column layout (near-zero layout-shift, phone→15″) with dark-mode polish.
- Diagnostics (
/diag) rewritten — terse, public-safe (no addresses/identifiers), reflects every detected field, and structured for a regression-test harness. - Debug sensor trace (
/sensortrace) for objective auto-brightness / proximity filter tuning.
New in 0.7.1:
- Hardware buttons via the daemon's evdev reader (WF1589T power key no longer locks the panel; the
TPA10 5th/orange
SW_MUTE_DEVICEbutton surfaced asKEYCODE_MUTE), CPU profile tiers (Performance / Efficiency / Auto), experimental display sizing, per-panel HA device identity, theme-aware App UI.
New in 0.7.0 (architecture-focused — no new entities):
- Device-profile architecture — each supported panel has a single canonical silo
(
device/<panel>.kt) declaring its quirks/paths; the LED, Zigbee and relay controllers read the active profile (detected once at startup) instead of hardcoding device specifics, while still runtime-probing to confirm. An unrecognised panel falls back to a Generic profile and works for whatever it physically has. The detected platform is shown on the info page. Design: docs/architecture/device-profiles.md. - Security — the Zigbee role-switch is allowlisted before any shell interpolation; the security posture (LAN-trust, network-layer access control, HA-auth as the future path) is documented in docs/architecture/security.md.
New in 0.6.2:
- CPU governor (
select.<panel>_cpu_governor, root/su panels) — set the scaling governor across all cores (powersave ↔ performance) to trade panel heat/noise against responsiveness; automatable from HA. - Persistent network adb (
switch.<panel>_network_adb, opt-in) — keepadb tcpip 5555across reboots. - IPv6 — the HTTP server binds dual-stack, so the panel UI/API answer on IPv6 as well as IPv4; the info page shows the IPv6 address.
- Local-only navigation —
text.<panel>_navigatestrips any scheme/host and navigates in-app via thehomeassistant://deep link, so an external URL can't trigger the Companion's disorienting WebView. - Smatek S9E (experimental) — onboard relays (
switch.<panel>_relay1/2) and button LEDs (light.<panel>_button_led1..4); see docs/hardware/s9e.md. - Hardening: more robust Zigbee detection + non-blocking toggle; fleet updates that always (re)launch
(
scripts/update-fleet.sh); structured GitHub issue forms.
New in 0.6.1:
- Zigbee router (
switch.<panel>_zigbee_router, Sonoff NSPanel Pro only) — turn the panel's built-in EFR32 radio into a Zigbee router/repeater to extend an existing mesh, and back off again. No reflash; ha-paneld drives the on-device Sonoff gateway over its local broker. The router then shows up as a normal device in your ZHA / Zigbee2MQTT coordinator. The info page reports the installed gateway driver/version, running state and current role.
New in 0.6.0 (all app-side):
- Temperature + humidity sensors (
sensor.<panel>_{temperature,humidity}, where fitted — e.g. the TPA10's CHT8305) via SensorManager; recorder-friendly gating. - Button-backlight light + remote nav buttons (Back/Recents via the accessibility service).
- Wake-on-wave — instant local screen wake on proximity (
switch.<panel>_wake_on_wave). - Auto-return to dashboard after an update, a config QR code, and a responsive App UI (fits 480×480 without scrolling, larger on roomy panels).
- Touch-sound switch for a consistent UI click across the fleet.
- Server-side TTS recipe (docs/tts.md).
New in 0.5.0:
- Zero-config MQTT — with no broker set, ha-paneld finds Home Assistant on the LAN over mDNS and uses its broker automatically. The config page shows the live connection state and distinguishes connected, reachable but auth rejected (set credentials) and unreachable.
- Redesigned web UI — a responsive masonry config page, an in-app config browser (so kiosk panels with no browser still configure on-device), and a proper launcher info screen.
- REST API explorer at
/api, plus an OpenAPI spec at/openapi.json(import into Swagger/Postman for fleet tooling). - Deeper performance insight — CPU clock vs hardware max (thermal throttling), a dashboard responsiveness metric, top-5 processes, and a 1-click WebView DevTools relay (no adb). The sampler is page-view gated and can be switched off, so the perf tool isn't itself a constant cost.
- Tunable proximity — calibrate the near/far cutoff from the page; the raw value stays on-device.
- Signed releases and one-command provisioning that fetches the latest release when you have no APK.
Carried from 0.4.x:
- Uniform MQTT control — screen (brightness + true backlight-off), RGB LED, navigate, volume, TTS, hardware buttons, reload, reboot, launcher, home (HA).
- Per-hardware LED HAL — rk3576 clean-room NDK
/dev/ledjni(app-direct); sysfs panels via the root helper daemon with a boot-persistentinitservice (helper/). - HA device card, MQTT auto-reconnect + retained-state restore, a
/diagdump and a Capabilities matrix for bug reports. See docs/performance.md for panel performance tuning (the WebSocket-event-volume problem and how to fix it).
- Proximity-calibration capture UX — clearer near/far capture steps to finish the calibration-card rework (the gauge auto-ranging shipped in 0.8.0).
- More performance tooling — deeper on-device instrumentation to measure, diagnose and tune dashboard performance on weak panels.
- Built-in relay control beyond the S9E — the same
switch.<panel>_relay*model on other panels with onboard relays, once each one's control path (GPIO / vendor node) is known. - Daemon boot-persistence on su-only (PX30) panels, if true-off is wanted without relying on
suat runtime. - DLNA renderer (under consideration — gauging interest) — advertise as a UPnP/DLNA media renderer
so HA auto-discovers a
media_player. Caveat: it would appear as a separate HA device, not part of the MQTT-discovery device that holds the panel's controls/sensors (MQTT discovery has no media_player platform); the shipped TTS recipe already covers the main announce use case.
- docs/hardware/ — reverse-engineered hardware references for the supported panels (SoC, LED control path, sensors, radios), since these devices are otherwise undocumented: NSPanel Pro (PX30), TPA10 (rk3566), WF1589T (rk3576) — plus a performance comparison.
- docs/tts.md — server-side TTS recipe: render a phrase with any HA engine (Piper, Cloud) and send it to a panel via a small script (no add-on, no on-device TTS).
- docs/performance.md — panel performance tuning: why dashboards lag on weak panels and how to fix it (the WebSocket-event-volume problem; the split-instance approach).
- docs/display-sizing.md (experimental / R&D) — matching dashboard size to a desktop browser via display density + system font scale (Android panels often ship these mismatched to the physical screen).
- helper/README.md — the root LED/control helper daemon for sysfs-LED panels (build + boot-persistent install).
- REST API — browse and try every endpoint at
http://<panel>:8888/api; the machine-readable spec is at/openapi.json. GET /diagon a panel — a copy-paste hardware/firmware/capability dump for bug reports.
| On-panel launcher screen | REST API explorer |
|---|---|
![]() |
![]() |
- HTTP — Ktor CIO engine (coroutine I/O, no thread-per-connection).
- MQTT — HiveMQ MQTT 5 client (NIO transport; ABI-agnostic).
- mDNS — JmDNS (chosen over
NsdManagerfor reliable TXT records across API levels).
ha-paneld has no donate button. It's free, and the "payment" that actually moves it forward is more panels supported — which takes hardware to study. Every panel here was added by hands-on adb analysis: probing the device and watching how it responds to real button, LED and sensor interaction.
So if you'd like to help:
- Open an issue with your panel's diagnostics. Visit
http://<panel-ip>:8888/diag(or the diag link on the panel's config page) and paste the dump into a new issue — build, SELinux,/dev+/syslistings, capability probe. That's enough to start; from there we'll work out a short interactive workflow to map the buttons/LEDs/sensors that need a person at the panel. - Or send me the panel. I'm UK-based and happy to do the reverse-engineering directly — the fastest route to a fully-supported new model. You'll get it back (I have way too many already); open an issue first so we can sort the details.
The result is always open: your panel becomes a profile everyone can use — a bit less per-vendor fragmentation for the next person. That's the donation.
Thanks to Seaky for NSPanel Pro Tools (releases), which showed what good panel-side Home Assistant tooling can do — genuinely excellent work. It targets the Sonoff NSPanel-Pro class and is distributed as a closed-source APK; ha-paneld exists to be an open, multi-vendor alternative that any Android panel can adopt and extend. The two solve overlapping problems for different audiences.
Apache-2.0. See LICENSE.


