Skip to content

Cancel in-flight cover/climate action before starting a new one - #28

Merged
adborbas merged 2 commits into
mainfrom
fix/cover-stop
Jul 16, 2026
Merged

Cancel in-flight cover/climate action before starting a new one#28
adborbas merged 2 commits into
mainfrom
fix/cover-stop

Conversation

@adborbas

Copy link
Copy Markdown
Owner

Changes

  • CoverCardViewModel: cancel the outstanding actionTask before each new action (setPosition, open, close, stop, toggle).
  • ClimateCardViewModel: apply the same cancel-before-reassign to all action methods (togglePower, setHVACMode, setTemperature, setTemperatureRange, setFanMode, setSwingMode, setPresetMode, setHumidity) — it had the identical latent race.
  • Tests: added cancellation coverage for cover stop/close/setPosition/toggle (via iconTapped) and a climate cancellation test, each using a hand-written slow spy that mirrors the demo controller's post-sleep guard !Task.isCancelled.

Why

Tapping Stop (or the card icon) while a cover was opening/closing appeared to stop it, but ~2s later it snapped fully open/closed anyway. Each action assigned actionTask = Task { ... } without cancelling the previous task, so the still-running demo open/close bypassed its guard !Task.isCancelled and overwrote the state the newer action had just set. ClimateCardViewModel shared the exact same pattern and demo guard, so the same fix is applied there.

Production behavior is unchanged: HAServiceCaller.callService is continuation-based and not cancellation-aware, so cancel() only sets the flag that the demo controllers check — it never aborts a real WebSocket call.

Notes

  • All HemeraTests pass (372 tests). The cancellation tests complete in milliseconds, confirming the in-flight 2s demo task is cancelled and bails at its guard rather than running to completion.

adborbas and others added 2 commits July 16, 2026 22:39
Tapping Stop (or the icon) while a cover is opening/closing appeared to stop
it, but ~2s later it snapped fully open/closed anyway. The VM assigned
actionTask = Task { ... } for each action without cancelling the previous task,
so the still-running demo open/close bypassed its `guard !Task.isCancelled`
guard and overwrote the state Stop had just set.

Cancel the outstanding actionTask before starting each new action
(open/close/stop/setPosition/toggle) so the in-flight demo travel bails.

Add a Swift Testing case with a slow spy CoverControlling that mirrors the
demo guard, verifying stop during open cancels the open so it never completes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The same fire-and-forget `actionTask = Task { ... }` pattern in
ClimateCardViewModel replaced the task reference without cancelling the prior
task, so a still-running demo action (post-sleep `guard !Task.isCancelled`)
could overwrite the state a newer action just applied — the same defect just
fixed for Cover. Cancel the outstanding actionTask before starting each new
climate action.

Also extend cover cancellation coverage to close(), setPosition(), and
toggle() (via iconTapped), which the original fix changed but left untested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adborbas
adborbas merged commit 5bab8f3 into main Jul 16, 2026
@adborbas
adborbas deleted the fix/cover-stop branch July 16, 2026 20:39
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