Support (HA) camera snapshots in image view.
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>`
|
||||
: ''}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user