Module M13 - Tent Camera Dashboard
Files: packages/grow_camera.yaml (this module). packages/grow_mapping_tool.yaml
gained 4 new mapping keys (camera1-camera4). dashboards/dashboard_1_configuration.yaml
gained a “Cameras (M13, optional)” mapping card. dashboards/dashboard_2_control_panel.yaml
gained a new “Cameras” view.
What this module is - and deliberately isn’t
A dedicated live-monitoring dashboard for up to 4 cameras (IP or USB) in the grow tent. Pure monitoring: no recording/archival, no motion detection, no security-style overlays (no REC light, no timestamp watermark) - explicitly requested as “focus on monitoring, not security gimmicks” (decision R19). It has zero effect on any control logic elsewhere in the project; Climate, Irrigation, Light, Air, CO2, and Energy all run exactly as before.
The central architecture decision: this module never manages camera connections
Home Assistant already has mature, native integrations for talking to
cameras - re-implementing stream URLs, authentication, and connection
testing as custom input_text helpers and automations would mean either:
- Violating R1 (native-only, no custom backend integration) - a real connection test for an RTSP/HTTP/MJPEG stream isn’t something you can build robustly out of pure automations/templates; doing it properly would mean writing an actual custom integration with its own config flow, and the project’s own review already rejected that path once (Section 3, “a real custom integration with a config flow would be more convenient, but contradicts R1”).
- Storing a camera’s username/password in an
input_texthelper, which means storing it in plaintext-visible entity state - readable by anyone with dashboard or Recorder access. Home Assistant’s own camera integrations store credentials in protected config-entry storage instead.
So this module works one layer up: you set up each camera as a normal
Home Assistant camera.* entity first (see below), and this module only
stores a reference to that entity, per slot:
input_text.grow_camera<N>_entity_id # e.g. "camera.tent_canopy_rtsp" - the ONLY # thing this project stores about the # camera's actual connectioninput_text.grow_camera<N>_name # free-text display name, e.g. "Canopy"input_boolean.grow_camera<N>_enabled # show/hide this slot on the dashboardThis is the exact same pattern already used for the GitHub Deploy-Key/PAT
(never touches this project, entered directly into the Git-Pull add-on -
Section 7.1) and for every other “assign a real HA entity to a Grow
Assistant function” field on Dashboard 1 (Section 3’s Helper-Mapping-Konzept).
Once both an IP camera and a USB camera exist as a camera.* entity, this
module treats them identically - it never needs to know or care which
kind a given slot originally was.
Setting up a camera in Home Assistant first (outside this project)
Do this in Home Assistant’s own UI before mapping a slot on Dashboard 1 - none of these steps involve GrowAssistant or this session in any way.
IP camera with RTSP/HTTP/MJPEG - Generic Camera
Settings -> Devices & Services -> Add Integration -> Generic Camera. Config-flow/UI-based (YAML setup for this integration has been deprecated for several Home Assistant versions). You’ll need a still-image URL and/or a stream URL, and optionally a username/password if the camera requires auth. Home Assistant validates connectivity during setup - if it fails here, the camera won’t work as a GrowAssistant slot either, so this is effectively your “test connection” step.
IP camera with ONVIF support - ONVIF
Settings -> Devices & Services -> Add Integration -> ONVIF. Supports auto-discovery and, on capable cameras, PTZ. Known limitation: the camera must expose at least one H.264 video profile - H.265-only cameras are a documented ONVIF-integration compatibility gap. Check your camera’s profile support before relying on ONVIF; Generic Camera (above) is the fallback if the camera exposes a direct RTSP/MJPEG URL independent of ONVIF.
USB camera (connected locally to the Home Assistant host)
There’s no first-class native HA integration for USB/V4L2 webcams. Two documented community paths:
- MotionEye add-on (recommended primary path). Install the official Home Assistant OS add-on “MotionEye”, add your USB camera as a “Local V4L2 Camera” source inside it. MotionEye republishes it as an MJPEG/RTSP stream, which you then add to Home Assistant exactly like an IP camera, via Generic Camera above. Reported in the community as more stable and lower-CPU than the alternative below - use this unless you have a specific reason not to.
- go2rtc (alternative). Bundled into Home Assistant Core since the
2024.11 release. Add a stream entry
(
streams: usbcam: "ffmpeg:device?video=/dev/video0&...") to expose the USB camera as RTSP/WebRTC. Reported to have latency advantages over MotionEye but more prone to artifacts/instability on weaker host hardware (e.g. a Raspberry Pi already busy running Home Assistant itself) - prefer MotionEye unless you’ve specifically outgrown it.
Either way, once the camera shows up as a normal camera.* entity in Home
Assistant (check Settings -> Devices & Services -> Entities, filter
camera.), it’s ready to be mapped exactly like an IP camera.
Mapping a slot in GrowAssistant
- Developer Tools -> Actions -> “Grow Assistant: Set Mapping”
(
script.grow_assign_mapping, same generic tool used for every other mapping field in this project - seedocs/docs/mapping-tool.md). - Mapping field -> “Camera 1” (through “Camera 4”).
- Target entity -> search for and pick your
camera.*entity. - Perform Action.
input_text.grow_camera<N>_entity_idis set for you. - On Dashboard 2’s Cameras view, give the slot a display Name and turn its Enabled toggle on.
Live view: how it actually works
Which camera a tile shows is picked dynamically at runtime from the
input_text.grow_camera<N>_entity_id mapping - but stock Lovelace cards
(picture-glance, picture-entity) take a literal entity: value in
their YAML, they cannot template which entity to show. The only stock card
that templates its content with Jinja is Markdown, and Home Assistant’s
markdown renderer safely passes through basic HTML including <img> tags.
So each camera tile is Jinja that resolves the mapped camera, then embeds:
<img src="{{ state_attr(eid, 'entity_picture') }}&v={{ tick }}">entity_picture is Home Assistant’s own signed camera-proxy snapshot URL -
no manual token handling needed. tick is sensor.grow_camera_refresh_tick,
a trigger-based template sensor that changes every 5 seconds independent of
the camera’s own state, appended as a cache-busting query parameter so the
browser actually re-fetches the image on a schedule rather than only when
the camera entity’s state/attributes happen to change.
This is a deliberate MVP tradeoff, not an oversight: you get a
frequently refreshing snapshot (every ~5 seconds), not a continuous
WebRTC-grade video feed, and tapping a tile doesn’t open Home Assistant’s
native more-info/fullscreen dialog the way a real picture-glance card
would - both are the price of being able to template which camera a tile
shows. A HACS custom:auto-entities “cards mode” approach (using
filter.template to emit a fully dynamic card object) was investigated
first and rejected: it depends on a documented, open upstream bug
(thomasloven/lovelace-auto-entities issue #570) when filter.template
is combined with full card objects, which would make the whole view
fragile against a future library update. The Markdown+<img> approach
uses zero HACS cards for the live tiles themselves - only native
Home Assistant functionality.
Status logic
Per slot, sensor.grow_camera<N>_status is one of 4 states, checked in
this order:
- Not Configured -
input_text.grow_camera<N>_entity_idis empty. - Disabled - an entity is mapped, but
input_boolean.grow_camera<N>_enabledis off. - Online - the mapped
camera.*entity’s own state isidle,recording, orstreaming(mirrored intobinary_sensor.grow_camera<N>_available). - Offline - anything else (
unavailable, the entity was deleted, etc.). Home Assistant’s camera domain doesn’t expose a distinct “connection error” state beyondunavailable, so a genuine connection failure and a simply-unreachable camera both show as “Offline” here - check the camera’s own integration (Generic Camera/ONVIF/MotionEye) in Home Assistant directly for the specific underlying error.
sensor.grow_camera_registry aggregates all 4 slots as one machine-readable
list (cameras attribute: slot/name/entity_id/enabled/status per camera),
the same pattern as sensor.grow_entity_registry (Section 3 of the Phase 0
plan) - available for any future automation/dashboard that wants a single
place to read every slot’s state without querying 4 entities individually.
Dashboard layout
- 1 Camera - the slot picked by
input_select.grow_camera_focusshown large, the other 3 slots as small thumbnails below. - 2 Cameras - always Slots 1 and 2 side by side (fixed, not “whichever 2 are enabled” - Lovelace YAML can’t reorder cards based on which slots happen to be enabled without a fragile custom-card workaround, so this MVP keeps the mapping to physical slots simple and predictable: use Slots 1/2 for whichever two cameras you want in this layout).
- 4 Cameras - all 4 slots in a 2x2 grid, regardless of which are actually enabled - an unconfigured/disabled slot shows its own grey placeholder tile instead of being hidden, so you always know the slot exists and can see at a glance why it’s not showing a picture.
Later expansion stages (not MVP)
- HACS “Advanced Camera Card” (
dermotduffy/advanced-camera-card) for a proper carousel/fullscreen multi-camera UI and native tap interactions- the same staged approach already used for
mushroom/card-modin this project (ship the simple native version first, add a HACS card later once the functional workflow has proven itself).
- the same staged approach already used for
- Continuous live streaming (WebRTC/
camera_proxy_stream) instead of the 5-second-refresh snapshot, once verified stable across browsers/viewers. - Snapshot/timelapse archival (e.g. one image per hour, for a grow timelapse).
- PTZ control, for ONVIF cameras that support it.
- More than 4 cameras, or multiple tents at once - belongs with Phase 10’s multi-grow management, not this module.
- Deliberately not planned, even later: motion detection or any other security-camera feature - out of scope by explicit request (“focus on monitoring, not security gimmicks”).
Rollout checklist
- Deploy
packages/grow_camera.yamland the updatedpackages/grow_mapping_tool.yaml, check config, restart (new helpers need one the first time). - Re-import
dashboards/dashboard_1_configuration.yaml(new “Cameras (M13, optional)” mapping card) anddashboards/dashboard_2_control_panel.yaml(new “Cameras” view). - Set up each physical camera in Home Assistant itself first (Generic
Camera / ONVIF / MotionEye+Generic Camera / go2rtc - see above) - confirm
it shows a working
camera.*entity in Settings -> Devices & Services -> Entities before mapping it here. - Map each slot via Developer Tools -> Actions -> “Grow Assistant: Set Mapping” (Camera 1-4), then give it a Name and turn Enabled on, on the Cameras view.
- Try all 3 layouts and confirm each configured camera’s tile shows a green “Online” dot and an actually-updating image; an unconfigured slot should show a grey “Not set up” placeholder, never a broken-image icon.