Add a refresh loop for the image view itself.

This commit is contained in:
Dermot Duffy
2022-01-19 19:27:23 -08:00
parent aa0ee88970
commit 317a262f2b
6 changed files with 103 additions and 23 deletions
+2 -2
View File
@@ -338,6 +338,7 @@ image:
| Option | Default | Overridable | Description | | 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. | | `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.| | `actions` | | :heavy_multiplication_x: | Actions to use for the `image` view. See [actions](#actions) below.|
### Dimension Options ### Dimension Options
@@ -1087,10 +1088,9 @@ This example fetches a static image every 10 seconds (in this case the latest im
[...] [...]
view: view:
default: image default: image
timeout: 10
update_force: true
image: image:
src: https://my-friage-server/api/living_room/latest.jpg src: https://my-friage-server/api/living_room/latest.jpg
refresh_seconds: 10
``` ```
</details> </details>
+78 -15
View File
@@ -1,26 +1,86 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import {
import { customElement, property } from 'lit/decorators.js'; 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 { dispatchMediaShowEvent } from '../common.js';
import type { ImageViewConfig } from '../types.js';
import defaultImage from '../images/frigate-bird-in-sky.jpg'; import defaultImage from '../images/frigate-bird-in-sky.jpg';
import imageStyle from '../scss/image.scss'; import imageStyle from '../scss/image.scss';
import { View } from '../view.js';
export class CachedValueController<T> 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') @customElement('frigate-card-image')
export class FrigateCardImage extends LitElement { export class FrigateCardImage extends LitElement {
@property({ attribute: false }) set imageConfig(imageConfig: ImageViewConfig) {
protected 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. // A new view should trigger an image re-render.
@property({ attribute: false }) @property({ attribute: false })
protected view?: Readonly<View>; protected view?: Readonly<View>;
protected _cachedValueController?: CachedValueController<string>;
protected _getImageSource(): string { protected _getImageSource(): string {
if (this.imageConfig?.src) { if (this._imageConfig?.src) {
const url = new URL(this.imageConfig.src); const url = new URL(this._imageConfig.src);
url.searchParams.append('t', String(Date.now())); url.searchParams.append('t', String(Date.now()));
return url.toString(); return url.toString();
} }
@@ -28,12 +88,15 @@ export class FrigateCardImage extends LitElement {
} }
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` <img const src = this._cachedValueController?.value;
src=${this._getImageSource()} return src
@load=${(e) => { ? html` <img
dispatchMediaShowEvent(this, e); src=${src}
}} @load=${(e) => {
/>`; dispatchMediaShowEvent(this, e);
}}
/>`
: html``;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
+1
View File
@@ -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_LIVE_WEBRTC_URL = `${CONF_LIVE_WEBRTC}.url` as const;
export const CONF_IMAGE = 'image' 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_IMAGE_SRC = `${CONF_IMAGE}.src` as const;
export const CONF_MENU = 'menu' as const; export const CONF_MENU = 'menu' as const;
+13 -4
View File
@@ -35,6 +35,7 @@ import {
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
CONF_EVENT_VIEWER_DRAGGABLE, CONF_EVENT_VIEWER_DRAGGABLE,
CONF_EVENT_VIEWER_LAZY_LOAD, CONF_EVENT_VIEWER_LAZY_LOAD,
CONF_IMAGE_REFRESH_SECONDS,
CONF_IMAGE_SRC, CONF_IMAGE_SRC,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
@@ -488,16 +489,18 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
*/ */
protected _renderStringInput( protected _renderStringInput(
configPath: string, configPath: string,
allowedPattern?: string, type?: 'text' | 'number',
): TemplateResult | void { ): TemplateResult | void {
if (!this._config) { if (!this._config) {
return; return;
} }
const allowedPattern = type == 'number' ? '[0-9]' : undefined
return html` <paper-input return html` <paper-input
label=${this._getLabel(configPath)} label=${this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')} .value=${getConfigValue(this._config, configPath, '')}
.configValue=${configPath} .configValue=${configPath}
allowed-pattern=${ifDefined(allowedPattern ? allowedPattern : undefined)} type=${type || 'text'}
allowed-pattern=${ifDefined(allowedPattern)}
prevent-invalid-input=${ifDefined(allowedPattern)} prevent-invalid-input=${ifDefined(allowedPattern)}
@change=${this._valueChangedHandler} @change=${this._valueChangedHandler}
></paper-input>`; ></paper-input>`;
@@ -654,7 +657,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html` ? html`
<div class="values"> <div class="values">
${this._renderDropdown(CONF_VIEW_DEFAULT, viewModes)} ${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)} ${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
</div> </div>
` `
@@ -779,7 +782,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
: ''} : ''}
${this._renderOptionSetHeader('image')} ${this._renderOptionSetHeader('image')}
${options.image.show ${options.image.show
? html` <div class="values">${this._renderStringInput(CONF_IMAGE_SRC)}</div>` ? html` <div class="values">
${this._renderStringInput(CONF_IMAGE_SRC)}
${this._renderStringInput(CONF_IMAGE_REFRESH_SECONDS, 'number')}
</div>`
: ''} : ''}
${this._renderOptionSetHeader('dimensions')} ${this._renderOptionSetHeader('dimensions')}
${options.dimensions.show ${options.dimensions.show
@@ -869,6 +875,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
value = target.checked; value = target.checked;
} else if (typeof target.value === 'string') { } else if (typeof target.value === 'string') {
value = target.value?.trim(); value = target.value?.trim();
if (target['type'] === 'number') {
value = Number(value);
}
} else { } else {
value = target.value; value = target.value;
} }
+2 -1
View File
@@ -98,7 +98,8 @@
} }
}, },
"image": { "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": { "menu": {
"buttons": { "buttons": {
+7 -1
View File
@@ -390,12 +390,17 @@ const viewConfigSchema = z
/** /**
* Image view configuration section. * Image view configuration section.
*/ */
const imageConfigDefault = {
refresh_seconds: 0,
};
const imageConfigSchema = z const imageConfigSchema = z
.object({ .object({
src: z.string().optional(), src: z.string().optional(),
refresh_seconds: z.number().min(0).default(imageConfigDefault.refresh_seconds)
}) })
.merge(actionsSchema) .merge(actionsSchema)
.optional(); .default(imageConfigDefault);
export type ImageViewConfig = z.infer<typeof imageConfigSchema>; export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
/** /**
@@ -710,6 +715,7 @@ export const frigateCardConfigDefaults = {
live: liveConfigDefault, live: liveConfigDefault,
event_viewer: viewerConfigDefault, event_viewer: viewerConfigDefault,
event_gallery: galleryConfigDefault, event_gallery: galleryConfigDefault,
image: imageConfigDefault,
}; };
const menuButtonSchema = z.union([ const menuButtonSchema = z.union([