Support (HA) camera snapshots in image view.

This commit is contained in:
Dermot Duffy
2022-02-13 14:02:38 -08:00
parent 6be88fe0b6
commit 5184d0df62
8 changed files with 86 additions and 24 deletions
+2 -1
View File
@@ -381,7 +381,8 @@ image:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `src` | | :white_check_mark: | [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. |
| `mode` | `url` | :white_check_mark: | Mode of the the `image` [view](#views). 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. |
| `url` | | :white_check_mark: | A static image URL to be used when the `mode` is set to `url`. 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.|
| `refresh_seconds` | 0 | :white_check_mark: | The number of seconds after which to refresh the image. `0` implies no refreshing. |
| `actions` | | :white_check_mark: | Actions to use for the `image` view. See [actions](#actions) below.|
+11 -2
View File
@@ -35,6 +35,7 @@ import type {
import { CARD_VERSION, REPO_URL } from './const.js';
import { FrigateCardElements } from './components/elements.js';
import { FrigateCardImage } from './components/image.js';
import { FRIGATE_BUTTON_MENU_ICON, FrigateCardMenu } from './components/menu.js';
import { View } from './view.js';
import {
@@ -153,6 +154,9 @@ export class FrigateCard extends LitElement {
@query('frigate-card-elements')
protected _elements?: FrigateCardElements;
@query('frigate-card-image')
protected _image?: FrigateCardImage;
// user interaction timer ("screensaver" functionality, return to default
// view after user interaction).
protected _interactionTimerID: number | null = null;
@@ -181,8 +185,8 @@ export class FrigateCard extends LitElement {
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
this._hass = hass;
// Manually set hass in the menu & elements. This is to allow these to
// update, without necessarily re-rendering the entire card (re-rendering
// Manually set hass in the menu, elements and image. This is to allow these
// to update, without necessarily re-rendering the entire card (re-rendering
// interrupts clip playing).
if (this._hass) {
if (this._menu) {
@@ -191,6 +195,9 @@ export class FrigateCard extends LitElement {
if (this._elements) {
this._elements.hass = this._hass;
}
if (this._image) {
this._image.hass = this._hass;
}
}
}
@@ -1210,6 +1217,8 @@ export class FrigateCard extends LitElement {
? html` <frigate-card-image
.imageConfig=${this._getConfig().image}
.view=${this._view}
.hass=${this._hass}
.cameraConfig=${cameraConfig}
>
</frigate-card-image>`
: ``}
+37 -12
View File
@@ -7,16 +7,16 @@ import {
ReactiveController,
ReactiveControllerHost,
} from 'lit';
import { HomeAssistant } from 'custom-card-helpers';
import { customElement, property, state } from 'lit/decorators.js';
import { ImageViewConfig } from '../types.js';
import { CameraConfig, ImageViewConfig } from '../types.js';
import { View } from '../view.js';
import { dispatchMediaShowEvent } from '../common.js';
import defaultImage from '../images/frigate-bird-in-sky.jpg';
import imageStyle from '../scss/image.scss';
export class CachedValueController<T> implements ReactiveController {
public value?: T;
@@ -59,6 +59,20 @@ export class CachedValueController<T> implements ReactiveController {
@customElement('frigate-card-image')
export class FrigateCardImage extends LitElement {
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false })
protected view?: Readonly<View>;
@property({ attribute: false })
protected cameraConfig?: CameraConfig;
@state()
protected _imageConfig?: ImageViewConfig;
protected _cachedValueController?: CachedValueController<string>;
set imageConfig(imageConfig: ImageViewConfig) {
this._imageConfig = imageConfig;
if (this._cachedValueController) {
@@ -70,19 +84,30 @@ export class FrigateCardImage extends LitElement {
this._getImageSource.bind(this),
);
}
@state()
protected _imageConfig?: ImageViewConfig;
// A new view should trigger an image re-render.
@property({ attribute: false })
protected view?: Readonly<View>;
protected _cachedValueController?: CachedValueController<string>;
/**
* 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).
*/
protected _buildImageURL(url: string): string {
const urlObj = new URL(url, document.baseURI);
urlObj.searchParams.append('_t', String(Date.now()));
return urlObj.toString();
}
protected _getImageSource(): string {
if (this._imageConfig?.src) {
const url = new URL(this._imageConfig.src);
url.searchParams.append('t', String(Date.now()));
return url.toString();
if (this._imageConfig?.mode === 'url' && this._imageConfig?.url) {
return this._buildImageURL(this._imageConfig.url);
} else if (this.hass && this._imageConfig?.mode === 'camera') {
const entity =
this.cameraConfig?.camera_entity || this.cameraConfig?.webrtc_card?.entity;
if (entity) {
const state = this.hass.states[entity];
if (state && state.attributes.entity_picture) {
return this._buildImageURL(state.attributes.entity_picture);
}
}
}
return defaultImage;
}
+5 -4
View File
@@ -11,7 +11,7 @@ import {
CONF_EVENT_VIEWER_AUTO_PLAY,
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_IMAGE_SRC,
CONF_IMAGE_URL,
CONF_LIVE_PRELOAD,
CONF_LIVE_WEBRTC_CARD,
CONF_MENU,
@@ -317,7 +317,7 @@ const upgradeToMultipleCameras = (): ((obj: RawFrigateCardConfig) => boolean) =>
* @param key A string key.
* @returns A safe key.
*/
const updateMenuConditionToMenuOverride = (): ((
const upgradeMenuConditionToMenuOverride = (): ((
obj: RawFrigateCardConfig,
) => boolean) => {
return function (obj: RawFrigateCardConfig): boolean {
@@ -363,14 +363,14 @@ const UPGRADES = [
upgradeMoveTo('menu_mode', CONF_MENU_MODE),
upgradeMoveTo('menu_buttons', 'menu.buttons'),
upgradeMoveTo('menu_button_size', CONF_MENU_BUTTON_SIZE),
upgradeMoveTo('image', CONF_IMAGE_SRC, isNotObject),
upgradeMoveTo('image', 'image.src', isNotObject),
// v2.0.0 -> v2.1.0
upgradeMoveTo('update_entities', CONF_VIEW_UPDATE_ENTITIES),
// v2.1.0 -> v3.0.0-rc.1
upgradeToMultipleCameras(),
updateMenuConditionToMenuOverride(),
upgradeMenuConditionToMenuOverride(),
upgradeMoveTo('view.timeout', CONF_VIEW_TIMEOUT_SECONDS, toNumberOrIgnore),
upgradeMoveTo('event_viewer.autoplay_clip', CONF_EVENT_VIEWER_AUTO_PLAY),
@@ -383,4 +383,5 @@ const UPGRADES = [
),
upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('webrtc', 'webrtc_card')),
upgradeMoveToWithOverrides('live.webrtc', CONF_LIVE_WEBRTC_CARD),
upgradeMoveToWithOverrides('image.src', CONF_IMAGE_URL),
];
+2 -1
View File
@@ -76,8 +76,9 @@ export const CONF_LIVE_TRANSITION_EFFECT = `${CONF_LIVE}.transition_effect` as c
export const CONF_LIVE_WEBRTC_CARD = `${CONF_LIVE}.webrtc_card` as const;
export const CONF_IMAGE = 'image' 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_SRC = `${CONF_IMAGE}.src` as const;
export const CONF_IMAGE_URL = `${CONF_IMAGE}.url` as const;
export const CONF_MENU = 'menu' as const;
export const CONF_MENU_BUTTONS_FRIGATE = `${CONF_MENU}.buttons.frigate` as const;
+14 -2
View File
@@ -39,8 +39,9 @@ import {
CONF_EVENT_VIEWER_DRAGGABLE,
CONF_EVENT_VIEWER_LAZY_LOAD,
CONF_EVENT_VIEWER_TRANSITION_EFFECT,
CONF_IMAGE_MODE,
CONF_IMAGE_REFRESH_SECONDS,
CONF_IMAGE_SRC,
CONF_IMAGE_URL,
CONF_LIVE_AUTO_UNMUTE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
@@ -274,6 +275,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
['slide', localize('config.event_viewer.transition_effects.slide')],
]);
protected _imageModes = new Map([
['', ''],
['camera', localize('config.image.modes.camera')],
['screensaver', localize('config.image.modes.screensaver')],
['url', localize('config.image.modes.url')],
]);
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -882,7 +890,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSetHeader('image')}
${options.image.show
? html` <div class="values">
${this._renderStringInput(CONF_IMAGE_SRC)}
${this._renderDropdown(
CONF_IMAGE_MODE,
this._imageModes,
)}
${this._renderStringInput(CONF_IMAGE_URL)}
${this._renderStringInput(CONF_IMAGE_REFRESH_SECONDS, 'number')}
</div>`
: ''}
+7 -1
View File
@@ -127,7 +127,13 @@
}
},
"image": {
"src": "Static image URL/data for image view",
"mode": "Image view mode",
"modes": {
"screensaver": "Embedded Frigate logo",
"url": "Arbitrary image specified by URL",
"camera": "Home Assistant camera snapshot of camera entity"
},
"url": "Static image URL for image view",
"refresh_seconds": "Number of seconds after which to refresh (0=never)"
},
"menu": {
+8 -1
View File
@@ -404,12 +404,19 @@ const viewConfigSchema = z
* Image view configuration section.
*/
export const IMAGE_MODES = [
'screensaver',
'camera',
'url',
] as const;
const imageConfigDefault = {
mode: 'url' as const,
refresh_seconds: 0,
};
const imageConfigSchema = z
.object({
src: z.string().optional(),
mode: z.enum(IMAGE_MODES).default(imageConfigDefault.mode),
url: z.string().optional(),
refresh_seconds: z.number().min(0).default(imageConfigDefault.refresh_seconds),
})
.merge(actionsSchema)