diff --git a/docs/configuration/cameras/live-provider.md b/docs/configuration/cameras/live-provider.md index 9c8eeb29..961c2c8c 100644 --- a/docs/configuration/cameras/live-provider.md +++ b/docs/configuration/cameras/live-provider.md @@ -55,13 +55,15 @@ cameras: # [...] ``` -| Option | Default | Description | -| ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `entity` | | The entity to use when `mode` is set to `entity`. This entity is expected to have an `entity_picture` attribute that specifies the image URL. | -| `entity_parameters` | | Optional URL parameters to add to the URL generated for entity-based modes (i.e. when `mode` is `camera` or `entity`). | -| `mode` | `auto` | Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera entity using either `camera_entity` or `webrtc_card.entity` in that order of precedence), `entity` to show an image associated with a named entity (see the `entity` parameter below), or `screensaver` (to show an [embedded image](https://github.com/dermotduffy/advanced-camera-card/blob/main/src/images/iris-screensaver.jpg)). If `auto`, the mode is chosen automatically based on whether `url` or `entity` parameters have been specified. | -| `refresh_seconds` | 1 | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. | -| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestamp]` cache-busting value will be added automatically. | +| Option | Default | Description | +| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `entity` | | The entity to use when `mode` is set to `entity`. This entity is expected to have an `entity_picture` attribute that specifies the image URL. | +| `entity_parameters` | | Optional URL parameters to add to the URL generated for entity-based modes (i.e. when `mode` is `camera` or `entity`). | +| `mode` | `auto` | Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera entity using either `camera_entity` or `webrtc_card.entity` in that order of precedence), `entity` (to show an image associated with a named entity, see the `entity` parameter below), `default` (to show the [default embedded image](https://github.com/dermotduffy/advanced-camera-card/blob/main/src/images/iris-screensaver.jpg)), or `screensaver` (to show a random image from [picsum.photos](https://picsum.photos/), refreshing every 60s by default). If `auto`, the mode is chosen automatically based on whether `url` or `entity` parameters have been specified. | +| `refresh_seconds` | `auto` | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. When set to `auto`, uses `1` for all modes except `screensaver` which uses `60`. | +| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestamp]` cache-busting value will be added automatically. | + +[](../common/screensaver-warning.md ':include') [](../common/proxy-warning.md ':include') @@ -150,7 +152,7 @@ cameras: live_provider: image image: mode: auto - refresh_seconds: 1 + refresh_seconds: auto url: 'https://path/to/image.png' entity: image.office_person entity_parameters: 'width=400&height=200' diff --git a/docs/configuration/common/screensaver-warning.md b/docs/configuration/common/screensaver-warning.md new file mode 100644 index 00000000..d4368092 --- /dev/null +++ b/docs/configuration/common/screensaver-warning.md @@ -0,0 +1,4 @@ +> [!WARNING] +> The `screensaver` mode requires internet access to fetch images from +> [picsum.photos](https://picsum.photos/). If picsum.photos is unavailable, the +> card falls back to the default embedded image. diff --git a/docs/configuration/image.md b/docs/configuration/image.md index da52edac..dd672e33 100644 --- a/docs/configuration/image.md +++ b/docs/configuration/image.md @@ -7,19 +7,21 @@ image: # [...] ``` -| Option | Default | Description | -| ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `actions` | | [Actions](actions/README.md) to use for the `image` view. | -| `entity` | | The entity to use when `mode` is set to `entity`. This entity is expected to have an `entity_picture` attribute that specifies the image URL. | -| `entity_parameters` | | Optional URL parameters to add to the URL generated for entity-based modes (i.e. when `mode` is `camera` or `entity`). | -| `mode` | `auto` | Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera entity using either `camera_entity` or `webrtc_card.entity` in that order of precedence), `entity` to show an image associated with a named entity (see the `entity` parameter below), or `screensaver` (to show an [embedded image](https://github.com/dermotduffy/advanced-camera-card/blob/main/src/images/iris-screensaver.jpg)). If `auto`, the mode is chosen automatically based on whether `url` or `entity` parameters have been specified. | -| `proxy` | | Proxy configuration for `url` mode images. See [proxy](#proxy) below. | -| `refresh_seconds` | 1 | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. | -| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestamp]` cache-busting value will be added automatically. | +| Option | Default | Description | +| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `actions` | | [Actions](actions/README.md) to use for the `image` view. | +| `entity` | | The entity to use when `mode` is set to `entity`. This entity is expected to have an `entity_picture` attribute that specifies the image URL. | +| `entity_parameters` | | Optional URL parameters to add to the URL generated for entity-based modes (i.e. when `mode` is `camera` or `entity`). | +| `mode` | `auto` | Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera entity using either `camera_entity` or `webrtc_card.entity` in that order of precedence), `entity` (to show an image associated with a named entity, see the `entity` parameter below), `default` (to show the [default embedded image](https://github.com/dermotduffy/advanced-camera-card/blob/main/src/images/iris-screensaver.jpg)), or `screensaver` (to show a random image from [picsum.photos](https://picsum.photos/), refreshing every 60s by default). If `auto`, the mode is chosen automatically based on whether `url` or `entity` parameters have been specified. | +| `proxy` | | Proxy configuration for `url` mode images. See [proxy](#proxy) below. | +| `refresh_seconds` | `auto` | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. When set to `auto`, uses `1` for all modes except `screensaver` which uses `60`. | +| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestamp]` cache-busting value will be added automatically. | > [!NOTE] > When `mode` is set to `camera` this is effectively providing the same image as the `image` [live provider](cameras/live-provider.md) would show in the live camera carousel. +[](common/screensaver-warning.md ':include') + ## `proxy` Configures whether and how the image URL is proxied via @@ -51,7 +53,7 @@ image: ```yaml image: mode: auto - refresh_seconds: 1 + refresh_seconds: auto url: 'https://path/to/image.png' entity: image.office_person entity_parameters: 'width=400&height=200' diff --git a/docs/configuration/view.md b/docs/configuration/view.md index 18f2ca16..5f068621 100644 --- a/docs/configuration/view.md +++ b/docs/configuration/view.md @@ -12,7 +12,7 @@ view: | `actions` | | [Actions](actions/README.md) to use for all views, individual actions may be overriden by view-specific actions. | | `camera_select` | `current` | The [view](view.md?id=supported-views) to show when a new camera is selected (e.g. in the camera menu). If `current` the view is unchanged when a new camera is selected. | | `dim` | `false` | Whether or not to 'dim' the brightness of the card (by 25%) if the card `interaction_seconds` has expired (i.e. card has been left unattended for that period of time). | -| `default` | `auto` | The view to show in the card by default. If `auto`, the card will choose `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (screensaver). The default camera is the first one listed. See [Supported Views](view.md?id=supported-views). | +| `default` | `auto` | The view to show in the card by default. If `auto`, the card will choose `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (default embedded image). The default camera is the first one listed. See [Supported Views](view.md?id=supported-views). | | `default_reset` | | The circumstances and behavior that cause the card to reset to the default view. See [`default_reset`](#default_reset). | | `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions.md?id=interaction) or with `default_reset.after_interaction` to reset the view after the interaction is complete. | | `keyboard_shortcuts` | See [usage](../usage/keyboard-shortcuts.md) for defaults. | Configure keyboard shortcuts. See [`keyboard_shortcuts`](#keyboard_shortcuts). | @@ -170,7 +170,7 @@ This card supports several different views. | `snapshots` | Shows a gallery of snapshots for this camera. | | `timeline` | Shows an event timeline. | -The default view is `auto`. It will select `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (screensaver). You can override this with `view.default`. +The default view is `auto`. It will select `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (default embedded image). You can override this with `view.default`. > [!NOTE] > When using views in a [`view` condition](conditions.md?id=view), the single-item viewer views (`clip`, `snapshot`, `review`, `recording`) are translated internally to `media` once the relevant media is fetched. You may need to match on `media` rather than the original view name in your condition. diff --git a/src/components/image-updating-player.ts b/src/components/image-updating-player.ts index 1edef9a7..1e589439 100644 --- a/src/components/image-updating-player.ts +++ b/src/components/image-updating-player.ts @@ -42,12 +42,21 @@ import { renderMessage } from './message.js'; // See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py . const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000; +const SCREENSAVER_URL = 'https://picsum.photos'; +const SCREENSAVER_REFRESH_SECONDS = 60; +const SCREENSAVER_DEFAULT_WIDTH = 500; +const SCREENSAVER_DEFAULT_HEIGHT = Math.round(SCREENSAVER_DEFAULT_WIDTH / (16 / 9)); + +// Round requested dimensions to the nearest multiple to avoid fetching a unique +// image for every minor size difference. +const SCREENSAVER_DIMENSION_BUCKET = 100; + export const resolveImageMode = (options?: { imageConfig?: ImageBaseConfig; cameraConfig?: CameraConfig; }): Exclude => { if (!options?.imageConfig?.mode) { - return 'screensaver'; + return 'default'; } else if (options?.imageConfig?.mode !== 'auto') { return options.imageConfig.mode; } @@ -60,7 +69,7 @@ export const resolveImageMode = (options?: { return 'camera'; } - return 'screensaver'; + return 'default'; }; /** @@ -96,7 +105,7 @@ export class AdvancedCameraCardImageUpdatingPlayer private _cachedValueController = new CachedValueController( this, - () => this.imageConfig?.refresh_seconds ?? null, + () => this._getEffectiveRefreshSeconds(), () => this._getImageSource(), () => dispatchMediaPlayEvent(this), () => dispatchMediaPauseEvent(this), @@ -235,6 +244,16 @@ export class AdvancedCameraCardImageUpdatingPlayer super.disconnectedCallback(); } + private _getScreensaverURL(): string { + const bucket = (value: number, fallback: number): number => + Math.ceil((value || fallback) / SCREENSAVER_DIMENSION_BUCKET) * + SCREENSAVER_DIMENSION_BUCKET; + const w = bucket(this.clientWidth, SCREENSAVER_DEFAULT_WIDTH); + const h = bucket(this.clientHeight, SCREENSAVER_DEFAULT_HEIGHT); + const urlObj = new URL(`${SCREENSAVER_URL}/${w}/${h}`); + return this._buildCacheBustURL(urlObj, 'query-string'); + } + /** * Handle document visibility changes. */ @@ -294,6 +313,19 @@ export class AdvancedCameraCardImageUpdatingPlayer return url; } + private _getEffectiveRefreshSeconds(): number | null { + const seconds = this.imageConfig?.refresh_seconds ?? null; + if (seconds !== 'auto') { + return seconds; + } + + const mode = resolveImageMode({ + imageConfig: this.imageConfig, + cameraConfig: this.cameraConfig, + }); + return mode === 'screensaver' ? SCREENSAVER_REFRESH_SECONDS : 1; + } + private _getRelevantEntityForMode(mode: Exclude): string | null { return mode === 'camera' ? getCameraEntityFromConfig(this.cameraConfig) @@ -341,6 +373,10 @@ export class AdvancedCameraCardImageUpdatingPlayer } } + if (mode === 'screensaver') { + return this._getScreensaverURL(); + } + return defaultImage; } @@ -397,7 +433,7 @@ export class AdvancedCameraCardImageUpdatingPlayer const mediaLoadedInfo = createMediaLoadedInfo(ev, { mediaPlayerController: this._mediaPlayerController, capabilities: { - supportsPause: !!this.imageConfig?.refresh_seconds, + supportsPause: !!this._getEffectiveRefreshSeconds(), }, }); // Avoid the media being reported as repeatedly loading unless the @@ -412,12 +448,10 @@ export class AdvancedCameraCardImageUpdatingPlayer imageConfig: this.imageConfig, cameraConfig: this.cameraConfig, }); - if (mode === 'camera' || mode === 'entity') { - // In camera or entity mode, the user has likely not made an - // error, but HA may be unavailble, so show the stock image. - // Don't let the URL override the stock image in this case, as - // this could create an error loop if that URL subsequently - // failed to load. + if (mode === 'camera' || mode === 'entity' || mode === 'screensaver') { + // In camera, entity, or screensaver mode the user has likely + // not made an error, but the source may be unavailable, so show + // the stock image. this._forceSafeImage(true); } else if (mode === 'url') { this._imageLoadError = true; diff --git a/src/config/schema/common/image.ts b/src/config/schema/common/image.ts index 1f883402..471b0b2c 100644 --- a/src/config/schema/common/image.ts +++ b/src/config/schema/common/image.ts @@ -2,16 +2,25 @@ import { z } from 'zod'; export const imageBaseConfigDefault = { mode: 'auto' as const, - refresh_seconds: 1, + refresh_seconds: 'auto' as const, }; -const IMAGE_MODES = ['auto', 'camera', 'entity', 'screensaver', 'url'] as const; +const IMAGE_MODES = [ + 'auto', + 'camera', + 'default', + 'entity', + 'screensaver', + 'url', +] as const; export type ImageMode = (typeof IMAGE_MODES)[number]; export const imageBaseConfigSchema = z.object({ mode: z.enum(IMAGE_MODES).default(imageBaseConfigDefault.mode), - refresh_seconds: z.number().min(0).default(imageBaseConfigDefault.refresh_seconds), + refresh_seconds: z + .union([z.literal('auto'), z.number().min(0)]) + .default(imageBaseConfigDefault.refresh_seconds), url: z.string().optional(), entity: z.string().optional(), diff --git a/src/editor.ts b/src/editor.ts index 9c74b30b..843bd137 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -714,8 +714,12 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard private _imageModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'camera', label: localize('config.common.image.modes.camera') }, + { value: 'default', label: localize('config.common.image.modes.default') }, { value: 'entity', label: localize('config.common.image.modes.entity') }, - { value: 'screensaver', label: localize('config.common.image.modes.screensaver') }, + { + value: 'screensaver', + label: localize('config.common.image.modes.screensaver'), + }, { value: 'url', label: localize('config.common.image.modes.url') }, ]; diff --git a/src/localize/languages/de.json b/src/localize/languages/de.json index f6251ac5..633b7129 100644 --- a/src/localize/languages/de.json +++ b/src/localize/languages/de.json @@ -133,8 +133,9 @@ "entity": "Entität für Entitäts-Modus", "entity_parameters": "Abfrage-Parameter für Entitäts-basierende Photo URLs (z.B. width=1920&height=1080)", "modes": { + "default": "Standard-Eingebettetes Bild", "entity": "Entität mit entity_picture Attribut", - "screensaver": "Eingebettetes Bild für Bildschirmschoner" + "screensaver": "Zufälliges Bild" } }, "media_action_conditions": { diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 34943a89..04130a08 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -298,8 +298,9 @@ "mode": "Image mode", "modes": { "camera": "Home Assistant camera snapshot of camera entity", + "default": "Default embedded image", "entity": "Entity with entity_picture attribute", - "screensaver": "Embedded screensaver image", + "screensaver": "Random image", "url": "Arbitrary image specified by URL" }, "proxy": { diff --git a/src/localize/languages/pl.json b/src/localize/languages/pl.json index d71e94a0..8c3e79c4 100644 --- a/src/localize/languages/pl.json +++ b/src/localize/languages/pl.json @@ -265,8 +265,9 @@ "mode": "Tryb obrazu", "modes": { "camera": "Zrzut (snapshot) z encji kamery Home Assistant", + "default": "Domyślny wbudowany obraz", "entity": "Encja z atrybutem entity_picture", - "screensaver": "Wbudowany wygaszacz ekranu", + "screensaver": "Losowy obraz", "url": "Dowolny obraz z URL" }, "refresh_seconds": "Sekundy do odświeżenia (0=nigdy)", diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts index 0aea2ef2..903cd1c0 100644 --- a/tests/config/types.test.ts +++ b/tests/config/types.test.ts @@ -28,7 +28,7 @@ describe('config defaults', () => { }, image: { mode: 'auto', - refresh_seconds: 1, + refresh_seconds: 'auto', }, live_provider: 'auto', motioneye: { @@ -83,7 +83,7 @@ describe('config defaults', () => { ssl_verification: 'auto', enabled: false, }, - refresh_seconds: 1, + refresh_seconds: 'auto', zoomable: true, }, live: {