183 lines
11 KiB
Markdown
183 lines
11 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 — preferring anyone **assigned**
|
|
that chore type, minus anyone `chore_exempt`, both 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.
|
|
|
|
## Assignment is a preference, not a lock
|
|
|
|
Set in `identity`'s admin panel (`admin.html`, People → a person → Chores), stored as
|
|
that person's `chore_assignments` and read off the same `/presence` call as everything
|
|
else here.
|
|
|
|
When someone assigned to a chore type **is home**, they get nudged instead of whoever
|
|
happens to be nearest — that's the point of assigning it. When **no assignee is home**,
|
|
the nudge falls through to the ordinary whoever's-here rotation rather than waiting,
|
|
because the stated principle is still *"I don't care who does it, as long as it gets
|
|
done."* An absent assignee doesn't get to be the reason the bin stays full.
|
|
|
|
Set **`CHORE_ASSIGNMENT_STRICT=true`** if your household wants the opposite: the chore
|
|
waits for its assignee and nobody else is asked. Both readings of "assign chores" are
|
|
legitimate and this file can't pick for you, so it's one env var rather than a
|
|
hard-coded opinion. Strict mode still never stalls a chore that was assigned to
|
|
*nobody* — that falls through regardless.
|
|
|
|
**Litter can't be assigned** (`_ASSIGNMENTS_DONT_APPLY` in `check.py`), for the same
|
|
reason it ignores exemptions: it goes to whoever left the mess, and cleaning up after
|
|
yourself was never a task anyone could be handed.
|
|
|
|
## Chore-exempt people — everyone except litter
|
|
|
|
Set in the same place (`identity`'s admin panel, or `POST /people/<id>/chore-settings`
|
|
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 confirmation that an assignee actually did it** — assignment changes who gets
|
|
asked, not who gets credited. Attribution when a chore closes is still the same
|
|
best-effort "whoever was nudged most recently" heuristic, which an assignment doesn't
|
|
make any more truthful. See "The tally is passive" above.
|
|
|
|
## 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.
|