Merge pull request #1296 from dermotduffy/new-way-of-casting
Add new method of casting that supports low latency live streams
This commit is contained in:
@@ -145,6 +145,7 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|
|||||||
| `dependencies` | | :white_check_mark: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. |
|
| `dependencies` | | :white_check_mark: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. |
|
||||||
| `triggers` | | :white_check_mark: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. |
|
| `triggers` | | :white_check_mark: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. |
|
||||||
| `webrtc_card` | | :white_check_mark: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
|
| `webrtc_card` | | :white_check_mark: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
|
||||||
|
| `cast` | | :white_check_mark: | Configuration that controls how this camera is "casted" / sent to media players. See below. |
|
||||||
|
|
||||||
<a name="live-providers"></a>
|
<a name="live-providers"></a>
|
||||||
|
|
||||||
@@ -332,6 +333,35 @@ cameras:
|
|||||||
| `occupancy` | `true` | :white_check_mark: | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera and its configured zones and labels. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant. If this camera has configured zones, only occupancy sensors for those zones are used -- if the overall _camera_ occupancy sensor is also required, it can be manually added to `entities`. If this camera has configured labels, only occupancy sensors for those labels are used.|
|
| `occupancy` | `true` | :white_check_mark: | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera and its configured zones and labels. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant. If this camera has configured zones, only occupancy sensors for those zones are used -- if the overall _camera_ occupancy sensor is also required, it can be manually added to `entities`. If this camera has configured labels, only occupancy sensors for those labels are used.|
|
||||||
| `entities` | | :white_check_mark: | Whether to not to trigger the camera when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). This works for Frigate or non-Frigate cameras.|
|
| `entities` | | :white_check_mark: | Whether to not to trigger the camera when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). This works for Frigate or non-Frigate cameras.|
|
||||||
|
|
||||||
|
#### Camera Cast Configuration
|
||||||
|
|
||||||
|
The `cast` block configures what how a camera is cast / sent to media players.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cameras:
|
||||||
|
- cast:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Overridable | Description |
|
||||||
|
| - | - | - | - |
|
||||||
|
| `method` | `standard` | :white_check_mark: | Whether to use `standard` media casting to send the live view to your media player, or to instead cast a `dashboard` you have manually setup. Casting a dashboard supports a much wider variety of video media, including low latency video providers (e.g. `go2rtc`). This setting has no effect on casting non-live media. |
|
||||||
|
| `dashboard` | | :white_check_mark: | Configuration for the dashboard to cast. See below. |
|
||||||
|
|
||||||
|
See the [dashboard method cast example](#cast-dashboard-example).
|
||||||
|
|
||||||
|
#### Camera Cast Dashboard Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cameras:
|
||||||
|
- cast:
|
||||||
|
dashboard:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Overridable | Description |
|
||||||
|
| - | - | - | - |
|
||||||
|
| `dashboard_path` | | :white_check_mark: | A required field that specifies the name of the dashboard to cast. You can see this name in your HA URL when you visit the dashboard. |
|
||||||
|
| `view_path` | | :white_check_mark: | A required field that specifies view/"tab" on that dashboard to cast. This is the value you have specified in the `url` field of the view configuration on the dashboard. |
|
||||||
|
|
||||||
<a name="camera-ids"></a>
|
<a name="camera-ids"></a>
|
||||||
|
|
||||||
#### Camera IDs: Referring to cameras in card configuration
|
#### Camera IDs: Referring to cameras in card configuration
|
||||||
@@ -1673,8 +1703,6 @@ Pan around a large camera view to only show part of the video feed in the card a
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Illustrative Expanded Configuration Reference
|
### Illustrative Expanded Configuration Reference
|
||||||
|
|
||||||
**Caution**: 🚩 Just copying this full reference into your configuration will cause you a significant maintenance burden. Don't do it! Please only specify what you need as defaults can / do change continually as this card develops. Almost all the values shown here are the defaults (except in cases where is no default, parameters are added here for illustrative purposes).
|
**Caution**: 🚩 Just copying this full reference into your configuration will cause you a significant maintenance burden. Don't do it! Please only specify what you need as defaults can / do change continually as this card develops. Almost all the values shown here are the defaults (except in cases where is no default, parameters are added here for illustrative purposes).
|
||||||
@@ -1710,6 +1738,8 @@ cameras:
|
|||||||
occupancy: true
|
occupancy: true
|
||||||
entities:
|
entities:
|
||||||
- binary_sensor.front_door_sensor
|
- binary_sensor.front_door_sensor
|
||||||
|
cast:
|
||||||
|
method: standard
|
||||||
- camera_entity: camera.entrance
|
- camera_entity: camera.entrance
|
||||||
live_provider: webrtc-card
|
live_provider: webrtc-card
|
||||||
engine: auto
|
engine: auto
|
||||||
@@ -1747,6 +1777,11 @@ cameras:
|
|||||||
- mjpeg
|
- mjpeg
|
||||||
stream: sitting_room
|
stream: sitting_room
|
||||||
url: 'https://my.custom.go2rtc.backend'
|
url: 'https://my.custom.go2rtc.backend'
|
||||||
|
cast:
|
||||||
|
method: dashboard
|
||||||
|
dashboard:
|
||||||
|
dashboard_path: cast
|
||||||
|
view_path: front-door
|
||||||
- camera_entity: camera.sitting_room_webrtc_card
|
- camera_entity: camera.sitting_room_webrtc_card
|
||||||
live_provider: webrtc_card
|
live_provider: webrtc_card
|
||||||
webrtc_card:
|
webrtc_card:
|
||||||
@@ -3942,6 +3977,49 @@ overrides:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<a name="cast-dashboard-example"></a>
|
||||||
|
|
||||||
|
### `dashboard` cast method
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Using the `dashboard` cast method to cast a low latency live provider</summary>
|
||||||
|
|
||||||
|
This example will configure a Frigate card that can cast a dashboard view to a media player, which has a second Frigate card in panel mode with a low-latency live provider.
|
||||||
|
|
||||||
|
#### Source card
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:frigate-card
|
||||||
|
cameras:
|
||||||
|
- camera_entity: camera.front_door
|
||||||
|
cast:
|
||||||
|
method: dashboard
|
||||||
|
dashboard:
|
||||||
|
dashboard_path: cast
|
||||||
|
view_path: front-door
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dashboard configuration
|
||||||
|
|
||||||
|
This dashboard is configured at the path `/cast/` (path referred to in `dashboard_path` above).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
title: Frigate Card Casting
|
||||||
|
views:
|
||||||
|
- title: Casting
|
||||||
|
# This path is referred to in `view_path` above.
|
||||||
|
path: front-door
|
||||||
|
# Ensure the video is "maximized"
|
||||||
|
type: panel
|
||||||
|
cards:
|
||||||
|
- type: custom:frigate-card
|
||||||
|
cameras:
|
||||||
|
- camera_entity: camera.front_door
|
||||||
|
live_provider: go2rtc
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<a name="media-layout-examples"></a>
|
<a name="media-layout-examples"></a>
|
||||||
|
|
||||||
## Card Refreshes
|
## Card Refreshes
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { CameraConfig } from '../config/types';
|
||||||
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const';
|
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const';
|
||||||
import { ViewMedia } from '../view/media';
|
import { localize } from '../localize/localize';
|
||||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
|
||||||
import { errorToConsole } from '../utils/basic';
|
import { errorToConsole } from '../utils/basic';
|
||||||
import { Entity } from '../utils/ha/entity-registry/types';
|
import { Entity } from '../utils/ha/entity-registry/types';
|
||||||
import { supportsFeature } from '../utils/ha/update';
|
import { supportsFeature } from '../utils/ha/update';
|
||||||
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||||
import { CardMediaPlayerAPI } from './types';
|
import { CardMediaPlayerAPI } from './types';
|
||||||
|
|
||||||
export class MediaPlayerManager {
|
export class MediaPlayerManager {
|
||||||
@@ -76,11 +78,27 @@ export class MediaPlayerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async playLive(mediaPlayer: string, cameraID: string): Promise<void> {
|
public async playLive(mediaPlayer: string, cameraID: string): Promise<void> {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
const cameraConfig = this._api
|
const cameraConfig = this._api
|
||||||
.getCameraManager()
|
.getCameraManager()
|
||||||
.getStore()
|
.getStore()
|
||||||
.getCameraConfig(cameraID);
|
.getCameraConfig(cameraID);
|
||||||
|
if (!cameraConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraConfig.cast?.method === 'dashboard') {
|
||||||
|
await this._playLiveDashboard(mediaPlayer, cameraConfig);
|
||||||
|
} else {
|
||||||
|
await this._playLiveStandard(mediaPlayer, cameraID, cameraConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _playLiveStandard(
|
||||||
|
mediaPlayer: string,
|
||||||
|
cameraID: string,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
const cameraEntity = cameraConfig?.camera_entity ?? null;
|
const cameraEntity = cameraConfig?.camera_entity ?? null;
|
||||||
|
|
||||||
if (!hass || !cameraEntity) {
|
if (!hass || !cameraEntity) {
|
||||||
@@ -102,6 +120,34 @@ export class MediaPlayerManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async _playLiveDashboard(
|
||||||
|
mediaPlayer: string,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
if (!hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashboardConfig = cameraConfig.cast?.dashboard;
|
||||||
|
if (!dashboardConfig?.dashboard_path || !dashboardConfig?.view_path) {
|
||||||
|
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||||
|
type: 'error',
|
||||||
|
icon: 'mdi:cast',
|
||||||
|
message: localize('error.no_dashboard_or_view'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When this bug is closed, a query string could be included:
|
||||||
|
// https://github.com/home-assistant/core/issues/98316
|
||||||
|
await hass.callService('cast', 'show_lovelace_view', {
|
||||||
|
entity_id: mediaPlayer,
|
||||||
|
dashboard_path: dashboardConfig.dashboard_path,
|
||||||
|
view_path: dashboardConfig.view_path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async playMedia(mediaPlayer: string, media?: ViewMedia | null): Promise<void> {
|
public async playMedia(mediaPlayer: string, media?: ViewMedia | null): Promise<void> {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
|||||||
@@ -23,16 +23,53 @@ export class QueryStringManager {
|
|||||||
|
|
||||||
public executeNonViewRelated = (): void => {
|
public executeNonViewRelated = (): void => {
|
||||||
this._executeNonViewRelated(this._calculateIntent());
|
this._executeNonViewRelated(this._calculateIntent());
|
||||||
}
|
};
|
||||||
|
|
||||||
public executeViewRelated = (): void => {
|
public executeViewRelated = (): void => {
|
||||||
this._executeViewRelated(this._calculateIntent());
|
this._executeViewRelated(this._calculateIntent());
|
||||||
}
|
};
|
||||||
|
|
||||||
public executeAll = (): void => {
|
public executeAll = (): void => {
|
||||||
const intent = this._calculateIntent();
|
const intent = this._calculateIntent();
|
||||||
this._executeViewRelated(intent);
|
this._executeViewRelated(intent);
|
||||||
this._executeNonViewRelated(intent);
|
this._executeNonViewRelated(intent);
|
||||||
|
};
|
||||||
|
|
||||||
|
public generateQueryString(action: FrigateCardCustomAction): string | null {
|
||||||
|
const baseKey =
|
||||||
|
'frigate-card-action.' + (action.card_id ? `${action.card_id}.` : '');
|
||||||
|
|
||||||
|
switch (action.frigate_card_action) {
|
||||||
|
case 'camera_select':
|
||||||
|
case 'live_substream_select':
|
||||||
|
return new URLSearchParams([
|
||||||
|
[baseKey + action.frigate_card_action, action.camera],
|
||||||
|
]).toString();
|
||||||
|
case 'camera_ui':
|
||||||
|
case 'clip':
|
||||||
|
case 'clips':
|
||||||
|
case 'default':
|
||||||
|
case 'diagnostics':
|
||||||
|
case 'download':
|
||||||
|
case 'expand':
|
||||||
|
case 'image':
|
||||||
|
case 'live':
|
||||||
|
case 'menu_toggle':
|
||||||
|
case 'recording':
|
||||||
|
case 'recordings':
|
||||||
|
case 'snapshot':
|
||||||
|
case 'snapshots':
|
||||||
|
case 'timeline':
|
||||||
|
return new URLSearchParams([
|
||||||
|
[baseKey + action.frigate_card_action, ''],
|
||||||
|
]).toString();
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
`Frigate card cannot convert unsupported action to query string:`,
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _executeViewRelated(intent: QueryStringViewIntent): void {
|
protected _executeViewRelated(intent: QueryStringViewIntent): void {
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ export interface CardMediaPlayerAPI {
|
|||||||
getHASSManager(): HASSManager;
|
getHASSManager(): HASSManager;
|
||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
getEntityRegistryManager(): EntityRegistryManager;
|
getEntityRegistryManager(): EntityRegistryManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
getQueryStringManager(): QueryStringManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardMessageAPI {
|
export interface CardMessageAPI {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
|
|||||||
'image',
|
'image',
|
||||||
'timeline',
|
'timeline',
|
||||||
] as const;
|
] as const;
|
||||||
|
export type FrigateCardUserSpecifiedView =
|
||||||
|
(typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED)[number];
|
||||||
|
|
||||||
const FRIGATE_CARD_VIEWS = [
|
const FRIGATE_CARD_VIEWS = [
|
||||||
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
@@ -213,6 +215,7 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
|||||||
'screenshot',
|
'screenshot',
|
||||||
'unmute',
|
'unmute',
|
||||||
] as const;
|
] as const;
|
||||||
|
export type FrigateCardGeneralAction = (typeof FRIGATE_CARD_GENERAL_ACTIONS)[number];
|
||||||
|
|
||||||
const FRIGATE_CARD_ACTIONS = [
|
const FRIGATE_CARD_ACTIONS = [
|
||||||
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
@@ -237,6 +240,9 @@ const frigateCardCameraSelectActionSchema = frigateCardCustomActionsBaseSchema.e
|
|||||||
frigate_card_action: z.literal('camera_select'),
|
frigate_card_action: z.literal('camera_select'),
|
||||||
camera: z.string(),
|
camera: z.string(),
|
||||||
});
|
});
|
||||||
|
export type FrigateCardCameraSelectAction = z.infer<
|
||||||
|
typeof frigateCardCameraSelectActionSchema
|
||||||
|
>;
|
||||||
|
|
||||||
const frigateCardLiveDependencySelectActionSchema =
|
const frigateCardLiveDependencySelectActionSchema =
|
||||||
frigateCardCustomActionsBaseSchema.extend({
|
frigateCardCustomActionsBaseSchema.extend({
|
||||||
@@ -920,6 +926,24 @@ const liveOverridesSchema = z
|
|||||||
.optional();
|
.optional();
|
||||||
export type LiveOverrides = z.infer<typeof liveOverridesSchema>;
|
export type LiveOverrides = z.infer<typeof liveOverridesSchema>;
|
||||||
|
|
||||||
|
// *************************************************************************
|
||||||
|
// Cast Configuration
|
||||||
|
// *************************************************************************
|
||||||
|
|
||||||
|
const castConfigDefault = {
|
||||||
|
method: 'standard' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const castSchema = z.object({
|
||||||
|
method: z.enum(['standard', 'dashboard']).default(castConfigDefault.method).optional(),
|
||||||
|
dashboard: z
|
||||||
|
.object({
|
||||||
|
dashboard_path: z.string().optional(),
|
||||||
|
view_path: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
// Camera Configuration
|
// Camera Configuration
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
@@ -1035,6 +1059,8 @@ export const cameraConfigSchema = z
|
|||||||
image: liveImageConfigSchema.default(cameraConfigDefault.image),
|
image: liveImageConfigSchema.default(cameraConfigDefault.image),
|
||||||
jsmpeg: jsmpegConfigSchema.optional(),
|
jsmpeg: jsmpegConfigSchema.optional(),
|
||||||
webrtc_card: webrtcCardConfigSchema.optional(),
|
webrtc_card: webrtcCardConfigSchema.optional(),
|
||||||
|
|
||||||
|
cast: castSchema.optional(),
|
||||||
})
|
})
|
||||||
.default(cameraConfigDefault);
|
.default(cameraConfigDefault);
|
||||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY =
|
|||||||
`${CONF_CAMERAS}.#.camera_entity` as const;
|
`${CONF_CAMERAS}.#.camera_entity` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME =
|
export const CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME =
|
||||||
`${CONF_CAMERAS}.#.frigate.camera_name` as const;
|
`${CONF_CAMERAS}.#.frigate.camera_name` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_CAST_METHOD = `${CONF_CAMERAS}.#.cast.method` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH =
|
||||||
|
`${CONF_CAMERAS}.#.cast.dashboard.dashboard_path` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH =
|
||||||
|
`${CONF_CAMERAS}.#.cast.dashboard.view_path` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID =
|
export const CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID =
|
||||||
`${CONF_CAMERAS}.#.frigate.client_id` as const;
|
`${CONF_CAMERAS}.#.frigate.client_id` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_FRIGATE_LABELS =
|
export const CONF_CAMERAS_ARRAY_FRIGATE_LABELS =
|
||||||
|
|||||||
+39
-1
@@ -1,4 +1,8 @@
|
|||||||
import { fireEvent, HomeAssistant, LovelaceCardEditor } from '@dermotduffy/custom-card-helpers';
|
import {
|
||||||
|
fireEvent,
|
||||||
|
HomeAssistant,
|
||||||
|
LovelaceCardEditor,
|
||||||
|
} from '@dermotduffy/custom-card-helpers';
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -29,6 +33,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
CONF_CAMERAS,
|
CONF_CAMERAS,
|
||||||
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
||||||
|
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH,
|
||||||
|
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH,
|
||||||
|
CONF_CAMERAS_ARRAY_CAST_METHOD,
|
||||||
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
|
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
|
||||||
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
|
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
|
||||||
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
|
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
|
||||||
@@ -191,6 +198,7 @@ import {
|
|||||||
|
|
||||||
const MENU_BUTTONS = 'buttons';
|
const MENU_BUTTONS = 'buttons';
|
||||||
const MENU_CAMERAS = 'cameras';
|
const MENU_CAMERAS = 'cameras';
|
||||||
|
const MENU_CAMERAS_CAST = 'cameras.cast';
|
||||||
const MENU_CAMERAS_DEPENDENCIES = 'cameras.dependencies';
|
const MENU_CAMERAS_DEPENDENCIES = 'cameras.dependencies';
|
||||||
const MENU_CAMERAS_ENGINE = 'cameras.engine';
|
const MENU_CAMERAS_ENGINE = 'cameras.engine';
|
||||||
const MENU_CAMERAS_FRIGATE = 'cameras.frigate';
|
const MENU_CAMERAS_FRIGATE = 'cameras.frigate';
|
||||||
@@ -561,6 +569,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: 'grid', label: localize('display_modes.grid') },
|
{ value: 'grid', label: localize('display_modes.grid') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected _castMethods: EditorSelectOption[] = [
|
||||||
|
{ value: '', label: '' },
|
||||||
|
{ value: 'standard', label: localize('config.cameras.cast.methods.standard') },
|
||||||
|
{ value: 'dashboard', label: localize('config.cameras.cast.methods.dashboard') },
|
||||||
|
];
|
||||||
|
|
||||||
public setConfig(config: RawFrigateCardConfig): void {
|
public setConfig(config: RawFrigateCardConfig): void {
|
||||||
// Note: This does not use Zod to parse the configuration, so it may be
|
// Note: This does not use Zod to parse the configuration, so it may be
|
||||||
// partially or completely invalid. It's more useful to have a partially
|
// partially or completely invalid. It's more useful to have a partially
|
||||||
@@ -1662,6 +1676,30 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
},
|
},
|
||||||
)}`,
|
)}`,
|
||||||
)}
|
)}
|
||||||
|
${this._putInSubmenu(
|
||||||
|
MENU_CAMERAS_CAST,
|
||||||
|
cameraIndex,
|
||||||
|
'config.cameras.cast.editor_label',
|
||||||
|
{ name: 'mdi:cast' },
|
||||||
|
html`
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_CAST_METHOD, cameraIndex),
|
||||||
|
this._castMethods,
|
||||||
|
)}
|
||||||
|
${this._renderStringInput(
|
||||||
|
getArrayConfigPath(
|
||||||
|
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH,
|
||||||
|
cameraIndex,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
${this._renderStringInput(
|
||||||
|
getArrayConfigPath(
|
||||||
|
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH,
|
||||||
|
cameraIndex,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
: ``}
|
: ``}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,18 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"cameras": {
|
"cameras": {
|
||||||
"camera_entity": "Camera Entity",
|
"camera_entity": "Camera Entity",
|
||||||
|
"cast": {
|
||||||
|
"dashboard": {
|
||||||
|
"dashboard_path": "Dashboard path",
|
||||||
|
"view_path": "View path"
|
||||||
|
},
|
||||||
|
"editor_label": "Cast Options",
|
||||||
|
"method": "Cast method",
|
||||||
|
"methods": {
|
||||||
|
"standard": "Standard",
|
||||||
|
"dashboard": "Dashboard"
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"all_cameras": "Show events for all cameras with this camera",
|
"all_cameras": "Show events for all cameras with this camera",
|
||||||
"cameras": "Show events for specific cameras with this camera",
|
"cameras": "Show events for specific cameras with this camera",
|
||||||
@@ -444,6 +456,7 @@
|
|||||||
"no_camera_entity_for_triggers": "A camera entity is required in order to autodetect triggers",
|
"no_camera_entity_for_triggers": "A camera entity is required in order to autodetect triggers",
|
||||||
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
|
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
|
||||||
"no_camera_name": "Could not determine a Frigate camera name for camera (or one of its dependents), please specify either 'camera_entity' or 'camera_name'",
|
"no_camera_name": "Could not determine a Frigate camera name for camera (or one of its dependents), please specify either 'camera_entity' or 'camera_name'",
|
||||||
|
"no_dashboard_or_view": "Both 'dashboard_path' and 'view_path' parameters are required for the 'dashboard' cast method",
|
||||||
"no_live_camera": "The camera_entity parameter must be set and valid for this live provider",
|
"no_live_camera": "The camera_entity parameter must be set and valid for this live provider",
|
||||||
"no_visible_cameras": "No visible cameras found, you must configure at least one non-hidden camera",
|
"no_visible_cameras": "No visible cameras found, you must configure at least one non-hidden camera",
|
||||||
"reconnecting": "Reconnecting",
|
"reconnecting": "Reconnecting",
|
||||||
@@ -521,4 +534,4 @@
|
|||||||
},
|
},
|
||||||
"select_date": "Choose date"
|
"select_date": "Choose date"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,18 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"cameras": {
|
"cameras": {
|
||||||
"camera_entity": "Entità della telecamera",
|
"camera_entity": "Entità della telecamera",
|
||||||
|
"cast": {
|
||||||
|
"dashboard": {
|
||||||
|
"dashboard_path": "",
|
||||||
|
"view_path": ""
|
||||||
|
},
|
||||||
|
"editor_label": "",
|
||||||
|
"method": "",
|
||||||
|
"methods": {
|
||||||
|
"standard": "",
|
||||||
|
"dashboard": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"all_cameras": "Mostra eventi per tutte le telecamere con questa telecamera",
|
"all_cameras": "Mostra eventi per tutte le telecamere con questa telecamera",
|
||||||
"cameras": "Mostra eventi per telecamere specifiche con questa telecamera",
|
"cameras": "Mostra eventi per telecamere specifiche con questa telecamera",
|
||||||
@@ -436,6 +448,7 @@
|
|||||||
"no_camera_entity_for_triggers": "È necessaria un'entità telecamera per rilevare automaticamente i trigger",
|
"no_camera_entity_for_triggers": "È necessaria un'entità telecamera per rilevare automaticamente i trigger",
|
||||||
"no_camera_id": "Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'",
|
"no_camera_id": "Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'",
|
||||||
"no_camera_name": "Impossibile determinare un nome della telecamera in Frigate, si prega di specificare 'camera_enty' o 'camera_name'",
|
"no_camera_name": "Impossibile determinare un nome della telecamera in Frigate, si prega di specificare 'camera_enty' o 'camera_name'",
|
||||||
|
"no_dashboard_or_view": "",
|
||||||
"no_live_camera": "Il parametro fotocamera_enty deve essere impostato e valido per questo provider live",
|
"no_live_camera": "Il parametro fotocamera_enty deve essere impostato e valido per questo provider live",
|
||||||
"no_visible_cameras": "Nessuna telecamera visibile trovata, è necessario configurare almeno una telecamera non nascosta",
|
"no_visible_cameras": "Nessuna telecamera visibile trovata, è necessario configurare almeno una telecamera non nascosta",
|
||||||
"reconnecting": "Riconnessione",
|
"reconnecting": "Riconnessione",
|
||||||
|
|||||||
@@ -10,6 +10,18 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"cameras": {
|
"cameras": {
|
||||||
"camera_entity": "Entidade da Câmera",
|
"camera_entity": "Entidade da Câmera",
|
||||||
|
"cast": {
|
||||||
|
"dashboard": {
|
||||||
|
"dashboard_path": "",
|
||||||
|
"view_path": ""
|
||||||
|
},
|
||||||
|
"editor_label": "",
|
||||||
|
"method": "",
|
||||||
|
"methods": {
|
||||||
|
"standard": "",
|
||||||
|
"dashboard": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"all_cameras": "Mostrar eventos para todas as câmeras nesta câmera",
|
"all_cameras": "Mostrar eventos para todas as câmeras nesta câmera",
|
||||||
"cameras": "Mostrar eventos para câmeras específicas nesta câmera",
|
"cameras": "Mostrar eventos para câmeras específicas nesta câmera",
|
||||||
@@ -443,6 +455,7 @@
|
|||||||
"no_camera_entity_for_triggers": "Uma entidade de câmera é necessária para detectar automaticamente os gatilhos",
|
"no_camera_entity_for_triggers": "Uma entidade de câmera é necessária para detectar automaticamente os gatilhos",
|
||||||
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
||||||
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
|
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
|
||||||
|
"no_dashboard_or_view": "",
|
||||||
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este provedor ativo",
|
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este provedor ativo",
|
||||||
"no_visible_cameras": "Nenhuma câmera visível encontrada, você deve configurar pelo menos uma câmera não oculta",
|
"no_visible_cameras": "Nenhuma câmera visível encontrada, você deve configurar pelo menos uma câmera não oculta",
|
||||||
"reconnecting": "Reconectando",
|
"reconnecting": "Reconectando",
|
||||||
|
|||||||
@@ -10,6 +10,18 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"cameras": {
|
"cameras": {
|
||||||
"camera_entity": "Entidade da Câmera",
|
"camera_entity": "Entidade da Câmera",
|
||||||
|
"cast": {
|
||||||
|
"dashboard": {
|
||||||
|
"dashboard_path": "",
|
||||||
|
"view_path": ""
|
||||||
|
},
|
||||||
|
"editor_label": "",
|
||||||
|
"method": "",
|
||||||
|
"methods": {
|
||||||
|
"standard": "",
|
||||||
|
"dashboard": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"all_cameras": "Mostrar eventos para todas as câmeras nesta câmera",
|
"all_cameras": "Mostrar eventos para todas as câmeras nesta câmera",
|
||||||
"cameras": "Mostrar eventos para câmeras específicas nesta câmera",
|
"cameras": "Mostrar eventos para câmeras específicas nesta câmera",
|
||||||
@@ -429,6 +441,7 @@
|
|||||||
"no_camera_entity_for_triggers": "Não existe camera para a acção",
|
"no_camera_entity_for_triggers": "Não existe camera para a acção",
|
||||||
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
||||||
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
|
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
|
||||||
|
"no_dashboard_or_view": "",
|
||||||
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este serviço ativo",
|
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este serviço ativo",
|
||||||
"no_visible_cameras": "Sem camaras visiveis",
|
"no_visible_cameras": "Sem camaras visiveis",
|
||||||
"reconnecting": "A voltar a ligar",
|
"reconnecting": "A voltar a ligar",
|
||||||
|
|||||||
@@ -143,52 +143,10 @@ describe('MediaPlayerManager', () => {
|
|||||||
|
|
||||||
describe('should play', () => {
|
describe('should play', () => {
|
||||||
describe('live', () => {
|
describe('live', () => {
|
||||||
it('successfully', async () => {
|
it('without camera config', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(null);
|
||||||
createCameraConfig({
|
|
||||||
camera_entity: 'camera.foo',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
|
||||||
title: 'camera title',
|
|
||||||
icon: 'icon',
|
|
||||||
});
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
const hass = createHASS({
|
|
||||||
'camera.foo': createStateEntity({
|
|
||||||
attributes: {
|
|
||||||
entity_picture: 'http://thumbnail',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
|
||||||
'media_player',
|
|
||||||
'play_media',
|
|
||||||
{
|
|
||||||
entity_id: 'media_player.foo',
|
|
||||||
media_content_id: 'media-source://camera/camera.foo',
|
|
||||||
media_content_type: 'application/vnd.apple.mpegurl',
|
|
||||||
extra: {
|
|
||||||
title: 'camera title',
|
|
||||||
thumb: 'http://thumbnail',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without camera_entity', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
|
||||||
createCameraConfig({}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
@@ -198,30 +156,176 @@ describe('MediaPlayerManager', () => {
|
|||||||
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
|
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without title and thumbnail', async () => {
|
describe('using standard method', () => {
|
||||||
|
it('successfully', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
||||||
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.foo',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
||||||
|
title: 'camera title',
|
||||||
|
icon: 'icon',
|
||||||
|
});
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
|
const hass = createHASS({
|
||||||
|
'camera.foo': createStateEntity({
|
||||||
|
attributes: {
|
||||||
|
entity_picture: 'http://thumbnail',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
|
await manager.playLive('media_player.foo', 'camera');
|
||||||
|
|
||||||
|
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
||||||
|
'media_player',
|
||||||
|
'play_media',
|
||||||
|
{
|
||||||
|
entity_id: 'media_player.foo',
|
||||||
|
media_content_id: 'media-source://camera/camera.foo',
|
||||||
|
media_content_type: 'application/vnd.apple.mpegurl',
|
||||||
|
extra: {
|
||||||
|
title: 'camera title',
|
||||||
|
thumb: 'http://thumbnail',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without camera_entity', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
||||||
|
createCameraConfig({}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
|
await manager.playLive('media_player.foo', 'camera');
|
||||||
|
|
||||||
|
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without title and thumbnail', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
||||||
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.foo',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
|
await manager.playLive('media_player.foo', 'camera');
|
||||||
|
|
||||||
|
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
||||||
|
'media_player',
|
||||||
|
'play_media',
|
||||||
|
{
|
||||||
|
entity_id: 'media_player.foo',
|
||||||
|
media_content_id: 'media-source://camera/camera.foo',
|
||||||
|
media_content_type: 'application/vnd.apple.mpegurl',
|
||||||
|
extra: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('using dashboard method', () => {
|
||||||
|
it('successfully', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
||||||
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.foo',
|
||||||
|
cast: {
|
||||||
|
method: 'dashboard',
|
||||||
|
dashboard: {
|
||||||
|
dashboard_path: 'dashboard_path',
|
||||||
|
view_path: 'view_path',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getQueryStringManager().generateQueryString).mockReturnValue('');
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
|
await manager.playLive('media_player.foo', 'camera');
|
||||||
|
|
||||||
|
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
||||||
|
'cast',
|
||||||
|
'show_lovelace_view',
|
||||||
|
{
|
||||||
|
entity_id: 'media_player.foo',
|
||||||
|
dashboard_path: 'dashboard_path',
|
||||||
|
view_path: 'view_path',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without hass', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
||||||
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.foo',
|
||||||
|
cast: {
|
||||||
|
method: 'dashboard',
|
||||||
|
dashboard: {
|
||||||
|
dashboard_path: 'dashboard_path',
|
||||||
|
view_path: 'view_path',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||||
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
|
await manager.playLive('media_player.foo', 'camera');
|
||||||
|
|
||||||
|
// No actual test can be performed here as nothing observable happens.
|
||||||
|
// This test serves only as code-coverage long-tail.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without required configuration', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
||||||
createCameraConfig({
|
createCameraConfig({
|
||||||
camera_entity: 'camera.foo',
|
camera_entity: 'camera.foo',
|
||||||
|
cast: {
|
||||||
|
method: 'dashboard',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
await manager.playLive('media_player.foo', 'camera');
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
expect(
|
||||||
'media_player',
|
vi.mocked(api.getMessageManager().setMessageIfHigherPriority),
|
||||||
'play_media',
|
).toBeCalledWith({
|
||||||
{
|
type: 'error',
|
||||||
entity_id: 'media_player.foo',
|
icon: 'mdi:cast',
|
||||||
media_content_id: 'media-source://camera/camera.foo',
|
message:
|
||||||
media_content_type: 'application/vnd.apple.mpegurl',
|
"Both 'dashboard_path' and 'view_path' parameters are required " +
|
||||||
extra: {},
|
"for the 'dashboard' cast method",
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { createCardAPI } from '../test-utils';
|
|
||||||
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
||||||
|
import {
|
||||||
|
FrigateCardGeneralAction,
|
||||||
|
FrigateCardUserSpecifiedView,
|
||||||
|
} from '../../src/config/types';
|
||||||
|
import { createCardAPI } from '../test-utils';
|
||||||
|
|
||||||
const setQueryString = (qs: string): void => {
|
const setQueryString = (qs: string): void => {
|
||||||
const location: Location = mock<Location>();
|
const location: Location = mock<Location>();
|
||||||
@@ -295,4 +299,74 @@ describe('QueryStringManager', () => {
|
|||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should generate query string', () => {
|
||||||
|
describe('that require no arguments', () => {
|
||||||
|
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', (actionName: FrigateCardGeneralAction | FrigateCardUserSpecifiedView) => {
|
||||||
|
const manager = new QueryStringManager(createCardAPI());
|
||||||
|
expect(
|
||||||
|
manager.generateQueryString({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: actionName,
|
||||||
|
}),
|
||||||
|
).toBe(`frigate-card-action.${actionName}=`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('that require camera argument', () => {
|
||||||
|
it.each([['camera_select' as const], ['live_substream_select' as const]])(
|
||||||
|
'%s',
|
||||||
|
(actionName: 'camera_select' | 'live_substream_select') => {
|
||||||
|
const manager = new QueryStringManager(createCardAPI());
|
||||||
|
expect(
|
||||||
|
manager.generateQueryString({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: actionName,
|
||||||
|
camera: 'camera',
|
||||||
|
}),
|
||||||
|
).toBe(`frigate-card-action.${actionName}=camera`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('that include a card_id', () => {
|
||||||
|
const manager = new QueryStringManager(createCardAPI());
|
||||||
|
expect(
|
||||||
|
manager.generateQueryString({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'clips',
|
||||||
|
card_id: 'card-id',
|
||||||
|
}),
|
||||||
|
).toBe(`frigate-card-action.card-id.clips=`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('that include an unsupported action', () => {
|
||||||
|
const spy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||||
|
const manager = new QueryStringManager(createCardAPI());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
manager.generateQueryString({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'microphone_unmute',
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
expect(spy).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user