A local computer-vision alert system that watches the floor beside a bed — not the bed itself. When the cat steps down, it sends a Telegram message so someone can move him before the carpet suffers. It stays completely silent when the only thing moving is you.
Four stages, each one cheap enough to throw away most of the work before the expensive stage runs.
The camera streams over RTSP to Frigate on the local network. A motion mask covers the bed, so movement there never wakes the detector.
Only surviving motion reaches the model, and only the cat class is
tracked. People are discarded outright.
An alert requires the object to be inside a hand-drawn floor polygon for three consecutive frames. The bed sits entirely outside it.
A small Flask service listens on MQTT, checks the armed state and a cooldown, then sends one Telegram message to a family group.
The parts that were actually interesting to get right.
An alert needs the label cat and presence in the floor zone.
The bed is outside that polygon, so no confidence threshold or model update can
produce an alert about the sleeping human.
The threshold is deliberately permissive. A false ping costs one message; a miss costs a mattress. Optimising for precision would optimise the wrong metric.
The wizard refuses bad input outright. The listener never crashes on a malformed MQTT payload or a failed send — it logs and carries on. A dead listener is worse than a missed message.
The arm/disarm page ships a strict CSP with no script-src. Every
animation — the cat waking, pupils dilating, lids lifting — is CSS keyframes on
clipped SVG geometry.
Recording is disabled by design — this is a bedroom. Frigate binds to localhost only, the MQTT broker publishes no host ports at all, and the camera has no route to the internet. The only outbound traffic is a line of text to Telegram. No footage is stored, streamed or uploaded, ever.
Which is why there is no hosted demo. A public instance would mean streaming a bedroom to the internet, and every architectural decision here exists to prevent exactly that. The code and the reasoning are the deliverable.
Worth being honest about, because these are the real limits.
The bugs that took longest were not in my code. These are the ones I would want to talk about.
Frigate silently rejected my config and booted into safe mode with no camera. The
cause was upstream: it decides whether zone coordinates are relative or absolute
with p > "1.0" — a string comparison. "1.000"
sorts after "1.0", so it treated fractions as pixel values and threw.
Writing 1.0 instead of 1.000 fixed it. Found by reading
the library source, not the logs.
The MQTT container crash-looped on exit 14. Its entrypoint starts as root and
drops privileges, which needs SETUID and SETGID — the
exact capabilities my cap_drop: [ALL] had removed. Rather than grant
them back, I start the container as the unprivileged user directly, so there is no
privilege drop to perform. Strictly stronger than the original.
The suite reported all-clear while silently skipping checks. Two causes:
${#VAR:-0} is invalid bash and aborted the enclosing block, and the
FPS probe called python3 — which on Windows resolves to a Microsoft
Store stub that exits non-zero. A green test run that isn't running the tests is
worse than a red one.
The control page ships default-src 'none' with no
script-src. The wake-up sequence — sleepy lids, pupils dilating —
is CSS keyframes on SVG geometry, with the eyelid as a rectangle sliding inside a
clipping mask so it can never paint outside the eye. State transitions ride on the
page load that the form POST already causes.