fix: #2204 recompute bin countdown at local midnight - #2205
Conversation
The main bin sensor caches its state string ("In N days" / "Tomorrow" /
"Today") and the `days` attribute, recomputing them only when the
coordinator pushes an update. Between coordinator refreshes the countdown
is frozen, so a sensor whose data updates infrequently (or, per #2193, not
at all in the mislabelled manual-refresh mode) shows a stale "In N days"
while the underlying next-collection date remains correct — exactly the
symptom reported in #2204.
Register an `async_track_time_change` callback at local midnight that
recomputes the date-relative state and writes it, so the countdown stays
correct independent of data fetches. Cleaned up automatically via
`async_on_remove`.
Adds tests for the midnight recompute advancing the countdown and for the
tracker being registered on `async_added_to_hass`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe sensor now tracks local midnight changes. Each main bin sensor recalculates its date-relative state and days remaining, then writes the updated state. Tests cover countdown updates and callback registration. ChangesMidnight refresh
Estimated code review effort: 3 (Moderate) | ~15 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
custom_components/uk_bin_collection/tests/test_sensor.py (1)
1541-1551: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winVerify the callback and cleanup registration.
The test verifies only the midnight fields. It does not verify that
async_track_time_changereceivessensor._async_midnight_updateor thatasync_on_removestores the unsubscribe callback. A change that leaves a daily callback active after entity removal will pass this test.Proposed test assertions
- mock_track.assert_called_once() - # Fires at local midnight (00:00:00). - assert mock_track.call_args.kwargs == {"hour": 0, "minute": 0, "second": 0} + mock_track.assert_called_once_with( + hass, + sensor._async_midnight_update, + hour=0, + minute=0, + second=0, + ) + sensor.async_on_remove.assert_called_once_with(mock_track.return_value)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@custom_components/uk_bin_collection/tests/test_sensor.py` around lines 1541 - 1551, Extend the test around async_added_to_hass to assert that async_track_time_change receives sensor._async_midnight_update as its callback and that the unsubscribe function it returns is registered through sensor.async_on_remove. Preserve the existing midnight argument assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@custom_components/uk_bin_collection/tests/test_sensor.py`:
- Around line 1541-1551: Extend the test around async_added_to_hass to assert
that async_track_time_change receives sensor._async_midnight_update as its
callback and that the unsubscribe function it returns is registered through
sensor.async_on_remove. Preserve the existing midnight argument assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 19736fbc-8656-46a0-bcc0-01b7a9403d47
📒 Files selected for processing (2)
custom_components/uk_bin_collection/sensor.pycustom_components/uk_bin_collection/tests/test_sensor.py
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2205 +/- ##
==========================================
+ Coverage 80.04% 83.30% +3.26%
==========================================
Files 12 12
Lines 1393 1402 +9
==========================================
+ Hits 1115 1168 +53
+ Misses 278 234 -44 ☔ View full report in Codecov by Harness. |
Summary
The main bin sensor's state (
"In N days"/"Tomorrow"/"Today") and itsdaysattribute are cached and only recomputed insideupdate_state(), which runs at init and on_handle_coordinator_update(). The entity doesn't poll and had no time-based recompute, so the countdown is frozen between coordinator refreshes — it only advances when new data is fetched.This surfaced in #2204: the "Next Collection Date" attribute (read live from the stored date) looked correct while the main sensor's "in two days" text was wrong. That's the exact fingerprint of a frozen relative countdown — the absolute date is still a valid future date, but the days-until value is stale.
It's most visible alongside #2193 (where automatic polling was disabled entirely by the
manual_refresh_onlyflip), but it's a real gap on its own: even with a healthy 12-hour refresh, the countdown would only update on coordinator ticks rather than at the day boundary, so it could read a day off.Change
Register an
async_track_time_changecallback at local midnight (00:00:00) inasync_added_to_hass. It recomputes the date-relative state and writes it, keeping the countdown correct independent of data fetches. Registered throughasync_on_removeso it's torn down with the entity.Only the date-relative fields are recomputed from already-fetched coordinator data — this does not trigger any council scraping at midnight.
Tests
test_bin_sensor_midnight_recompute_advances_countdown— "In 5 days" → after a day passes with no new data, the midnight recompute yields "In 4 days" (anddays == 4).test_bin_sensor_registers_midnight_tracker—async_added_to_hassschedules the tracker athour=0, minute=0, second=0.All 69 sensor tests pass locally; black clean.
Related
sensor.py.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests