diff --git a/README.md b/README.md index 160cf72f..d6215aa5 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,7 @@ image: | Option | Default | Overridable | Description | | - | - | - | - | | `src` | | :heavy_multiplication_x: | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views). Note that a `t=[timestsamp]` query parameter will be automatically added to this URL such that the image will not be cached by the browser. | +| `refresh_seconds` | 0 | :heavy_multiplication_x: | The number of seconds after which to refresh the image. `0` implies no refreshing. | | `actions` | | :heavy_multiplication_x: | Actions to use for the `image` view. See [actions](#actions) below.| ### Dimension Options @@ -1087,10 +1088,9 @@ This example fetches a static image every 10 seconds (in this case the latest im [...] view: default: image - timeout: 10 - update_force: true image: src: https://my-friage-server/api/living_room/latest.jpg + refresh_seconds: 10 ``` diff --git a/src/components/image.ts b/src/components/image.ts index 5f4f5e97..e2052e3f 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -1,26 +1,86 @@ -import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { + CSSResultGroup, + LitElement, + TemplateResult, + html, + unsafeCSS, + ReactiveController, + ReactiveControllerHost, +} from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ImageViewConfig } from '../types.js'; +import { View } from '../view.js'; import { dispatchMediaShowEvent } from '../common.js'; - -import type { ImageViewConfig } from '../types.js'; import defaultImage from '../images/frigate-bird-in-sky.jpg'; import imageStyle from '../scss/image.scss'; -import { View } from '../view.js'; + + +export class CachedValueController implements ReactiveController { + public value?: T; + + protected _host: ReactiveControllerHost; + protected _timerSeconds: number; + protected _callback: () => T; + protected _timerID?: number; + + constructor(host: ReactiveControllerHost, timerSeconds: number, callback: () => T) { + (this._host = host).addController(this); + this._timerSeconds = timerSeconds; + this._callback = callback; + } + + public removeController(): void { + this._host.removeController(this); + } + + protected _updateValue(): void { + this.value = this._callback(); + this._host.requestUpdate(); + } + + hostConnected(): void { + this._updateValue(); + + // Start a timer when the host is connected + if (this._timerSeconds > 0) { + this._timerID = window.setInterval(() => { + this._updateValue(); + }, this._timerSeconds * 1000); + } + } + hostDisconnected(): void { + // Clear the timer when the host is disconnected + clearInterval(this._timerID); + this._timerID = undefined; + } +} @customElement('frigate-card-image') export class FrigateCardImage extends LitElement { - @property({ attribute: false }) - protected imageConfig?: ImageViewConfig; + set imageConfig(imageConfig: ImageViewConfig) { + this._imageConfig = imageConfig; + if (this._cachedValueController) { + this._cachedValueController.removeController(); + } + this._cachedValueController = new CachedValueController( + this, + this._imageConfig.refresh_seconds, + this._getImageSource.bind(this), + ); + } + @state() + protected _imageConfig?: ImageViewConfig; // A new view should trigger an image re-render. @property({ attribute: false }) protected view?: Readonly; + protected _cachedValueController?: CachedValueController; protected _getImageSource(): string { - if (this.imageConfig?.src) { - const url = new URL(this.imageConfig.src); + if (this._imageConfig?.src) { + const url = new URL(this._imageConfig.src); url.searchParams.append('t', String(Date.now())); return url.toString(); } @@ -28,12 +88,15 @@ export class FrigateCardImage extends LitElement { } protected render(): TemplateResult | void { - return html` { - dispatchMediaShowEvent(this, e); - }} - />`; + const src = this._cachedValueController?.value; + return src + ? html` { + dispatchMediaShowEvent(this, e); + }} + />` + : html``; } static get styles(): CSSResultGroup { diff --git a/src/const.ts b/src/const.ts index 5b70c54b..c22b7745 100644 --- a/src/const.ts +++ b/src/const.ts @@ -61,6 +61,7 @@ export const CONF_LIVE_WEBRTC_ENTITY = `${CONF_LIVE_WEBRTC}.entity` as const; export const CONF_LIVE_WEBRTC_URL = `${CONF_LIVE_WEBRTC}.url` as const; export const CONF_IMAGE = 'image' as const; +export const CONF_IMAGE_REFRESH_SECONDS = `${CONF_IMAGE}.refresh_seconds` as const; export const CONF_IMAGE_SRC = `${CONF_IMAGE}.src` as const; export const CONF_MENU = 'menu' as const; diff --git a/src/editor.ts b/src/editor.ts index 7241c481..4da5ec43 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -35,6 +35,7 @@ import { CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_EVENT_VIEWER_DRAGGABLE, CONF_EVENT_VIEWER_LAZY_LOAD, + CONF_IMAGE_REFRESH_SECONDS, CONF_IMAGE_SRC, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, @@ -488,16 +489,18 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor */ protected _renderStringInput( configPath: string, - allowedPattern?: string, + type?: 'text' | 'number', ): TemplateResult | void { if (!this._config) { return; } + const allowedPattern = type == 'number' ? '[0-9]' : undefined return html` `; @@ -654,7 +657,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ? html`
${this._renderDropdown(CONF_VIEW_DEFAULT, viewModes)} - ${this._renderStringInput(CONF_VIEW_TIMEOUT, '[0-9]')} + ${this._renderStringInput(CONF_VIEW_TIMEOUT, 'number')} ${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
` @@ -779,7 +782,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor : ''} ${this._renderOptionSetHeader('image')} ${options.image.show - ? html`
${this._renderStringInput(CONF_IMAGE_SRC)}
` + ? html`
+ ${this._renderStringInput(CONF_IMAGE_SRC)} + ${this._renderStringInput(CONF_IMAGE_REFRESH_SECONDS, 'number')} +
` : ''} ${this._renderOptionSetHeader('dimensions')} ${options.dimensions.show @@ -869,6 +875,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor value = target.checked; } else if (typeof target.value === 'string') { value = target.value?.trim(); + if (target['type'] === 'number') { + value = Number(value); + } } else { value = target.value; } diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 499ec34d..de88faab 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -98,7 +98,8 @@ } }, "image": { - "src": "Static image URL/data for image view" + "src": "Static image URL/data for image view", + "refresh_seconds": "Number of seconds after which to refresh" }, "menu": { "buttons": { diff --git a/src/types.ts b/src/types.ts index d20d42f0..36cc05e0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -390,12 +390,17 @@ const viewConfigSchema = z /** * Image view configuration section. */ + +const imageConfigDefault = { + refresh_seconds: 0, +}; const imageConfigSchema = z .object({ src: z.string().optional(), + refresh_seconds: z.number().min(0).default(imageConfigDefault.refresh_seconds) }) .merge(actionsSchema) - .optional(); + .default(imageConfigDefault); export type ImageViewConfig = z.infer; /** @@ -710,6 +715,7 @@ export const frigateCardConfigDefaults = { live: liveConfigDefault, event_viewer: viewerConfigDefault, event_gallery: galleryConfigDefault, + image: imageConfigDefault, }; const menuButtonSchema = z.union([