Use state-aware icons for Wear OS shortcut tiles#6618
Conversation
There was a problem hiding this comment.
It seems you haven't yet signed a CLA. Please do so here.
Once you do that we will be able to review and accept this pull request.
Thanks!
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
Pull request overview
Updates Wear OS shortcut tiles to use state-aware icons (via full entity state + Entity.getIcon()), improving visual accuracy (e.g., locked vs unlocked) while preserving the existing domain-based fallback behavior.
Changes:
- Fetch full entity state for shortcut entries when building tile resources and resolve icons via
Entity.getIcon() - Keep domain-based fallback icon resolution when entity fetch fails
- Add a Wear changelog entry describing the user-visible change
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| wear/src/main/kotlin/io/homeassistant/companion/android/tiles/ShortcutsTile.kt | Concurrently fetches full entity state and uses state-aware icon resolution for tile resources |
| app/src/main/res/xml/changelog_master.xml | Documents the Wear shortcut tile icon behavior change |
|
Has the situation described in #2045 get resolved now? Last I recall we could not reliably add this because its very easy for the state to get out of sync. |
3bdbd68 to
9189b98
Compare
Shortcut tiles previously used the same icon regardless of entity state. This switches to using Entity.getIcon(), which returns state-specific icons for locks, fans, switches, covers, and more — matching the behavior already used in complications and other tiles. Full entities are fetched in parallel to minimize latency, with a graceful fallback to the original domain-only icon if a fetch fails. Custom icons set in Home Assistant are still respected.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Rethrow CancellationException instead of swallowing it in catch block - Add Timber.w logging for failed entity fetches - Wrap entity fetch in serverManager.isRegistered() check Co-Authored-By: Claude Opus 4.6 <[email protected]>
…t tiles Include entity state in the tile resource version string so Wear OS invalidates cached icon bitmaps when entity state changes. Previously the version only reflected which entities were configured, causing stale icons to persist indefinitely. Also add a loading indicator (progress clock icon) when an entity is tapped. The tile polls for state changes at 1s and 4s after tap, clearing the loading state once the entity state has changed or after a 5s timeout. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace REST-based entity fetching during tile rendering with a WebSocket subscription to entity state changes. Tile rendering now reads from an in-memory cache populated by the subscription, which eliminates network calls in the tile request path and avoids the Wear OS renderer timeout. The subscription is started in onTileEnterEvent and cancelled in onTileLeaveEvent. Loading state is tracked per-entity in a set encoded into the resource version string, so it survives service recreation. A 45s safety timeout clears stale loading entries. Co-Authored-By: Claude Opus 4.7 <[email protected]>
f4582c4 to
fc885e0
Compare
The loading icon layered on top of WebSocket state updates introduced several failure modes (stuck loading state, stale subscriptions) for marginal UX value. WebSocket state updates are fast enough on their own to give visual feedback without a separate loading state. Also: always restart the WebSocket subscription on tile enter to handle cases where the entity list changed after the previous entry. Co-Authored-By: Claude Opus 4.7 <[email protected]>
onTileRequest and onTileResourcesRequest are independent framework calls; the live cache can mutate between them, producing a layout that references a resource ID the bundle never produced. Freeze a snapshot in onTileRequest keyed by the resources version and reuse it in onTileResourcesRequest so the two handlers are always in agreement. requestUpdate() does not reliably trigger a re-render while a tile is visible, so WebSocket-driven state changes often went unseen until the next user interaction — making every tap appear one step behind reality. Flip the cached state optimistically on click (for known toggle domains) so the display snaps to the expected post-tap state immediately. Drop the onTileEnterEvent batch getEntities() call (which fetched every entity in the instance) in favor of bounded parallel getEntity() fetches from onTileRequest on cold cache, capped by a 2s timeout so slow networks fall back to domain-default icons rather than blowing the tile timeout. Add unit tests covering snapshot immutability, resource-id invariants, LRU stash eviction, optimistic-click prediction, and the full request/resources race. Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
@dshokouhi have a look at how I did it and tell me what you think. It was very difficult to get the UI in sync with the actual state and this was the best I could do. Realistically I've found that the state renders as indeterminate when first visiting the tile, but switches to correct state after first and subsequent taps, and after leaving and revisiting the tile. I don't think it's perfect by any means, but given the testing I did on my devices and network, I'd use it. |
Summary
Shortcut tiles on Wear OS use domain-based fallback icons that don't reflect entity state — a lock always shows the same padlock icon, a switch always shows the same toggle. This change renders state-aware icons (e.g., locked vs unlocked padlock, powered vs unpowered outlet) and keeps them in sync with Home Assistant in real time.
Before: All lock entities show the same generic padlock icon.
After: Locked entities show a closed padlock, unlocked entities show an open padlock (and similar state-aware icons for other domains like covers, fans, switches).
How it works
IntegrationRepository.getEntityUpdates(entityIds)drives a shared in-memory cache. No REST polling; nogetEntities()batch over the full entity list.getEntity()calls bounded by a 2s timeout. If the network is slow the tile still renders on time using domain-default icons, and the cache fills in on the next render.onTileRequestfreezes the cache into aTileSnapshotkeyed by the resources version;onTileResourcesRequestlooks the snapshot up again and generates bitmaps from the same frozen view. Resource IDs include the state (switch.foo@onvsswitch.foo@off) so the Wear bitmap cache can't serve a stale image.requestUpdate()doesn't reliably trigger a re-render while a tile is visible.onTileEnterEvent, stops inonTileLeaveEventandonTileRemoveEvent. State survives service recreation viacompanion object.Checklist
Tests
Pure-logic tests in
ShortcutsTileStateTestcover:onTileRequestandonTileResourcesRequestmust not break the resource-ID invariantI did extensive manual testing loading this onto my actual watch and updating my actual entities. This was the only strategy that worked well. The UI very difficult to get right.
Screenshots
Fallback icons (no entity state):
State-aware icons (locked vs unlocked):
Notes
Entity.getIcon()(already the app-wide pattern) for cached entities and falls back togetIcon(icon, domain, context)when the cache is cold."on","off","locked","unlocked","open","closed") match existing uses throughoutEntity.ktand the wear module — these are part of Home Assistant's public API.