Commit Graph

9 Commits (f4de1e7cec9a08f6887e616c99fe8dd7ceb58154)

Author SHA1 Message Date
Amir Alexander Abdelbaki 483cbce728 Exact positions inside a room, fused from radar and BLE
The 3D floorplan could say which room somebody was in. This makes it able to
say where in the room — from a different sensor, kept deliberately separate
from the one that knows who they are.

Room-level presence comes from BLE, which cannot give coordinates: RSSI-to-
distance is noisy enough that trilateration in a house lands in the wrong
room. Coordinates come from mmWave radar (LD2450-class), which tracks moving
targets and reports x/y — and cannot say who anybody is, because it sees a
moving blob.

So the two are fused, by a rule that refuses far more often than it commits:

    exactly one occupant in the room AND exactly one target in the room
        -> that target is that person
    anything else
        -> targets stay anonymous, occupants stay unpositioned

Two people in a room are two blobs that cannot be told apart. Guessing which
is which would put a name on the wrong person, and a display that does that
occasionally is worse than one that never tries — its wrong answers are
indistinguishable from its right ones. position_ambiguous says so out loud.

Which room a target is in is computed from the polygon rather than from which
sensor saw it: a radar in an open-plan kitchen sees into the living room, and
attributing by sensor would put people through walls.

identity
  floorplan_levels gains metres_wide; without it positions are not computed
  and the API reports that rather than guessing a scale. New floorplan_sensors
  table holds where each radar sits on the plan and which way it faces —
  drawn by a human, because a wrong rotation mirrors every target it reports
  and the result looks plausible rather than broken. Targets at exactly (0,0)
  are dropped: that is how these radars say "nothing here", and treating it
  as a detection grows a phantom person on top of every sensor.

render/floorplan-3d
  A fused person is drawn at their coordinate with a footprint dot, since a
  marker floated above the floor otherwise reads as further back in the room.
  An unattributed target is a hollow dashed puck with a question mark — no
  colour, no initial, because every visual language here for a person is
  reserved for people the system can name. A radar target lights the room even
  with nobody named: somebody is in there, and that the house cannot say who
  is a fact about the house.

Hardware: HLK-LD2450 added to components.md, ~EUR 15-25 per room, with the
advice to buy one and check its facing before buying more.

Position maths and the fusion rule are unit-tested headlessly. No radar has
been bought, mounted or read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FanS1vyE2gLhGkqKq6HtYj
2026-08-10 15:03:45 +02:00
Amir Alexander Abdelbaki a948f4b375 Workshop assistant, fleet monitoring, infra health, and shared endpoint surfaces
Adds the workshop/office assistant and the plumbing several other features
were waiting on. The through-line: every new capability that could act on
its own proposes instead, and says out loud when it does not know something.

New service — workshop/
  Project notebook (workshop.db) plus a never-pruned knowledge store
  (workshop-knowledge.db): standing workflow instructions by activity,
  keyword facts, durable project learnings, and the household's ONE hardware
  inventory. GET /context returns everything applying right now in one call,
  so the assistant is told the standing considerations rather than reminded
  of them. Two databases because they have different lifetimes: rebuilding
  the project store must not take the note about how you solder with it.

  Hardware statuses distinguish reserved (still on the shelf) from in_use
  (installed and working) — "can I use this right now" has different answers
  for the two, and naming a project on an in_use item never silently demotes
  it.

  Gitea repos with append-only history: commit/push/branch yes, unattended;
  force-push/rebase/amend/reset/filter-repo never, enforced server-side by
  branch protection rather than only by this code refusing. When history
  genuinely must be scrubbed, /scrub-request prints the commands for a human
  to run — the manual step is the safety mechanism.

  Fleet scripts: one monitoring-agent script per kind of machine, fetched by
  each endpoint's fleet-bootstrap timer. Remote code execution by design, so
  the constraints are the design — upload is a draft, publishing is separate,
  scripts live in SQLite rather than on the writable share, every version is
  kept, and the endpoint verifies the checksum and reports pass or fail.
  Slots exist for the ESP32s and network appliances that cannot run a script
  at all, holding the CheckMK-server-side config instead.

Infrastructure health
  opnsense becomes a LIST of firewalls, each named, keyed by name rather than
  index. CheckMK joins it. Both are polled by workshop (always-on) and read
  by digest-engine, so the digest can say "critical since Tuesday" instead of
  quoting a six-hour-old snapshot. Three states, because "I could not ask" is
  not "nothing is wrong".

