diff --git a/README.md b/README.md index 53ecfb13..3b58fa62 100644 --- a/README.md +++ b/README.md @@ -135,12 +135,12 @@ view: | Option | Default | Overridable | Description | | - | - | - | - | | `default` | `live` | :heavy_multiplication_x: | The view to show in the card by default. See [views](#views) below.| -| `timeout_seconds` | | :heavy_multiplication_x: | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.| -| `actions` | | :heavy_multiplication_x: | Actions to use for all views, individual actions may be overriden by view-specific actions. See [actions](#actions) below.| -| `update_force` | `false` | :heavy_multiplication_x: | Whether card updates/refreshes should ignore playing media and human interaction. See [card updates](#card-updates) below for behavior and usecases.| +| `timeout_seconds` | `300` | :heavy_multiplication_x: | A numbers of seconds of inactivity after human interaction, after which the card will reset to the default configured view (i.e. 'screensaver' functionality). Inactivity is defined as lack of mouse/touch interaction with the Frigate card. If the default view occurs sooner (e.g. via `update_seconds` or manually) the timer will be stopped. `0` means disable this functionality. | +| `update_seconds` | `0` | :heavy_multiplication_x: | A number of seconds after which to automatically update/refresh the default view. See [card updates](#card-updates) below for behavior and usecases. If the default view occurs sooner (e.g. manually) the timer will start over. `0` disables this functionality.| +| `update_force` | `false` | :heavy_multiplication_x: | Whether automated card updates/refreshes should ignore human interaction. See [card updates](#card-updates) below for behavior and usecases.| | `update_entities` | | :heavy_multiplication_x: | **YAML only**: A list of entity ids that should cause the view to reset to the default. See [card updates](#card-updates) below for behavior and usecases.| | `update_cycle_camera` | `false` | :heavy_multiplication_x: | When set to `true` the selected camera is cycled on each default view change. | - +| `actions` | | :heavy_multiplication_x: | Actions to use for all views, individual actions may be overriden by view-specific actions. See [actions](#actions) below.| ### Menu Options All configuration is under: @@ -412,8 +412,6 @@ item, that has both of the following parameters set: | `conditions` | | :heavy_multiplication_x: | A set of conditions that must evaluate to `true` in order for the overrides to be applied. See [Frigate Card Conditions](#frigate-card-conditions). | | `overrides` | | :heavy_multiplication_x: |Configuration overrides to be applied. Any configuration parameter described in this documentation as 'Overridable' is supported. | - - ### Using WebRTC WebRTC support blends the use of the ultra-realtime [WebRTC live @@ -1099,31 +1097,34 @@ image: ## Card Refreshes -Three sets of flags govern when the card will automatically re-render in the +Four sets of flags govern when the card will automatically refresh in the absence of human interaction. -The following table describes the behavior these 3 flags have. +The following table describes the behavior these flags have. ### Card Update Truth Table -| `view.timeout_seconds` | `view.update_force` | `view.update_entities` | Behavior | -| :-: | :-: | :-: | - | -| Unset or `0` | *(Any value)* | Unset | Card will not automatically refresh. | -| Unset or `0` | `false` | *(Any entity)* | Card will reload default view when entity state changes, unless media is playing. | -| Unset or `0` | `true` | *(Any entity)* | Card will reload default view when entity state changes. | -| `X` seconds | `false` | Unset | Card will reload default view `X` seconds after human interaction stops, unless media is playing. | -| `X` seconds | `false` | *(Any entity)* | Card will reload default view `X` seconds after human interaction stops or when entity state changes -- in both cases unless media is playing. | -| `X` seconds | `true` | Unset | Card will reload default view every `X` seconds. | -| `X` seconds | `true` | *(Any entity)* | Card will reload default view every `X` seconds or when entity state changes. | +| `view.update_seconds` | `view.timeout_seconds` | `view.update_force` | `view.update_entities` | Behavior | +| :-: | :-: | :-: | :-: | - | +| `0` | `0` | *(Any value)* | Unset | Card will not automatically refresh. | +| `0` | `0` | *(Any value)* | *(Any entity)* | Card will reload default view when entity state changes. | +| `0` | `X` seconds | *(Any value)* | Unset | Card will reload default view `X` seconds after human interaction stops. | +| `0` | `X` seconds | `false` | *(Any entity)* | Card will reload default view `X` seconds after human interaction stops, or when entity state changes (as long as human interaction has not occurred in the last `X` seconds). | +| `0` | `X` seconds | `true` | *(Any entity)* | Card will reload default view `X` seconds after human interaction stops or when entity state changes. | +| `Y` seconds | `0` | *(Any value)* | Unset | Card will reload default view every `Y` seconds. | +| `Y` seconds | `0` | *(Any value)* | *(Any entity)* | Card will reload default view every `Y` seconds, or whenever entity state changes. | +| `Y` seconds | `X` seconds | `false` | Unset | Card will reload default view `X` seconds after human interaction stops, and every `Y` seconds (as long as there hasn't been human interaction in the last `X` seconds). | +| `Y` seconds | `X` seconds | `false` | *(Any entity)* | Card will reload default view `X` seconds after human interaction stops, and every `Y` seconds or whenever entity state changes (in both cases -- as long as there hasn't been human interaction in the last `X` seconds). | +| `Y` seconds | `X` seconds | `true` | Unset | Card will reload default view `X` seconds after human interaction stops, and every `Y` seconds. | +| `Y` seconds | `X` seconds | `true` | *(Any entity)* | Card will reload default view `X` seconds after human interaction stops, and every `Y` seconds or whenever entity state changes. | ### Usecases For Automated Refreshes - * Refreshing the `live` thumbnails periodically. + * Refreshing the `live` thumbnails every 30 seconds. ```yaml view: default: live - timeout_seconds: 30 - force: true + update_seconds: 30 ``` * Using `clip` or `snapshot` as the default view (for the most recent clip or snapshot respectively) and having the card automatically refresh (to fetch a @@ -1135,6 +1136,19 @@ view: update_entities: - binary_sensor.office_person_motion ``` + * Cycle the live view of the camera every 60 seconds +```yaml +view: + update_cycle_camera: true + update_seconds: 60 +``` + * Return to the most recent clip of the default camera 30 seconds after human + interaction with the card stops. +```yaml +view: + default: clip + timeout_seconds: 30 +``` ## Troubleshooting diff --git a/src/card.ts b/src/card.ts index 05e5e336..b230f392 100644 --- a/src/card.ts +++ b/src/card.ts @@ -151,11 +151,12 @@ export class FrigateCard extends LitElement { @query('frigate-card-elements') _elements?: FrigateCardElements; - // Human interaction timer ID. + // Human interaction timer ("screensaver" functionality, return to default + // view after human interaction). protected _interactionTimerID: number | null = null; - // Whether or not media is actively playing (live or clip). - protected _mediaPlaying = false; + // Automated refreshes of the default view. + protected _updateTimerID: number | null = null; // Information about the most recently loaded media item. protected _mediaShowInfo: MediaShowInfo | null = null; @@ -619,10 +620,6 @@ export class FrigateCard extends LitElement { this._cameras = undefined; this._view = undefined; - if (this._getConfig().view.update_force) { - // If update force is enabled, start a timer right away. - this._resetInteractionTimer(); - } this._changeView(); } @@ -640,6 +637,7 @@ export class FrigateCard extends LitElement { } if (args?.view === undefined) { + // Load the default view. let camera = this._view?.camera; if (this._cameras?.size) { if (!camera) { @@ -658,6 +656,14 @@ export class FrigateCard extends LitElement { camera: camera, }); this._generateConditionState(); + + // The default view has been loaded, so can abandon any running + // 'screensaver' timer. + this._clearInteractionTimer(); + + // Restart the update timer, so the default view is refreshed at a fixed + // interval from now (if so configured). + this._startUpdateTimer(); } } else { this._view = args.view; @@ -692,8 +698,7 @@ export class FrigateCard extends LitElement { // Assistant update if there's been recent interaction (e.g. clicks on the // card) or if there is media active playing. if ( - (this._getConfig().view.update_force || - !(this._interactionTimerID && this._mediaPlaying)) && + this._isAutomatedViewUpdateAllowed() && shouldUpdateBasedOnHass( this._hass, oldHass, @@ -701,9 +706,8 @@ export class FrigateCard extends LitElement { ) ) { // If entities being monitored have changed then reset the view to the - // default and allow a re-render. Note that as per the Lit lifecycle, - // the setting of the view itself will not trigger an *additional* - // re-render here. + // default. Note that as per the Lit lifecycle, the setting of the view + // itself will not trigger an *additional* re-render here. this._changeView(); return true; } @@ -891,25 +895,66 @@ export class FrigateCard extends LitElement { ) { handleAction(node, this._hass as HomeAssistant, config, ev.detail.action); } - this._resetInteractionTimer(); + + // Set the 'screensaver' timer. + this._startInteractionTimer(); } - protected _resetInteractionTimer(): void { + /** + * Clear the human interaction ('screensaver') timer. + */ + protected _clearInteractionTimer(): void { + if (this._interactionTimerID) { + window.clearTimeout(this._interactionTimerID); + this._interactionTimerID = null; + } + } + + /** + * Start the human interaction ('screensaver') timer to reset the view to + * default `view.timeout_seconds` after human interaction. + */ + protected _startInteractionTimer(): void { + this._clearInteractionTimer(); if (this._getConfig().view.timeout_seconds) { - if (this._interactionTimerID) { - window.clearTimeout(this._interactionTimerID); - } this._interactionTimerID = window.setTimeout(() => { - this._interactionTimerID = null; this._changeView(); - if (this._getConfig().view.update_force) { - // If force is enabled, the timer just resets and starts over. - this._resetInteractionTimer(); - } }, this._getConfig().view.timeout_seconds * 1000); } } + /** + * Set the update timer to trigger an update refresh every + * `view.update_seconds`. + */ + protected _startUpdateTimer(): void { + if (this._updateTimerID) { + window.clearTimeout(this._updateTimerID); + this._updateTimerID = null; + } + if (this._getConfig().view.update_seconds) { + this._updateTimerID = window.setTimeout(() => { + if (this._isAutomatedViewUpdateAllowed()) { + this._changeView(); + } else { + // Not allowed to update this time around, but try again at the next + // interval. + this._startUpdateTimer(); + } + }, this._getConfig().view.update_seconds * 1000); + } + } + + /** + * Determine if an automated view update is allowed. + * @returns `true` if it's allowed, `false` otherwise. + */ + protected _isAutomatedViewUpdateAllowed(): boolean { + return ( + this._getConfig().view.update_force || !this._interactionTimerID + ); + } + /** * Render the card menu. * @returns A rendered template. @@ -929,20 +974,6 @@ export class FrigateCard extends LitElement { `; } - /** - * Handler for media play event. - */ - protected _playHandler(): void { - this._mediaPlaying = true; - } - - /** - * Handler for media pause event. - */ - protected _pauseHandler(): void { - this._mediaPlaying = false; - } - /** * Set the message to display and trigger an update. * @param message The message to display. @@ -1115,8 +1146,6 @@ export class FrigateCard extends LitElement { @frigate-card:message=${this._messageHandler} @frigate-card:change-view=${this._changeViewHandler} @frigate-card:media-show=${this._mediaShowHandler} - @frigate-card:pause=${this._pauseHandler} - @frigate-card:play=${this._playHandler} > ${this._getConfig().menu.mode == 'above' ? this._renderMenu() : ''}
diff --git a/src/common.ts b/src/common.ts index 46327533..b589cac9 100644 --- a/src/common.ts +++ b/src/common.ts @@ -114,22 +114,6 @@ export function dispatchFrigateCardEvent( ); } -/** - * Dispatch a Frigate card play event. - * @param element The element to send the event. - */ -export function dispatchPlayEvent(element: HTMLElement): void { - dispatchFrigateCardEvent(element, 'play'); -} - -/** - * Dispatch a Frigate card pause event. - * @param element The element to send the event. - */ -export function dispatchPauseEvent(element: HTMLElement): void { - dispatchFrigateCardEvent(element, 'pause'); -} - /** * Create a MediaShowInfo object. * @param source An event or HTMLElement that should be used as a source. diff --git a/src/components/live.ts b/src/components/live.ts index be86e2a2..59aeffe1 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -40,8 +40,6 @@ import { dispatchExistingMediaShowInfoAsEvent, dispatchMediaShowEvent, dispatchMessageEvent, - dispatchPauseEvent, - dispatchPlayEvent, getCameraIcon, getCameraTitle, homeAssistantSignPath, @@ -701,7 +699,7 @@ export class FrigateCardLiveWebRTC extends LitElement { * @returns The player or `null` if not found. */ protected _getPlayer(): HTMLVideoElement | null { - return this.renderRoot.querySelector('#video') as HTMLVideoElement | null; + return this.renderRoot?.querySelector('#video') as HTMLVideoElement | null; } /** @@ -763,8 +761,6 @@ export class FrigateCardLiveWebRTC extends LitElement { const video = this._getPlayer(); if (video) { const onloadedmetadata = video.onloadedmetadata; - const onplay = video.onplay; - const onpause = video.onpause; video.onloadedmetadata = (e) => { if (onloadedmetadata) { @@ -772,18 +768,6 @@ export class FrigateCardLiveWebRTC extends LitElement { } dispatchMediaShowEvent(this, video); }; - video.onplay = (e) => { - if (onplay) { - onplay.call(video, e); - } - dispatchPlayEvent(this); - }; - video.onpause = (e) => { - if (onpause) { - onpause.call(video, e); - } - dispatchPauseEvent(this); - }; } }); } @@ -864,14 +848,6 @@ export class FrigateCardLiveJSMPEG extends LitElement { url, { canvas: this._jsmpegCanvasElement, - hooks: { - play: () => { - dispatchPlayEvent(this); - }, - pause: () => { - dispatchPauseEvent(this); - }, - }, }, { pauseWhenHidden: false, diff --git a/src/patches/ha-hls-player.ts b/src/patches/ha-hls-player.ts index c2f06f43..ff255eaf 100644 --- a/src/patches/ha-hls-player.ts +++ b/src/patches/ha-hls-player.ts @@ -12,12 +12,7 @@ import { TemplateResult, css, html } from 'lit'; import { Ref, createRef, ref } from 'lit/directives/ref'; import { customElement } from 'lit/decorators.js'; - -import { - dispatchMediaShowEvent, - dispatchPauseEvent, - dispatchPlayEvent, -} from '../common.js'; +import { dispatchMediaShowEvent } from '../common.js'; customElements.whenDefined('ha-hls-player').then(() => { @customElement('frigate-card-ha-hls-player') @@ -54,8 +49,6 @@ customElements.whenDefined('ha-hls-player').then(() => { @loadeddata=${(e) => { dispatchMediaShowEvent(this, e); }} - @pause=${() => dispatchPauseEvent(this)} - @play=${() => dispatchPlayEvent(this)} > `; } diff --git a/src/types.ts b/src/types.ts index c428d4a4..34579767 100644 --- a/src/types.ts +++ b/src/types.ts @@ -370,7 +370,8 @@ export type PictureElements = z.infer; */ const viewConfigDefault = { default: 'live' as const, - timeout: 180, + timeout_seconds: 300, + update_seconds: 0, update_force: false, update_cycle_camera: false, }; @@ -380,7 +381,8 @@ const viewConfigSchema = z .enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED) .optional() .default(viewConfigDefault.default), - timeout_seconds: z.number().default(viewConfigDefault.timeout), + timeout_seconds: z.number().default(viewConfigDefault.timeout_seconds), + update_seconds: z.number().default(viewConfigDefault.update_seconds), update_force: z.boolean().default(viewConfigDefault.update_force), update_cycle_camera: z.boolean().default(viewConfigDefault.update_cycle_camera), update_entities: z.string().array().optional(),