From b3940685c4e58fbb084a882a456c7e57c636c6b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 19:10:37 +0000 Subject: [PATCH 1/2] Add camera selector dropdown to doorbell page Replaces the hardcoded CAMERA_NAME with a CAMERAS list. The page renders a dropdown when more than one camera is configured and remembers the choice in localStorage; switching tears down and reopens the WebRTC connection. https://claude.ai/code/session_013XZ1vmgk78k2PEQ5DmJhF3 --- pi/README.md | 14 ++++++++++---- pi/server.py | 54 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/pi/README.md b/pi/README.md index 4e959cd..902a520 100644 --- a/pi/README.md +++ b/pi/README.md @@ -52,13 +52,19 @@ sudo journalctl -u doorbell -f # live logs Add to home screen (Chrome menu -> Add to home screen) for an app-like experience. -## Changing the camera +## Choosing the camera -`server.py` near the top: -```js -const CAMERA_NAME = "front_door"; +`server.py` near the top has the list of cameras the page offers: +```python +CAMERAS = ["front_door"] ``` +Each name must match a `go2rtc.streams` entry in +`frigate_config/config.yml` and the camera must be `enabled: true` in +Frigate. With more than one entry the page shows a dropdown above the +talk button; the choice is remembered per-browser in `localStorage`. The +first entry is the default for new visitors. + After editing: ```bash sudo systemctl restart doorbell diff --git a/pi/server.py b/pi/server.py index 5fd342b..857dd37 100644 --- a/pi/server.py +++ b/pi/server.py @@ -20,6 +20,11 @@ from flask_sock import Sock app = Flask(__name__) sock = Sock(app) +# Cameras to offer in the page's dropdown. First entry is the default. +# Names must match go2rtc stream names in frigate_config/config.yml and +# the corresponding camera must be enabled in Frigate. +CAMERAS = ["front_door"] + PAGE = """ @@ -46,6 +51,9 @@ PAGE = """ .row{display:flex;gap:8px} .row button{flex:1;padding:10px;background:#333;color:#fff;border:none; border-radius:8px;font-size:13px} + .row select{flex:1;padding:10px;background:#333;color:#fff;border:none; + border-radius:8px;font-size:14px;appearance:none;-webkit-appearance:none; + text-align:center;text-align-last:center} #status{font-size:12px;color:#888;text-align:center;min-height:1em} @@ -55,6 +63,9 @@ PAGE = """
+
@@ -67,21 +78,43 @@ PAGE = """ @@ -192,7 +226,7 @@ setupPTT(); @app.route('/') def index(): - return render_template_string(PAGE) + return render_template_string(PAGE, cameras=CAMERAS) @app.route('/healthz') From 09d5b28e3de23af0f6d6533173646d7120f1a4d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 21:23:17 +0000 Subject: [PATCH 2/2] Revert camera dropdown; add PEER_LINKS for sibling Pis The dropdown allowed switching the video feed but PTT always pointed at this Pi's local speaker, so picking a non-co-located camera could have let a user talk into the wrong room. Each Pi is now hardcoded to the camera at its own location via CAMERA_NAME, with optional PEER_LINKS to render quick-jump buttons to sibling doorbell Pis at other URLs. https://claude.ai/code/session_013XZ1vmgk78k2PEQ5DmJhF3 --- pi/README.md | 35 ++++++++++++++++++------ pi/server.py | 76 ++++++++++++++++++++++++---------------------------- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/pi/README.md b/pi/README.md index 902a520..9ac170a 100644 --- a/pi/README.md +++ b/pi/README.md @@ -54,22 +54,41 @@ experience. ## Choosing the camera -`server.py` near the top has the list of cameras the page offers: +Each Pi is hardcoded to one camera -- the one whose mic and speaker are +physically co-located with this Pi. The PTT button on this page only +talks to *this* Pi's speaker, so mixing cameras here would let a misclick +talk into the wrong room. + +`server.py` near the top: ```python -CAMERAS = ["front_door"] +CAMERA_NAME = "front_door" ``` -Each name must match a `go2rtc.streams` entry in +The name must match a `go2rtc.streams` entry in `frigate_config/config.yml` and the camera must be `enabled: true` in -Frigate. With more than one entry the page shows a dropdown above the -talk button; the choice is remembered per-browser in `localStorage`. The -first entry is the default for new visitors. - -After editing: +Frigate. After editing: ```bash sudo systemctl restart doorbell ``` +## Multiple doorbell Pis + +Run one copy of this app per Pi, each on its own subdomain (e.g. +`frontdoor.yourdomain.com`, `backdoor.yourdomain.com`). Add a Caddy +block per subdomain pointing at that Pi's LAN IP -- same shape as the +existing `doorbell.yourdomain.com` block in `../caddy/Caddyfile`. + +To render quick-jump buttons to the other Pis at the top of the page, +fill in `PEER_LINKS` near the top of `server.py`: +```python +PEER_LINKS = [ + {"label": "Back door", "url": "https://backdoor.yourdomain.com"}, + {"label": "Squirrel", "url": "https://squirrel.yourdomain.com"}, +] +``` +Leave it as `[]` (the default) and the row is hidden. Restart with +`sudo systemctl restart doorbell` after editing. + ## Audio stack ALSA-only -- no PipeWire/PulseAudio. Lighter on the Pi Zero. If you ever diff --git a/pi/server.py b/pi/server.py index 857dd37..e2d22db 100644 --- a/pi/server.py +++ b/pi/server.py @@ -20,10 +20,17 @@ from flask_sock import Sock app = Flask(__name__) sock = Sock(app) -# Cameras to offer in the page's dropdown. First entry is the default. -# Names must match go2rtc stream names in frigate_config/config.yml and -# the corresponding camera must be enabled in Frigate. -CAMERAS = ["front_door"] +# Camera this Pi corresponds to. Must match a go2rtc stream name in +# frigate_config/config.yml. The PTT button talks to the speaker physically +# attached to this Pi, so this should be the camera at the same location. +CAMERA_NAME = "front_door" + +# Optional jump-links to sibling doorbell Pis (each running its own copy of +# this app, hardcoded to its own camera). Rendered as a row of buttons above +# the PTT button when non-empty. Leave empty if there are no other Pis. +PEER_LINKS = [ + # {"label": "Back door", "url": "https://backdoor.yourdomain.com"}, +] PAGE = """ @@ -51,9 +58,9 @@ PAGE = """ .row{display:flex;gap:8px} .row button{flex:1;padding:10px;background:#333;color:#fff;border:none; border-radius:8px;font-size:13px} - .row select{flex:1;padding:10px;background:#333;color:#fff;border:none; - border-radius:8px;font-size:14px;appearance:none;-webkit-appearance:none; - text-align:center;text-align-last:center} + .row a{flex:1;padding:10px;background:#333;color:#fff;border-radius:8px; + font-size:13px;text-decoration:none;text-align:center; + display:flex;align-items:center;justify-content:center} #status{font-size:12px;color:#888;text-align:center;min-height:1em} @@ -63,9 +70,7 @@ PAGE = """
- +
@@ -78,43 +83,33 @@ PAGE = """ @@ -226,7 +220,7 @@ setupPTT(); @app.route('/') def index(): - return render_template_string(PAGE, cameras=CAMERAS) + return render_template_string(PAGE, camera_name=CAMERA_NAME, peers=PEER_LINKS) @app.route('/healthz')