Skip to content

Add per-cat feeding sensors and household activity timeline - #353

Open
Veldkornet wants to merge 1 commit into
FredrikM97:mainfrom
Veldkornet:feat/feeding-sensors-v2
Open

Veldkornet wants to merge 1 commit into
FredrikM97:mainfrom
Veldkornet:feat/feeding-sensors-v2

Conversation

@Veldkornet

Copy link
Copy Markdown
Contributor

Summary

  • Add two new per-cat sensors: feedings_today (visit count) and food_today (total/wet/dry grams), sourced from the household timeline API. The existing (disabled-by-default) feeding sensor, which reports only the most recent feed event, is unchanged. Both use state_class total_increasing (not total) so long-term statistics carry the sum correctly across each midnight reset.
  • Add a household-wide household_activity_today sensor: a chronological feed mixing feeding visits with bowl fill/zero maintenance events, with pet/device photos listed once each in pet_photos/device_photos rather than repeated per event (keeps state attributes under the recorder's 16 KiB limit on an active day). No state_class, since its count is redundant with feedings_today's own per-pet statistics.
  • SurePetCareFeedingTimelineCoordinator rebuilds from scratch (a backward walk to local midnight) only on its first poll, after local midnight, or when nothing's happened yet today; every other poll instead folds in new events via a single forward since_id request. The cursor lags one poll behind with a bounded seen-id set for dedup, mirroring SurePetCareHouseholdTimelineCoordinator's own polling, since timeline ids aren't strictly ordered by created_at.
  • Also: scripts/check_timeline_frames.py, a diagnostic for checking whether the timeline API's weights[].frames data is populated for a live account, and a py-surepetcare 0.6.4 lockfile sync.

An earlier version of this (#336) predates the current one-config-entry-per-household architecture and typed TimelineEvent API, and was closed unmerged and without comment; this is a rebuild against the current codebase, not a rebase of that branch.

Test plan

  • uv run pytest — 172 passed, 1 skipped, 610 snapshots passed
  • uv run ruff check . / uv run ruff format --check . clean
  • pre-commit run --all-files (ruff + mypy) clean

This allows you to build a dashboard like the following:
Unknown

@Veldkornet
Veldkornet requested a review from FredrikM97 as a code owner August 27, 2026 20:55
Copilot AI lite review requested due to automatic review settings August 27, 2026 20:55
@Veldkornet
Veldkornet force-pushed the feat/feeding-sensors-v2 branch from b3299a4 to 8d63836 Compare August 27, 2026 21:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new feeding-timeline coordinator currently ignores the existing user-configurable timeline polling interval option, so runtime behavior won’t match the integration’s configured polling setting.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds new timeline-backed feeding and household-activity sensors to the SurePetCare Home Assistant integration, including an incremental polling coordinator and supporting fixtures/tests, to enable per-pet “today” stats and a combined household activity feed.

Changes:

  • Introduces feedings_today, food_today (per pet) and household_activity_today (per household) sensors backed by a new feeding-timeline aggregation module and coordinator.
  • Implements cold-rebuild-at-midnight + incremental since_id polling with bounded deduplication for timeline events.
  • Adds fixtures, regression tests, snapshots, and a diagnostic script; updates uv.lock to py-surepetcare==0.6.4.
File summaries
File Description
uv.lock Switches py-surepetcare dependency to PyPI 0.6.4.
tests/test_sensor.py Freezes time for stable snapshots; adds integration-level assertions for new sensors.
tests/test_feeding_timeline.py Adds regression tests for timeline parsing/aggregation (incl. frames-based splits).
tests/test_feeding_timeline_coordinator.py Adds tests for cold vs incremental polling and cursor-lag dedup behavior.
tests/snapshots/test_sensor.ambr Updates entity/state snapshots for new sensors and reordered entities.
tests/snapshots/test_init.ambr Adds device-registry snapshots for per-household “Household” devices.
tests/fixtures/feeding_timeline_frames.json Adds fixture events with weights[].frames populated for gram-splitting tests.
tests/init.py Extends mock API to serve per-household timeline events + household name lookups.
scripts/check_timeline_frames.py Adds an interactive diagnostic script to inspect real timeline frames presence.
custom_components/surepcha/translations/en.json Adds translations for the new sensor keys/attributes.
custom_components/surepcha/translations/de.json Adds translations for the new sensor keys/attributes.
custom_components/surepcha/translations/sv.json Adds translations for the new sensor keys/attributes.
custom_components/surepcha/sensor.py Registers new sensors; wires per-household timeline coordinator into pet sensors + household sensor.
custom_components/surepcha/feeding_timeline.py New module to aggregate per-day feeding stats + household activity feed from timeline events.
custom_components/surepcha/coordinator.py Adds SurePetCareFeedingTimelineCoordinator with cold rebuild + incremental polling logic.
Review details
  • Files reviewed: 14/15 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread custom_components/surepcha/coordinator.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

There are correctness issues in the new timeline paging logic and fixture consistency, plus an availability semantics deviation that should be aligned with existing entity behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 14/15 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread custom_components/surepcha/sensor.py
Comment thread custom_components/surepcha/feeding_timeline.py Outdated
Comment thread tests/fixtures/feeding_timeline_frames.json Outdated
New sensors per cat: feedings_today (visit count) and food_today (grams,
wet/dry split), sourced from the household timeline API. New household-wide
household_activity_today sensor: a chronological feed mixing feeding visits
with bowl fill/zero maintenance events, with pet/device photos listed once
each rather than repeated per event. feedings_today/food_today use
state_class total_increasing so long-term statistics carry the sum across
each midnight reset; household_activity_today has none, since its count is
redundant with feedings_today's per-pet statistics.

SurePetCareFeedingTimelineCoordinator rebuilds from scratch (a backward walk
to local midnight) only on its first poll, after local midnight, or when
there's no cursor yet; otherwise it polls incrementally via a single
since_id request. The cursor lags one poll behind with a bounded seen-id
set for dedup, mirroring SurePetCareHouseholdTimelineCoordinator's own
polling, since timeline ids aren't strictly ordered by created_at.

pet_photos/device_photos are scoped to the household they're attached to,
not built from every coordinator in the config entry - otherwise a legacy
multi-household entry would leak one household's photos into another's.

Also: scripts/check_timeline_frames.py, a diagnostic for checking whether
the timeline API's weights[].frames data is populated, and a py-surepetcare
0.6.4 lockfile sync.

Test coverage: direct unit tests for the aggregation/pagination/incremental-
fold logic, plus an end-to-end test through the real config-entry setup -
the shared test mock now correctly answers the timeline/household-detail
endpoints, rather than every feeding sensor silently reporting
"unavailable" regardless of what the aggregation code did.
@Veldkornet
Veldkornet force-pushed the feat/feeding-sensors-v2 branch from 8f95ce2 to b65a9c4 Compare August 27, 2026 21:15
@FredrikM97

Copy link
Copy Markdown
Owner

Hi! I closed it since the tests were not passing. I also think there are quite a few things that should be reworked or moved. For example, all API calls should be moved to py-surepetcare.

I’m also sceptical about the custom sensor entities, as they add extra complexity that shouldn’t be necessary on top of the existing base-class entities.

Another blocker was the separation of households, which is currently merged. Fixing that allowed me to add a basic timeline event, which was added in release 2.2.4. Could you review whether the latest release contains all the information you need from the events?

I’m also looking into whether it would be possible to provide events alongside the pet entities, but this could cause confusion due to mismatched data. For now, the timeline is therefore provided only as events and not as actual sensors.

@Veldkornet

Copy link
Copy Markdown
Contributor Author

Thanks for the context, appreciate the detail.

Hi! I closed it since the tests were not passing. I also think there are quite a few things that should be reworked or moved. For example, all API calls should be moved to py-surepetcare.

I'm not sure what API calls you're referring to... just to clarify, there aren't any direct calls to the Sure Petcare API here. Everything goes through client.api() / the Command builders from py-surepetcare, the same pattern SurePetCareHouseholdTimelineCoordinator already uses (household.get_timeline(since_id=...) etc.). The one thing that is Sure-Petcare-shape-aware on the HA side is parsing TimelineEvent.data (the wet/dry food_type split comes from json.loads-ing that field) — it's exposed as an opaque string rather than a typed value, so that parsing ended up here rather than in the library. Happy to hear if that's closer to what you meant, or if there's something else you had in mind.

I’m also sceptical about the custom sensor entities, as they add extra complexity that shouldn’t be necessary on top of the existing base-class entities.

The new ones (feedings_today, food_today, household_activity_today) follow the same coordinator/entity shape already in the codebase, not a new pattern. The reason they're sensors rather than just consuming EVENT_TIMELINE is state_class: total_increasing, which is what gets a "today's total" onto HA's built-in long-term statistics/history graphs. That's not something a bus event can give for free — it needs something maintaining a running "since local midnight" total, whether that's a sensor entity or a template sensor/automation built on top of the event stream.

Another blocker was the separation of households, which is currently merged. Fixing that allowed me to add a basic timeline event, which was added in release 2.2.4. Could you review whether the latest release contains all the information you need from the events?

I’m also looking into whether it would be possible to provide events alongside the pet entities, but this could cause confusion due to mismatched data. For now, the timeline is therefore provided only as events and not as actual sensors.

yes, this PR is built directly on SurePetCareHouseholdTimelineCoordinator/EVENT_TIMELINE from #346, not duplicating it. Checked what the event payload carries: it has the raw ingredients (weights[].frames[].{index,current_weight,change}, device_id, pet ids, created_at), so the gram math could be replicated downstream. What it doesn't carry is any running total (not really an event-stream concept) or pet/device display names (just ids) — so events alone get you the data, not the "today's total" graphable sensor.

@FredrikM97

Copy link
Copy Markdown
Owner

I have taken some more time on the PR and I agree I did some wrong assumptions but its quite a big PR to cover. I apologize for that. I forgot I exposed a version of the timeline since I had no feedback on it so it never worked as expected. The new version should work much better.

What is the reason for a dedicated Feeder coordinator? I assume a coordinator for all events is required but we only need one to manage the timeline (feeding,drinking,movement)?

HouseholdTimelineData is a good idea though to be explicit of the data it manages.

It feels like we add lots of logic in feeding_timeline to handle the weights etc but is nothttps://github.com/FredrikM97/hass-surepetcare/blob/main/custom_components/surepcha/timeline.py enough for that or what is missing? Can you elaborate?

In my opinion the best would be if we did not even need the timeline.py and instead just parse the data from py-surepetcare but that will be a refactor for the future.

The changes to sensor.py also changes a lot compared to existing logic. I prefer to have one base entity instead of creating a new entity per sensor since that reduce maintenance and testing. Can you elaborate why this is needed instead of using the existing setup and adding more sensors?

I have been thinking to add a sensor for the timeline data, however I think the best approach would be to listen to the events (similar toSurePetCareTimelineSensorBase) but but reuse the existing SensorBase. If possible to extend it with support for the events. So your approach this is on the right track but needs to be more generic not just feeding.

If we replace the sensor with event data it is important not to mix data. So sensors not show the data out of sync. This is one of the main obstacles at the moment and I don't think we should add extra sensors which show same data.

You' mentioned a running total. This is better to use a helper (check wiki for example). Since Surepetcare does not natively support it we should avoid combining and keeping daily states in the codebase.

Last thing is to avoid storing so much data in extra attributes. This is not recommended by HA. Since the photos never change it is better to keep them in a dedicated sensor like it is right now. Otherwise we keep a copy with each sensor update instead of just keeping one copy of it.

I hope this is a better reply so we can add some of the features you requested and I apologize for the previous lack of proper response.

@Veldkornet

Copy link
Copy Markdown
Contributor Author

Thanks — this is much more actionable, no need to apologise. I think you're right on most of it. Going through it in order:

Dedicated feeding coordinator — agreed, one is enough. SurePetCareFeedingTimeline Coordinator ended up re-implementing the same since_id cursor / _seen_ids / lag-one-poll logic SurePetCareHouseholdTimelineCoordinator already has, polling the same /timeline/household/{id} endpoint on the same interval (plus an extra get_household call per poll for the name). I think that's just how it happened in the previous PR and then I re-implemented the same logic on top. It should be the one existing coordinator: it keeps the single cursor, keeps firing EVENT_TIMELINE, and additionally exposes "today so far" as its .data. The only feeding-specific piece is the cold-start backward walk to local midnight, which fits in _async_setup.

HouseholdTimelineData — I'll keep it, as the .data of that one coordinator.

"Is timeline.py not enough / what's missing?"build_event_payload only assembles a generic serialisable bus payload; it never looks inside event.data, which is where weight.food_type (per-bowl wet/dry) and tare_type live — that's a raw JSON string on TimelineEvent, not a typed field. The "extra" weight logic is really just: parse that blob, split the frame change values by food type, label the tare. Aggregation + the local-midnight cutoff are HA-specific and stay here; the blob parsing is exactly what should move upstream.

Not needing timeline.py — agreed, that's a py-surepetcare refactor. I'll open an issue there for typed event.data fields.

New entity classes in sensor.py — the reason is they're fed by a different coordinator than SurePetCareBaseEntity / MethodField assume (that abstraction is built entirely around a single device object). I agree it's not worth the maintenance cost. Going event-driven lets the base entity stay close to what's there.

Event-driven and generic — on board. Natural fit is an EventEntity per pet subscribing to EVENT_TIMELINE, one generic base with an event-type filter, covering feeding / drinking / movement rather than feeding only. Each visit fires with grams / wet_grams / dry_grams / duration at fire time — no retained state, so nothing to drift out of sync with the existing feeding sensor.

Running totals — agreed the TOTAL_INCREASING + implicit midnight reset is wrong. The fix is state_class TOTAL with an explicit last_reset at local midnight — HA's documented pattern for a periodically-resetting total, and the same mechanism utility_meter uses internally. It's not a fragile accumulator: the coordinator already holds today's events for the feed, so feedings_today / food_today are just the count / sum over events since midnight, recomputed each poll and rebuilt from the API on restart — nothing to drift or corrupt. That keeps the dashboard working out of the box and still feeds long-term statistics correctly, without asking users to wire up a helper. If you'd rather they not feed LTS at all, they can be plain measurement gauges instead.

Attributes / photos — I'd like to keep household_activity_today, but fix the actual problem (recorder bloat) properly, with _unrecorded_attributes:

_unrecorded_attributes = frozenset({"events", "pet_photos", "device_photos"})

Listed keys are stripped before the recorder serialises the state, and before the 16 KiB check (recorder/db_schema.py), so they never hit the DB — but stay live in the state machine, which is all a markdown/template card needs. That's a stronger fix than moving photos to a dedicated sensor (which still records the URLs on every update): here the recorder keeps zero copies, and the photo maps are built once from coordinator data at setup since they never change. The recorded state is just the event count — one row per new event.

Its events does overlap the per-pet event entities. The reason to keep it: template cards can't read entity history, only current attributes, so an at-rest feed is the only way to drive them. If you'd rather it not exist, that's the one spot a user drops back to the Logbook and loses a styled card.

Revised shape:

  1. One SurePetCareHouseholdTimelineCoordinator — single cursor, fires EVENT_TIMELINE, cold-start walk to midnight, .data = today's events + per-pet aggregates.
  2. Generic EventEntity per pet (feeding / drinking / movement) off the bus.
  3. feedings_today / food_today as TOTAL + last_reset sensors derived from that .data (recomputed, not accumulated).
  4. household_activity_today kept, with _unrecorded_attributes for events + pet_photos + device_photos; recorded state is just the event count.
  5. Follow-up issue on py-surepetcare for typed event.data fields.

Two things before I rework the branch: (a) do you want the existing disabled-by-default feeding sensor retired in favour of the feeding event entity, or kept alongside? (b) OK to keep the wet/dry food-type split as event-entity attributes (and in the feed sensor's events) — it's the main thing the timeline adds over the existing feeding sensor.

@FredrikM97

Copy link
Copy Markdown
Owner

I would like smaller PRs if possible.

Replacing the existing feeding (same goes for drinking and movement) if possible and decouple it from the existing coordinator data. I do not want to mix data that can be out of sync.

I think better to have a field which describe the fields. Something like this because a field cant be wet and dry at the same time.

'weights': list([
        dict({
          'device_id': 1376800,
          'duration': 13,
          'frames': list([
                "index": frame.index,
                "current_weight": frame.current_weight,
                "change": frame.change,
                "type": WET/DRY
            }
          ]),
        }),
      ]),

One pain point will be to rewrite sensor tests to take data from events. They will otherwise be empty.

I have checked a bit with logic similar to utility_meter but I don't think it is possible to apply on an integration? Depending on complexity it could be better to be a helper and let the integration only feed raw data to the user. This can be a separate PR to decide the proper course of action.

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.

3 participants