Repository navigation
Releases: dckiller51/astrion-custom-dashboard
Release list
v1.3.0
[1.3.0] - 2026-10-17
Everything since 1.2.0, from the 1.2.1 to 1.2.9 betas.
⚠️ Good to know when updating from 1.2.0
- Settings: a long press on the Menu key opens Settings when no hotkey of yours does (swiping down opens Activities).
- New remotes start with a minimal one-page dashboard instead of the original author's layout. Existing dashboards are not touched.
- No "Disconnected" banner without Home Assistant: a remote used only with Harmony, IR or Apple TV has no banner any more.
tv_remotecard: withoutapps, it now shows Netflix, YouTube, Prime Video and Plex; withoutmedia_entity, the app buttons usemedia_player.<same name as remote_entity>. Set both if yours differ.- Select card follows your theme colours.
Added
Dashboard & builder
- Version history of dashboard.json. The remote keeps every save from the builder or its web page (the last 30, plus pinned ones) in
/sdcard/astrion/history, with an optional note. Changes made outside the web page are kept too before the next save. From the builder's 🕘 button: open a version in the editor (nothing changes until you save), restore it on the remote, download it, pin it, add a note, delete it. The device page links to it ("Version history"). New routes:GET /history,GET /history/<id>,POST /history/{restore,pin,note,delete};POST /dashboard.jsontakes anote. - Undo / redo in the builder: ↶ ↷ above the preview, or Ctrl+Z / Ctrl+Y. Rapid changes (typing a colour, dragging a slider) count as one step.
- The builder and the device page speak French, Italian and Spanish, following the browser's language — built on Rob Johnston's (@ClemsonJeeper) i18n work. The app itself is also translated in Spanish (thanks @enigmat14) and gets a language setting independent of Android's.
- A form for every card type: monitor, speaker group, row (its cards edited with the normal card form), picture elements (click the floor plan to place a button), TV remote (apps, volume target, command renames). Card types are grouped (Common, Media, Grids, Remotes, Home & layout). Options written by hand are kept when a card is edited.
- Deleting a card asks first.
Activities
- Harmony-style Activities. With device profiles imported from the Harmony archive (power type, delays, repeats, input sequences), composed Activities start, switch and stop like a Harmony hub: shared devices stay on, inputs change only when needed.
- IR repeats sent the way the original remote does (Harmony
pressMinRepeats), locally and through IR extenders. - Device and Activity state remembered across restarts and updates (on/off, current input, running Activity per room). Help in Active Activities corrects what Astrion believes, then Fix replays the Activity.
- "Off" for a room:
"stopActivity": "<room>"(or"*") on tiles and hotkeys. - Pages that follow an Activity:
"hideMode": "full"hides a page completely while its Activity is off. - Activity wizard: network Apple TVs (wake, sleep, app to launch) and a Home Assistant entity picker.
Remote & dock
- Dock screen: battery screen, dim night clock (time, date, weather, now playing) or the dashboard. "Now playing" can follow the Activity of the remote's own room (
dock.room,nowPlayingEntity). - Standby & battery (
powerblock): Home Assistant goes quiet a while after the screen turns off (only listed entities stay live), motion-wake limited in time, screen timeout held by the app. - Settings reorganized into groups (Display, Connections, Web configuration, Sound & touch, Language, About), with Reload dashboard, Restart the app and Test Find my remote.
- Find my remote can be stopped from the remote ("Found me!").
- Logs without adb:
http://<remote>:8080/logs.
Home Assistant & several remotes
- Each remote has an identity (stable id + name,
GET /info, rename from Settings or the web page), sent with every webhook push. - Battery pushed to Home Assistant through the webhook.
- Hotkey action "Home Assistant event" (
astrion_button, with the key, page and remote). - Works with the Astrion Custom Dashboard integration 2026.10.0 (devices, Activities, page select, ring, events).
Cards
- Long press on
button_grid/scene_gridtiles (long_pressblock). apple_tv_remote: compact and full layouts, app shortcuts, long press on the trackpad (context menu, ±10 s), fast-forward / rewind, seeking — and a builder preview matching the chosen layout.- Hotkeys and tiles can open a page as a popup (
"pageMode": "popup"). - Monitor: vertical layout and scrolling long values.
- Select: "dropdown" layout and text size (#93 by @dpdonovan-cyber).
- TV remote: volume row (to the TV or a soundbar's
remote). - Switch and picture elements: PNG icons; picture elements
on_color. - Web page: delete icons and IR database files (with a warning when still used).
Changed
- Settings always reachable (long press Menu, see above) and minimal starter dashboard for new remotes.
- Connection banner: none without Home Assistant; on a new remote it shows the remote's web address; with Home Assistant unreachable it also gives that address. Tap it to reconnect.
- Home Assistant connection, lighter: updates published as they arrive, one heartbeat, reconnects follow the network.
- Beta updates are offered only when newer than the installed app, under their real name.
- Hotkey lists show keys as named on the remote ("Menu (MAIN)").
- The IR sniffer's "Send to my remote" merges models into the IR database instead of replacing the file.
- Select card follows the theme; TV remote card defaults (see above).
Fixed
- Apple TV: the remote-control session no longer drops every 30 s (tvOS AirPlay 2); title and artwork no longer flicker on live streams.
- IR: devices imported from the Harmony archive work in Activities; codes captured with old IR sniffer versions are repaired on load.
- The screen no longer wakes by itself on a still remote; D-pad focus outline no longer stays visible on bound arrow keys.
- A Harmony Activity tile no longer stays outlined after turning it off (single hub).
- Builder: importing a dashboard keeps popup pages'
openMode. - The downloaded update APK no longer stays in the app cache.
- Cards: switch
#RRGGBBcolours, source select icon, speaker group labels, clock & weather 24 h calendar event, climate without fan modes, fansteplayout.
Internal
- Android Gradle Plugin 8.13.2, Gradle 9.5.1.
v1.2.0
[1.2.0] - 2026-10-07
Added
- Apple TV: direct control over the network — no Home Assistant, no Harmony hub. A new native client (
appletv/package) speaks Apple's Companion protocol: mDNS discovery (_companion-link._tcpthroughNsdManager), HAP PIN pairing (SRP-3072 → Ed25519 long-term keys), pair-verify, and a ChaCha20-Poly1305-encrypted OPACK session. It gives remote-control keys (D-pad, Select, Menu, Home, Play/Pause, volume, Siri), wake/sleep, playback commands, the installed-app list and app launching. The Companion port is dynamic, so each connect re-resolves the device by its mDNS service name (falling back to the last known address, which is updated and saved when it changes), and a supervisor reconnects with back-off. Pairing produces credentials in the sameltpk:ltsk:atv_id:client_idformat pyatv/Home Assistant use.- Each paired Apple TV is exposed as a Home-Assistant-shaped
media_playerentity (media_player.appletv_<name>, generated once and kept stable across renames) via a new local-entity overlay inHaClient(setLocalEntity, merged intoentities, plus alocalServiceHandlerhook incallService). Its attributes mirror what HA's Apple TV integration gives:source_list(app names),supported_features,volume_level, and aplaying/paused/idle/off/unavailablestate derived from the controls the Apple TV currently offers. Because of that, every existingmedia_player,source_select, button, hotkey and picker works with it unchanged, with or without Home Assistant configured — includingselect_sourceto launch an app by name. apple_tv_remotegained anappleTvoption (a paired Apple TV's entity id): commands go straight to it via a newastrion_appletv.send_commandservice.deviceId/hub(Harmony) still work exactly as before;appleTvwins when both are present. The play/pause button uses a real toggle instead of tracking state locally.- Devices page: the placeholder "Apple TV — Coming soon" tile is now real: scan the network, enter the PIN shown on the TV, rename, remove. New
ConfigServerendpoints/appletv-scan,/appletv-pair-start,/appletv-pair-finish, and/devices-confignow includesappleTvs./save-connectiononly rewrites the Apple TV list when the form carriesatv_present=1, so older clients can't wipe paired TVs. Builder: theapple_tv_remoteform offers "Control via → Apple TV (direct) / Harmony hub" once at least one Apple TV is paired. - Lint/style cleanup for the above: split
AppleTvDevice's command/service dispatch into small
lookup tables (and moved the stateless remote-button mapping into a newAppleTvCommands.kt),
brokeCompanionAuth's pair-verify/pair-setup into single-purpose steps, splitCompanionLink's
frame read/write paths, and movedConfigServer'sappleTvsJSON block into its own function —
all to stay under this project's ktlint/detekt limits (line length, cyclomatic complexity, nested
blocks, throw count, parameter count).AppleTvRegistrynow persists an Apple TV's updated
IP/port itself (it already has theContextit needs) instead ofMainActivitydoing it, which
also keepsMainActivity's own footprint for this feature to eight lines. - Known limits (Companion protocol): the currently playing title/artist/artwork and the foreground app are not available over Companion (they need the separate AirPlay/MRP channel), so those attributes are absent rather than guessed. Adds the
org.bouncycastle:bcprov-jdk18ondependency (X25519/Ed25519/ChaCha20-Poly1305 aren't in the Android platform until API 33, andminSdkis 26).
- Each paired Apple TV is exposed as a Home-Assistant-shaped
scene_gridtiles can now be highlighted off an arbitrary HA entity's state, not just the Activity-tracking already there. The existing Activity-based border-highlight (a tile with"activity"or"track"+"room") stays exactly as it was, and is now joined by a second, independent, combinable mechanism: the same"state_entity"/"state_value"/"active_color"/"active_border"fields already onbutton_grid(see its own class doc) — useful for ascene_gridtile that isn't an Activity at all, e.g. a plain navigation tile to a "Lights" page lit up while any light in the room is on. Unlike the Activity mechanism (border only), this one also changes the tile's background via"active_color", same asbutton_grid. Web builder (cards.js): thescene_griditem form gained the same State entity/Target state/Active color/Accent border fields already onbutton_grid's form.- Media player: when there's no artwork at all (not just an external-URL loading failure — see the Fixed entry above), the full-card placeholder now shows the active app's name (
app_name) under the cast icon. Previously a bare, unlabeled cast icon regardless of which app was active. No change to the compact/tile header's small circular avatar (too small to fit a label). This doesn't retrieve an actual per-app icon: neither Home Assistant's Apple TV integration nor its docs appear to expose one (onlyapp_id/app_nameas plain text), soapp_nameis the most specific thing there currently is to show. button_gridbuttons couldn't close a popup after their action —"closePopup": truewas silently ignored. An oversight in the Harmony/IR support added just above:fire()never read"closePopup"at all (and the web builder'sbutton_gridform didn't even show the checkbox), even though it's exactly as applicable to abutton_gridbutton as to ascene_gridone — arguably more so, sincebutton_gridis the natural fit for a TV/Projector source-picker popup.fire()now checks"closePopup"last, after every other action on the button, same asscene_grid's own. Web builder: thebutton_griditem form gained the same "Close the open popup after this button's action" checkbox already onscene_grid's form.- Web builder: a page opened as a popup only by a tile (not
linkedPage/openWhenEntity) can now have its own popup size/position, in the builder and on JSON import. The page dialog's popup width/height/position row, andnormalizePageFromJson's JSON-import path, only ever showed/kept those fields when the page itself hadlinkedPageMode: "popup"oropenMode: "popup"— neither of which a page that's purely opened via a tile's own"pageMode": "popup"(added above) has, since that trigger lives on a tile on some other page, invisible to this page's own dialog. Editing such a page in the builder silently ignored any popup size already in the JSON, and re-saving it from the builder would drop it outright. Added a new standalone checkbox, "Opened as a popup from a tile elsewhere on the dashboard", next to the existing "Linked page"/"Auto-open" popup-mode pickers — checking it reveals and persists the same width/height/position fields independent of the other two triggers.normalizePageFromJson(the paste-box and device-load import path) now keepspopupWidth/popupHeight/popupPositionwhenever present on a page, rather than only whenlinkedPage+linkedPageMode: "popup"were also both set, and the dialog now pre-checks the new checkbox itself when it detects that case on open (saved geometry present with neither other trigger set to popup). button_grid: a button can now fire a direct Harmony command/Activity or a local IR command, not just an HA service call. Previouslybutton_gridwas hard-wired to a plainservicecall (seeButtonGridCard.kt's old doc: "each firing a HA service call") whilescene_gridhad the richer action set — an inconsistency there was no real reason for.fire()now fires every action a button carries independently:service(unchanged, now optional), a direct Harmony device command (harmonyDevice+harmonyCommand, optionallyhub), a direct Harmony Activity start (activityId, optionallyhub), and/or a local IR command (irDevice+irCommand, no hub needed) — same fields, same behavior asscene_grid's own Harmony/IR options, just withoutscene_grid's page/composed-Activity/tracking options, which don't apply to a plain action button. A button can combine more than one (e.g. call an HA service and send a direct IR command in the same tap). Web builder (cards.js): thebutton_griditem form gained the same Harmony action picker and IR device/command fields already used onscene_grid, reusing the existinggiHarmonyMode/giIrDevicewidgets rather than duplicating them; a button now needs at least one of Service/IR/Harmony set, validated on save.- Open a page as a popup from a normal tile, and optionally close it again after an action. Until now the floating popup overlay (
LinkedPagePopup) could only be triggered by alinkedPageswipe-up or byopenWhenEntity+openMode: "popup"— ascene_grid/titletile's own"page"always did a full pager navigation.CardContextgainedopenPagePopup(pageName)andclosePopup(), built inDashboard()from the exact samelinkedPopupPage/popupOpenedByEntityIdslot the other two triggers already share (so only one popup is ever on screen, however it was opened), reusing the target page's ownpopupWidthFraction/popupHeightFraction/popupPosition— same asopenWhenEntity. New per-tile options:"pageMode": "popup"next toscene_grid's existing"page"(and"${prefix}_pageMode"forTitleCard'stitle_page/subtitle_page) opens it as a popup instead of navigating;"closePopup": true(and"${prefix}_closePopup") dismisses whichever popup is currently open, fired after every other action on that tile — e.g. a small TV/Projector popup where tapping one source tile sends the IR/Harmony command and closes the popup in the same tap.closePopupis independent ofpage/pageModeon the same tile, so a tile elsewhere can open the popup while a different tile inside it closes it. Web builder (cards.js): the gri...
v1.1.0
[1.1.0] - 2026-09-06
Added
- Devices catalog (
/), rebuilt as a proper "add a device" hub instead of a connection form. Opening the remote's own IP now lands on a hero + four category tiles (Home Assistant, Harmony, IR Devices, Apple TV — the last a placeholder for direct control, not built yet, present so cards will be ready to reference it once it is), each with a live device count. Tapping a tile drills into that category's own list + "+ Add device" flow, Home-Assistant-integrations style, instead of the old single long scrolling form.ConfigServer.kt's previousserveForm()(one large server-rendered HTML string) is replaced by two static single-page apps served fromassets/docs/:devices.html/js/devices-page.js(this new page, root/) and the existingindex.html/js/*.js(the dashboard/card builder,/builder/) — both now share the same static-asset serving path (serveDocsAsset()), so any file dropped intoassets/docs/(styles, scripts, images) is reachable from either page without a route added per file. - Home Assistant device catalog. Cards that reference an HA entity (Light, Switch, Cover, Climate, Media Player, Camera, Fan, Vacuum, Weather, Select) no longer take a free-typed
entity_id— pick a domain and a live entity on the Devices page, give it a name (pre-filled from HA's ownfriendly_name, editable), and it shows up by that name in the matching card's picker in/builder/. NewhaDevicesarray indashboard.json(parallel toirDevices), newGET /devices-configendpoint (full HA connection + Harmony hub read-back, used to pre-fill the edit forms —GET /harmony-hubsstays id+name-only, unchanged, still used by the card/hotkey pickers). Editing an existing card that references an entity not in the catalog (an older dashboard, or one hand-edited via raw JSON) doesn't silently blank the field — an extra "(not in catalog)" option is injected so the real value round-trips even though it can't be changed without adding it to the catalog first. Cache-Control: no-cache, no-store, must-revalidateon everydocs/asset response (serveDocsAsset()) — these ship inside the APK and change on every app update; a stale cached.jssilently paired with a fresh.html(or vice versa) produces exactly the kind of confusing, hard-to-reproduce bug this is meant to rule out.- Charging screen. While the remote is genuinely sitting on its charging dock, the dashboard is replaced with a big battery percentage and a pulsing charging glyph — same idea as a phone's own charging screen. Auto-dims (window brightness down to near-zero) after 15s with no touch, undims instantly on the next one; fully restored the moment it's picked up or unplugged.
- Docked vs. plugged into a computer for development is a real distinction here, and a non-obvious one. Both report identically through
BatteryManager— confirmed on real hardware, this device's dock has two power-only charging pins, no USB data lines at all, and Android still classifies that as generic "AC" charging just like a PC's USB port would. What actually differs is whether a USB data connection exists:UsbManager's own connection state (mConnected/mConfiguredindumpsys usb) readsfalseon the dock andtrueover a real cable. That state isn't exposed through the publicUsbManagerSDK constants (ACTION_USB_STATE/USB_CONNECTEDexist in AOSP and fire at runtime, but aren't in the public stub jar) — used as raw string literals instead, which is legitimate since both are just string values handed to fully public APIs (IntentFilter.addAction,Intent.getBooleanExtra), not a reach into a hidden class member. - New
ChargeDockMonitor.kt: owns the detection (aBroadcastReceiveronACTION_POWER_CONNECTED/_DISCONNECTED+ the USB state action) and the dim timer, as its own class rather than inline inMainActivity.kt— which was starting to accumulate too many unrelated concerns in one place (detekt'sLargeClassagreed).MainActivity.ktnow just owns one instance and delegates:start()/stop()from its own lifecycle methods,onUserInteraction()(the standard Android hook that fires on any touch/key regardless of which view or Compose node consumed it — no separate touch listener needed) forwarded straight through. - New
ChargingScreen.kt, themed via the sameProvideTheme/LocalThemesystem every other screen uses.rememberBatteryState()andbatteryGlyph()— both previously private toTopstatusbar.kt— are now shared between the status bar's compact battery indicator and this screen's much larger one, rather than duplicated. - 2 new strings (
charging_screen_charging/charging_screen_full) invalues//values-fr/.
- Docked vs. plugged into a computer for development is a real distinction here, and a non-obvious one. Both report identically through
- IR devices can now reference the external ir-database picker instead of embedding pre-resolved codes. Previously every
dashboard.jsonhad to carry a fully-resolved{freq, pattern}table for every IR command, baked in once by the web builder at build time from either a hand-pasted Pronto code or a bundledir-database/catalog. That catalog (docs/ir-database/and itssrc/main/assets/docs/ir-database/mirror) is removed in this release — instead, pick the devices you actually own on the picker, copy the generated JSON files onto the remote's/sdcard/astrion/ir-database/, and reference them from the web builder ("2. IR Devices" → "Reference the ir-database", category + brand + model). The app resolves Pronto →{freq, pattern}itself, on-device, the first time a command is used, and caches the result — a database update or a newly-added device model no longer means re-touching or re-flashing every dashboard.json built against it, and it's no longer blocked by Logitech's own Harmony device catalog being unmaintained (most recent hardware was never going to be in there in the first place). Hand-pasting a single Pronto code for a one-off button is still supported side by side, unchanged — new devices choose either mode per device, not globally.AppConfig.kt: newIrDeviceSourcesealed class (Inline— the old embedded-commands shape,SdCardRef— acategory/brand/modelpointer).IrDeviceConfigcarries asourceinstead of always embeddingcommandsdirectly;DashboardLoader.kt's parser and encoder round-trip both shapes, so every existingdashboard.jsonwith inline-resolved commands keeps working exactly as before.- New
IrDatabaseRuntime.kt: a Kotlin port of the web builder'sprontoToPattern()(verified value-for-value against the original on real captured codes), plus the/sdcard/astrion/ir-database/<category>.jsonloader/resolver, with a two-level cache (parsed file per category, resolved step per command) so repeat button presses skip both the disk read and the Pronto math. Dashboard.kt(tile taps) andMainActivity.kt(hardware hotkeys) both now resolve through this same runtime instead of each readingIrDeviceConfig.commandsdirectly — one sharedIrDatabaseRuntime.resolve()/resolveFrom(), not parallel copies of the same lookup.- Web builder (
docs/andsrc/main/assets/docs/, identical patch to both): the "Add IR device" form now offers the two modes above instead of the old category→brand→model→command cascade fetched from the now-removed bundled catalog.giIrCommand(scene_grid tiles) and the Activity wizard's power/input/volume command fields are now free-text inputs with<datalist>suggestions instead of<select>s, since a referenced device's real command list only exists on the phone at runtime, not in the builder session.
- New "IR devices" panel on the local config page (
/), next to Icons. A link out to the picker, plus a same-shape-as-Icons upload form for its own curated<category>.jsonfiles straight from the device — for when you'd rather not hand-copy them onto/sdcard/astrion/ir-database/yourself. NewPOST /ir-databaseroute inConfigServer.kt: validates the upload actually has"category"+"brands"before writing anything (a half-written or garbled file here would otherwise silently break every IR button sourced from it, not fail loudly once), then callsIrDatabaseRuntime.invalidate()— picked up on the very next command send, no restart needed. The same route also answers the picker's own "Send to my remote" button directly over the LAN (Accept: application/jsonswitches the response from the page's usual redirect-with-flash-message to a plain JSON body); whether that button actually reaches the device depends on the calling browser's mixed-content policy, since the picker is HTTPS (GitHub Pages) and this server is plain HTTP — when it's blocked, downloading and copying the files by hand (or this new on-device upload form) works regardless. 5 new strings acrossvalues//values-fr/for the panel's heading, description, link, button, and note. - The web builder can now read straight off this remote's own
/sdcard/astrion/ir-database/when opened via/builder/. NewGET /ir-database(which category files are actually present) andGET /ir-database/<category>.json(raw contents) — same-origin, so none of the picker's mixed-content caveats apply here. The "Reference the ir-database" device form picks this up automatically: a category → brand → model quick-pick appears above the manual fields whenever this remote actually has matching files already, filling in category/brand/model and every real command id from that file — no more typing "known command ids" by hand for anything already copied over. The manual fields stay right below it, unchanged, for anything not on this remote yet. - Fixed: ir-database category filenames are matched case-insensitively (
TV.jsonnow resolves the same astv.json). Android's filesystem is case-sensitive, and a strict match here failed silently — not a crash, just every IR button in that category quietly...
Dev build 1.3.0
chore(release): 1.3.0...
v1.0.0
[1.0.0] - 2026-08-22
Added
- scene_grid:
icon_fillmode. New optionalicon_fill: trueon a scene_grid card's options scales each tile's icon to fill the tile's full height (ContentScale.FillHeight) instead of the default fitted size (ContentScale.Fit) — useful for icon-only grids ("show_labels": false). New checkbox in the web editor's scene_grid card form, with a live preview. (#22 by @ClemsonJeeper) - Live HA entity data in the dashboard editor preview. Card previews in the web editor (
/builder/) now render with real state, names, and attributes fetched from Home Assistant via a new/ha-statesendpoint, instead of always showing the static example data — falls back to the mocks automatically when there's no HA connection or the editor is opened outside the device (e.g. GitHub Pages). Entity ID fields also get autocomplete against the live entity list. (#13 by @ClemsonJeeper) - Getting Started guide. New
GETTING_STARTED.md— first-time setup walkthrough for a Sanytron Astrion HA100, from enabling ADB through connecting Home Assistant and loading the firstdashboard.json, plus troubleshooting and update instructions. Linked from the README. (#18 by @dskudrin) - VacuumCard: configurable room-clean service. Room buttons used to always fire the Roborock/Xiaomi-style
vacuum.send_command/app_segment_clean— silent no-op on integrations that expect their own service instead, e.g. Dreame'sdreame_vacuum.vacuum_clean_segment. New optionaloptions.room_clean_action: { domain, service, parameter }lets a card call anydomain.servicewith the room/segment id written into a named field instead —entity_idstill goes through the sametargetmechanism every other card's service call already uses. Leaving it unset keeps today's exact default call, so every existing dashboard.json is unaffected. New optional fields in the web editor's vacuum card form. - Hardware button → open Settings or Active Activities. A hotkey can now be assigned
openOverlay: "settings"oropenOverlay: "activities"instead of a page/service/Harmony action — same two overlays already reachable by swiping down from the top bar or up from the page dots, now also bindable to a physical remote button. New "Open overlay" action in the web editor's hotkey dialog. - Hardware button → return to the current Activity, per room. A hotkey can now be assigned
openCurrentActivityRoom: "<room>"to jump straight to the page of whichever tracked Activity is currently active in that room — the direct, no-picker counterpart toopenOverlay: "activities", for a remote/page already dedicated to one room. Resolved live viaActivityRuntime.activeActivity(room); does nothing if that room has nothing active. New "Return to current Activity" action in the web editor's hotkey dialog. - Configurable "return to parent" button. Child pages (
PageConfig.parent) no longer hardcode the hardware BACK key for jumping back to their parent — a new optionalparentKeyfield picks any physical button instead (defaults to"BACK", so existing configs are unaffected). If the page also binds its own hotkey on that button, the hotkey wins and the parent-jump is skipped, same precedence as before. New picker in the web editor's page dialog, shown once a parent page is selected. - Camera card. New
cameracard type for displaying a Home Assistantcamera.*feed, in two modes: live stream (MJPEG, with automatic fallback to still snapshots if the stream stalls) or snapshot-only (still frame refreshed at a configurable interval). Aspect ratio and fit (cover/contain) are configurable. Tapping the card opens a fullscreen viewer with pinch-to-zoom (1x–8x) and pan, without interrupting the live stream. (#8 by @ClemsonJeeper) - Configurable theme colors. The app's color palette (12 semantic tokens: backgrounds, text, icons, accents, state colors) is now configurable via
dashboard.json, with a new "Theme & Colors" section in the web editor to adjust it visually (swatch + hex field + per-token reset) without touching the JSON. An empty theme block reproduces the original palette. Existing color fields (SwitchCard.on_color, per-scene color inSceneGridCard) now use the same color picker. (#11 by @ClemsonJeeper) - Drag-to-reorder page tabs in the web editor, alongside the existing card reordering. Editor-only, no native app changes. (#10 by @ClemsonJeeper)
- Toast notifications. The "Save to device" confirmation in the editor now uses a non-blocking, auto-dismissing toast instead of a modal
alert(), matching the editor's other inline feedback (icon upload, Harmony discovery, etc.). (#9 by @ClemsonJeeper) - Fan card "step" style. New
"style": "step"option for fan cards, for fans that report discrete speed levels rather than a continuous percentage: a single row showing the current speed name/percentage with-/+buttons that call Home Assistant'sfan.increase_speed/fan.decrease_speedservices directly, instead of computing and sending a percentage viafan.set_percentagelike thesimple/fullstyles do. No power button or name row in this layout — just the stepper. Builder support in the fan card's style dropdown and a matching live preview. Existingsimple/fullstyles and their own percentage-step config (options.step) are unaffected. (#14 by @ClemsonJeeper)
Internal
- Migrated all hardcoded
Color(0x...)literals in Compose UI to the new theme system (ThemeColors/LocalTheme), across every card and screen (dashboard, settings, status bar, overlays). HaClient: new dedicated HTTP client with no timeout for long-lived MJPEG streaming, plus a raw-bytes fetch method.ConfigServer: newGET /camera-snapshotendpoint proxying a single HA camera frame to the web editor (the browser has no HA token of its own).ConfigServer: newGET /batteryendpoint exposing the wall tablet's own battery level and charging state as JSON ({ level, charging }), for the companion Home Assistant integration.HaClient.onResult(): no longer assumes every WebSocketresultpayload is an array — service calls (e.g. the fan step buttons'increase_speed/decrease_speed) can returnresult: null/{}, which is now handled safely instead of throwing during response parsing.
Fixed
- Theme migration: several card tiles lost their button/icon contrast.
CoverCard,LightCard, andMediaPlayerCardpainted their outer tile background withtheme.controlBackground— the same token their round buttons and icon backgrounds also use — so those buttons visually disappeared into the tile (no more "circle" around the cover up/down icons, the main icon, the light card's cycle button, or the media player's off-state avatar). Tile backgrounds now usetheme.cardSurfaceinstead, restoring the contrast the pre-theme hardcoded colors had (matching the convention already used byFanCard/ClimateCardand byMediaPlayerCard's own detail dialog). - Activity wizard: adding a Harmony device silently failed. In the "Devices" step,
renderWizardHarmonyAddFields()rendered the Hub/Device pickers straight into the same container as the "+ Add Harmony device" button — but that picker's own render call replaces its container's entire content, wiping out the button (and the placeholder selects the code then tried, and failed, to swap in) the moment a Harmony hub was reachable. Net effect: picking a hub and device had no button to add them with, so the wizard always reported "Add at least one device." on Next. Now renders the picker into its own sub-container and reuses the same shared Hub→Device wiring as the hotkey and scene-item forms. (#16 reported by @bilbon0bud) - Weather forecast bar (Clock & Weather card) rendered as a flat color instead of a two-tone gradient — restored using
theme.successat two alpha levels. - Compact Fan card layout used the same background color for on/off — on now uses
theme.successat low alpha, matching the pattern already used elsewhere in the same card and inClimateCard. - Connection banner no longer distinguished "connecting" from "disconnected" (both used
controlBackground) — connecting now usestheme.accentSecondary, keepingdangerfor the failure states andcontrolBackgroundfor disconnected.
uninstall
Astrion Custom - Uninstallation Tool
This package contains the automated tool to cleanly remove the Astrion Custom Dashboard from your Sanytron Astrion HA100 remote control and restore the stock environment.
What's Included
uninstall.bat: Automated script to completely remove the application via ADB.
How to Use
- Prepare your remote:
- Make sure USB Debugging is enabled in your remote's Developer Options.
- Connect your device:
- Plug your remote into your Windows PC using a data USB cable.
- Accept the USB debugging security prompt on your screen if requested.
- Run the uninstaller:
- Download and extract this
.ziparchive into a folder on your computer. - Double-click on
uninstall.batand follow the on-screen instructions.
- Download and extract this
Once completed, the remote will automatically fall back to the stock HaRemote launcher.
initial-installation-v0.9.0
Initial APK Installer (Bootstrap)
This package is designed for the initial installation of the application on Android devices using ADB (Android Debug Bridge).
Note: Once this initial version is installed on your device, all future updates will be handled automatically by the application itself. You will not need to use this tool again.
Prerequisites (Android Device)
Before running the installer, make sure your phone is properly configured:
- Enable Developer Options: Go to Settings > About phone, and tap on Build number 7 times.
- Enable USB Debugging: Go to Settings > Developer options and toggle on USB Debugging.
- Connect & Authorize: Plug your phone into your PC via a data USB cable and allow the debugging prompt that appears on your phone screen.
How to Use
- Extract the contents of this
.ziparchive into a folder on your computer. - Make sure your Android device is plugged in and recognized.
- Double-click on
install.bat. - Follow the on-screen instructions. The script will automatically detect your device and install the application.
v0.9.0
[0.9.0] - 2026-08-19
Added
ConfigServer: newGET /activities,GET /activities/active,POST /activities/start, andPOST /activities/stoproutes, exposingActivityRuntime's per-room Activity state (previously only readable on-device, via the Active Activities overlay from 0.8.0) to an external controller — built for the companion Home Assistant integration's newsensor.active_activity_<room>andselect.activity_<room>entities, but usable by anything that can speak plain HTTP./activitieslists every trackable Activity (composed ortrack: true) as{id, name, room, icon};/activities/activereports which one (if any) is active per room;/activities/startstarts one by id;/activities/stopstops whichever is active in a given room.ActivityRuntime.clear(room)— the counterpart tomarkActive(), explicitly marking a room as having nothing active. Only meant to be called once the room's Activity has actually been stopped (its own poweroff devices sent, or a Harmony hub's own"-1"/PowerOff confirmation arriving throughbind()), never speculatively ahead of the stop action itself.Dashboard.kt:stopActivity(room)— the missing counterpart toswitchActivity()/startActivity(). For a composed Activity, sends each device's ownpowerOffCommand(same asswitchActivity()'s outgoing-diff branch, just with nothing incoming) and clears the room. For a Harmony-backed Activity, sends PowerOff to that Activity's own hub only — Harmony has no per-Activity stop command, a hub always runs exactly one Activity at a time, so this is the narrowest possible "stop" and, unlike the existing PowerOff hotkey, never touches a different room sharing the same hub. This closes a real gap from 0.8.0: the Active Activities overlay could show what was running per room but had no way to end just one of them, short of a blanket "turn everything off" hotkey.- Active Activities overlay: each row now has a dedicated "Stop" button (
R.string.stop_activity— "Arrêter" in French) wired to the newstopActivity(), so a running Activity can be ended directly from the list without hunting for its poweroff button elsewhere. Dashboard()gainedonActivityRuntimeReady/onStartActivityReady/onStopActivityReadycallback parameters, invoked once perActivityRuntimeinstance (i.e. once perdashboard.jsonload, same lifetimeremember(config) { ActivityRuntime(config) }already had).MainActivitystores what they hand up and feeds it toConfigServeras plain lambdas — the same hoisting patternonSetPagealready used for the page pager, needed here becauseActivityRuntime/switchActivity/startActivityare created inside Compose (closing overactivitiesById,harmonyRegistry,client) rather than inMainActivityitself.- Hierarchical pages:
PageConfiggained an optionalparent(another page's name), making it a child page in a navigation tree instead of a flat top-level one. Entering a child is unchanged — any card's ownnavigateToPage, e.g. ascene_gridtile with"page": "Apple TV"on its parent's page, exactly like flat navigation always worked. Leaving is new: the hardware BACK key, previously a no-op unless a page-specific hotkey bound it (MainActivity.dispatchKeyEventonly falls through toonBackPressed()once every hotkey handler for that key comes back empty), now jumps to the current page's parent when nothing else claims it first — so a page with its own BACK hotkey (an Apple TV's Back/Menu, say) is completely unaffected. A dashboard with noparentset anywhere behaves identically to before this existed. PageIndicatorreworked around siblings instead of the whole flat page list: the dots now represent only the current page's siblings (every page sharing the sameparent, root pages included), windowed to at most 5 with a small "…" on a clipped edge — the fix for large dashboards' dot row growing to take up a quarter of the screen height. A child page also shows a‹ ParentNamechevron on the left (44dp min tap target), the on-screen twin of the hardware BACK key's new behavior. Swiping itself is unaffected by any of this — still one continuous pager over the full flatpageslist in file order, same as always.- Builder (
docs/js/pages.js,index.html): the page dialog gained a "Parent page" dropdown, offering every page except the one being edited and any of its own descendants (pagesUnavailableAsParentOf()) — hierarchies can't be built into a cycle from the UI. Renaming a page that others point to as their parent cascades the new name to those children automatically; deleting a page with children asks for confirmation and promotes them to top-level rather than deleting or orphaning them to a name that no longer exists.docs/js/preview.js's tab bar prefixes a child page's label with↳so the hierarchy is visible in the tab strip itself, not just inside each page's own settings. ConfigServer: newGET /versionroute reporting{version, versionCode}— built for the companion Home Assistant integration's newupdate.app_updateentity, which compares it against the latest GitHub release.- New
SelectCard("type": "select") for anyinput_select.*orselect.*entity — an adaptation of Home Assistant's Mushroom select card, the same wayCoverCardadapts Mushroom's cover card: icon in a colored shape, a name/state column, and a dedicated control area (the option picker itself) below/beside/stacked perlayout(default/horizontal/vertical, same 3 variants asCoverCard), instead of the whole tile being one flat clickable dropdown. Reads the entity'soptionsattribute and currentstate, calls<domain>.select_optionon pick — the domain (input_selectorselect) is read straight fromentity_id, so one card type covers both without extra config. Answers a GitHub feature request for exactly this (e.g. picking between "TV"/"Projector" for a room's video output) — deliberately a separate renderer fromSourceSelectCardrather than a shared/branching one, since that card is specific to media_player'ssource_list/sourceattributes andmedia_player.select_sourceservice, none of which exist on an input_select/select entity. - Builder (
docs/js/cards.js,index.html,docs/js/preview.js,docs/js/mocks.js): added the "Select / dropdown (select)" card type to the picker, its own form (entity, name, icon color, layout), and a full live preview (newSELECT_MOCK, aninput_select.video_output_living_roomwith "TV"/"Projector" options — the same example from the feature request) matchingSelectCard.kt's actual rendering, reusing the same.pc-icon/.pc-namestate/.pc-slider-shaped CSS the cover/light previews already established rather than introducing new classes.
Fixed
MainActivity.DEBUG_KEYSwas lefttrue, showing an "Unmapped key: N" Toast on screen every time an unbound remote button was pressed. Nowfalse; unmapped key codes are still logged viaLog.i(KEY_TAG, ...)foradb logcatwhen actually debugging a new remote.
v0.8.0
[0.8.0] - 2026-08-16
Added
- AV Activities: any
scene_gridtile or hotkey can now be marked"track": truewith a"room", turning it into a tracked Activity ("Watch Apple TV", "Listen to Music"...) — no separate config section, the tile's existingentity_id/activityId+hub/harmonyDevice+harmonyCommandis still what actually runs;track/roomjust add bookkeeping. At most one Activity is active per room at a time; different rooms are fully independent. - Composed Activities (
ActivityConfig/ActivityDeviceConfig,AppConfig.activities): for setups where more than one device needs orchestrating — the case a Harmony hub normally handles internally — Astrion can now do it itself, mixing IR/Harmony/HA devices freely within the same Activity. On switching Activities within a room, a device used by both the outgoing and incoming Activity is left alone (no needless power cycle, just a possible input change); a device only in the outgoing one gets powered off (switchActivity()inDashboard.kt). Referenced from ascene_gridtile's new"activity"field; always implicitly tracked. - IR Devices (
IrDeviceConfig,AppConfig.irDevices— replaces the oldIrActivityConfig/irActivitiesmacro list): a registry of named commands per physical device ("power","hdmi1","volume_up"...), resolved once at build time from either a communityir-database/entry or a hand-pasted Pronto Hex code. This is the fully-offline baseline for AV control — no Harmony hub, no Home Assistant, no cloud dependency, works even if every external service involved disappears. ActivityRuntime: scans every page/hotkey fortrack: trueitems, and every entry inAppConfig.activities, once perdashboard.jsonload; tracks which Activity is active per room (activeByRoom), updated viatrackTap()(right afterSceneGridCardfires a single-action tile),markActiveById()(after a composed Activity finishes its start sequence), andbind()(mirrors a Harmony hub's live Activity state into the matching room, so an Activity started from elsewhere — another remote, an HA automation, the physical Harmony remote — shows up too).HarmonyHubClient:currentActivityId/activityState(StateFlow), populated from the hub's unsolicitedconnect.stateDigest?notifypush frames (activityId+activityStatus, distinguishing "starting" from "running" — confirmed against several real hub captures) and from a new one-shotgetCurrentActivity()request/response call made right after connecting, since the push only fires on the next change.HarmonyHubRegistry.clientsByLocalId— exposes every configured hub's client, not just the default one (client()), for callers likeActivityRuntime.bind()that need to wire up all of them.- New "Active Activities" overlay: swipe up from the bottom edge (page-indicator dots) — the mirror gesture of the existing swipe-down-for-Settings — lists every currently active Activity grouped by room; tapping one jumps straight to its page. Reached only by this gesture, like Settings, so it never appears in the page pager or its dots.
- Builder: new "IR Devices" section — the community
ir-database/cascade picker (category → brand → model → command) now feeds a per-device command registry instead of a one-shot macro; a "Import all commands from this model" button pulls every command from a picked model in one go (ids already present are left alone, so re-importing after a manual rename doesn't clobber it); a manual Pronto Hex paste path covers devices/buttons not in the database; each saved command can be renamed inline (✎, no browserprompt(), consistent with the rest of the builder). - Builder: new "Activities" section — a page-by-page wizard for composed Activities, mirroring Logitech Harmony's own Activity setup flow rather than one long form: pick a type (just prefills a name) → name/room/icon/page → select devices (local IR, Harmony hub+device, or a bare HA entity id — mixable) → one screen per selected device for its power-on/power-off/input commands, on/off-cycle toggles, and a delay → which device handles volume → review (flags any device with no commands set — it would silently do nothing at runtime) → save. Editing an existing Activity jumps straight to the name/room screen. A device already added can be edited in place, not just removed and re-added.
- Volume routing for a composed Activity: after picking a volume device, a further screen picks its volume-up/volume-down/mute commands (skipped for an HA device — it always uses the fixed
media_player.volume_up/volume_down/volume_muteservices). On save, if the Activity has apage, these are written as page-scoped hotkeys on it (PageConfig.hotkeys, which already override global bindings while that page is on screen — no new runtime mechanism needed). If that page already has a hotkey onVOLUME_UP/VOLUME_DOWN/MUTE, the builder confirms before overwriting rather than silently taking priority over an existing binding. HotkeyConfiggainedirDevice/irCommand(send a local IR command directly from a physical-button binding, same as ascene_gridtile already could) anddevices(a bookkeeping hint — see next point).TrackedActivity/HotkeyConfig/scene_grid items gained an optionaldeviceslist: which physical devices an Activity is known to involve, independent of whether it's composed or a lightweighttrack-only tile.switchActivity()inDashboard.ktnow reads this from whatever Activity was previously active in a room — Harmony-backed or composed — instead of only from a composedActivityConfig's own device list. This closes a real gap: switching from a Harmony Activity to a composed one had no way to know a device shared between them (e.g. a soundbar with only aPowerTogglecommand, no discrete on/off) was already on, and could send an unwanted toggle that turned it off instead of confirming it stayed on.- Builder (
docs/js/cards.js): ascene_gridtile with "Track as Activity" checked can now list which physical devices it involves (comma-separated ids) — mainly useful on a Harmony-backed tile, since Astrion has no other way to know which devices a Harmony Activity touches.scene_gridtiles also gained an IR device+command picker and a "Composed Activity" picker referencing the new Activities section. MainActivity.kt:runHotkey()now dispatchesirDevice+irCommandbindings directly through the device's own IR blaster (ConsumerIrManager) — the same local IR pathscene_grid/composed Activities already had, now available on physical-button bindings too. Closes the loop for a composed Activity's volume routing when the volume device is local IR.- New
titlecard type — a section header for grouping cards on a page (title + optional subtitle, independently alignable), ported from Home Assistant's Mushroom Title card. Where Mushroom uses its owntitle_tap_action/subtitle_tap_action(a full HAActionConfig— call-service, navigate, more-info, url, assist...), this instead reuses the sametitle_/subtitle_-prefixed action fieldsscene_gridalready has, so it also covers Harmony/IR/composed-Activity actions with nothing new to learn. Also supports an optionalicon(a PNG file path, sameBitmapFactory.decodeFileconvention every other icon in the app uses — deliberately not a bundled/named vector set) and adividerline filling the rest of the row, Bubble-Card-style; either forces the title row left-aligned regardless ofalignment, since that layout has no sensible centered/right-aligned form. An optionalcolor(same ARGB hex convention asscene_grid's) tints the title text and, whendivideris on, the line at reduced alpha to match — the subtitle and icon are untouched by it, icons are never tinted anywhere in the app. Builder: new "Title" entry under "Common" with a title/subtitle/alignment/icon/divider/color form; a tappable title needs the "Other / custom type…" raw-JSON editor instead, since re-saving through the simple form — like every card type here — rewrites the card's options from scratch.
Changed
- Builder: editing an existing Harmony-backed tile (
scene_gridorapple_tv_remote) that has no explicithubno longer silently defaults the Hub dropdown to the first configured hub — with more than one hub, that default could be the wrong one and would get silently re-saved as correct. The dropdown now shows empty and the existing "pick a hub" validation blocks saving until one is chosen explicitly. - Builder: ids generated from a name (IR devices, Activities) now strip accents via Unicode NFD normalization instead of mangling them —
"Xbox Série X"becomes"xbox_serie_x", not the previous"xbox_s_rie_x". SettingsOverlay/ActivitiesOverlay: the swipe-to-close gesture now lives on a dedicated strip (bottom for Settings, top for Activities — mirrored) instead of on the same node asverticalScroll, which meant the scroll consumer ate every vertical drag before the close gesture ever fired. Incorporates theSettingsOverlayfix from PR #3 (see Fixed, below), applied toActivitiesOverlayas well.
Fixed
- Builder (
docs/js/cards.js):fillGiHarmonySection()was defined twice (dead duplicate); removed. - Builder:
dashboard.json'sirDevices/activitieswere silently dropped every time the builder reloaded a config — bothpages.js's "paste JSON" import anddevice.js's auto-reload-from-device path had a field whitelist that pre-dated those two sections. - Builder: the Activities section's IR-device picker didn't refresh after saving or removing an IR device (only the
scene_gridform's picker did, via a different refresh path). Dashboard.kt:SettingsOverlaycalledSettingsMenu(ctx, onClose = onClose), butSettingsMenuonly ever declaredctx— a pre-existing compile error.- `HaMod...
v0.7.0
[0.7.0] - 2026-08-14
Added
MediaPlayerCard: full Mushroom-style rewrite of thecompactlayout — round album-art/icon avatar, name + state row, and a control row below that switches between transport buttons (previous/play-pause/next, plus optional power/shuffle/repeat) and volume controls (mute, -/+ buttons, or a slider) via a small swap button, matching Home Assistant's own Mushroom media-player card behavior. Tap the tile to toggle play/pause; long-press opens the newMediaPlayerDetailDialog(see below).MediaPlayerCard: newMediaPlayerDetailDialog, opened by long-pressing the compact tile (same gestureLightCard/CoverCarduse) — large artwork, a live progress bar, the full transport row, a volume slider with mute, and a power toggle when the entity supports it.MediaPlayerCard:variantconfig option ("compact"(default) /"full") replaces the old fixed layout; thefullvariant keeps the dedicated-media-page look (large art,top_buttons) but adds a live progress bar (computed frommedia_position+media_position_updated_at, advancing every second while playing) and the same feature-gated transport/volume rows as the compact tile.MediaPlayerCard:use_media_info(default true — showsmedia_title/media_artist/app_nameinstead of the entity's friendly name/state),show_volume_level(appends "⸱ N%" to the state line),media_controls(comma list:on_off,shuffle,previous,play_pause,next,repeat), andvolume_controls(comma list:mute,buttons,set) config options — every button is checked live against the entity'ssupported_featuresbitmask, so a control requested in config but not supported by the entity never renders.MdiIcons:Play,Pause,SkipPrevious,SkipNext,VolumeHigh,VolumeOff,Repeat,Shuffle,Cast,CastOffglyphs, replacing the generic Material icons the old media player card used.Cast/CastOffalso become the compact tile's (and full page's) fallback avatar — device on/off — whenever the entity reports noentity_picture, instead of guessing a play/pause icon.- New
media_state_playing/paused/idle/buffering/on/offstring resources (English and French), used on the state line whenever the entity reports nomedia_title/media_artist/app_name. - Dashboard builder (
docs/index.html):media_playercards now have their own dedicated form (previously only the raw-JSON fallback) — variant, name, entity,use_media_info/show_volume_levelcheckboxes, per-control checkboxes formedia_controls/volume_controls, and atop_buttonsJSON field (full variant only) — plus a real preview matching the on-device look, including the swap-button behavior and shuffle/repeat's tinted-not-swapped active state. CoverCard/LightCard: new optional controls, matching Home Assistant's Mushroom cards. Covers can show open/stop/close buttons, a position slider, and a tilt slider (any combination); lights can show a brightness slider, a colour temperature slider, and colour swatches. If more than one is turned on for a card, a small button lets you flip between them. All configurable from the dashboard builder, with a matching preview. Existing dashboards aren't affected unless you turn these on.- Dashboard builder: the remote preview now lights up the physical buttons that already have a hotkey assigned to them (on the current page or globally), so it's easy to see at a glance what's already mapped.
Changed
MediaPlayerCard: the compact layout's control row now offers mute and full transport (previous/play/pause/next, optionally shuffle/repeat/power) — the previous compact layout only ever showed a fixed volume up/down pair with no way to control playback.- Shuffle and repeat only have one glyph each (no separate "active" variant) —
MediaPlayerCard/MediaPlayerDetailDialogtint the button instead of swapping its icon when shuffle is on or repeat isn't"off". - Dashboard builder: the remote preview is now a proper redrawing of the physical remote instead of a photo, and much closer to true scale — icon/text sizes, how many cards fit on screen before scrolling, and button positions all now match the real device.
Fixed
CoverCard/LightCard: the new sliders' fill bar was centering and shrinking from both sides instead of filling left to right.- Dashboard builder: the zoomed-in preview rendered everything about 9% smaller than the real device.
- Dashboard builder: some elements in the remote preview (screen, buttons) could end up slightly out of position depending on your browser.
- Dashboard builder: two of the physical media buttons could render slightly past the edge of the remote.
Removed
remote.png— the static photo the dashboard builder used to overlay the preview onto, now unused: the remote preview is a fully HTML/CSS recreation of the physical device (see Changed above).