Aiming and cat height are the half of a camera install people photograph. The other half is a table with one row per camera, and the rule that no camera exists in any config unless it is in the table. Ours is below, followed by the naming schema, the line it draws between the house and the show, and the two checklists a new camera goes through. The Frigate blocks that consume it are on the camera-setup page and in full on the annotated config.
The table
Every camera gets a DHCP reservation on the router the day it is unboxed, so the address in config.yml never moves. All eleven are on one 192.168.1.0/24; the host numbers are yours to choose, and the rest of this site writes them as 192.168.1.<N>.
| Frigate id · job | Address · public label · reach |
|---|---|
cat_water_bowl · fountain, from above | 192.168.1.<N> · Water Cam · public stills, mosaic, Shorts (always eligible) |
cat_food_bowl · kibble and wet dish, body and bowl | 192.168.1.<N> · Food Cam · public stills, Shorts |
cat_treat_dispenser · the dispenser the treat button fires | 192.168.1.<N> · Treat Cam · public stills, mosaic, Shorts |
cat_scratching_pad · overhead of the whole room (the Court) | 192.168.1.<N> · Scratch Cam on the proxy, "Court" everywhere else · public stills, Shorts |
helen_food_face · head-on at the bowl | 192.168.1.<N> · Face Cam · not on the website; the single-camera 24/7 stream and the mosaic's top-left pane, Shorts |
bed_cam · the bed | 192.168.1.<N> · Bed · mosaic only |
playroom_cam | 192.168.1.<N> · Playroom · Frigate only, detecting |
bed_cam_2 | 192.168.1.<N> · audio trial; enabled: false |
front_door, carport · house | 192.168.1.<N> · no label · record only, 21 days, detect off, never leaves the box |
back_yard · house, the one non-Tapo camera | 192.168.1.<N> · paths /live/ch0 and /live/ch1 instead of /stream1//stream2 · record only, never leaves the box |
Notice the one leftover. cat_scratching_pad was a scratching pad; the camera was tilted up in August and is now the room's overhead. The Frigate id stayed, the proxy still labels it "Scratch Cam", and everything editorial calls it the Court. The cameras page says to trust the picture and rename later, but renaming a Frigate camera means renaming its recordings directory, its events and every reference in four files, so the id stays and the alias list below absorbs it. Name the job right the first time.
The naming rule, and the four places a name lives
Frigate id: cat_<job> for a Helen station, <place>_cam for an angle that is not a station, plain <place> for the house. Public label: <Job> Cam. Lower-case, underscores, no numbers unless there is a second angle on the same job. The id is typed into four places and each one has to match exactly:
go2rtc:
streams:
cat_water_bowl:
- rtsp://<CAMERA_USER>:<CAMERA_PASSWORD>@192.168.1.<N>:554/stream1
cat_water_bowl_sub:
- rtsp://<CAMERA_USER>:<CAMERA_PASSWORD>@192.168.1.<N>:554/stream2
cameras:
cat_water_bowl:
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/cat_water_bowl_sub
roles:
- detect
- path: rtsp://127.0.0.1:8554/cat_water_bowl
roles:
- record
detect:
width: 640
height: 360
fps: 2
enabled: true
audio:
enabled: false
objects:
track:
- cat
- person
HELEN_CAMS=cat_water_bowl:Water Cam,cat_food_bowl:Food Cam,cat_treat_dispenser:Treat Cam,cat_scratching_pad:Scratch Cam
STATIONS = {
"water": {
"title": "Water Cam",
"touch": "her water bowl",
"aliases": ("cat_water_bowl", "water_bowl", "waterbowl", "cat_water", "water_cam"),
"weight": 3.0, # the drinking shot is the money shot
"always_keep": True, # eligible at any length, as today
},
# ...
# cat_scratching_pad was re-aimed into an OVERHEAD of the whole room.
# The Frigate id is legacy; the station is "court".
"court": {
"title": "Court Cam",
"touch": "her court",
"aliases": ("cat_scratching_pad", "scratching_pad", "scratch_pad",
"cat_scratch", "scratch_cam", "scratching_post",
"court_cam", "overhead"),
"weight": 1.5,
"always_keep": False,
},
}
HOUSE_DENY = ("front_door", "back_yard", "driveway", "porch", "remote_", ...) # ignored at discovery
NEVER_PUBLISH = frozenset({...}) # bedroom cameras: may record, may never publish. Checked first.
powershell -ExecutionPolicy Bypass -File C:\ProgramData\HelenLive\tile-live.ps1 -Cam cat_water_bowl -Port 5103
The pipeline is the forgiving one: it reads the camera list from Frigate's /api/config at every run and matches ids against aliases, so a second angle named cat_water_bowl_side is picked up as the Water station with no code change. The proxy is the strict one: an id not in HELEN_CAMS is a 403, full stop. Discovery is meant to be easy and publication hard.
House versus public: the allowlist is a privacy boundary
Three of the eleven cameras watch the house: a front door, a carport, a back yard. They are in Frigate because Frigate is a good NVR and the same disk is already spinning. They differ from the Helen cameras in four ways, and every one of the four is a fence:
- No go2rtc entry. Frigate pulls them straight from the camera. Nothing else on the box can ask for them by name on
:8554, so no tile script, no proxy and no pipeline can restream one by accident. detect: enabled: false. No events, so nothing for the Shorts pipeline to find even if it looked.- Not in
HELEN_CAMS, not inLIVE_CAM_PATHS. The two allowlists on the public-proxy page refuse them by name. - In
HOUSE_DENY. The pipeline ignores them at discovery, and any camera that is a bedroom is in a separate deny list that is checked before anything else and cannot be overridden by an alias.
The Helen cameras have their own gradations. Face is on YouTube and in the mosaic but not on the website. Bed is in the mosaic and nowhere else. Playroom is in Frigate and nowhere else. The rule that generates all of this is the one on the cameras page: no bedrooms, no people in frame, one sitting room. A camera's reach is a decision made once, in the table, and the configs are just the table written out.
What to block at the router
One caveat: we have not audited this end to end on our own router, so it is written as advice rather than as a description of our box. Consumer Wi-Fi cameras phone home. Tapo cameras register with TP-Link's cloud so the app works from anywhere, and they will keep trying whether or not you use the app. RTSP on the LAN does not need any of that. So:
- Block the cameras' outbound internet at the router, by reservation. Most routers can do this per device ("parental control", "block internet access", or a firewall rule from the camera's address to WAN). RTSP to the NVR host keeps working and the app's remote view stops.
- If your router does VLANs, put the cameras on one with no route to WAN and a single allow rule to the NVR host on TCP 554. If it does not, the per-device block above is most of the benefit.
- Keep the NVR host's Frigate port off the WAN. No port forward to
:5000or:8554, ever. The public site reaches Frigate over Tailscale; see the proxy page. - Firmware updates then need the block lifted for an afternoon. Write that down next to the table so it is a chore and not a surprise.
The one credential
Every camera gets the same purpose-made RTSP account, created in the Tapo app as a "camera account" (TP-Link's FAQ 2680) and not the login you use for the app. It lives in a password manager and in exactly one file: the go2rtc block of config.yml, plus the direct inputs of the three house cameras. Everything downstream, from Frigate's own camera blocks to the tile scripts to the proxy, reads rtsp://127.0.0.1:8554/<id> or http://<TAILSCALE_IP>:5000 with no camera password at all. That is the practical reason to put go2rtc in front of the cameras even if you never restream anything: the secret is written once, and a config backup of any other file on the system contains nothing worth stealing. On this site it is always <CAMERA_USER> / <CAMERA_PASSWORD>.
Two checklists
| Check | How |
|---|---|
| Job written on tape on the camera | Water, Food, Treat, Court, Face. Before it goes on the wall. |
| Row in the table | Job, id per the schema, reserved address, public label or "none". |
| DHCP reservation made | Router → reservations → the camera's MAC → the address in the row. Reboot the camera and confirm it comes back on that address. |
| Camera account created | The shared RTSP account, same on every camera; from the password manager, not from memory. |
| Both streams open in VLC | /stream1 and /stream2 at the reserved address. Which-camera page, step five. |
| Outbound blocked at the router | Per-device block, then confirm VLC still works. |
| go2rtc entry, then camera block | Two entries under go2rtc: streams: (<id>, <id>_sub), then the camera block reading 127.0.0.1:8554. House cameras: camera block only, direct URLs, detect: enabled: false. |
| Frigate agrees | /api/stats shows the id with camera_fps ≈ 2 for a Helen cam and skipped_fps 0.0. |
| Check | How |
|---|---|
| The table says public | If the row says "none", stop. Change the table first, on purpose, and say why. |
| Nothing private in frame | Sit with the live view for a day. Entrances, windows onto neighbours, a person's chair. The mosaic dropped the Court pane for the Face pane for exactly this reason. |
| Proxy allowlist | Add <id>:<Label> to HELEN_CAMS, restart the proxy. |
| Site allowlist | Add the path to LIVE_CAM_PATHS, deploy the backend. A second, separate change, so it cannot happen by accident. |
| Pipeline station | Confirm station_of(id) is a real station and may_publish says yes; both fail closed for anything unknown. |
| Refusals still refuse | Every other id still returns 404 from the site and 403 from the proxy. |