pantry-vision
  All four stock movements are camera-driven; stock counts individual units
  and folds brand-free via Grocy product groups. Door-sensor-triggered
  appliance cameras record sightings as hints with timestamps, never as
  stock — a camera at a door cannot tell in from out.

identity
  Per-person colour and settable profile picture, assigned to avoid collisions
  between people sharing an initial, on the 2-bit-per-channel lattice a colour
  Pebble renders natively.

render/ — shared, vendored, dependency-free
  media-visualiser: two-tier by necessity, since most endpoints have no local
  audio; the synthetic tier says on screen that it is not an analysis.
  floorplan-3d: canvas 2D rather than three.js — the scene is prisms on a
  plane, which an isometric projection draws in ~200 lines, predictably on
  weak panels, with the frontend still at zero dependencies.

Config and fleet plumbing
  Rooms are one vocabulary (an HA area_id) from CoreSystemConfig through the
  builders to suggested_area. Keycloak and FreeIPA are coupled as one
  decision with USR_HA_ group naming, declaration-only for now and validated
  as such. Immich alongside the photo share, read-only. Thin clients get the
  full media-key set for a wireless remote.

Docs: fridge-item-location, workshop-assistant, rooms-and-endpoints,
endpoint-surfaces, pebble-presence-watchface.

Testing is stubbed suites and headless unit checks only — no real Grocy,
camera, vision model, CheckMK, Gitea, Samba or browser has been involved.
The CheckMK API shape and Gitea's branch-protection payload are written from
documentation and have version-sensitive field names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FanS1vyE2gLhGkqKq6HtYj
2026-08-10 14:54:37 +02:00
Amir Alexander Abdelbaki c10d803a12 Per-person digests, RCI sources, agendas, archive and TV power
digest-engine grows from a single household digest into four per-person
sections, and gains the memory and inputs to make them worth reading.

Per-person sections. identity owns a digest_sections column per person,
edited in the admin panel and read by digest-engine at the start of every
run (GET /digest-preferences). A run generates the union the household
asked for: a section nobody wants costs no LLM call and no ingestion of
its sources. Each surface then filters to the person HA resolved. The
display half is a filter, not an access control, and says so.

Network is its own section, split out of household so the two can be
wanted separately.

