{"id":50579828,"url":"https://github.com/maxlyth/ha-paneld","last_synced_at":"2026-06-07T12:01:29.835Z","repository":{"id":362581258,"uuid":"1258299774","full_name":"maxlyth/ha-paneld","owner":"maxlyth","description":"Android agent for Home Assistant wall panels — TTS, brightness, LEDs, buttons via HTTP + MQTT auto-discovery + mDNS","archived":false,"fork":false,"pushed_at":"2026-06-05T00:58:03.000Z","size":808,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T01:12:16.533Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maxlyth.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T12:59:11.000Z","updated_at":"2026-06-05T00:58:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/maxlyth/ha-paneld","commit_stats":null,"previous_names":["maxlyth/ha-paneld"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/maxlyth/ha-paneld","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxlyth%2Fha-paneld","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxlyth%2Fha-paneld/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxlyth%2Fha-paneld/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxlyth%2Fha-paneld/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maxlyth","download_url":"https://codeload.github.com/maxlyth/ha-paneld/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxlyth%2Fha-paneld/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34020187,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-07T02:00:07.652Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-05T01:00:35.006Z","updated_at":"2026-06-07T12:01:29.816Z","avatar_url":"https://github.com/maxlyth.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ha-paneld\n\nA small Android agent for **wall-mounted Home Assistant panels**. It exposes panel-side hardware\nto Home Assistant over HTTP + MQTT auto-discovery + mDNS, so a panel pairs itself with HA when you\nsideload the APK — no per-device YAML.\n\nIt is built for panel-class Android (Sonoff NSPanelPro, Tuya TPA10, and similar), **not** personal\nphones. The official HA Companion app remains the HOME launcher and dashboard; ha-paneld runs as a\nheadless foreground service alongside it and never takes the foreground.\n\n![ha-paneld's on-panel configuration page — responsive cards for panel info, capabilities, live performance and configuration](docs/img/config-ui.png)\n\n\u003e **Status: v0.x preview.** Entity names and the API may still change before v1.0.0. It's an ordinary\n\u003e app install — no root-partition or firmware changes — so it uninstalls cleanly if you change your mind.\n\n## Install\n\nFirst enable network ADB on the panel (Developer options → \"ADB debugging\"). Then, from any machine\nwith `adb` on the same LAN, paste:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/maxlyth/ha-paneld/main/scripts/install.sh | bash\n```\n\nNo checkout, no parameters: it checks your tools (with fix-it hints if `adb`/`curl` are missing),\nprompts for the panel IP (and optional id / MQTT broker), downloads the **latest signed release**, and\nprovisions the panel. For scripted/fleet installs use [`scripts/provision.sh`](scripts/provision.sh)\ndirectly (see [Provisioning](#provisioning-no-device-ui-on-rooteduserdebug-panels)).\n\n## Why not just the Companion app?\n\nThe Companion app targets personal phones. Wall panels need different primitives: arbitrary-URL\naudio announcements, screen/LED/button control (via the bundled NDK or a small root helper),\nhardware-button events back to HA, and turnkey mDNS pairing. ha-paneld covers those; Companion keeps\ndoing what it does (and remains the dashboard host).\n\n## Capabilities\n\n| Cap | Surface |\n|-----|---------|\n| TTS / announce audio | `POST /play` + `number.\u003cpanel\u003e_volume` (HA has no MQTT media_player platform) |\n| Screen brightness | `light.\u003cpanel\u003e_screen` brightness |\n| Screen on/off (true backlight off, no lock/PIN) | `light.\u003cpanel\u003e_screen` on/off |\n| RGB LED | `light.\u003cpanel\u003e_led` (per-panel HAL: rk3576 NDK `/dev/ledjni`, or sysfs via the root helper) |\n| URL navigate | `text.\u003cpanel\u003e_navigate` |\n| Hardware-button events | `event.\u003cpanel\u003e_button` (a11y key capture) |\n| Ambient light / proximity (data only) | `sensor.\u003cpanel\u003e_illuminance`, `binary_sensor.\u003cpanel\u003e_proximity` |\n| Reload dashboard / reboot | `button.\u003cpanel\u003e_reload`, `button.\u003cpanel\u003e_reboot` |\n| Launcher / Home Assistant (bring a launcher or the HA dashboard forward) | `button.\u003cpanel\u003e_launcher`, `button.\u003cpanel\u003e_home` |\n| Panel info + config web page | `GET /` (the device \"Visit\" link) |\n\n\u003e [!NOTE]\n\u003e ha-paneld exposes the panel's light + proximity sensors as data (standard `SensorManager`), but\n\u003e they are **not** the occupancy/lux authority — room-level HA sensors (motion, lux, occupancy) are,\n\u003e being better placed and already calibrated. Brightness is therefore **HA-driven**: ha-paneld\n\u003e exposes the brightness actuator; the policy (from room sensors) lives in Home Assistant. Zigbee\n\u003e gateway and app-watchdog are out of scope (coexist with a dedicated tool if you need them).\n\n## The control API — uniform MQTT entities\n\nEvery panel publishes the **same** Home Assistant MQTT-discovery entities, regardless of underlying\nhardware (the per-panel HAL is hidden behind them). Configure an MQTT broker and they appear with\nno YAML:\n\n| Entity | Capability | Notes |\n|--------|------------|-------|\n| `light.\u003cpanel\u003e_screen` | brightness + on/off | on = backlight on, off = true backlight-off (no keyguard/PIN); JSON schema, brightness 0–255 |\n| `light.\u003cpanel\u003e_led` | RGB | published only when a LED backend is present (NDK `/dev/ledjni` or the root helper) |\n| `text.\u003cpanel\u003e_navigate` | push a URL to the panel | depends on Companion intent handling; last URL restored on reconnect |\n| `event.\u003cpanel\u003e_button` | hardware button presses | published only when the a11y key-filter is enabled |\n| `number.\u003cpanel\u003e_volume` | TTS/announce volume | 0–100% → `STREAM_MUSIC`; playback is the HTTP `/play` contract below |\n| `sensor.\u003cpanel\u003e_illuminance` | ambient lux | standard `SensorManager` `TYPE_LIGHT`; published only if present |\n| `binary_sensor.\u003cpanel\u003e_proximity` | proximity (occupancy) | standard `SensorManager` `TYPE_PROXIMITY`; published only if present |\n| `button.\u003cpanel\u003e_reload` | reload dashboard | force-stop + relaunch the configured dashboard package (root helper, else `su`) |\n| `button.\u003cpanel\u003e_reboot` | reboot panel | root helper, else `su` |\n| `button.\u003cpanel\u003e_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 |\n| `button.\u003cpanel\u003e_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 |\n\nThe device's display name (`configuration_url` \"Visit\" link, friendly name) and the LED/screen\nstates are re-published on every (re)connect, and the MQTT client auto-reconnects, so HA stays in\nsync after a panel reboot or broker blip.\n\n## HTTP contract\n\n```text\nGET  /              panel info + config page (versions, hardware, status; panel_id,\n                    friendly name, MQTT broker/creds, dashboard package). This is the\n                    device's configuration_url, so HA shows a \"Visit\" link.\nGET  /api           interactive REST API explorer (renders the OpenAPI spec)\nGET  /openapi.json  OpenAPI 3 spec of this API (import into Swagger / Postman)\nPOST /config        form-encoded settings from the page; persists + live-reconfigures\nGET  /config        full config as JSON (MQTT password redacted) for fleet tooling\nGET  /perf          live performance JSON (CPU %/clock, GPU, RAM, temp, top procs,\n                    responsiveness) — polled by the page; sampled only while viewed\nPOST /instrumentation   enabled=true|false — turn the perf sampler on/off\nGET  /proximity     live proximity raw + calibration (raw stays on the panel)\nPOST /proximity/{capture,threshold,sensitivity,reset}   tune the cutoff\nGET  /inspect · POST /inspect/{start,stop}              WebView DevTools relay (:9222)\nGET  /diag          copy-paste diagnostics dump (build, SELinux, su probe, /dev +\n                    /sys node listings, packages, capability assessment)\nGET  /health        -\u003e 200 \"ha-paneld \u003cversion\u003e panel=\u003cid\u003e\"\nPOST /play          body contains an audio URL (raw or {\"url\":\"…\"})\n                    -\u003e 200 \"playing\"  (download + play happen in the background)\n                    -\u003e 400 \"no-url\"   (no URL found in body)\n```\n\nThe web page at `/` is how a user sets the **MQTT broker** without adb — find the panel's IP (mDNS\n`_ha-paneld._tcp`, or the router), open `http://\u003cip\u003e:8888/`, fill in the broker + credentials, Save.\n\nThe agent listens on **:8888**. Self-signed HTTPS sources are accepted (panels live on a trusted\nLAN). This is the same contract as the reference shell receiver it replaces, so HA-side automation\nneeds no change when a panel migrates from the shell receiver to ha-paneld.\n\n## Pairing\n\nThe agent advertises `_ha-paneld._tcp.local.` with TXT records (`ver`, `caps`, `path`). If an MQTT\nbroker is configured it publishes Home Assistant MQTT-discovery configs so panel entities appear\nwithout YAML.\n\n**Zero-config:** with no broker set, ha-paneld browses for Home Assistant's own `_home-assistant._tcp`\nadvert on the LAN and uses its MQTT broker on :1883 — so a fresh install pairs itself. If the broker\nneeds a login (e.g. the HA Mosquitto add-on), set the username/password on the config page; the MQTT\nstatus reads *auth rejected* until they're right. Set the broker explicitly if it's elsewhere or your\nLAN has more than one Home Assistant. With nothing found, the HTTP surface still works standalone.\n\n## Provisioning (no device UI on rooted/userdebug panels)\n\nAll permissions are granted over adb — no per-device tap-through. Run the same script on every panel:\n\n```bash\nscripts/provision.sh \u003cpanel-ip:5555\u003e [--id NAME] [--mqtt tcp://host:1883] [--latest] [--force]\n```\n\nWith no `--apk` and no local build it downloads the **latest signed release** from GitHub (`--latest`\nforces that even when a local build exists). It connects, installs, grants the permissions below,\nstarts the agent, optionally sets the panel id / MQTT, and ends with a self-verify checklist. It is\n**idempotent** — re-run the same command to finish after any interruption — and warns before\nreinstalling the same or an older version (`--force` skips that). `scripts/provision.sh \u003cip\u003e --verify`\nre-checks a panel without changing anything.\n\nNon-root panels: use the in-app setup screen, which fires the standard system permission intents.\n\n**Permission → why:**\n\n| Permission | For | Grant |\n|------------|-----|-------|\n| `POST_NOTIFICATIONS` | foreground-service notification | runtime / `pm grant` |\n| `WRITE_SETTINGS` | screen brightness | `appops set \u003cpkg\u003e WRITE_SETTINGS allow` |\n| Accessibility (key filter) | hardware-button events | `settings put secure enabled_accessibility_services …` |\n\nScreen-off via `lockNow()` additionally needs the **optional device admin** (enable once in Settings,\nor `dpm set-active-admin \u003cpkg\u003e/.control.PanelAdminReceiver`); the daemon/`su` `bl_power` path is\npreferred and needs no admin. Device-admin uses active-admin, not device-owner (device-owner needs an\naccount-free device and would conflict with the logged-in Companion). To uninstall later, remove the\nadmin first — see the signing note below.\n\n## RGB LED — clean-room NDK, no vendor library\n\nOn the rk3576 panel (Electron WF1589T) the front RGB LED is reached via the char device\n`/dev/ledjni`, which is world-rwx and labelled app-accessible (SELinux `device` domain), so a\nnormal app drives it **without root or a helper**. ha-paneld ships its **own** ~70-line NDK driver\n([`app/src/main/cpp/led_jni.c`](app/src/main/cpp/led_jni.c)) doing the ioctls directly. The\nprotocol (request numbers, value range, open flags) was reverse-engineered clean-room from a\nhardware sample — an interop fact, not vendor code — so **no vendor library is bundled or required**\n(`libjnielc.so` is no longer used). The LED entity is published only on panels where `/dev/ledjni`\nis openable; it is simply absent elsewhere.\n\nOther panels (e.g. Tuya TPA10) expose their LED only through root-only `/sys/class/leds/*`. A\nsandboxed app cannot reach those (SELinux `untrusted_app` cannot exec `su` nor write `sysfs_leds`),\nso those panels need a small **root helper daemon** that ha-paneld talks to over a localhost socket.\nThe daemon and its boot-persistent installer live in [`helper/`](helper/) — build and install it with\n[`helper/README.md`](helper/README.md).\n\n## Supported hardware\n\nha-paneld needs no system-signed install. Standard-Android capabilities (brightness, sleep,\nnavigate, TTS) work on any panel; LED/buttons depend on a per-panel HAL.\n\n| Panel class | SoC | Android | ABI | Notes |\n|-------------|-----|---------|-----|-------|\n| Sonoff NSPanelPro / Pro120 | Rockchip PX30 | 8.1 (API 27) | arm64-v8a | toolbox `su` |\n| Tuya TPA10 | Rockchip rk3566 | 11 (API 30) | armeabi-v7a | 32-bit userspace |\n| Electron WF1589T | Rockchip rk3576 | userdebug (`adb root`) | arm64-v8a | RGB LED via clean-room NDK ioctl on `/dev/ledjni` (no vendor lib) |\n\nOther Android panels are welcome — contribute a HAL adapter for your hardware.\n\n**minSdk is 26.** API \u003c 26 is unsupported (the MQTT client cannot connect below API 26).\n\n## Build\n\n### Option A — Docker (no toolchain, no CI access needed)\n\nOnly Docker is required. The script builds a version-pinned image (JDK 17 + Android SDK 35 + NDK +\nCMake, matching CI) and runs Gradle inside it; the APK lands in your working tree.\n\n```sh\n./tools/build/build.sh                       # debug APK -\u003e app/build/outputs/apk/debug/\n./tools/build/build.sh :app:assembleRelease  # any Gradle task(s) instead\n```\n\nThe image is built once and cached; Gradle caches persist in a named Docker volume, so repeat\nbuilds are fast. See [`tools/build/`](tools/build/) (and the `HOST_WORKDIR` note in `build.sh` if\nyou run from inside a container talking to an outer Docker daemon).\n\n### Option B — local toolchain\n\n```sh\n./gradlew :app:assembleDebug      # debug APK -\u003e app/build/outputs/apk/debug/\n./gradlew :app:assembleRelease    # release APK (unsigned in CI unless signing is configured)\n```\n\nRequires **JDK 17** and an Android SDK with **NDK 27.0.12077973 + CMake 3.22.1** (for the native\n`/dev/ledjni` LED driver). The Gradle wrapper pins the Gradle version; nothing else needs installing.\n\n### Toolchain note\n\nThe build is pinned to a conservative AGP 8.7 / Kotlin 2.0 / Gradle 8.10 combo for reliable\nfirst-run CI. Newer AGP/Kotlin is fine to adopt during the v0.x line — versions live in\n[`gradle/libs.versions.toml`](gradle/libs.versions.toml).\n\n### Signing — what forkers need to know\n\nYou don't need to configure signing to build and run ha-paneld. Two cases:\n\n- **Dev / fork builds** are signed with the **committed `debug.keystore`** (password `android`). It's\n  in the repo on purpose — not a secret — so every build (yours, mine, CI's) shares one signature.\n  That's what lets `install -r` update a panel in place without uninstalling. Just build and install.\n- **Official releases** are signed with a private key held in GitHub Actions secrets\n  (`ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`).\n  A fork won't have those, so a tagged release in your fork falls back to a **debug-signed** APK —\n  fine for personal use.\n\n\u003e [!IMPORTANT]\n\u003e Android refuses to update an installed app with an APK signed by a **different** key. So you cannot\n\u003e install your own debug-signed build over an installed *official* (release-signed) build, or vice\n\u003e versa — `adb`/the installer rejects it with a signature mismatch. Uninstall first\n\u003e (`adb uninstall io.github.maxlyth.hapaneld`), then install the other build. Uninstalling clears the\n\u003e panel's saved config, so re-run provisioning afterwards. This is the one thing that trips people up.\n\u003e\n\u003e If you enabled ha-paneld's optional device-admin (used for screen-off via `lockNow()`), the\n\u003e uninstall fails with `DELETE_FAILED_DEVICE_POLICY_MANAGER` — an active device admin can't be\n\u003e removed. Disable it first, then uninstall:\n\u003e\n\u003e ```sh\n\u003e adb shell dpm remove-active-admin io.github.maxlyth.hapaneld/.control.PanelAdminReceiver\n\u003e adb uninstall io.github.maxlyth.hapaneld\n\u003e ```\n\n**Signing your own fork's releases (optional):**\n\n```sh\nkeytool -genkeypair -storetype PKCS12 -keystore release.jks -alias ha-paneld \\\n  -keyalg RSA -keysize 2048 -validity 10000 -dname \"CN=ha-paneld\"\nbase64 -w0 release.jks      # paste the output into the ANDROID_KEYSTORE_BASE64 repo secret\n```\n\nUse one password for both `ANDROID_KEYSTORE_PASSWORD` and `ANDROID_KEY_PASSWORD`, and `ha-paneld`\n(your alias) for `ANDROID_KEY_ALIAS`. Back up `release.jks` and the password safely — losing them\nmeans you can never publish an in-place update again. Never commit the keystore (`*.jks` is gitignored).\n\n## Status \u0026 roadmap\n\n**v0.5.0 (preview)** — validated across the panel fleet: Sonoff NSPanel Pro (PX30, Android 8.1),\nTuya TPA10 (rk3566, Android 11), Electron WF1589T (rk3576, Android 14).\n\nNew in 0.5.0:\n\n- **Zero-config MQTT** — with no broker set, ha-paneld finds Home Assistant on the LAN over mDNS and\n  uses its broker automatically. The config page shows the live connection state and distinguishes\n  *connected*, *reachable but auth rejected* (set credentials) and *unreachable*.\n- **Redesigned web UI** — a responsive masonry config page, an in-app config browser (so kiosk panels\n  with no browser still configure on-device), and a proper launcher info screen.\n- **REST API explorer** at `/api`, plus an OpenAPI spec at `/openapi.json` (import into Swagger/Postman\n  for fleet tooling).\n- **Deeper performance insight** — CPU clock vs hardware max (thermal throttling), a dashboard\n  responsiveness metric, top-5 processes, and a 1-click WebView DevTools relay (no adb). The sampler\n  is page-view gated and can be switched off, so the perf tool isn't itself a constant cost.\n- **Tunable proximity** — calibrate the near/far cutoff from the page; the raw value stays on-device.\n- **Signed releases** and one-command provisioning that fetches the latest release when you have no APK.\n\nCarried from 0.4.x:\n\n- Uniform MQTT control — screen (brightness + **true** backlight-off), RGB LED, navigate, volume,\n  TTS, hardware buttons, reload, reboot, launcher, home (HA).\n- Per-hardware LED HAL — rk3576 clean-room NDK `/dev/ledjni` (app-direct); sysfs panels via the root\n  helper daemon with a boot-persistent `init` service ([`helper/`](helper/)).\n- HA device card, MQTT auto-reconnect + retained-state restore, a `/diag` dump and a **Capabilities**\n  matrix for bug reports. See **[docs/performance.md](docs/performance.md)** for panel performance\n  tuning (the WebSocket-event-volume problem and how to fix it).\n\nPlanned:\n\n- Daemon boot-persistence on su-only (PX30) panels, if true-off is wanted without relying on `su`\n  at runtime.\n- A monochrome (themed-icon) variant and a published documentation site.\n\n## Documentation\n\n- **[docs/performance.md](docs/performance.md)** — panel performance tuning: why dashboards lag on\n  weak panels and how to fix it (the WebSocket-event-volume problem; the split-instance approach).\n- **[helper/README.md](helper/README.md)** — the root LED/control helper daemon for sysfs-LED panels\n  (build + boot-persistent install).\n- **REST API** — browse and try every endpoint at `http://\u003cpanel\u003e:8888/api`; the machine-readable\n  spec is at `/openapi.json`.\n- **`GET /diag`** on a panel — a copy-paste hardware/firmware/capability dump for bug reports.\n\n## Screenshots\n\n| On-panel launcher screen | REST API explorer |\n|---|---|\n| ![ha-paneld launcher screen](docs/img/launcher.png) | ![REST API explorer](docs/img/api-explorer.png) |\n\n## Stack\n\n- **HTTP** — Ktor CIO engine (coroutine I/O, no thread-per-connection).\n- **MQTT** — HiveMQ MQTT 5 client (NIO transport; ABI-agnostic).\n- **mDNS** — JmDNS (chosen over `NsdManager` for reliable TXT records across API levels).\n\n## Acknowledgements\n\nThanks to **Seaky** for **NSPanelTools**, which showed what good panel-side Home Assistant tooling can\ndo — genuinely excellent work. NSPanelTools targets Smatek / NSPanel-class hardware and is closed-source;\nha-paneld exists to be an **open, multi-vendor** alternative that any Android panel can adopt and extend.\nThe two solve overlapping problems for different audiences.\n\n## Licence\n\nApache-2.0. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxlyth%2Fha-paneld","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxlyth%2Fha-paneld","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxlyth%2Fha-paneld/lists"}