Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ const recording: HassEntity = {
supported_features: 0,
},
};
const mqttRecording: HassEntity = {
entity_id: "select.front_door_event_select",
state: "Ding 1",
attributes: {
friendly_name: "Front Door Event Select",
eventId: "demo-event-1",
recordingUrl: query.get("mqtt_recording") === "missing"
? "<Recording Not Found>"
: "/demo/pending-recording.mp4?event=1",
},
};
const live: HassEntity = {
entity_id: "camera.live_view",
state: "idle",
Expand Down Expand Up @@ -171,6 +182,13 @@ let hass: HomeAssistant = {
entity_id: recording.entity_id,
platform: "ring",
},
[mqttRecording.entity_id]: {
entity_id: mqttRecording.entity_id,
platform: "mqtt",
device_id: "demo-ring-device",
unique_id: "demo-ring-device_event_select",
original_name: "Event Select",
},
[live.entity_id]: {
entity_id: live.entity_id,
platform: query.get("live_platform") || "ring",
Expand Down Expand Up @@ -208,6 +226,7 @@ let hass: HomeAssistant = {
},
states: {
[recording.entity_id]: recording,
[mqttRecording.entity_id]: mqttRecording,
[live.entity_id]: live,
[snapshot.entity_id]: snapshot,
[snapshotRefresh.entity_id]: snapshotRefresh,
Expand All @@ -230,6 +249,13 @@ let hass: HomeAssistant = {
) {
queueMicrotask(() => window.demoRefreshSnapshot());
}
if (
domain === "select"
&& service === "select_option"
&& target?.entity_id === mqttRecording.entity_id
) {
queueMicrotask(() => window.demoRefreshRecording());
}
},
connection: {
subscribeMessage: async <T>(callback: (message: T) => void) => {
Expand Down Expand Up @@ -292,7 +318,9 @@ const previewSource: PreviewSource =
: "last_recording";
card.setConfig({
type: "custom:ring-view",
recording_entity: recording.entity_id,
recording_entity: query.get("recording_source") === "mqtt"
? mqttRecording.entity_id
: recording.entity_id,
live_entity: live.entity_id,
snapshot_entity: snapshot.entity_id,
last_activity_entity:
Expand Down Expand Up @@ -367,6 +395,26 @@ window.demoRefreshSnapshot = () => {
dialogManager.updateHass(hass);
};

window.demoRefreshRecording = () => {
const current = hass.states[mqttRecording.entity_id];
if (!current) return;
hass = {
...hass,
states: {
...hass.states,
[mqttRecording.entity_id]: {
...current,
attributes: {
...current.attributes,
recordingUrl: `/demo/pending-recording.mp4?event=${Date.now()}`,
},
},
},
};
card.hass = hass;
dialogManager.updateHass(hass);
};

declare global {
interface Window {
demoActiveStreams?: number;
Expand All @@ -380,5 +428,6 @@ declare global {
}>;
demoSetEntityState: (entityId: string, state: string) => void;
demoRefreshSnapshot: () => void;
demoRefreshRecording: () => void;
}
}
2,322 changes: 1,258 additions & 1,064 deletions dist/ring-view.js

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Every Ring View setting is available through Home Assistant's visual card config
| Option | UI configuration | Default | Accepted values | Purpose |
| --- | --- | --- | --- | --- |
| `type` | No — added automatically | Required | `custom:ring-view` | Identifies the custom card. Added automatically by the card picker. |
| `recording_entity` | Yes — Config tab | Required | `camera.*` entity ID | Camera entity containing the latest recording. |
| `recording_entity` | Yes — Config tab | Required | `camera.*` or `select.*` entity ID | Official Ring Last recording camera, or the Ring-MQTT Event Select entity whose current option identifies the recording to play. |
| `live_entity` | Yes — Config tab | Required | `camera.*` entity ID | Camera entity that starts the Ring live view. |
| `snapshot_entity` | Yes — Dashboard card | Not set | `camera.*` entity ID | Device snapshot camera, such as the snapshot entity created by Ring-MQTT. Used by snapshot previews and preferred for manual snapshots when configured and available. |
| `name` | Yes — Card appearance | Entity name | Text | Optional label used instead of the recording entity's friendly name. |
Expand Down Expand Up @@ -381,14 +381,32 @@ treated as media capture times.

## Choosing camera entities

Use the official Ring integration's last-recording and live-view entities. With a suitable Ring subscription, Home Assistant provides both but [disables Last recording by default](https://www.home-assistant.io/integrations/ring/#camera).
For the official Ring integration, use its last-recording and live-view entities. With a suitable Ring subscription, Home Assistant provides both but [disables Last recording by default](https://www.home-assistant.io/integrations/ring/#camera).

1. Open **Settings → Devices & services → Ring**, then open your Ring device and its entity list.
2. Show disabled entities. Before enabling it, the disabled camera entry is **Last recording**; enable it if you have the required Ring subscription. The camera enabled by default is **Live view**.
3. Open each entry and copy its exact entity ID into the corresponding Ring View field.

The `_last_recording` and `_live_view` suffixes in this guide are examples, not requirements. Home Assistant entity IDs can be changed and may be assigned differently. In one [field report covering fresh 2K and 4K Ring doorbell installations](https://community.home-assistant.io/t/ring-doorbell-live-stream/855118/8), both camera entries appeared alike and neither entity ID used the expected suffix. Identify the entities by their roles and default enabled state rather than relying on their displayed names or ID suffixes.

For Ring-MQTT recording playback, choose the device's **Event Select** entity as
`recording_entity`. Its current option—such as **Ding 1**, **Motion 1**, or a
transcoded variant—determines which event Ring View plays. Ring View reads the
published `recordingUrl` directly. If the signed URL is missing, is within 30
seconds of expiry, or fails once in the browser, Ring View re-selects the
current option and waits up to 15 seconds for Ring-MQTT to publish a different,
playable URL. The wait is driven by Home Assistant state updates; it does not
poll. Closing the viewer, changing modes, hiding the page, or suspending an
inline card cancels the wait and cannot start a late recording.

Ring-MQTT can report **Recording Not Found** or **Transcoding in Progress**
instead of a URL. Ring View treats both as unavailable media and shows Retry
after the refresh wait. If the original event format is not playable in the
browser, select that event's **(Transcoded)** option in Home Assistant and try
again. Configure the Ring-MQTT snapshot camera as `snapshot_entity`; it supplies
the still image used behind the Last recording view because Event Select is not
a camera entity.

For **two-way audio**, `live_entity` must be the official Ring `live_view` camera. A Ring-MQTT or Generic Camera RTSP entity does not expose the microphone return path Ring View needs. Ring-MQTT can still supply the optional `snapshot_entity` alongside the official Ring live camera.

The editor warns when two-way audio is selected for an unsupported live camera. The viewer then uses Home Assistant's normal player without **Hold to talk**.
Expand Down
15 changes: 10 additions & 5 deletions docs/playback-and-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ Live or you select Live. Only the active mode's renderer remains mounted. The
highlight around a mode icon identifies the selected view, so Recording stays
highlighted when its video is paused or has ended.

For recordings, Ring View uses the current ephemeral `video_url` when
available, with Home Assistant's camera renderer as the alternative if it is
missing or fails. Normal Live uses Home Assistant's camera renderer and its
WebRTC/HLS/MJPEG selection. Enabling supported two-way audio uses Ring View's
single-session WebRTC player for Live instead.
For an official Ring recording camera, Ring View uses the current ephemeral
`video_url` when available, with Home Assistant's camera renderer as the
alternative if it is missing or fails. A Ring-MQTT Event Select source instead
uses its direct `recordingUrl`. Ring View refreshes an absent, nearly expired,
or once-failed URL by re-selecting the current event and waiting for an explicit
entity update; it never passes a select entity to the camera renderer. Normal
Live uses Home Assistant's camera renderer and its WebRTC/HLS/MJPEG selection.
Enabling supported two-way audio uses Ring View's single-session WebRTC player
for Live instead.

Home Assistant's application-level dialog manager owns the viewer and its
history entry, independently of responsive dashboard card rearrangements.
Expand Down Expand Up @@ -96,6 +100,7 @@ an operation result. Configuration warnings stay in the visual editor.
| Snapshot works with Ring-MQTT but not the official Live camera | Keep the Ring-MQTT camera configured as `snapshot_entity`. Ring View prefers it automatically. The official fallback can save only the still image exposed by that entity, not pixels from the active WebRTC player, so it may be unavailable or show the latest recording. |
| Missing or indistinguishable camera entries | Show disabled entities on the Ring device. Before enabling it, the camera disabled by default is Last recording; the camera enabled by default is Live view. Copy the exact IDs and do not rely on their suffixes. See [Choosing camera entities](configuration.md#choosing-camera-entities). |
| Last recording remains on an old clip | On some newer wired Ring cameras, 24/7 recording can leave Home Assistant's `last_recording` entity stuck on an old event. This is an [upstream Home Assistant issue](https://github.com/home-assistant/core/issues/176299), not a Ring View cache: the card's preview and Last recording view can update only when Home Assistant supplies a new `last_video_id` or `video_url`. The issue reporter found that disabling 24/7 recording and using periodic snapshots restored updates. Live view, talkback, and Ding alerts use separate entities and remain available. |
| Ring-MQTT recording says it is unavailable | Open the Event Select entity and choose the intended event. **Recording Not Found** means Ring-MQTT has no clip for that slot. **Transcoding in Progress** can require more than one 15-second attempt; wait and retry. If the original MP4 is rejected by the browser, choose the matching **(Transcoded)** option. Signed URLs are refreshed automatically when Ring-MQTT publishes their replacement. |
| Old card behavior after updating | Confirm the installed HACS version and refresh the frontend. In the iOS Companion app, **Clear Web View Cache** may be necessary. |
| No new phone preview | Check the blueprint's selected recording entity and whether a new `last_video_id` appeared within two minutes. See [notifications](notifications.md). |

Expand Down
14 changes: 12 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ The unit suite covers configuration defaults and validation, manual snapshot
source selection, paths, timezone-aware filenames and service feedback,
snapshot timestamp parsing and freshest-preview fallbacks, official and
Ring-MQTT last-activity timestamp formats, sibling resolution and localization,
Ring-MQTT on-demand snapshot discovery and event-driven refresh waits, native
editor structure and progressive
Ring-MQTT on-demand snapshot discovery and event-driven refresh waits,
Ring-MQTT Event Select identification, signed-URL expiry, event-driven recording
refresh and cancellation, camera-poster selection, native editor structure and progressive
dashboard fields, entity and talkback capability states, unsupported-camera
fallback, doorbell alerts, timeout invalidation, passive-dashboard privacy,
single-renderer switching, close teardown, disconnect teardown, single-offer
Expand Down Expand Up @@ -120,6 +121,15 @@ Verify each item on current stable Home Assistant and, where practical, the prev
both reuse the same centered viewer status display as snapshot failures,
remain clear of the visitor-action dock, and disappear without leaving a
second pill or toast behind.
26. Set `recording_entity` to a renamed Ring-MQTT Event Select and choose a
playable event. Confirm Last recording uses the direct MP4 and the configured
snapshot camera as its poster, without mounting Home Assistant's camera
renderer. Test a missing and an expired `recordingUrl`: Ring View must call
`select.select_option` with the entity's current option, wait for a changed
playable URL, and cancel cleanly when switching to Live, closing, hiding the
page, or suspending the card. Also test Recording Not Found, Transcoding in
Progress, a service failure, a 15-second timeout, and a browser playback
error followed by one refresh attempt. No signed URL may appear in logs.

## Visual matrix

Expand Down
11 changes: 10 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,20 @@ function assertCameraEntity(
}
}

function assertRecordingSource(value: unknown): asserts value is string {
if (
typeof value !== "string"
|| (!value.startsWith("camera.") && !value.startsWith("select."))
) {
throw new Error(localize(undefined, "config.recording_source_required"));
}
}

export function validateConfig(config: RingViewConfig): void {
if (!config || typeof config !== "object") {
throw new Error(localize(undefined, "config.invalid"));
}
assertCameraEntity(config.recording_entity, "config.recording_entity");
assertRecordingSource(config.recording_entity);
assertCameraEntity(config.live_entity, "config.live_entity");
if (config.snapshot_entity !== undefined && config.snapshot_entity !== "") {
assertCameraEntity(config.snapshot_entity, "config.snapshot_entity");
Expand Down
Loading
Loading