Skip to content

Use state-aware icons for Wear OS shortcut tiles#6618

Open
kdavh wants to merge 7 commits into
home-assistant:mainfrom
kdavh:state-aware-shortcut-tile-icons
Open

Use state-aware icons for Wear OS shortcut tiles#6618
kdavh wants to merge 7 commits into
home-assistant:mainfrom
kdavh:state-aware-shortcut-tile-icons

Conversation

@kdavh
Copy link
Copy Markdown

@kdavh kdavh commented Mar 23, 2026

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

  • Real-time state via WebSocket: while the tile is visible, IntegrationRepository.getEntityUpdates(entityIds) drives a shared in-memory cache. No REST polling; no getEntities() batch over the full entity list.
  • Cold-start warming: on first render, any shortcut entity not yet in the cache is fetched via parallel 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.
  • Snapshot-per-render: onTileRequest freezes the cache into a TileSnapshot keyed by the resources version; onTileResourcesRequest looks the snapshot up again and generates bitmaps from the same frozen view. Resource IDs include the state (switch.foo@on vs switch.foo@off) so the Wear bitmap cache can't serve a stale image.
  • Optimistic click: on tap, the cached state is flipped to the predicted post-tap value (for known toggle domains: switch, light, lock, cover, input_boolean, fan) before the layout is built. The WebSocket subscription reconciles shortly after. This avoids a framework quirk where requestUpdate() doesn't reliably trigger a re-render while a tile is visible.
  • Lifecycle: subscription starts in onTileEnterEvent, stops in onTileLeaveEvent and onTileRemoveEvent. State survives service recreation via companion object.

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

Tests

Pure-logic tests in ShortcutsTileStateTest cover:

  • Resource-ID invariants (state suffixing, null/empty handling, mismatch detection)
  • Snapshot immutability against later cache mutation
  • LRU stash eviction and replacement
  • The runtime race: cache mutation between onTileRequest and onTileResourcesRequest must not break the resource-ID invariant
  • Optimistic click prediction per domain, plus safe no-op on unknown domains and unrecognized states

I 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):

tile_screenshot

State-aware icons (locked vs unlocked):

tile_screenshot_stateaware

Notes

  • Icon resolution uses Entity.getIcon() (already the app-wide pattern) for cached entities and falls back to getIcon(icon, domain, context) when the cache is cold.
  • Hardcoded state strings ("on", "off", "locked", "unlocked", "open", "closed") match existing uses throughout Entity.kt and the wear module — these are part of Home Assistant's public API.
  • Unknown domains and unrecognized states are a silent no-op on the optimistic path: the tap still fires the broadcast, the display stays on the current state, and the subscription updates it when the server confirms.

Copilot AI review requested due to automatic review settings March 23, 2026 14:55
Copy link
Copy Markdown

@home-assistant home-assistant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @khart-twilio

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!

@home-assistant home-assistant Bot marked this pull request as draft March 23, 2026 14:55
@home-assistant
Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread wear/src/main/kotlin/io/homeassistant/companion/android/tiles/ShortcutsTile.kt Outdated
Comment thread wear/src/main/kotlin/io/homeassistant/companion/android/tiles/ShortcutsTile.kt Outdated
@dshokouhi
Copy link
Copy Markdown
Member

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.

@kdavh kdavh marked this pull request as ready for review March 23, 2026 15:01
@kdavh kdavh marked this pull request as draft March 23, 2026 15:06
@kdavh kdavh force-pushed the state-aware-shortcut-tile-icons branch from 3bdbd68 to 9189b98 Compare March 23, 2026 17:52
Copy link
Copy Markdown

@home-assistant home-assistant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kdavh

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!

kdavh and others added 5 commits May 11, 2026 15:34
  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.
- 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]>
@kdavh kdavh force-pushed the state-aware-shortcut-tile-icons branch from f4582c4 to fc885e0 Compare May 11, 2026 19:43
kdavh and others added 2 commits May 11, 2026 17:28
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]>
@kdavh kdavh marked this pull request as ready for review May 12, 2026 01:29
@home-assistant home-assistant Bot dismissed their stale review May 12, 2026 01:29

Stale

@kdavh
Copy link
Copy Markdown
Author

kdavh commented May 12, 2026

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants