diff --git a/public/static/icons/spinner-48x48.svg b/public/static/icons/spinner-48x48.svg
new file mode 100644
index 000000000000..a83f127af0b9
--- /dev/null
+++ b/public/static/icons/spinner-48x48.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts
index 11280c3833e1..387d0c83b33d 100644
--- a/src/components/ha-hls-player.ts
+++ b/src/components/ha-hls-player.ts
@@ -180,7 +180,15 @@ class HaHLSPlayer extends LitElement {
let playlist_url: string;
if (match !== null && matchTwice === null) {
// Only send the regular playlist url if we match exactly once
- playlist_url = new URL(match[3], this._url).href;
+ // In case we arrive here with a relative URL, we need to provide a valid
+ // base/absolute URL to avoid the URL() constructor throwing an error.
+ let base_url: string;
+ try {
+ base_url = new URL(this._url).href;
+ } catch (error) {
+ base_url = new URL(this._url, window.location.href).href;
+ }
+ playlist_url = new URL(match[3], base_url).href;
} else {
playlist_url = this._url;
}
diff --git a/src/data/preview.ts b/src/data/preview.ts
index 0018f1fe1c46..a26b030b8f2c 100644
--- a/src/data/preview.ts
+++ b/src/data/preview.ts
@@ -1,7 +1,7 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
-const HAS_CUSTOM_PREVIEW = ["template"];
+const HAS_CUSTOM_PREVIEW = ["generic_camera", "template"];
export interface GenericPreview {
state: string;
diff --git a/src/dialogs/config-flow/previews/flow-preview-generic.ts b/src/dialogs/config-flow/previews/flow-preview-generic.ts
index 436bf1f35cd6..baa839895a47 100644
--- a/src/dialogs/config-flow/previews/flow-preview-generic.ts
+++ b/src/dialogs/config-flow/previews/flow-preview-generic.ts
@@ -11,7 +11,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
@customElement("flow-preview-generic")
-class FlowPreviewGeneric extends LitElement {
+export class FlowPreviewGeneric extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
@@ -26,9 +26,9 @@ class FlowPreviewGeneric extends LitElement {
@property() public stepData!: Record;
- @state() private _preview?: HassEntity;
+ @state() protected _preview?: HassEntity;
- @state() private _error?: string;
+ @state() protected _error?: string;
private _unsub?: Promise;
diff --git a/src/dialogs/config-flow/previews/flow-preview-generic_camera.ts b/src/dialogs/config-flow/previews/flow-preview-generic_camera.ts
new file mode 100644
index 000000000000..ece1af723cc6
--- /dev/null
+++ b/src/dialogs/config-flow/previews/flow-preview-generic_camera.ts
@@ -0,0 +1,39 @@
+import { html } from "lit";
+import { customElement } from "lit/decorators";
+import { FlowPreviewGeneric } from "./flow-preview-generic";
+import "../../../components/ha-hls-player";
+
+@customElement("flow-preview-generic_camera")
+class FlowPreviewGenericCamera extends FlowPreviewGeneric {
+ protected override render() {
+ if (!this._preview) {
+ return html`${this._error}`;
+ }
+
+ const stillUrl = this._preview.attributes.still_url;
+ const streamUrl = this._preview.attributes.stream_url;
+
+ return html` ${stillUrl
+ ? html`Still image:
+
+
+
`
+ : ""}
+ ${streamUrl
+ ? html`Stream:
+ `
+ : ""}`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "flow-preview-generic_camera": FlowPreviewGenericCamera;
+ }
+}