Vendored
+2
-1
@@ -10,6 +10,7 @@
|
||||
"redhat.vscode-yaml",
|
||||
"lokalise.i18n-ally",
|
||||
"vitest.explorer",
|
||||
"inlang.vs-code-extension"
|
||||
"inlang.vs-code-extension",
|
||||
"yoavbls.pretty-ts-errors"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ certain configurations (in `overrides`) or to display "picture elements" (in
|
||||
|
||||
## `camera`
|
||||
|
||||
Matches based on the selected camera.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: camera
|
||||
@@ -29,6 +31,8 @@ conditions:
|
||||
|
||||
## `expand`
|
||||
|
||||
Matches based on whether the card is in "expanded" mode.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: expand
|
||||
@@ -42,6 +46,8 @@ conditions:
|
||||
|
||||
## `fullscreen`
|
||||
|
||||
Matches based on whether the card is in fullscreen.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: fullscreen
|
||||
@@ -55,6 +61,8 @@ conditions:
|
||||
|
||||
## `interaction`
|
||||
|
||||
Matches based on whether the card has been interacted with.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: interaction
|
||||
@@ -68,6 +76,8 @@ conditions:
|
||||
|
||||
## `key`
|
||||
|
||||
Matches based on key state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: key
|
||||
@@ -86,6 +96,8 @@ conditions:
|
||||
|
||||
## `media_loaded`
|
||||
|
||||
Matches based on whether the selected live or media stream has loaded.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: media_loaded
|
||||
@@ -99,6 +111,8 @@ conditions:
|
||||
|
||||
## `microphone`
|
||||
|
||||
Matches based on microphone state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: microphone
|
||||
@@ -111,18 +125,25 @@ conditions:
|
||||
| `connected` | Optional: If `true` or `false` the condition is satisfied if the microphone is connected or disconnected respectively. |
|
||||
| `muted` | Optional: If `true` or `false` the condition is satisfied if the microphone is muted or unmuted respectively. |
|
||||
|
||||
When multiple parameters are specified they must all match for the condition to
|
||||
match.
|
||||
|
||||
## `numeric_state`
|
||||
|
||||
Matches based on numeric Home Assistant state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: numeric_state
|
||||
# [...]
|
||||
```
|
||||
|
||||
This stock Home Assistant condition works out of the box. See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#numeric-state).
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#numeric-state).
|
||||
|
||||
## `screen`
|
||||
|
||||
Matches based on [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: screen
|
||||
@@ -138,16 +159,20 @@ See the [screen conditions examples](../examples.md?id=screen-conditions).
|
||||
|
||||
## `state`
|
||||
|
||||
Matches based on Home Assistant state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: state
|
||||
# [...]
|
||||
```
|
||||
|
||||
This stock Home Assistant condition works out of the box. See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#state).
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#state).
|
||||
|
||||
## `triggered`
|
||||
|
||||
Matches based on whether the selected camera has been triggered.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: triggered
|
||||
@@ -161,16 +186,40 @@ conditions:
|
||||
|
||||
## `user`
|
||||
|
||||
Matches based on the Home Assistant user that is logged in. See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#user).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: user
|
||||
# [...]
|
||||
```
|
||||
|
||||
This stock Home Assistant condition works out of the box. See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#user).
|
||||
## `user_agent`
|
||||
|
||||
Matches based on the [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: user_agent
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `user_agent`. |
|
||||
| `user_agent` | Exactly matches a user-agent, e.g. `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` |
|
||||
| `user_agent_re` | Matches a user-agent based on a regular expression, e.g. `Chrome/`. |
|
||||
| `companion` | If `true` matches if the user-agent is the Home Assistant companion app, if `false` matches if the user-agent is _NOT_ the Home Assistant companion app. |
|
||||
|
||||
When multiple parameters are specified they must all match for the condition to
|
||||
match.
|
||||
|
||||
See the [user-agent overrides example](../examples.md?id=disable-ptz-controls-in-the-home-assistant-companion-app).
|
||||
|
||||
## `view`
|
||||
|
||||
Matches based on the selected view.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: view
|
||||
@@ -230,6 +279,10 @@ conditions:
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
- condition: user_agent
|
||||
user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
user_agent_re: "Chrome/"
|
||||
companion: true
|
||||
- condition: view
|
||||
views:
|
||||
- live
|
||||
|
||||
@@ -39,13 +39,13 @@ Principles used in the selection of options set by `low-performance` profile mod
|
||||
- Get 'out of the box' performance similar to the basic "Home Assistant Picture Glance" card.
|
||||
- Do not break the visual aesthetic of the card.
|
||||
|
||||
See the [source code](https://github.com/dermotduffy/frigate-hass-card/blob/dev/src/config/profiles/low-performance.ts) for an exhaustive list of defaults set by this profile.
|
||||
See the [source code](https://github.com/dermotduffy/frigate-hass-card/blob/main/src/config/profiles/low-performance.ts) for an exhaustive list of defaults set by this profile.
|
||||
|
||||
## `scrubbing`
|
||||
|
||||
Configures the `live` view and media viewer to allow media "scrubbing" as the timeline is dragged back and forth.
|
||||
|
||||
See the [source code](https://github.com/dermotduffy/frigate-hass-card/blob/dev/src/config/profiles/scrubbing.ts) for an exhaustive list of options set by this profile.
|
||||
See the [source code](https://github.com/dermotduffy/frigate-hass-card/blob/main/src/config/profiles/scrubbing.ts) for an exhaustive list of options set by this profile.
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
|
||||
@@ -624,6 +624,22 @@ overrides:
|
||||
- 'cameras[1]'
|
||||
```
|
||||
|
||||
### Disable PTZ controls in the Home Assistant Companion App
|
||||
|
||||
This example disables the PTZ controls when the card is viewed on the Companion app:
|
||||
|
||||
```yaml
|
||||
type: custom:frigate-card
|
||||
cameras:
|
||||
- camera_entity: camera.office
|
||||
overrides:
|
||||
- conditions:
|
||||
- condition: user_agent
|
||||
companion: true
|
||||
set:
|
||||
live.controls.ptz.mode: 'off'
|
||||
```
|
||||
|
||||
## PTZ control
|
||||
|
||||
The card supports using PTZ controls to conveniently control pan, tilt and zoom
|
||||
|
||||
@@ -127,6 +127,10 @@ export class CardElementManager {
|
||||
this._api.getQueryStringManager().requestExecution,
|
||||
);
|
||||
|
||||
this._api.getConditionsManager()?.setState({
|
||||
user_agent: navigator.userAgent,
|
||||
});
|
||||
|
||||
// Manually request query string execute as the card will be
|
||||
// disconnected/reconnected when dashboard 'tab' changes happen within HA.
|
||||
this._api.getQueryStringManager().requestExecution();
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { localize } from '../localize/localize';
|
||||
import { FrigateCardError } from '../types';
|
||||
import { desparsifyArrays } from '../utils/basic';
|
||||
import { isCompanionApp } from '../utils/companion';
|
||||
import { CardConditionAPI, KeysState } from './types';
|
||||
|
||||
interface MicrophoneConditionState {
|
||||
@@ -39,6 +40,7 @@ interface ConditionState {
|
||||
microphone?: MicrophoneConditionState;
|
||||
user?: CurrentUser;
|
||||
keys?: KeysState;
|
||||
user_agent?: string;
|
||||
}
|
||||
|
||||
class OverrideConfigurationError extends FrigateCardError {}
|
||||
@@ -353,6 +355,15 @@ export class ConditionsManager {
|
||||
(conditionObj.shift === undefined ||
|
||||
conditionObj.shift === !!state.keys[conditionObj.key].shift)
|
||||
);
|
||||
case 'user_agent':
|
||||
return (
|
||||
!!state.user_agent &&
|
||||
(!conditionObj.user_agent || conditionObj.user_agent === state.user_agent) &&
|
||||
(conditionObj.companion === undefined ||
|
||||
conditionObj.companion === isCompanionApp(state.user_agent)) &&
|
||||
(conditionObj.user_agent_re === undefined ||
|
||||
new RegExp(conditionObj.user_agent_re).test(state.user_agent))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface CardDownloadAPI {
|
||||
export interface CardElementAPI {
|
||||
getActionsManager(): ActionsManager;
|
||||
getCameraManager(): CameraManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getDefaultManager(): DefaultManager;
|
||||
getExpandManager(): ExpandManager;
|
||||
|
||||
@@ -726,6 +726,25 @@ const keyConditionSchema = z.object({
|
||||
alt: z.boolean().optional(),
|
||||
meta: z.boolean().optional(),
|
||||
});
|
||||
const userAgentConditionSchema = z.object({
|
||||
condition: z.literal('user_agent'),
|
||||
user_agent: z.string().optional(),
|
||||
user_agent_re: z
|
||||
.string()
|
||||
.refine(
|
||||
(val) => {
|
||||
try {
|
||||
new RegExp(val);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ message: 'Invalid regular expression' },
|
||||
)
|
||||
.optional(),
|
||||
companion: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const frigateCardConditionSchema = z.discriminatedUnion('condition', [
|
||||
// Stock conditions:
|
||||
@@ -745,6 +764,7 @@ export const frigateCardConditionSchema = z.discriminatedUnion('condition', [
|
||||
interactionConditionSchema,
|
||||
microphoneConditionSchema,
|
||||
keyConditionSchema,
|
||||
userAgentConditionSchema,
|
||||
]);
|
||||
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const isCompanionApp = (userAgent: string): boolean => {
|
||||
return !!userAgent.match(/Home ?Assistant/);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { localize } from '../localize/localize';
|
||||
import { ExtendedHomeAssistant, FrigateCardError } from '../types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { errorToConsole } from './basic';
|
||||
import { isCompanionApp } from './companion';
|
||||
import { homeAssistantSignPath } from './ha';
|
||||
|
||||
export const downloadURL = (url: string, filename = 'download'): void => {
|
||||
@@ -11,11 +12,7 @@ export const downloadURL = (url: string, filename = 'download'): void => {
|
||||
const isSameOrigin = new URL(url).origin === window.location.origin;
|
||||
const dataURL = url.startsWith('data:');
|
||||
|
||||
if (
|
||||
navigator.userAgent.startsWith('Home Assistant/') ||
|
||||
navigator.userAgent.startsWith('HomeAssistant/') ||
|
||||
(!isSameOrigin && !dataURL)
|
||||
) {
|
||||
if (isCompanionApp(navigator.userAgent) || (!isSameOrigin && !dataURL)) {
|
||||
// Home Assistant companion apps cannot download files without opening a
|
||||
// new browser window.
|
||||
//
|
||||
|
||||
@@ -1023,5 +1023,75 @@ describe('ConditionsManager', () => {
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with user agent condition', () => {
|
||||
const userAgent =
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||
|
||||
it('should match exact user agent', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'user_agent' as const, user_agent: userAgent }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: userAgent,
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: 'Something else',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should match user agent regex', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [
|
||||
{ condition: 'user_agent' as const, user_agent_re: 'Chrome/' },
|
||||
];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: userAgent,
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: 'Something else',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should match companion app', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'user_agent' as const, companion: true }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: 'Home Assistant/',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: userAgent,
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should match multiple parameters', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [
|
||||
{
|
||||
condition: 'user_agent' as const,
|
||||
companion: true,
|
||||
user_agent: 'Home Assistant/',
|
||||
user_agent_re: 'Home.Assistant',
|
||||
},
|
||||
];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: 'Home Assistant/',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: 'Something else',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
conditionalSchema,
|
||||
customSchema,
|
||||
dimensionsConfigSchema,
|
||||
frigateCardConditionSchema,
|
||||
frigateCardCustomActionsBaseSchema,
|
||||
frigateCardCustomActionSchema,
|
||||
} from '../../src/config/types';
|
||||
@@ -401,6 +402,29 @@ it('should transform dimensions.aspect_ratio', () => {
|
||||
).toEqual(expect.objectContaining({ aspect_ratio: [16, 9] }));
|
||||
});
|
||||
|
||||
describe('should refine user_agent_re conditions', () => {
|
||||
it('should successfully parse valid user_agent_re condition', () => {
|
||||
expect(
|
||||
frigateCardConditionSchema.parse({
|
||||
condition: 'user_agent',
|
||||
user_agent_re: 'Chrome/',
|
||||
}),
|
||||
).toEqual({
|
||||
condition: 'user_agent',
|
||||
user_agent_re: 'Chrome/',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid user_agent_re conditions', () => {
|
||||
expect(() =>
|
||||
frigateCardConditionSchema.parse({
|
||||
condition: 'user_agent',
|
||||
user_agent_re: '[',
|
||||
}),
|
||||
).toThrowError(/Invalid regular expression/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform action', () => {
|
||||
expect(
|
||||
frigateCardCustomActionsBaseSchema.parse({
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isCompanionApp } from '../../src/utils/companion';
|
||||
|
||||
describe('isCompanionApp', () => {
|
||||
it('should return true for userAgent starting with "Home Assistant/"', () => {
|
||||
expect(isCompanionApp('Home Assistant/1.0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for userAgent starting with "HomeAssistant/"', () => {
|
||||
expect(isCompanionApp('HomeAssistant/1.0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for userAgent not starting with "Home Assistant/" or "HomeAssistant/"', () => {
|
||||
expect(isCompanionApp('Mozilla/5.0')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an empty userAgent', () => {
|
||||
expect(isCompanionApp('')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [
|
||||
'utils/basic.ts',
|
||||
'utils/camera.ts',
|
||||
'utils/casting.ts',
|
||||
'utils/companion.ts',
|
||||
'utils/custom-icons.ts',
|
||||
'utils/debug.ts',
|
||||
'utils/diagnostics.ts',
|
||||
|
||||
Reference in New Issue
Block a user