Skip to content

Commit

Permalink
Accept . as a query string delimiter.
Browse files Browse the repository at this point in the history
  • Loading branch information
dermotduffy committed Sep 7, 2023
1 parent 97dac53 commit 568a5ce
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 13 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3807,7 +3807,7 @@ The card can respond to actions in the query string (see [below](#query-string-a
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
https://ha.mydomain.org/lovelace-test/0?frigate-card-action.camera_select=kitchen&frigate-card-action.expand
```
</details>
Expand All @@ -3826,7 +3826,7 @@ cameras:
```

```
https://ha.mydomain.org/lovelace-test/0?frigate-card-action:main:clips
https://ha.mydomain.org/lovelace-test/0?frigate-card-action.main.clips
```
</details>

Expand Down Expand Up @@ -3859,15 +3859,15 @@ elements:
left: 30%
tap_action:
action: navigate
navigation_path: /lovelace-frigate/map?frigate-card-action:camera_select=camera.living_room
navigation_path: /lovelace-frigate/map?frigate-card-action.camera_select=camera.living_room
- type: icon
icon: mdi:cctv
style:
top: 71%
left: 42%
tap_action:
action: navigate
navigation_path: /lovelace-frigate/map?frigate-card-action:camera_select=camera.landing
navigation_path: /lovelace-frigate/map?frigate-card-action.camera_select=camera.landing
```

</details>
Expand Down Expand Up @@ -3998,13 +3998,13 @@ The Frigate card will execute these actions in the following circumstances:
To send an action to *all* Frigate cards:

```
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action:[ACTION]=[VALUE]
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action.[ACTION]=[VALUE]
```

To send an action to a named Frigate card:

```
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action:[CARD_ID]:[ACTION]=[VALUE]
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action.[CARD_ID].[ACTION]=[VALUE]
```

| Parameter | Description |
Expand All @@ -4013,6 +4013,8 @@ To send an action to a named Frigate card:
| `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. |

**Note**: Both `.` and `:` may be used as the delimiter. If you use `:` some browsers may require it be escaped to `%3A`.

**Note**: If a dashboard has multiple Frigate cards on it, even if they are on
different 'tabs' within that dashboard, they will all respond to the actions
unless the action is targeted with a `CARD_ID` as shown above.
Expand Down
6 changes: 4 additions & 2 deletions src/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ class FrigateCard extends LitElement {
// the default view and the querystring view, see:
// https://github.com/dermotduffy/frigate-hass-card/issues/1200
if (!this._view) {
const querystringActions = getActionsFromQueryString();
const querystringActions = getActionsFromQueryString(window.location.search);
if (
!querystringActions.find(
(action) =>
Expand Down Expand Up @@ -1594,7 +1594,9 @@ class FrigateCard extends LitElement {
protected _locationChangeHandler = (): void => {
// Only execute actions when the card has rendered at least once.
if (this.hasUpdated) {
getActionsFromQueryString().forEach((action) => this._cardActionHandler(action));
getActionsFromQueryString(window.location.search).forEach((action) =>
this._cardActionHandler(action),
);
}
};

Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
.or(z.literal('fire-dom-event')),

// Card this command is intended for.
card_id: z.string().optional(),
card_id: z
.string()
.regex(/^\w+$/, 'card_id parameter can only contain [a-z][A-Z][0-9_]')
.optional(),
});

const FRIGATE_CARD_GENERAL_ACTIONS = [
Expand Down
11 changes: 7 additions & 4 deletions src/utils/querystring.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { FrigateCardCustomAction } from '../types';
import { createFrigateCardCustomAction } from './action.js';

export const getActionsFromQueryString = (): FrigateCardCustomAction[] => {
const params = new URLSearchParams(window.location.search);
export const getActionsFromQueryString = (
queryString: string,
): FrigateCardCustomAction[] => {
const params = new URLSearchParams(queryString);
const actions: FrigateCardCustomAction[] = [];
const actionRE = new RegExp(/^frigate-card-action(:(?<cardID>\w+))?:(?<action>\w+)/);

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) {
Expand Down
102 changes: 102 additions & 0 deletions tests/utils/querystring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { getActionsFromQueryString } from '../../src/utils/querystring';

// @vitest-environment jsdom
describe('getActionsFromQueryString', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('should reject malformed query string', () => {
expect(getActionsFromQueryString(`?BOGUS_KEY=BOGUS_VALUE`)).toEqual([]);
});

it('should accept colon as delimiter', () => {
expect(getActionsFromQueryString(`?frigate-card-action:id:clips=`)).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: 'clips',
},
]);
});

describe('should get simple action from query string', () => {
it.each([
['camera_ui' as const],
['clip' as const],
['clips' as const],
['default' as const],
['diagnostics' as const],
['download' as const],
['expand' as const],
['image' as const],
['live' as const],
['menu_toggle' as const],
['recording' as const],
['recordings' as const],
['snapshot' as const],
['snapshots' as const],
['timeline' as const],
])('%s', (action: string) => {
expect(getActionsFromQueryString(`?frigate-card-action.id.${action}=`)).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: action,
},
]);
});
});

it('should get camera_select action', () => {
expect(
getActionsFromQueryString(`?frigate-card-action.id.camera_select=camera.foo`),
).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: 'camera_select',
camera: 'camera.foo',
},
]);
});

it('should get live_substream_select action', () => {
expect(
getActionsFromQueryString(
`?frigate-card-action.id.live_substream_select=camera.bar`,
),
).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: 'live_substream_select',
camera: 'camera.bar',
},
]);
});

describe('should reject value-based actions without value', () => {
it.each([['camera_select' as const], ['live_substream_select' as const]])(
'%s',
(action: string) => {
expect(getActionsFromQueryString(`?frigate-card-action.id.${action}=`)).toEqual(
[],
);
},
);
});

it('should log unknown but correctly formed action', () => {
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);

expect(
getActionsFromQueryString(`?frigate-card-action.id.not_a_real_action}=`),
).toEqual([]);

expect(spy).toBeCalledWith(
'Frigate card received unknown card action in query string: not_a_real_action',
);
});
});

0 comments on commit 568a5ce

Please sign in to comment.