SmartestHome/chores/README.md

159 lines
9.3 KiB
Markdown

# chores
Camera-verified household task distribution, from
[Phase 20 of the project plan](../docs/project-plan.md).
**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 tally** —
`chore_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
```sh
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.