Repository: dermotduffy/frigate-hass-card
Pull request #1518 (2024-09-08 23:51:12)

Add support for arbitrary image entities
* Add support for use of arbitrary image entities (as long as they have an `entity_picture` attribute). Replaces #1478 .
* Have the image live provider support all arguments for the image view .



Patch:
diff --git a/docs/configuration/cameras/README.md b/docs/configuration/cameras/README.md
index e1da9a0c..af6d895b 100644
--- a/docs/configuration/cameras/README.md
+++ b/docs/configuration/cameras/README.md
@@ -356,7 +356,11 @@ cameras:
   - camera_entity: camera.back_yard
     live_provider: image
     image:
+      mode: auto
       refresh_seconds: 1
+      url: 'https://path/to/image.png'
+      entity: image.office_person
+      entity_parameters: 'width=400&height=200'
   - camera_entity: camera.office_motioneye
     motioneye:
       images:
diff --git a/docs/configuration/cameras/live-provider.md b/docs/configuration/cameras/live-provider.md
index a9b01412..af8ddb28 100644
--- a/docs/configuration/cameras/live-provider.md
+++ b/docs/configuration/cameras/live-provider.md
@@ -44,10 +44,13 @@ cameras:
       # [...]
 ```
 
-| Option            | Default | Description                                                                                                                                                                                                                                                                                                                                                          |
-| ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `refresh_seconds` | 1       | The image will be refreshed at least every `refresh_seconds`. `0` implies no refreshing.                                                                                                                                                                                                                                                                             |
-| `url`             |         | **Advanced**: A static image URL to be fetched in lieu of the Home Assistant image for the given camera. This may be useful for advanced configurations where the camera image is being provided by some non-Home Assistant system. This will also set the temporary loading image used when `show_image_during_load` is set to true under the `live` configuration. |
+| 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 stock Frigate card logo](https://github.com/dermotduffy/frigate-hass-card/blob/main/src/images/frigate-bird-in-sky.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=[timestsamp]` query parameter will be automatically added to all URLs such that the image will not be cached by the browser.                                                                                                                                                                                                                                                         |
 
 ## `jsmpeg`
 
