Support executing actions from the query string.

This commit is contained in:
Dermot Duffy
2023-03-13 20:59:07 -07:00
parent ad22ee7e00
commit 292a8083d7
6 changed files with 209 additions and 11 deletions
+115 -1
View File
@@ -882,6 +882,21 @@ If multiple cameras are configured in the card, use [overrides](#overrides) to c
See [media layout examples](#media-layout-examples). See [media layout examples](#media-layout-examples).
<a name="other-options"></a>
### Other Options
All listed configuration options are under the top level, e.g.:
```yaml
type: custom:frigate-card
...
```
| Option | Default | Overridable | Description |
| - | - | - | - |
| `card_id` | | :heavy_multiplication_x: | **Advanced users only**: An optional ID to uniquely identify this card. For use when actions are being sent to card(s) via the [query string](#query-string-actions).|
<a name="webrtc"></a> <a name="webrtc"></a>
### Using AlexxIT's WebRTC Card ### Using AlexxIT's WebRTC Card
@@ -1100,7 +1115,9 @@ Parameters for the `custom:frigate-card-ptz` element:
| `action` | Must be `custom:frigate-card-action`. | | `action` | Must be `custom:frigate-card-action`. |
| `frigate_card_action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `recording`, `recordings`, `snapshot`, `snapshots`, `download`, `timeline`, `camera_ui`, `fullscreen`, `camera_select`, `menu_toggle`, `media_player`, `live_substream_select`, `expand_toggle`.| | `frigate_card_action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `recording`, `recordings`, `snapshot`, `snapshots`, `download`, `timeline`, `camera_ui`, `fullscreen`, `camera_select`, `menu_toggle`, `media_player`, `live_substream_select`, `expand_toggle`.|
##### Command descriptions <a name="custom-actions"></a>
##### Action descriptions
| Value | Description | | Value | Description |
| - | - | | - | - |
@@ -2441,6 +2458,16 @@ performance:
``` ```
</details> </details>
<details>
<summary>Expand: Other options</summary>
Reference: [Other Options](#other-options).
```yaml
card_id: main
```
</details>
### Basic cameras configuration ### Basic cameras configuration
<details> <details>
@@ -3377,6 +3404,41 @@ mode: single
``` ```
</details> </details>
<a name="query-string-examples"></a>
### Passing the card actions from the URL
The card can respond to actions in the query string (see [below](#query-string-actions)).
<details>
<summary>Expand: Selecting the kitchen camera and opening the expanded view</summary>
This example assumes the dashboard URL is `https://ha.mydomain.org/lovelace-test/0`.
```
https://ha.mydomain.org/lovelace-test/0?frigate-card-action/camera_select=kitchen&frigate-card-action/expand_toggle
```
</details>
<details>
<summary>Expand: Choosing the clips view on a named card</summary>
This example assumes the dashboard URL is `https://ha.mydomain.org/lovelace-test/0`.
It assumes that one card (of potentially multiple Frigate Cards on the dashboard) is configured with a `card_id` parameter:
```yaml
type: custom:frigate-card
card_id: main
cameras:
[...]
```
```
https://ha.mydomain.org/lovelace-test/0?frigate-card-action/main/clips
```
</details>
<a name="media-layout-examples"></a> <a name="media-layout-examples"></a>
## Card Refreshes ## Card Refreshes
@@ -3437,6 +3499,58 @@ view:
timeout_seconds: 30 timeout_seconds: 30
``` ```
<a name="query-string-actions"></a>
### Passing the card actions from the URL
It is possible to pass the Frigate card one or more actions from the URL (e.g. select a particular camera, open the live view in expanded mode, etc).
To send an action to *all* Frigate cards on a dashboard:
```
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action/[ACTION]=[VALUE]
```
To send an action to a named Frigate card on the dashboard:
```
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action/[CARD_ID]/[ACTION]=[VALUE]
```
| Parameter | Description |
| - | - |
| `ACTION` | One of the supported Frigate Card custom actions (see below). |
| `CARD_ID` | When specified only cards that have a `card_id` parameter will act. |
| `VALUE` | An optional value to use with the `camera_select` and `live_substream_select` actions. |
#### Actions
| Action | Supported in query string | Explanation |
| - | - | - |
| `camera_select` | :white_check_mark: | |
| `camera_ui`| :white_check_mark: | |
| `clip` | :white_check_mark: | |
| `clips` | :white_check_mark: | |
| `default` | :white_check_mark: | |
| `download`| :heavy_multiplication_x: | Latest media information is not available on initial render. |
| `expand_toggle` | :white_check_mark: | |
| `fullscreen` | :heavy_multiplication_x: | Javascript does not support activating fullscreen without direct human interaction. Use `expand_toggle` as an alternative. |
| `image` | :white_check_mark: | |
| `live_substream_select` | :white_check_mark: | |
| `live` | :white_check_mark: | |
| `media_player`| :heavy_multiplication_x: | Please [request](https://github.com/dermotduffy/frigate-hass-card/issues) if you need this. |
| `menu_toggle` | :white_check_mark: | |
| `recording` | :white_check_mark: | |
| `recordings` | :white_check_mark: | |
| `snapshot` | :white_check_mark: | |
| `snapshots` | :white_check_mark: | |
See [custom actions](#custom-actions) for a description of what the actions do.
#### Examples
See [query string examples](#query-string-examples) for examples of usage.
### Casting the Card ### Casting the Card
This card can be (Chrome) casted to a device (such as a [Nest Hub](https://store.google.com/us/product/nest_hub_2nd_gen)) through the use of [Home Assistant Cast](https://cast.home-assistant.io/). This card can be (Chrome) casted to a device (such as a [Nest Hub](https://store.google.com/us/product/nest_hub_2nd_gen)) through the use of [Home Assistant Cast](https://cast.home-assistant.io/).
+24 -7
View File
@@ -92,6 +92,7 @@ import merge from 'lodash-es/merge';
import { FrigateCardInitializer } from './utils/initializer.js'; import { FrigateCardInitializer } from './utils/initializer.js';
import 'web-dialog'; import 'web-dialog';
import { downloadMedia } from './utils/download.js'; import { downloadMedia } from './utils/download.js';
import { getActionsFromQueryString } from './utils/querystring.js';
/** A note on media callbacks: /** A note on media callbacks:
* *
@@ -1398,15 +1399,26 @@ class FrigateCard extends LitElement {
} }
} }
/** protected _cardActionEventHandler(ev: CustomEvent<ActionType>): void {
* Handle a request for a card action.
* @param ev The action requested.
*/
protected _cardActionHandler(ev: CustomEvent<ActionType>): void {
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail); const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
if (!this._view || !frigateCardAction) { if (frigateCardAction) {
this._cardActionHandler(frigateCardAction);
}
}
protected _cardActionHandler(frigateCardAction: FrigateCardCustomAction): void {
if (!this._view) {
return; return;
} }
if (
frigateCardAction.card_id &&
this._getConfig().card_id !== frigateCardAction.card_id
) {
// Command not intended for this card (e.g. query string command).
return;
}
const action = frigateCardAction.frigate_card_action; const action = frigateCardAction.frigate_card_action;
switch (action) { switch (action) {
@@ -1985,7 +1997,7 @@ class FrigateCard extends LitElement {
class="${classMap(cardClasses)}" class="${classMap(cardClasses)}"
style="${styleMap(cardStyle)}" style="${styleMap(cardStyle)}"
@action=${(ev: CustomEvent) => this._actionHandler(ev, actions)} @action=${(ev: CustomEvent) => this._actionHandler(ev, actions)}
@ll-custom=${this._cardActionHandler.bind(this)} @ll-custom=${this._cardActionEventHandler.bind(this)}
@frigate-card:message=${this._messageHandler.bind(this)} @frigate-card:message=${this._messageHandler.bind(this)}
@frigate-card:view:change=${this._changeViewHandler.bind(this)} @frigate-card:view:change=${this._changeViewHandler.bind(this)}
@frigate-card:view:change-context=${this._addViewContextHandler.bind(this)} @frigate-card:view:change-context=${this._addViewContextHandler.bind(this)}
@@ -2116,6 +2128,11 @@ class FrigateCard extends LitElement {
`; `;
} }
protected firstUpdated(): void {
// Execute query string actions after first render is complete.
getActionsFromQueryString().forEach((action) => this._cardActionHandler(action));
}
/** /**
* Return compiled CSS styles (thus safe to use with unsafeCSS). * Return compiled CSS styles (thus safe to use with unsafeCSS).
*/ */
+2 -2
View File
@@ -52,9 +52,9 @@ export function getLanguage(hass?: HomeAssistant): string {
export const loadLanguages = async (hass: HomeAssistant): Promise<void> => { export const loadLanguages = async (hass: HomeAssistant): Promise<void> => {
const lang = getLanguage(hass); const lang = getLanguage(hass);
if (lang === 'it') { if (lang === 'it') {
languages['it'] = await import('./languages/it.json'); languages[lang] = await import('./languages/it.json');
} else if (lang === 'pt_BR') { } else if (lang === 'pt_BR') {
languages['pt_BR'] = await import('./languages/pt-BR.json'); languages[lang] = await import('./languages/pt-BR.json');
} }
if (lang) { if (lang) {
+6
View File
@@ -205,6 +205,9 @@ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
// Syntactic sugar to avoid 'fire-dom-event' as part of an external API. // Syntactic sugar to avoid 'fire-dom-event' as part of an external API.
.transform((): 'fire-dom-event' => 'fire-dom-event') .transform((): 'fire-dom-event' => 'fire-dom-event')
.or(z.literal('fire-dom-event')), .or(z.literal('fire-dom-event')),
// Card this command is intended for.
card_id: z.string().optional(),
}); });
const FRIGATE_CARD_GENERAL_ACTIONS = [ const FRIGATE_CARD_GENERAL_ACTIONS = [
@@ -1316,6 +1319,9 @@ export const frigateCardConfigSchema = z.object({
// Support for card_mod (https://github.com/thomasloven/lovelace-card-mod). // Support for card_mod (https://github.com/thomasloven/lovelace-card-mod).
card_mod: z.unknown(), card_mod: z.unknown(),
// Card ID (used for query string commands).
card_id: z.string().optional(),
// Stock lovelace card config. // Stock lovelace card config.
type: z.string(), type: z.string(),
test_gui: z.boolean().optional(), test_gui: z.boolean().optional(),
+5 -1
View File
@@ -19,7 +19,7 @@ import {
* @returns A FrigateCardCustomAction or null if it cannot be converted. * @returns A FrigateCardCustomAction or null if it cannot be converted.
*/ */
export function convertActionToFrigateCardCustomAction( export function convertActionToFrigateCardCustomAction(
action: ActionType | null, action: unknown,
): FrigateCardCustomAction | null { ): FrigateCardCustomAction | null {
if (!action) { if (!action) {
return null; return null;
@@ -38,6 +38,7 @@ export function convertActionToFrigateCardCustomAction(
export function createFrigateCardCustomAction( export function createFrigateCardCustomAction(
action: FrigateCardAction, action: FrigateCardAction,
args?: { args?: {
cardID?: string;
camera?: string; camera?: string;
media_player?: string; media_player?: string;
media_player_action?: 'play' | 'stop'; media_player_action?: 'play' | 'stop';
@@ -51,6 +52,7 @@ export function createFrigateCardCustomAction(
action: 'fire-dom-event', action: 'fire-dom-event',
frigate_card_action: action, frigate_card_action: action,
camera: args.camera as string, camera: args.camera as string,
...(args.cardID && { card_id: args.cardID})
}; };
} }
if (action === 'media_player') { if (action === 'media_player') {
@@ -62,11 +64,13 @@ export function createFrigateCardCustomAction(
frigate_card_action: action, frigate_card_action: action,
media_player: args.media_player, media_player: args.media_player,
media_player_action: args.media_player_action, media_player_action: args.media_player_action,
...(args.cardID && { card_id: args.cardID})
}; };
} }
return { return {
action: 'fire-dom-event', action: 'fire-dom-event',
frigate_card_action: action, frigate_card_action: action,
...(args?.cardID && { card_id: args.cardID})
}; };
} }
+57
View File
@@ -0,0 +1,57 @@
import { FrigateCardCustomAction } from '../types';
import { createFrigateCardCustomAction } from './action.js';
export const getActionsFromQueryString = (): FrigateCardCustomAction[] => {
const params = new URLSearchParams(window.location.search);
const actions: FrigateCardCustomAction[] = [];
const actionRE = new RegExp(/^frigate-card-action(\/(?<cardID>\w+))?\/(?<action>\w+)/);
for (const [key, value] of params.entries()) {
const match = key.match(actionRE);
if (!match || !match.groups) {
continue;
}
const cardID: string | undefined = match.groups['cardID'];
const action = match.groups['action'];
let customAction: FrigateCardCustomAction | null = null;
switch (action) {
case 'camera_select':
case 'live_substream_select':
if (value) {
customAction = createFrigateCardCustomAction(action, {
camera: value,
cardID: cardID,
});
}
break;
case 'camera_ui':
case 'clip':
case 'clips':
case 'default':
case 'diagnostics':
case 'download':
case 'expand_toggle':
case 'image':
case 'live':
case 'menu_toggle':
case 'recording':
case 'recordings':
case 'snapshot':
case 'snapshots':
case 'timeline':
customAction = createFrigateCardCustomAction(action, {
cardID: cardID,
});
break;
default:
console.warn(
`Frigate card received unknown card action in query string: ${action}`,
);
}
if (customAction) {
actions.push(customAction);
}
}
return actions;
};