️ Skip redundant energy fetches and emit statistics at real timestamps #18

Merged
mat merged 1 commit from fix/5-energy-hour-offset into main 2026-07-13 14:27:43 +00:00
Owner

Summary

Closes MateoGreil/homeassistant-comwatt#3, MateoGreil/homeassistant-comwatt#5, MateoGreil/homeassistant-comwatt#42.

Two related fixes to ComwattEnergySensor bundled because they live in the same code path:

Skip the call when the bucket can't have changed (#3)

The QUANTITY/HOUR endpoint only publishes a new bucket once per hour, so polling every 2 min was 29 wasted calls per hour per device. The coordinator now tracks the last successful fetch time per device and skips the call while the cached result is < 55 min old. Power fetches are unchanged.

Emit statistics at the real bucket timestamp (#5, #42)

Previously the energy value was published under now even though the API bucket represents the previous hour, so the Energy dashboard attributed each reading to the wrong hour. The coordinator now forwards each new hourly bucket to async_add_external_statistics with the real bucket timestamp and a running cumulative sum, under an external statistic id comwatt:<device_id>_total_energy.

  • Existing sensor.<device>_total_energy entity unchanged — users whose Energy dashboard points at it keep working.
  • Parallel statistic comwatt:*_total_energy attributes energy to the correct hour; users can opt into it by adding it to the dashboard.

Guarded on "recorder" in hass.config.components so the coordinator stays usable when recorder is disabled.

Test plan

  • New test_energy_fetch_is_skipped_within_interval — second refresh within the window does not re-call QUANTITY; advancing the cached timestamp triggers the call again.
  • New test_new_energy_buckets_are_pushed_as_external_statistics — two new buckets produce one async_add_external_statistics call with the correct statistic id, cumulative sum, unit, and top-of-hour timestamp.
  • pytest tests/ — 26 passed; ruff + mypy clean.
  • Hands-on: after a few hours on your HA, open Settings → Dashboards → Energy → add a new energy source and check that comwatt:<device>_total_energy appears in the statistic picker. Compare the hour alignment to the existing sensor.*_total_energy entity.
## Summary Closes [MateoGreil/homeassistant-comwatt#3](https://github.com/MateoGreil/homeassistant-comwatt/issues/3), [MateoGreil/homeassistant-comwatt#5](https://github.com/MateoGreil/homeassistant-comwatt/issues/5), [MateoGreil/homeassistant-comwatt#42](https://github.com/MateoGreil/homeassistant-comwatt/issues/42). Two related fixes to `ComwattEnergySensor` bundled because they live in the same code path: ### Skip the call when the bucket can't have changed (#3) The QUANTITY/HOUR endpoint only publishes a new bucket once per hour, so polling every 2 min was 29 wasted calls per hour per device. The coordinator now tracks the last successful fetch time per device and skips the call while the cached result is < 55 min old. Power fetches are unchanged. ### Emit statistics at the real bucket timestamp (#5, #42) Previously the energy value was published under `now` even though the API bucket represents the previous hour, so the Energy dashboard attributed each reading to the wrong hour. The coordinator now forwards each new hourly bucket to `async_add_external_statistics` with the real bucket timestamp and a running cumulative sum, under an external statistic id `comwatt:<device_id>_total_energy`. - Existing `sensor.<device>_total_energy` entity **unchanged** — users whose Energy dashboard points at it keep working. - Parallel statistic `comwatt:*_total_energy` attributes energy to the correct hour; users can opt into it by adding it to the dashboard. Guarded on `"recorder" in hass.config.components` so the coordinator stays usable when recorder is disabled. ## Test plan - [x] New `test_energy_fetch_is_skipped_within_interval` — second refresh within the window does not re-call QUANTITY; advancing the cached timestamp triggers the call again. - [x] New `test_new_energy_buckets_are_pushed_as_external_statistics` — two new buckets produce one `async_add_external_statistics` call with the correct statistic id, cumulative sum, unit, and top-of-hour timestamp. - [x] `pytest tests/` — 26 passed; `ruff` + `mypy` clean. - [ ] Hands-on: after a few hours on your HA, open Settings → Dashboards → Energy → add a new energy source and check that `comwatt:<device>_total_energy` appears in the statistic picker. Compare the hour alignment to the existing `sensor.*_total_energy` entity.
♻️ Skip redundant energy fetches and emit statistics at real timestamps
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 1m45s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m45s
Validate / type-check-mypy (pull_request) Successful in 1m48s
4a14c4d425
Closes #3, #5 and #42.

Two related energy-sensor issues addressed in one pass:

### #3 — "Adjust Update Frequency for ComwattEnergySensor"

The Comwatt QUANTITY/HOUR endpoint publishes a new bucket once per
hour, so polling it every 2 minutes was 29 wasted calls per hour per
device. The coordinator now tracks the last successful fetch time per
device and skips the call while the cached result is less than 55
minutes old. Power fetches are unchanged.

### #5 / #42 — hour offset / "délais de mise à jour"

Previously the energy sensor published the accumulated total under
timestamp `now`, even though the API bucket it came from represents
the **previous** hour. The Energy dashboard therefore attributed each
reading to the wrong hour, and new values only appeared after HA had
caught up with the 2-minute poll cadence.

The coordinator now forwards each new hourly bucket to
`async_add_external_statistics` with the real bucket timestamp and a
running cumulative sum, under an external statistic id
`comwatt:<device_id>_total_energy`. This:

- keeps the existing `sensor.<device>_total_energy` entity unchanged
  (no breaking change for users whose Energy dashboard points at it);
- exposes a parallel statistic attributed to the correct hour that
  users can opt into by adding `comwatt:*_total_energy` to the
  dashboard instead.

The push is guarded on `"recorder" in hass.config.components` so the
coordinator stays usable on the rare setup where the recorder is
disabled.

### Tests

- `test_energy_fetch_is_skipped_within_interval`: a second refresh
  within the window does not re-call QUANTITY; advancing the cached
  `last_fetched_at` triggers the call again.
- `test_new_energy_buckets_are_pushed_as_external_statistics`: two
  new buckets produce one `async_add_external_statistics` call with
  the correct statistic id, cumulative sum, unit, and top-of-hour
  timestamp.
mat force-pushed fix/5-energy-hour-offset from 4a14c4d425
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 1m45s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m45s
Validate / type-check-mypy (pull_request) Successful in 1m48s
to 1b23292466
All checks were successful
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m44s
Validate / type-check-mypy (pull_request) Successful in 1m49s
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 1m45s
Validate / type-check-mypy (push) Successful in 1m48s
2026-04-24 21:53:00 +00:00
Compare
mat self-assigned this 2026-04-29 10:03:15 +00:00
🐛 Handle real Comwatt API types in energy statistics
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 10s
Validate / test-pytest (push) Successful in 1m50s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m46s
Validate / type-check-mypy (pull_request) Successful in 1m50s
8d78841b39
The /aggregations/time-series endpoint returns timestamps as ISO 8601
strings (e.g. "2026-04-29T10:00:00.000+0000") and device IDs as ints —
not the seconds-since-epoch ints and string IDs the test fixtures used.
Both shapes broke at runtime on first deploy.

- Add _parse_bucket_ts to normalise timestamps to UTC datetime,
  accepting ISO 8601 (with Z, +0000, +HH:MM, naive), epoch seconds,
  epoch milliseconds, and numeric strings; return None for garbage.
- Compare and store buckets as datetime instead of int, so the dedup
  check no longer mixes types.
- Stringify device_id before slugify; slugify rejects ints with a
  decoding error.
- Cover the parser with edge-case tests so future API drift is caught
  at the boundary.
mat referenced this pull request from a commit 2026-07-13 13:03:58 +00:00
mat force-pushed fix/5-energy-hour-offset from 8d78841b39
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 10s
Validate / test-pytest (push) Successful in 1m50s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m46s
Validate / type-check-mypy (pull_request) Successful in 1m50s
to 7da21eb849
All checks were successful
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 3m2s
Validate / type-check-mypy (pull_request) Successful in 3m7s
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 2m59s
Validate / type-check-mypy (push) Successful in 3m6s
2026-07-13 14:05:37 +00:00
Compare
mat force-pushed fix/5-energy-hour-offset from 7da21eb849
All checks were successful
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 3m2s
Validate / type-check-mypy (pull_request) Successful in 3m7s
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 2m59s
Validate / type-check-mypy (push) Successful in 3m6s
to 0bbb7a0560
All checks were successful
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 3m0s
Validate / type-check-mypy (push) Successful in 3m8s
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 3m11s
Validate / type-check-mypy (pull_request) Successful in 3m11s
2026-07-13 14:20:39 +00:00
Compare
mat changed title from ♻️ Skip redundant energy fetches and emit statistics at real timestamps to ️ Skip redundant energy fetches and emit statistics at real timestamps 2026-07-13 14:20:56 +00:00
mat merged commit dbbe8a5a68 into main 2026-07-13 14:27:43 +00:00
mat deleted branch fix/5-energy-hour-offset 2026-07-13 14:27:43 +00:00
mat referenced this pull request from a commit 2026-07-13 15:52:52 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mat/homeassistant-comwatt!18
No description provided.