diff --git a/docs/configuration/image.md b/docs/configuration/image.md
index 9141db6d..2727e289 100644
--- a/docs/configuration/image.md
+++ b/docs/configuration/image.md
@@ -7,12 +7,14 @@ image:
   # [...]
 ```
 
-| Option            | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
-| ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `actions`         |         | [Actions](actions/README.md) to use for the `image` view.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
-| `mode`            | `url`   | Mode of the the `image` view. Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera using either `camera_entity` or `webrtc_card.entity` in that order of precedence), or `screensaver` (to show an [embedded stock Frigate card logo](https://github.com/dermotduffy/frigate-hass-card/blob/main/src/images/frigate-bird-in-sky.jpg)). In either `url` or `camera` mode, the `screensaver` content is used as a fallback if a URL is not specified or cannot be derived. |
-| `refresh_seconds` | 0       | 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=[timestsamp]` query parameter will be automatically added to all URLs such that the image will not be cached by the browser.                                                                                                                                                                                                      |
+| 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 stock Frigate card logo](https://github.com/dermotduffy/frigate-hass-card/blob/main/src/images/frigate-bird-in-sky.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=[timestsamp]` query parameter will be automatically added to all URLs such that the image will not be cached by the browser.                                                                                                                                                                                                                                                         |
 
 ?> 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.
 
@@ -22,8 +24,11 @@ image:
 
 ```yaml
 image:
-  mode: url
-  refresh_seconds: 0
+  mode: auto
+  refresh_seconds: 1
+  url: 'https://path/to/image.png'
+  entity: image.office_person
+  entity_parameters: 'width=400&height=200'
   actions:
     entity: light.office_main_lights
     tap_action:
diff --git a/src/card-controller/status-bar-item-manager.ts b/src/card-controller/status-bar-item-manager.ts
index f37bd791..3727c237 100644
--- a/src/card-controller/status-bar-item-manager.ts
+++ b/src/card-controller/status-bar-item-manager.ts
@@ -48,7 +48,7 @@ export class StatusBarItemManager {
     const engineLogoIcon = cameraMetadata?.engineLogo ?? null;
     const title = options?.view?.is('live')
       ? cameraMetadata?.title ?? null
-      : options?.view?.isAnyMediaView()
+      : options?.view?.isViewerView()
         ? options?.view.queryResults?.getSelectedResult()?.getTitle() ?? null
         : null;
     const resolution = options?.mediaLoadedInfo
diff --git a/src/components/image.ts b/src/components/image.ts
index 63335d86..c731a299 100644
--- a/src/components/image.ts
+++ b/src/components/image.ts
@@ -12,8 +12,9 @@ import { customElement, property } from 'lit/decorators.js';
 import { live } from 'lit/directives/live.js';
 import { createRef, ref, Ref } from 'lit/directives/ref.js';
 import isEqual from 'lodash-es/isEqual';
+import { CameraManager } from '../camera-manager/manager.js';
 import { CachedValueController } from '../components-lib/cached-value-controller.js';
-import { CameraConfig, ImageViewConfig } from '../config/types.js';
+import { CameraConfig, ImageMode, ImageViewConfig } from '../config/types.js';
 import defaultImage from '../images/frigate-bird-in-sky.jpg';
 import { localize } from '../localize/localize.js';
 import imageStyle from '../scss/image.scss';
@@ -28,7 +29,6 @@ import {
 } from '../utils/media-info.js';
 import { View } from '../view/view.js';
 import { dispatchErrorMessageEvent } from './message.js';
-import { CameraManager } from '../camera-manager/manager.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;
@@ -119,14 +119,12 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
       return false;
     }
 
-    const cameraEntity = this._getCameraEntity();
-    if (
-      changedProps.has('hass') &&
-      changedProps.size == 1 &&
-      this.imageConfig?.mode === 'camera' &&
-      cameraEntity
-    ) {
-      if (isHassDifferent(this.hass, changedProps.get('hass'), [cameraEntity])) {
+    const relevantEntity = this._getRelevantEntityForMode(
+      this._resolveMode(this.imageConfig?.mode),
+    );
+
+    if (changedProps.has('hass') && changedProps.size == 1 && relevantEntity) {
+      if (isHassDifferent(this.hass, changedProps.get('hass'), [relevantEntity])) {
         // If the state of the camera entity has changed, remove the cached
         // value (will be re-calculated in willUpdate). This is important to
         // ensure a changed access token is immediately used.
@@ -158,6 +156,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
       }
     }
 
+    const relevantEntity = this._getRelevantEntityForMode(
+      this._resolveMode(this.imageConfig?.mode),
+    );
+
     // If the camera or view changed, immediately discard the old value (view to
     // allow pressing of the image button to fetch a fresh image). Likewise, if
     // the state is not acceptable, discard the old value (to allow a stock or
@@ -165,8 +167,7 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
     if (
       changedProps.has('cameraConfig') ||
       changedProps.has('view') ||
-      (this.imageConfig?.mode === 'camera' &&
-        !this._getAcceptableState(this._getCameraEntity()))
+      (relevantEntity && !this._getAcceptableState(relevantEntity))
     ) {
       this._cachedValueController?.clearValue();
     }
@@ -245,24 +246,75 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
   /**
    * Build a working absolute image URL that the browser will not cache.
    * @param url An input URL (may be relative to document origin)
-   * @returns A new URL (absolute, will not be browser cached).
+   * @returns A new URL as a string (absolute, will not be browser cached).
    */
-  protected _buildImageURL(url: string): string {
-    const urlObj = new URL(url, document.baseURI);
-    urlObj.searchParams.append('_t', String(Date.now()));
-    return urlObj.toString();
+  protected _buildImageURL(url: URL): string {
+    url.searchParams.append('_t', String(Date.now()));
+    return url.toString();
+  }
+
+  protected _addQueryParametersToURL(url: URL, parameters?: string): URL {
+    if (parameters) {
+      const searchParams = new URLSearchParams(parameters);
+      for (const [key, value] of searchParams.entries()) {
+        url.searchParams.append(key, value);
+      }
+    }
+    return url;
+  }
+
+  protected _getRelevantEntityForMode(mode: Exclude<ImageMode, 'auto'>): string | null {
+    return mode === 'camera'
+      ? this._getCameraEntity()
+      : mode === 'entity'
+        ? this.imageConfig?.entity ?? null
+        : null;
+  }
+
+  protected _resolveMode(mode?: ImageMode): Exclude<ImageMode, 'auto'> {
+    if (!mode) {
+      return 'screensaver';
+    } else if (mode !== 'auto') {
+      return mode;
+    }
+
+    const cameraEntity = this._getCameraEntity();
+    if (this.imageConfig?.entity) {
+      return 'entity';
+    } else if (this.imageConfig?.url) {
+      return 'url';
+    } else if (cameraEntity) {
+      return 'camera';
+    }
+
+    return 'screensaver';
   }
 
   protected _getImageSource(): string {
-    if (this.hass && this.imageConfig?.mode === 'camera') {
+    const mode = this._resolveMode(this.imageConfig?.mode);
+
+    if (this.hass && mode === 'camera') {
       const state = this._getAcceptableState(this._getCameraEntity());
       if (state?.attributes.entity_picture) {
-        return this._buildImageURL(state.attributes.entity_picture);
+        const urlObj = new URL(state.attributes.entity_picture, document.baseURI);
+        this._addQueryParametersToURL(urlObj, this.imageConfig?.entity_parameters);
+        return this._buildImageURL(urlObj);
       }
     }
-    if (this.imageConfig?.mode !== 'screensaver' && this.imageConfig?.url) {
-      return this._buildImageURL(this.imageConfig.url);
+
+    if (this.hass && mode === 'entity' && this.imageConfig?.entity) {
+      const state = this._getAcceptableState(this.imageConfig?.entity);
+      if (state?.attributes.entity_picture) {
+        const urlObj = new URL(state.attributes.entity_picture, document.baseURI);
+        this._addQueryParametersToURL(urlObj, this.imageConfig?.entity_parameters);
+        return this._buildImageURL(urlObj);
+      }
     }
+
+    if (mode === 'url' && this.imageConfig?.url) {
+      return this._buildImageURL(new URL(this.imageConfig.url, document.baseURI));
+    }
+
     return defaultImage;
   }
 
@@ -300,13 +352,15 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
               }
             }}
             @error=${() => {
-              if (this.imageConfig?.mode === 'camera') {
-                // In camera 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.
+              const mode = this._resolveMode(this.imageConfig?.mode);
+              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.
                 this._forceSafeImage(true);
-              } else if (this.imageConfig?.mode === 'url') {
+              } else if (mode === 'url') {
                 // In url mode, the user likely specified a URL that cannot be
                 // resolved. Show an error message.
                 dispatchErrorMessageEvent(this, localize('error.image_load_error'), {
diff --git a/src/components/live/live-image.ts b/src/components/live/live-image.ts
index a4d20cbd..92c833e1 100644
--- a/src/components/live/live-image.ts
+++ b/src/components/live/live-image.ts
@@ -64,15 +64,8 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
     return html`
       <frigate-card-image
         ${ref(this._refImage)}
-        .imageConfig=${{
-          mode: this.cameraConfig.image.url ? ('url' as const) : ('camera' as const),
-          refresh_seconds: this.cameraConfig.image.refresh_seconds,
-          url: this.cameraConfig.image.url,
-
-          // The live provider will take care of zoom and layout options.
-          zoomable: false,
-        }}
         .hass=${this.hass}
+        .imageConfig=${this.cameraConfig.image}
         .cameraConfig=${this.cameraConfig}
       >
       </frigate-card-image>
diff --git a/src/config/types.ts b/src/config/types.ts
index 952777bb..3f7abb28 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -917,25 +917,25 @@ export type PTZControlsConfig = z.infer<typeof ptzControlsConfigSchema>;
 // `image` card view.
 // *************************************************************************
 
-const imageBaseConfigDefault = {
+const imageConfigDefault = {
+  mode: 'auto' as const,
   refresh_seconds: 1,
 };
 
+const IMAGE_MODES = ['auto', 'camera', 'entity', 'screensaver', 'url'] as const;
+export type ImageMode = (typeof IMAGE_MODES)[number];
+
 const imageBaseConfigSchema = z.object({
+  mode: z.enum(IMAGE_MODES).default(imageConfigDefault.mode),
+
+  refresh_seconds: z.number().min(0).default(imageConfigDefault.refresh_seconds),
+
   url: z.string().optional(),
-  refresh_seconds: z.number().min(0).default(imageBaseConfigDefault.refresh_seconds),
+  entity: z.string().optional(),
+  entity_parameters: z.string().optional(),
 });
 
-const IMAGE_MODES = ['screensaver', 'camera', 'url'] as const;
-const imageConfigDefault = {
-  mode: 'url' as const,
-  ...imageBaseConfigDefault,
-};
-
 const imageConfigSchema = imageBaseConfigSchema
-  .extend({
-    mode: z.enum(IMAGE_MODES).default(imageConfigDefault.mode),
-  })
   .merge(actionsSchema)
   .default(imageConfigDefault);
 export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
@@ -1127,8 +1127,6 @@ const go2rtcConfigSchema = z.object({
   stream: z.string().optional(),
 });
 
-const liveImageConfigSchema = imageBaseConfigSchema;
-
 const webrtcCardConfigSchema = z
   .object({
     entity: z.string().optional(),
@@ -1302,9 +1300,6 @@ const cameraConfigDefault = {
   frigate: {
     client_id: 'frigate' as const,
   },
-  image: {
-    refresh_seconds: 1,
-  },
   live_provider: 'auto' as const,
   motioneye: {
     images: {
@@ -1408,7 +1403,7 @@ export const cameraConfigSchema = z
     // Live provider options.
     live_provider: z.enum(LIVE_PROVIDERS).default(cameraConfigDefault.live_provider),
     go2rtc: go2rtcConfigSchema.optional(),
-    image: liveImageConfigSchema.default(cameraConfigDefault.image),
+    image: imageBaseConfigSchema.optional().default(imageConfigDefault),
     jsmpeg: jsmpegConfigSchema.optional(),
     webrtc_card: webrtcCardConfigSchema.optional(),
 
diff --git a/src/const.ts b/src/const.ts
index 08783be4..c9b1eb9f 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -31,6 +31,10 @@ export const CONF_CAMERAS_ARRAY_GO2RTC_STREAM =
   `${CONF_CAMERAS}.#.go2rtc.stream` as const;
 export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const;
 export const CONF_CAMERAS_ARRAY_ID = `${CONF_CAMERAS}.#.id` as const;
+export const CONF_CAMERAS_ARRAY_IMAGE_ENTITY = `${CONF_CAMERAS}.#.image.entity` as const;
+export const CONF_CAMERAS_ARRAY_IMAGE_ENTITY_PARAMETERS =
+  `${CONF_CAMERAS}.#.image.entity_parameters` as const;
+export const CONF_CAMERAS_ARRAY_IMAGE_MODE = `${CONF_CAMERAS}.#.image.mode` as const;
 export const CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS =
   `${CONF_CAMERAS}.#.image.refresh_seconds` as const;
 export const CONF_CAMERAS_ARRAY_IMAGE_URL = `${CONF_CAMERAS}.#.image.url` as const;
@@ -290,6 +294,8 @@ export const CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED =
 export const CONF_LIVE_ZOOMABLE = `${CONF_LIVE}.zoomable` as const;
 
 const CONF_IMAGE = 'image' as const;
+export const CONF_IMAGE_ENTITY = `${CONF_IMAGE}.entity` as const;
+export const CONF_IMAGE_ENTITY_PARAMETERS = `${CONF_IMAGE}.entity_parameters` as const;
 export const CONF_IMAGE_MODE = `${CONF_IMAGE}.mode` as const;
 export const CONF_IMAGE_REFRESH_SECONDS = `${CONF_IMAGE}.refresh_seconds` as const;
 export const CONF_IMAGE_URL = `${CONF_IMAGE}.url` as const;
diff --git a/src/editor.ts b/src/editor.ts
index cc2e8318..8b9c0da3 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -66,6 +66,9 @@ import {
   CONF_CAMERAS_ARRAY_GO2RTC_STREAM,
   CONF_CAMERAS_ARRAY_ICON,
   CONF_CAMERAS_ARRAY_ID,
+  CONF_CAMERAS_ARRAY_IMAGE_ENTITY,
+  CONF_CAMERAS_ARRAY_IMAGE_ENTITY_PARAMETERS,
+  CONF_CAMERAS_ARRAY_IMAGE_MODE,
   CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
   CONF_CAMERAS_ARRAY_IMAGE_URL,
   CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
@@ -85,6 +88,8 @@ import {
   CONF_DIMENSIONS_ASPECT_RATIO_MODE,
   CONF_DIMENSIONS_MAX_HEIGHT,
   CONF_DIMENSIONS_MIN_HEIGHT,
+  CONF_IMAGE_ENTITY,
+  CONF_IMAGE_ENTITY_PARAMETERS,
   CONF_IMAGE_MODE,
   CONF_IMAGE_REFRESH_SECONDS,
   CONF_IMAGE_URL,
@@ -519,9 +524,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
 
   protected _imageModes: EditorSelectOption[] = [
     { value: '', label: '' },
-    { value: 'camera', label: localize('config.image.modes.camera') },
-    { value: 'screensaver', label: localize('config.image.modes.screensaver') },
-    { value: 'url', label: localize('config.image.modes.url') },
+    { value: 'camera', label: localize('config.common.image.modes.camera') },
+    { value: 'entity', label: localize('config.common.image.modes.entity') },
+    { value: 'screensaver', label: localize('config.common.image.modes.screensaver') },
+    { value: 'url', label: localize('config.common.image.modes.url') },
   ];
 
   protected _timelineEventsMediaTypes: EditorSelectOption[] = [
@@ -1733,6 +1739,36 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
     );
   }
 
+  protected _renderImageOptions(
+    configPathMode: string,
+    configPathUrl: string,
+    configPathEntity: string,
+    configPathEntityParameters: string,
+    configPathRefreshSeconds: string,
+  ): TemplateResult {
+    return html`
+      ${this._renderOptionSelector(configPathMode, this._imageModes, {
+        label: localize('config.common.image.mode'),
+      })}
+      ${this._renderStringInput(configPathUrl, {
+        label: localize('config.common.image.url'),
+      })}
+      ${this._renderOptionSelector(
+        configPathEntity,
+        this.hass ? getEntitiesFromHASS(this.hass) : [],
+        {
+          label: localize('config.common.image.entity'),
+        },
+      )}
+      ${this._renderStringInput(configPathEntityParameters, {
+        label: localize('config.common.image.entity_parameters'),
+      })}
+      ${this._renderNumberInput(configPathRefreshSeconds, {
+        label: localize('config.common.image.refresh_seconds'),
+      })}
+    `;
+  }
+
   /**
    * Render a camera section.
    * @param cameras The full array of cameras.
@@ -2006,17 +2042,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
                   true,
                   'config.cameras.image.editor_label',
                   { name: 'mdi:image' },
-                  html`
-                    ${this._renderNumberInput(
-                      getArrayConfigPath(
-                        CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
-                        cameraIndex,
-                      ),
-                    )}
-                    ${this._renderStringInput(
-                      getArrayConfigPath(CONF_CAMERAS_ARRAY_IMAGE_URL, cameraIndex),
-                    )}
-                  `,
+                  this._renderImageOptions(
+                    getArrayConfigPath(CONF_CAMERAS_ARRAY_IMAGE_MODE, cameraIndex),
+                    getArrayConfigPath(CONF_CAMERAS_ARRAY_IMAGE_URL, cameraIndex),
+                    getArrayConfigPath(CONF_CAMERAS_ARRAY_IMAGE_ENTITY, cameraIndex),
+                    getArrayConfigPath(
+                      CONF_CAMERAS_ARRAY_IMAGE_ENTITY_PARAMETERS,
+                      cameraIndex,
+                    ),
+                    getArrayConfigPath(
+                      CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
+                      cameraIndex,
+                    ),
+                  ),
                 )}
                 ${this._putInSubmenu(
                   MENU_CAMERAS_WEBRTC_CARD,
@@ -2725,9 +2763,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
         ${this._renderOptionSetHeader('image')}
         ${this._expandedMenus[MENU_OPTIONS] === 'image'
           ? html` <div class="values">
-              ${this._renderOptionSelector(CONF_IMAGE_MODE, this._imageModes)}
-              ${this._renderStringInput(CONF_IMAGE_URL)}
-              ${this._renderNumberInput(CONF_IMAGE_REFRESH_SECONDS)}
+              ${this._renderImageOptions(
+                CONF_IMAGE_MODE,
+                CONF_IMAGE_URL,
+                CONF_IMAGE_ENTITY,
+                CONF_IMAGE_ENTITY_PARAMETERS,
+                CONF_IMAGE_REFRESH_SECONDS,
+              )}
             </div>`
           : ''}
         ${this._renderOptionSetHeader('timeline')}
diff --git a/src/localize/languages/ca.json b/src/localize/languages/ca.json
index 27a008ef..01521890 100644
--- a/src/localize/languages/ca.json
+++ b/src/localize/languages/ca.json
@@ -219,6 +219,19 @@
         "grid_selected_width_factor": "Augmenta l'amplada del suport multimèdia seleccionat en aquest factor",
         "mode": "Mode"
       },
+      "image": {
+        "entity": "",
+        "entity_parameters": "",
+        "mode": "Mode de visualització d'imatges",
+        "modes": {
+          "camera": "Instantània de l'entitat de la càmera de Home Assistant",
+          "entity": "",
+          "screensaver": "Logotip de Frigate incrustat",
+          "url": "Imatge arbitrària especificada per URL"
+        },
+        "refresh_seconds": "Nombre de segons després dels quals cal actualitzar (0=mai)",
+        "url": "URL d'imatge estàtica per a la visualització d'imatges"
+      },
       "media_action_conditions": {
         "all": "Totes les oportunitats",
         "hidden": "A l'amagat del navegador/pestanya",
@@ -257,17 +270,6 @@
       "max_height": "Alçada màxima de la targeta en unitats CSS (p. ex., '100vh')",
       "min_height": "Alçada mínima de la targeta en unitats CSS (p. ex., '100 px')"
     },
-    "image": {
-      "mode": "Mode de visualització d'imatges",
-      "modes": {
-        "camera": "Instantània de l'entitat de la càmera de Home Assistant",
-        "screensaver": "Logotip de Frigate incrustat",
-        "url": "Imatge arbitrària especificada per URL"
-      },
-      "refresh_seconds": "Nombre de segons després dels quals cal actualitzar (0=mai)",
-      "url": "URL d'imatge estàtica per a la visualització d'imatges",
-      "zoomable": "La imatge es pot ampliar/escombrar"
-    },
     "live": {
       "auto_mute": "Silencia automàticament les càmeres en directe",
       "auto_pause": "Posa en pausa automàticament les càmeres en directe",
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index f2279649..692edddf 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -219,6 +219,19 @@
         "grid_selected_width_factor": "Increase selected media width by this factor",
         "mode": "Mode"
       },
+      "image": {
+        "entity": "Entity for use with entity mode",
+        "entity_parameters": "Query parameters added to the entity-based picture URLs (e.g. width=1920&height=1080)",
+        "mode": "Image mode",
+        "modes": {
+          "camera": "Home Assistant camera snapshot of camera entity",
+          "entity": "Entity with entity_picture attribute",
+          "screensaver": "Embedded Frigate logo",
+          "url": "Arbitrary image specified by URL"
+        },
+        "refresh_seconds": "Number of seconds after which to refresh (0=never)",
+        "url": "Static image URL"
+      },
       "media_action_conditions": {
         "all": "All opportunities",
         "hidden": "On browser/tab hiding",
@@ -257,17 +270,6 @@
       "max_height": "Maximum card height in CSS units (e.g. '100vh')",
       "min_height": "Minimum card height in CSS units (e.g. '100px')"
     },
-    "image": {
-      "mode": "Image view mode",
-      "modes": {
-        "camera": "Home Assistant camera snapshot of camera entity",
-        "screensaver": "Embedded Frigate logo",
-        "url": "Arbitrary image specified by URL"
-      },
-      "refresh_seconds": "Number of seconds after which to refresh (0=never)",
-      "url": "Static image URL for image view",
-      "zoomable": "Image can be zoomed/panned"
-    },
     "live": {
       "auto_mute": "Automatically mute live cameras",
       "auto_pause": "Automatically pause live cameras",
diff --git a/src/localize/languages/fr.json b/src/localize/languages/fr.json
index 89128777..981d9524 100644
--- a/src/localize/languages/fr.json
+++ b/src/localize/languages/fr.json
@@ -219,6 +219,19 @@
         "grid_selected_width_factor": "Augmenter la largeur du média sélectionnée par ce facteur",
         "mode": "Mode"
       },
+      "image": {
+        "entity": "",
+        "entity_parameters": "",
+        "mode": "Mode d'affichage des images",
+        "modes": {
+          "camera": "Instantané de la caméra Home Assistant de l'entité caméra",
+          "entity": "",
+          "screensaver": "Logo Frigate intégré",
+          "url": "Image arbitraire spécifiée par URL"
+        },
+        "refresh_seconds": "Nombre de secondes après lesquelles actualiser (0=jamais)",
+        "url": "URL d'image statique pour l'affichage de l'image"
+      },
       "media_action_conditions": {
         "all": "Toutes les opportunités",
         "hidden": "Sur le navigateur/onglet masqué",
@@ -257,17 +270,6 @@
       "max_height": "Hauteur maximale de la carte en unités CSS (par exemple '100vh')",
       "min_height": "Hauteur minimale de la carte en unités CSS (par exemple « 100 px »)"
     },
-    "image": {
-      "mode": "Mode d'affichage des images",
-      "modes": {
-        "camera": "Instantané de la caméra Home Assistant de l'entité caméra",
-        "screensaver": "Logo Frigate intégré",
-        "url": "Image arbitraire spécifiée par URL"
-      },
-      "refresh_seconds": "Nombre de secondes après lesquelles actualiser (0=jamais)",
-      "url": "URL d'image statique pour l'affichage de l'image",
-      "zoomable": "L'image peut être zoomée/panoramique"
-    },
     "live": {
       "auto_mute": "Couper automatiquement le son des caméras en direct",
       "auto_pause": "Mettre automatiquement en pause les caméras en direct",
diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json
index a87f9b75..b0a0a09e 100644
--- a/src/localize/languages/it.json
+++ b/src/localize/languages/it.json
@@ -219,6 +219,19 @@
         "grid_selected_width_factor": "",
         "mode": ""
       },
+      "image": {
+        "entity": "",
+        "entity_parameters": "",
+        "mode": "Modalità Visualizza immagine",
+        "modes": {
+          "camera": "Istantanea della telecamera di Home Assistant dell'entità telecamera",
+          "entity": "",
+          "screensaver": "Logo Frigate incorporato",
+          "url": "Immagine arbitraria specificata dall'URL"
+        },
+        "refresh_seconds": "Numero di secondi dopo i quali aggiornare (0 = mai)",
+        "url": "URL di immagine statica per la vista dell'immagine"
+      },
       "media_action_conditions": {
         "all": "Tutte le opportunità",
         "hidden": "Sul browser/nascondere le schede",
@@ -257,17 +270,6 @@
       "max_height": "",
       "min_height": ""
     },
-    "image": {
-      "mode": "Modalità Visualizza immagine",
-      "modes": {
-        "camera": "Istantanea della telecamera di Home Assistant dell'entità telecamera",
-        "screensaver": "Logo Frigate incorporato",
-        "url": "Immagine arbitraria specificata dall'URL"
-      },
-      "refresh_seconds": "Numero di secondi dopo i quali aggiornare (0 = mai)",
-      "url": "URL di immagine statica per la vista dell'immagine",
-      "zoomable": ""
-    },
     "live": {
       "auto_mute": "Muta automaticamente le telecamere in diretta",
       "auto_pause": "Metti in pausa automaticamente le telecamere in diretta",
diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json
index 2fac32db..c54f68fe 100644
--- a/src/localize/languages/pt-BR.json
+++ b/src/localize/languages/pt-BR.json
@@ -219,6 +219,19 @@
         "grid_selected_width_factor": "",
         "mode": ""
       },
+      "image": {
+        "entity": "",
+        "entity_parameters": "",
+        "mode": "Modo de visualização de imagem",
+        "modes": {
+          "camera": "Instantâneo da câmera do Home Assistant, da entidade de câmera",
+          "entity": "",
+          "screensaver": "Logo Frigate embutido",
+          "url": "Imagem arbitrária especificada por URL"
+        },
+        "refresh_seconds": "Número de segundos após o qual atualizar (0 = nunca)",
+        "url": "Imagem arbitrária especificada por URL"
+      },
       "media_action_conditions": {
         "all": "Todas as oportunidades",
         "hidden": "Ao ocultar o navegador/aba",
@@ -257,17 +270,6 @@
       "max_height": "",
       "min_height": ""
     },
-    "image": {
-      "mode": "Modo de visualização de imagem",
-      "modes": {
-        "camera": "Instantâneo da câmera do Home Assistant, da entidade de câmera",
-        "screensaver": "Logo Frigate embutido",
-        "url": "Imagem arbitrária especificada por URL"
-      },
-      "refresh_seconds": "Número de segundos após o qual atualizar (0 = nunca)",
-      "url": "Imagem arbitrária especificada por URL",
-      "zoomable": ""
-    },
     "live": {
       "auto_mute": "Silenciar câmeras ao vivo automaticamente",
       "auto_pause": "Pausar câmeras ao vivo automaticamente",
diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json
index 7fdb3de5..10655859 100644
--- a/src/localize/languages/pt-PT.json
+++ b/src/localize/languages/pt-PT.json
@@ -219,6 +219,19 @@
         "grid_selected_width_factor": "",
         "mode": ""
       },
+      "image": {
+        "entity": "",
+        "entity_parameters": "",
+        "mode": "Modo de visualização de imagem",
+        "modes": {
+          "camera": "Instantâneo da câmera do Home Assistant, da entidade de câmera",
+          "entity": "",
+          "screensaver": "Logo Frigate embutido",
+          "url": "Imagem arbitrária especificada por URL"
+        },
+        "refresh_seconds": "Número de segundos após o qual atualizar (0 = nunca)",
+        "url": "Imagem arbitrária especificada por URL"
+      },
       "media_action_conditions": {
         "all": "Todas as oportunidades",
         "hidden": "Ao ocultar o navegador/aba",
@@ -257,17 +270,6 @@
       "max_height": "",
       "min_height": ""
     },
-    "image": {
-      "mode": "Modo de visualização de imagem",
-      "modes": {
-        "camera": "Instantâneo da câmera do Home Assistant, da entidade de câmera",
-        "screensaver": "Logo Frigate embutido",
-        "url": "Imagem arbitrária especificada por URL"
-      },
-      "refresh_seconds": "Número de segundos após o qual atualizar (0 = nunca)",
-      "url": "Imagem arbitrária especificada por URL",
-      "zoomable": ""
-    },
     "live": {
       "auto_mute": "Silenciar câmeras ao vivo automaticamente",
       "auto_pause": "Parar câmeras ao vivo automaticamente",


Test patch:
diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts
index cd313042..aa6bb356 100644
--- a/tests/config/types.test.ts
+++ b/tests/config/types.test.ts
@@ -24,6 +24,7 @@ describe('config defaults', () => {
           client_id: 'frigate',
         },
         image: {
+          mode: 'auto',
           refresh_seconds: 1,
         },
         live_provider: 'auto',
@@ -58,7 +59,7 @@ describe('config defaults', () => {
         min_height: '100px',
       },
       image: {
-        mode: 'url',
+        mode: 'auto',
         refresh_seconds: 1,
       },
       live: {