Political section rebuilt around four questions (global class struggle,
organising in Vorarlberg, mid-term consequences, the International and
comrades' reports). ~30 international feeds added, each carrying owner
and bias, with a symmetric ownership analysis rather than a
reliability ranking; Zionist outlets get an explicit zero-trust rule that
is not inversion. RCI social/podcast ingestion (YouTube Atom, podcast RSS,
public Telegram via the existing session) feeds a watch-later window.
Globe markers carry summaries with fold-out sources; counter_run drops
citations whose URLs are not in the context.

Meeting agendas: a Tagesordnung arriving by mail or WhatsApp is matched to
its calendar event, read with pypdf, and its points extracted
mechanically. The political section owns the contents and derives
"Political todos"; the household section is told only that an agenda
exists, enforced structurally.

The archive keeps every ingested item and measured number across runs, so
trends may finally be stated with figures and dates attached.

ntfy push after each run, assembled from existing narrations, gated by the
same per-person sections.

identity gains GET /speaker: automatic recognition for the voice path from
BLE plus recent face sightings. Unresolved means show less, never ask, and
nothing displays a digest because someone walked past a screen.

OPNsense credentials move into CoreSystemConfig.json; thin clients gain a
Display switch (HDMI-CEC, DPMS fallback) so an empty room stops powering a
TV.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NpKsMV1Q2EHJ5cZVoLvK9M
2026-08-06 13:30:47 +02:00
Amir Alexander Abdelbaki 2ae5cb3449 Add a floorplan editor to the admin panel
Closes the oldest deferred item in the plan (open decision #22). /presence has
reported a best-effort `room` since Phase 6, but there was nothing to plot it
on: no floor plan, no room list, no coordinate format. The reason it stayed
deferred was sound — nothing here knows the shape of this flat, and inventing a
coordinate format against a guess would have been the wrong kind of work. So the
plan is DRAWN, by hand, in a new Floorplan tab.

Levels hold rooms; rooms are polygons (real rooms aren't rectangles) with an
optional background image to trace over. Click corners to draw, drag handles to
adjust. Each room maps to the HA area string /presence already reported, and the
editor offers a pick-list of the areas HA is actually reporting rather than
asking anyone to retype an area_id from Developer Tools — same anti-typo
reasoning as CoreSystemConfig.json. Tick "Show who's home" and occupied rooms
light up.

Coordinates are normalised 0-1 rather than pixels: the plan has to render on a
laptop now and possibly a wall panel later, and pixels would be right on exactly
one of them. The tradeoff is documented — replacing a background with one of a
different aspect ratio distorts existing rooms.

Two choices about failure worth calling out. A room with no HA area is legal and
labelled as such on the plan, because drawing the flat and wiring up presence are
separate jobs. And anyone who can't be placed — home with no room resolved, or
reporting an area no room claims — is listed under the plan, with unclaimed areas
named so you know what's left to draw. A floor plan that quietly lost people
would be worse than no floor plan.

The whole feature is still gated on AREA_ATTRIBUTE being right, which remains an
unconfirmed guess at what Bermuda publishes; every room can be drawn correctly
and still never light up. The editor makes that diagnosable rather than
mysterious, but can't fix it. Added to identity's verification list.

38 backend checks: level and room CRUD, polygon validation (too few points, out
of range, non-numeric, over the vertex cap), duplicate-area refusal, image
round-trip, the presence join including both unplaceable cases, area suggestions
degrading when HA is down, and cascade on level delete. The editor UI itself has
not been opened in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:45:45 +02:00
Amir Alexander Abdelbaki 15e6715469 Document the LAN-only ntfy decision and the WireGuard split tunnel
Records the networking decision behind Phase 6b's arrival notifications so it
doesn't get re-derived later.

ntfy stays LAN-only: identity -> ntfy is a container-to-container call that never
reaches OPNsense, and remote delivery rides a WireGuard split tunnel routing only
the smart-home VLAN. No DMZ, no port forward, no certificates, no inter-VLAN rule.

Both alternatives were weighed and rejected, with reasons recorded:
- DMZ + port forward buys only "notifications without the VPN connected," at the
  cost of a public listener, TLS upkeep, and mandatory ntfy auth — an exposed
  topic leaks "the house is empty right now".
- DMZ + NAT reflection does avoid a new inter-VLAN rule (the smart-home VLAN's
  existing outbound-WAN allow already covers traffic to your own public IP), but
  makes a purely internal path depend on the ISP and external DNS being up, so
  two devices metres apart stop talking during a WAN outage. CGNAT breaks it
  outright, and the rule it avoids was the safe direction anyway.

New §2.1 covers the split tunnel itself, including the trap that motivated it:
AllowedIPs must be the actual VLAN prefix, never 192.168.0.0/16, which collides
with typical café/hotel LANs and breaks the phone's connectivity there. Plus
always-on VPN yes / lockdown no, and no DNS= so queries aren't pulled home.

The household uses no Apple devices, which removes the only forcing function for
exposure (ntfy's iOS app needs APNs and therefore an upstream relay). Open
decision #37 closes as a household decision; the Apple case is kept as a
footnote in case that ever changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:42:52 +02:00
Amir Alexander Abdelbaki 0caa21fc42 Add opt-in arrival push notifications; fix chores' unreachable container URLs
Rides the same arrival transition the visit log is built from, so "arrived" has
exactly one definition in this service rather than two that could disagree.

- notify_on_arrival (default off) is the "if enabled" half: push me when someone
  gets home. announce_arrivals (default ON) is a separate per-person opt-OUT of
  being announced, for anyone who doesn't want their comings and goings
  broadcast. The asymmetry is deliberate — if both defaulted off, ticking
  "notify me" would look broken until everyone else opted in too.
- Per-person notify_topic falling back to NTFY_DEFAULT_TOPIC. Topics are
  deduplicated, or a five-person household sharing one topic would get five
  identical pushes per arrival. The arriver is never told about themselves;
  subscribers who are away still are, since that's most of the point.
- The first sample after startup notifies nobody and establishes a baseline
  instead — otherwise a restart after a gap would fire "X just got home" for
  everyone already on the sofa. Costs one missed notification in that window.
- Face-recognition arrivals say "was just recognised at home", not "just got
  home" — the signals aren't equally reliable and the reader should know which.
- Pushes are sent outside _db_lock and isolated from each other, so a hanging
  ntfy can't stall request handling and one bad push can't swallow the rest.
- POST /people/<id>/test-notification, because the alternative way to find a
  typo'd topic is to wait for someone to walk in and notice nothing happened.

identity itself never touches the WAN — it POSTs to the ntfy already in this
stack. Whether the push reaches a phone that's AWAY is a network question, and
docs/network-integration.md's existing answer (WireGuard in, never a
port-forward) applies unchanged; iOS can't do this locally at all, since ntfy's
iOS app needs APNs and therefore an upstream relay. Documented as a table.

Also fixes two pre-existing bugs found while wiring this up: chores' template
pointed IDENTITY_URL and NTFY_URL at 127.0.0.1, which inside that container is
the container itself. chores would have reached neither identity nor ntfy, and
done nothing at all — silently, since both paths fail soft by design. Deployed
chores.env files still carry the old values and need editing by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 09:37:10 +02:00
Amir Alexander Abdelbaki 5c05b67a5a Add identity admin panel: nicknames, visit history, device rights, chore assignment
Extends Phase 6's identity rather than adding a service — all of this is facts
about who someone is, already identity's job.

- admin.html/admin.js: manage people/guests, edit every field, prune, history,
  access log. Deliberately not kiosk-shaped and not linked from any wall panel —
  a panel anyone can walk up to shouldn't carry a "prune these people" button.
- Nicknames are an input alias only. /resolve accepts them; every payload also
  carries speak_name (always the real name) and chores/ uses it for reminders.
  A nickname colliding with anyone's name or nickname is refused; a spoken string
  matching two people refuses with ambiguous_name rather than picking one.
- Visit history sampled from identity's own /presence. home:null writes nothing,
  ever — an HA outage must not record a departure that didn't happen. Departures
  need sustained absence (BLE flaps) and are stamped at the last time actually
  seen. "With whom" is an interval-overlap query, not a second table.
- Pruning splits selecting from deleting: the filter fills the checkboxes, the
  delete goes by explicit id, and the filter is never re-run at delete time.
- Device rights are an answer, never an action — identity stores grants and
  answers GET /device-access; HA asks and HA acts. Deny is the default and the
  only fallback. Grants can expire; every check is logged, denials included.
- Chore assignment prefers an assignee who's home but doesn't let an absent one
  block the chore (CHORE_ASSIGNMENT_STRICT flips that). Litter still ignores
  both exemption and assignment.
- Admin-added identifiers still enforce TRUSTED_ENTITY_PREFIXES; only the
  "in range right now" check is relaxed, never the allowlist.

Resolves open decision #26; adds #34-37 for the new unknowns (DEPARTURE_GRACE_
SECONDS is untuned, the panel has never been opened in a browser).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 09:05:14 +02:00
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
Amir Alexander Abdelbaki bad217c369 Add identity registration system and Phase 18 door panel
identity/ (Phase 6, rewritten from the original Node-RED sketch): a
person <-> BLE-identifier registry built directly as a Python service.
Solves multi-phone (multiple identifiers per person), anti-spoofing
(only allowlisted IRK-resolved/fixed-tag entities are ever accepted as
registration candidates, never a raw or randomized MAC), device-less
people (a grandmother without a smartphone gets a no_device registration
plus a hand-operated home/away toggle, reported as "unknown" rather than
a false "away"), and anonymous guests (POST /register/guest, no name
needed). Every person gets an automatic profile picture from their most
recent registration photo. /presence also reports a best-effort room per
person as groundwork for an eventual floor-plan view (not built).
Registration is single-utterance voice ("register me as <name>") or a
touchscreen form; ambiguous/conflicting candidates never auto-commit.

hosts/door-panel/ (Phase 18, new host): structurally kitchen-display's
twin - one Sway workspace, one Chromium kiosk window - defaulting to
identity's weather+clothing/who's-home/groceries-running-low dashboard,
with voice registration as its actual purpose (mic on by default, unlike
every other host's opt-in).

hosts/kitchen-display/: adds opt-in voice satellite and a "Show
registration" screen pointed at identity's register.html, reusing the
same camera-equipped-endpoint registration flow.

pantry-vision/: adds GET /shopping-list (Grocy's own volatile/missing
products, reshaped) for the door panel's "running low" section.

setup-container-host.sh: wires ENABLE_IDENTITY (identity + identity-web,
published like pantry-vision since kiosk browsers call it directly, plus
a persistent SQLite/photos volume unlike pantry-vision's stateless
design) and fixes the HA_URL example to the host's real LAN IP (HA runs
network_mode: host, unreachable by container name - the same situation
Node-RED's own config already documents).

docs/: Phase 6 rewritten, Phase 18 added, hardware/software/guardrail/
open-decision entries throughout project-plan.md and README.md.
components.md gains RuView presence nodes (one per room), fixed BLE tags,
and 2 spare webcams, with the price estimate updated to match.

Nothing here has been run against real hardware, a real HA instance, or
real Private BLE Device entities - TRUSTED_ENTITY_PREFIXES above all
needs checking against a live instance before registration finds
anything. See identity/README.md and hosts/door-panel/README.md for the
itemized verification lists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CrKjW3yVptUnjG35HjC1
2026-07-30 13:40:04 +02:00