Merge pull request #280 from dermotduffy/play-pause-events

Improve card automated updates
This commit is contained in:
Dermot Duffy
2022-01-23 22:09:29 -08:00
committed by GitHub
9 changed files with 117 additions and 115 deletions
+36 -22
View File
@@ -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 user 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 user 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
@@ -605,7 +603,7 @@ format](https://www.home-assistant.io/lovelace/actions/#tap-action) as well as
the custom [Frigate card action](#frigate-card-action) to trigger Frigate card
changes.
**Note:** The card itself obviously relies on human interactions to function
**Note:** The card itself obviously relies on user interactions to function
(e.g. `tap` on the menu should activate that button, `tap` on a gallery thumbnail
should open that piece of media, etc). These internal actions are executed
_also_, which means that a card-wide `tap` action probably isn't that useful as
@@ -1099,31 +1097,34 @@ image:
## Card Refreshes
Three sets of flags govern when the card will automatically re-render in the
absence of human interaction.
Four sets of flags govern when the card will automatically refresh in the
absence of user 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 user interaction stops. |
| `0` | `X` seconds | `false` | *(Any entity)* | Card will reload default view `X` seconds after user interaction stops, or when entity state changes (as long as user interaction has not occurred in the last `X` seconds). |
| `0` | `X` seconds | `true` | *(Any entity)* | Card will reload default view `X` seconds after user 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 user interaction stops, and every `Y` seconds (as long as there hasn't been user interaction in the last `X` seconds). |
| `Y` seconds | `X` seconds | `false` | *(Any entity)* | Card will reload default view `X` seconds after user interaction stops, and every `Y` seconds or whenever entity state changes (in both cases -- as long as there hasn't been user interaction in the last `X` seconds). |
| `Y` seconds | `X` seconds | `true` | Unset | Card will reload default view `X` seconds after user interaction stops, and every `Y` seconds. |
| `Y` seconds | `X` seconds | `true` | *(Any entity)* | Card will reload default view `X` seconds after user 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 user
interaction with the card stops.
```yaml
view:
default: clip
timeout_seconds: 30
```
## Troubleshooting
+69 -40
View File
@@ -151,11 +151,12 @@ export class FrigateCard extends LitElement {
@query('frigate-card-elements')
_elements?: FrigateCardElements;
// Human interaction timer ID.
// user interaction timer ("screensaver" functionality, return to default
// view after user 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;
@@ -532,7 +533,7 @@ export class FrigateCard extends LitElement {
* where the 'real' error is vs simply a union option not matching. This
* function finds all ZodError "issues" that don't have an error with 'type'
* in that object ('type' is the union discriminator for picture elements,
* the major union in the schema). An array of human-readable error
* the major union in the schema). An array of user-readable error
* locations is returned, or an empty list if none is available. None being
* available suggests the configuration has an error, but we can't tell
* exactly why (or rather Zod simply says it doesn't match any of the
@@ -564,7 +565,7 @@ export class FrigateCard extends LitElement {
}
/**
* Convert an array of strings and indices into a more human readable string,
* Convert an array of strings and indices into a more user readable string,
* e.g. [a, 1, b, 2] => 'a[1] -> b[2]'
* @param path An array of strings and numbers.
* @returns A single string.
@@ -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 user interaction ('screensaver') timer.
*/
protected _clearInteractionTimer(): void {
if (this._interactionTimerID) {
window.clearTimeout(this._interactionTimerID);
this._interactionTimerID = null;
}
}
/**
* Start the user interaction ('screensaver') timer to reset the view to
* default `view.timeout_seconds` after user 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() : ''}
<div class="container outer" style="${styleMap(outerStyle)}">
-16
View File
@@ -114,22 +114,6 @@ export function dispatchFrigateCardEvent<T>(
);
}
/**
* 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.
+1 -25
View File
@@ -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,
+1
View File
@@ -24,6 +24,7 @@ export const CONF_VIEW_TIMEOUT_SECONDS = `${CONF_VIEW}.timeout_seconds` as const
export const CONF_VIEW_UPDATE_CYCLE_CAMERA = `${CONF_VIEW}.update_cycle_camera` as const;
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const;
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
export const CONF_EVENT_GALLERY_MIN_COLUMNS = `${CONF_EVENT_GALLERY}.min_columns` as const;
+2
View File
@@ -59,6 +59,7 @@ import {
CONF_VIEW_TIMEOUT_SECONDS,
CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS,
} from './const.js';
import { arrayMove, getEntityTitle, prettifyFrigateName } from './common.js';
import {
@@ -659,6 +660,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="values">
${this._renderDropdown(CONF_VIEW_DEFAULT, viewModes)}
${this._renderStringInput(CONF_VIEW_TIMEOUT_SECONDS, 'number')}
${this._renderStringInput(CONF_VIEW_UPDATE_SECONDS, 'number')}
${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
${this._renderSwitch(CONF_VIEW_UPDATE_CYCLE_CAMERA, defaults.view.update_cycle_camera)}
</div>
+3 -2
View File
@@ -41,9 +41,10 @@
"snapshots": "Snapshots gallery",
"image": "Static image"
},
"timeout_seconds": "View timeout seconds (before returning to default, 0=never)",
"timeout_seconds": "Reset to default view X seconds after user action (0=never)",
"update_cycle_camera": "Cycle through cameras when default view updates",
"update_force": "Force card updates (ignore media playing / interaction)"
"update_force": "Force card updates (ignore user interaction)",
"update_seconds": "Refresh default view every X seconds (0=never)"
},
"event_gallery": {
"min_columns": "Minimum number of columns"
+1 -8
View File
@@ -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)}
></video>
`;
}
+4 -2
View File
@@ -370,7 +370,8 @@ export type PictureElements = z.infer<typeof pictureElementsSchema>;
*/
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(),