The Shinobi NVR custom component for Home Assistant creates a camera, an image, a status sensor, an object-count sensor, enable/recording switches, and (where PTZ Control is enabled) a set of PTZ buttons for every monitor. It also browses recordings through Home Assistant's Media Browser and fires real-time shinobi_detector_trigger events over Shinobi's own socket.io server.
ke) the API Key belongs to.
Copy the custom_components/shinobi folder into Home Assistant's config/custom_components/ directory (or add it as a custom repository in HACS), then restart Home Assistant.
Create a dedicated API Key for Home Assistant rather than reusing an existing one, so its permissions can be scoped to exactly what the integration uses. In Shinobi: Account → API. Every permission below was confirmed against the actual route handlers in Shinobi's source, not assumed from the permission's name:
set_mode service, PTZ buttons and ptz service, and the trigger_motion service.
embed_url attribute.
uid, which Shinobi's socket.io handshake requires alongside the key itself.
Not needed for anything the integration does: Delete Videos, Edit Monitors, Get Alarms, Edit Alarms, Get Logs.
8080), API Key, and Group Key.
Each monitor becomes a standard Home Assistant camera entity: live view through HA's built-in stream dialog, a JPEG snapshot as the entity picture, and casting support. On modest hardware this built-in dialog can look jittery — it re-muxes Shinobi's HLS output through Home Assistant's own stream component, an extra hop Shinobi's own player doesn't need. For a smoother live view, use the embed method described below instead of the camera entity's default dialog.
Every camera.* entity exposes an embed_url attribute (Developer Tools → States) — Shinobi's own embeddable player page. It runs hls.js/flv.js directly in the browser, so there is no re-muxing on the Home Assistant side at all.
http://[SHINOBI HOST]:[PORT]/[API KEY]/embed/[GROUP KEY]/[MONITOR ID]/fullscreen|jquery|gui
jquery — required. The embed player script references jQuery at load time and never starts without it; Shinobi only loads jQuery when this flag is present.
fullscreen — adds a fullscreen toggle button, useful inside an iframe.
gui — adds the stream-chrome CSS (timestamp overlay, unmute button).
If the embed page loads but the video never starts, and the server is reached through a reverse proxy or a different hostname than Shinobi auto-detects, add relative to the addon list and a ?host= query parameter pointing at the address the browser should use to reach Shinobi:
http://[SHINOBI HOST]:[PORT]/[API KEY]/embed/[GROUP KEY]/[MONITOR ID]/fullscreen|jquery|gui|relative?host=http://[SHINOBI HOST]:[PORT]/
Shinobi has no push notification for "a monitor was added" or "a monitor was deleted", so this is handled by polling — the following happens automatically, without restarting Home Assistant or reloading the integration:
unavailable; the device disappears from Settings → Devices.
One case is not automatic: changing a setting on a monitor that Home Assistant already knows about — most notably, turning PTZ Control on or off for an existing monitor. Since Home Assistant only decides whether to create PTZ buttons the first time it sees a given monitor id, toggling Control later does not retroactively add or remove them. To pick up that kind of in-place change, reload the integration:
A clean per-camera card — embedded live stream on top, a row of tiles for status and controls below — is a vertical-stack with a Webpage (iframe) card followed by a Grid of Tile cards. Use the camera's own embed_url attribute for the iframe, and swap your_camera_name below for the actual entity id slug shown in Developer Tools → States.
type: vertical-stack
cards:
- type: iframe
url: >-
http://[SHINOBI HOST]:[PORT]/[API KEY]/embed/[GROUP KEY]/[MONITOR ID]/fullscreen|jquery|gui
aspect_ratio: 56%
- type: grid
columns: 2
square: false
cards:
- type: tile
entity: switch.your_camera_name_recording
- type: tile
entity: switch.your_camera_name_enabled
- type: tile
entity: sensor.your_camera_name_status
- type: tile
entity: binary_sensor.your_camera_name_motion
For several cameras, adding one card per camera by hand gets old fast. On an editable dashboard, use the ⋮ menu → Edit in YAML (view-level, not per-card) and paste one vertical-stack block like the one above per camera.
Home Assistant fires a shinobi_detector_trigger event on the bus for every motion/object detection, in real time rather than on the polling interval, using Shinobi's own socket.io detector_on mechanism — no MQTT broker or topic configuration needed. Event data includes monitor_id, time, reason, confidence, matrices, and a best-effort snapshot_url (resolved from Shinobi's timelapse-frame index around the moment of detection, populated only for genuine object detections).
trigger:
- platform: event
event_type: shinobi_detector_trigger
event_data:
reason: object
condition:
- condition: template
value_template: "{{ 'person' in trigger.event.data.matrices | map(attribute='tag') | list }}"
action:
- service: notify.mobile_app_your_phone
data:
message: "Person detected on {{ trigger.event.data.monitor_id }}"
data:
image: "{{ trigger.event.data.snapshot_url }}"
jquery addon flag is missing from the URL. embed_url already includes it; this only matters if the URL was built by hand.
shinobi_detector_trigger never fires — confirm the API Key has Auth via Socket enabled in Shinobi (Account → API); Home Assistant logs a warning at startup if it cannot resolve a uid for the key.
snapshot_url is often null — expected on a monitor with a sparse timelapse-capture interval; Home Assistant only searches a window around the detection's own timestamp, it does not take a fresh snapshot. Configure a shorter timelapse interval on monitors used for notification images.