Skip to content

Fix on/off-only light overlay; add hasOverlay flag; harden VM cache - #23

Merged
adborbas merged 4 commits into
mainfrom
fix/light-onoff-overlay
Jul 16, 2026
Merged

Fix on/off-only light overlay; add hasOverlay flag; harden VM cache#23
adborbas merged 4 commits into
mainfrom
fix/light-onoff-overlay

Conversation

@adborbas

Copy link
Copy Markdown
Owner

Changes

  • On/off-only lights (supported_color_modes == ["onoff"]) no longer get a non-functional brightness slider or fill:
    • LightCardViewModel.supportedModes returns [] for onoff-only lights.
    • New isDimmable gates the medium-tile CardFillOverlay.
    • New hasOverlay (!supportedModes.isEmpty) makes makeOverlayView return nil, so tapping the card body presents no dead panel — the icon toggle remains the control.
  • hasOverlay capability flag added to EntityCardViewModel (default false). handleCardTap now branches on it instead of building an overlay view just to nil-check it. Overridden to true in Cover/Switch/Climate/Automation VMs (every VM whose makeOverlayView returns non-nil); Light derives it from supportedModes.
  • ViewModelFactory stale-VM guard: makeViewModel(forEntityId:) re-confirms the backing @Model still exists before returning a cached VM. The probe is domain-targeted (via the id's <domain>.<object> prefix) and allocation-free — one cheap fetch against the owning registration rather than a fan-out that built a throwaway VM per domain.

Why

  • HA's "onoff" color mode means the light has no brightness; the old panel showed a 0–255 slider that did nothing and the medium tile drew an always-empty fill.
  • The old handleCardTap constructed and discarded an overlay AnyView on every card tap solely to test != nil.
  • The cache probe originally ran on every cached lookup on a hot render path (tiles(from:), tileView(for:)), fanning out across all registrations — up to 8 SwiftData fetches plus an allocation per lookup. It is now one targeted fetch.

Notes

  • The cache guard uses a lookup-time existence check (Option B) rather than eviction-on-delete (Option A): entities are never individually deleted mid-session (missing ones are marked unavailable; full deletion only happens in wipeLocalData, which discards the whole factory), so there is no per-entity deletion chokepoint to hook.
  • For on/off-only lights the card body tap is intentionally inert (no overlay); the icon toggle is the sole control, matching the documented design.
  • Tests: supportedModes/isDimmable/hasOverlay for lights, hasOverlay for Switch/Automation (true) and Scene (false), and ViewModelFactory cache eviction / same-instance / domain-targeting. Full HemeraTests suite green (381 tests).

adborbas and others added 4 commits July 16, 2026 22:41
On/off-only lights (supported_color_modes == ["onoff"]) have no brightness or
color controls, so:
- LightCardViewModel.supportedModes returns [] for onoff-only lights
- new isDimmable gates the medium-tile brightness fill in LightCard
- new hasOverlay (== !supportedModes.isEmpty) makes makeOverlayView return nil,
  so the card body presents no dead brightness panel — icon toggle remains

Add EntityCardViewModel.hasOverlay capability flag (default false) so
handleCardTap branches on it instead of building an overlay view just to
nil-check it. Override to true in Cover/Switch/Climate/Automation VMs
(every VM whose makeOverlayView returns non-nil); Light drives it from
supportedModes.

Guard ViewModelFactory.makeViewModel(forEntityId:) with a domain-agnostic
existence check so a cached VM whose backing @model was deleted mid-session is
evicted rather than returned. Entities are never individually deleted during
sync (missing ones are marked unavailable; full deletion only happens in
wipeLocalData, which discards the whole factory), so there is no per-entity
deletion chokepoint to hook — hence the lookup-time guard (Option B) over an
unused evict method (Option A).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the project comment-style convention for the comments added in the
previous commit; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
makeViewModel(forEntityId:) re-validates a cached VM before returning it, to
avoid handing back a VM whose backing @model was deleted. The probe ran on
every cached lookup — a hot path (CuratedHomeViewModel.tiles(from:),
CuratedHomeView.tileView(for:), AreaDetailViewModel per-entity) re-run on each
render — and fanned out across all registrations, building a throwaway VM per
domain (up to 8 SwiftData fetches + one allocation per lookup).

Add a cheap `entityExists` closure and a `domain` to ViewModelFactory
.Registration, plus a domain index on the factory. The probe now derives the
domain from the entity id ("<domain>.<object>") and consults only the owning
registration — one fetch, no allocation. Correctness is unchanged: deleted id
evicts and returns nil; live id returns the same cached instance.

Add a domain-targeting test (a spy registration whose probe must never be
called for a light lookup) alongside the existing eviction / same-instance
tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adborbas
adborbas force-pushed the fix/light-onoff-overlay branch from ede770b to bec944b Compare July 16, 2026 20:42
@adborbas
adborbas merged commit a3972eb into main Jul 16, 2026
@adborbas
adborbas deleted the fix/light-onoff-overlay branch July 16, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant