SmartestHome/chores
Amir Alexander Abdelbaki 2565cf99d0 Add trash/transit/chores services, RuView + BLE proxy firmware, network docs
Phase 19: trash-calendar (Kennelbach ICS -> CalDAV) and transit (GTFS
departures + OpenTripPlanner-backed voice route planning, slow walking
speed assumed). Phase 20: Tapo camera integration via identity's new
Frigate face-recognition presence signal, and chores, a presence/
calendar-driven household task nudger ("I don't care who does it, as
long as it gets done") with a passive fairness tally, litter culprit
attribution, and per-person chore_exempt/chore_reminder_style settings
on identity.

Also: the HA integrations + hardware-monitoring catalogs, Music
Assistant wiring, docs/network-integration.md (OPNsense VLAN
segmentation, no WAN port-forwards), the ESPHome BLE-proxy firmware,
and RuView CSI-presence integration (github.com/ruvnet/ruview) with
household automations for sleep/distress/concurrent-vitals/bathroom-
occupancy — all flagged with their real unverified assumptions and
open decisions. Fixed two real port collisions surfaced while wiring
this in (OTP vs. zigbee2mqtt on 8080, Music Assistant vs. pantry-vision
on 8095).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CrKjW3yVptUnjG35HjC1
2026-07-30 16:53:24 +02:00
..
Dockerfile Add trash/transit/chores services, RuView + BLE proxy firmware, network docs 2026-07-30 16:53:24 +02:00
README.md Add trash/transit/chores services, RuView + BLE proxy firmware, network docs 2026-07-30 16:53:24 +02:00
check.py Add trash/transit/chores services, RuView + BLE proxy firmware, network docs 2026-07-30 16:53:24 +02:00
chores.env.example Add trash/transit/chores services, RuView + BLE proxy firmware, network docs 2026-07-30 16:53:24 +02:00
requirements.txt Add trash/transit/chores services, RuView + BLE proxy firmware, network docs 2026-07-30 16:53:24 +02:00

README.md

chores

Camera-verified household task distribution, from Phase 20 of the project plan.

The principle, stated once because it drives every design choice here: "I don't care who does it, as long as it gets done." This is not an assignment system that picks one fair person and waits on them — it's a nudge system that keeps redirecting to whoever is actually around until the chore is done. Fairness is tracked for comparison, not enforced by any algorithm — see "The tally is passive" below.

Runs every ~2 hours via a systemd timer with RandomizedDelaySec=1800 (systemd's own built-in jitter — the "+/-30 min in case something else is running" the phase was specified with, not custom code). Three steps, in order — see check.py's own module docstring for the full reasoning on each:

  1. Trash-day-eve — reads the same personal collection-date feed trash-calendar reads (WASTE_ICS_URL); if pickup is tomorrow, opens a "trash" chore.
  2. Camera checks (opt-in, needs FRIGATE_URL + CAMERA_WATCHPOINTS) — grabs a Frigate snapshot per configured watch point (optionally moving a PTZ camera to a preset first), asks an Ollama vision model a one-word question ("is this bin FULL/PARTIAL/EMPTY", "is this counter DIRTY/CLEAN", "is there litter left out here, YES/NO"), opens a chore on "needs attention," auto-closes one on "clear."
  3. Nudging — ASAP, not on a fixed schedule: the first run after a chore opens nudges whoever identity reports home right now (minus anyone chore_exempt, see below). If the chore is still open NEGLECT_THRESHOLD_HOURS after the last nudge (and the household calendar isn't showing a busy window), the nudge goes to someone different from who was last asked — "the next person that walks by" — rather than re-nagging the same person. litter chores are special-cased to prefer whoever the camera most recently recognized nearby (a best-effort "who left this" guess), since the point there is telling the actual person, not just whoever's around — and litter also ignores chore_exempt entirely, see below. Each nudge's wording is a plain template unless the target has a chore_reminder_style set, see below.

Chore-exempt people — everyone except litter

Set via identity's POST /people/<id>/chore-settings (see identity/README.md, no frontend for it yet — call the endpoint directly). A chore_exempt person is dropped from the nudge rotation entirely — the "cousin visits often but doesn't owe me chores" case. Litter is the deliberate exception (_EXEMPTIONS_DONT_APPLY in check.py): an exempt person still gets told to put trash they left out into the bin, because that isn't "doing a chore," it's cleaning up after yourself. If everyone currently home is chore_exempt for a non-litter chore, that run just logs and skips — the chore stays open until someone eligible is around.

Reminder tone is per-person and LLM-phrased, but never LLM-decided

identity's chore_reminder_style free-text field ("be assertive, don't let up" / "be gentle, give me a few minutes of grace") is passed to OLLAMA_TEXT_MODEL (if configured) purely to phrase the ntfy message text — see _compose_message(). This is deliberately scoped narrower than the general chore-distribution LLM idea from earlier in the project: it changes how the reminder is worded, never who gets nudged or when — that decision stays presence/calendar-driven, per the module docstring's stated principle. No OLLAMA_TEXT_MODEL configured, no style set for that person, or the call fails/returns nothing for any reason: the plain un-styled template is used verbatim, same wording as before this feature existed.

The tally is passive

Every nudge is logged with who got it. Nothing reads that log to decide who to nudge next — that decision is presence/calendar-driven only, per the stated principle. The log exists purely so a household member can look at the numbers later and judge fairness for themselves ("comparison for fairness's sake," not an automated fairness algorithm) — print_tally() logs a rolling 30-day count each run; nothing renders it anywhere yet, see "What's not built."

The calendar busy-check is household-wide, not per-person

_household_currently_busy() looks for a currently-active event on the one shared household calendar whose summary contains a configured keyword (busy, meeting, call, movie, sleep, ...) and pauses nudging for that run if it finds one. This is a real, honest limitation: it can't tell that only one person is in that meeting and nudge someone else who's free — everyone's nudges pause together. A real per-person calendar/availability model would need per-person calendars, which this project doesn't have (see docs/project-plan.md's open decisions). Fails open (treats an unreachable/misconfigured calendar as "not busy") so a broken calendar check can never be the reason chores stop getting nudged entirely.

Never auto-completes from nudging

Only a fresh camera check finding a watch point clear, or a manual close (not built here — see "What's not built" below), ever marks a chore done. A nudge firing is not the same as the chore being done; conflating the two would let something that was repeatedly nudged but never actually done silently vanish.

Why this is a separate service, not folded into identity or pantry-vision

It reads from identity (who's home, who's nearby, who was recently seen by a camera) and reuses trash-calendar's own ICS feed config and digest-engine's own CalDAV credential names, but owns a genuinely different job — task state, presence-driven nudging, notification escalation — that doesn't belong bolted onto any of those services' own single responsibilities. Same reasoning as trash-calendar itself being separate from digest-engine's read-only calendar ingestion.

What's not built

  • No way to manually mark a chore done — no HA button, no voice phrase, no API endpoint. Right now the only way a chore closes is a camera re-check finding it clear, or direct SQLite surgery. A real deployment probably wants an HA button/voice "mark the trash as done" — deliberately left out of this pass rather than guessed at without knowing how the household actually wants to interact with it.
  • No per-person ntfy topics — one shared NTFY_TOPIC, message text names who it's for. Set up real per-person routing yourself if that's not enough.
  • No web UI / dashboard for chore history or the fairness tallychore_events logs everything (created, nudged, auto_closed) and the tally is logged each run, but nothing renders either anywhere yet.
  • No per-person calendar availability — see "household-wide, not per-person" above.
  • No re-check of _compose_message()'s LLM output — whatever the model returns (if anything) is sent as-is, no validation that it's actually on-topic, on-tone, or even non-empty garbage beyond the plain empty-string fallback check.
  • No frontend for setting chore_exempt/chore_reminder_style — set via a direct POST /people/<id>/chore-settings call to identity until one exists.

Configure

cp chores/chores.env.example /opt/smart-home/chores/chores.env
chmod 600 /opt/smart-home/chores/chores.env
$EDITOR /opt/smart-home/chores/chores.env

Works with nothing but IDENTITY_URL/IDENTITY_TOKEN and ntfy filled in — the trash-day-eve check, calendar busy-check, and camera checks all individually no-op when left unconfigured.

Manual verification still outstanding

  1. Frigate's PTZ move-to-preset API shape (_frigate_snapshot()'s POST /api/<camera>/ptz/move/<preset>) is assumed from Frigate's general PTZ feature set, not confirmed against a real Tapo PTZ camera wired into Frigate — see docs/project-plan.md §1.18/Phase 20's own callout. A wrong endpoint just means the snapshot is taken from wherever the camera already was.
  2. Whether the vision model's one-word FULL/PARTIAL/EMPTY/DIRTY/CLEAN/YES/NO answers are actually reliable for a real bin/sink/hallway from a real camera angle — completely unmeasured, same caveat as pantry-vision's own vision-model accuracy note.
  3. _likely_culprit()'s reliance on identity's face_seen_recently field assumes Frigate face-recognition presence corroboration is actually wired up and working (identity/README.md's own "Camera face recognition" section is itself unverified) — until then, litter chores just fall back to "whoever's around," same as every other chore type.
  4. _household_currently_busy()'s CalDAV read has never been run against a real Nextcloud instance from this specific code path (digest-engine's own ingest/caldav.py is a separate, independently-tested read).
  5. No camera hardware has been chosen (docs/project-plan.md §1.18) — nothing here has been run against a real Tapo camera or Frigate PTZ integration at all.
  6. _compose_message()'s LLM-phrased reminders have never been checked against a real OLLAMA_TEXT_MODEL for whether the output actually respects a given chore_reminder_style reliably, vs. just producing generically-toned text — same "no measured accuracy" caveat as the vision checks above. A bad or off-style result still gets sent (there's no re-check of the LLM's own output here), just not blocked — see "What's not built" for why there's no re-check.