Merge pull request #169 from dermotduffy/config-reorganize
Significant configuration re-organization to simplify config & code
This commit is contained in:
@@ -65,27 +65,56 @@ lovelace:
|
||||
|
||||
## Options
|
||||
|
||||
### Basic
|
||||
### Basic options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | - | --------------------------------------------- |
|
||||
| `camera_entity` | | The optional Frigate camera entity to use in the `frigate` live provider view. Also used to automatically detect the `frigate_camera_name`.|
|
||||
| `frigate_camera_name` | Autodetected from `camera_entity` if that is specified. | The Frigate camera name to use when communicating with the Frigate server, e.g. for viewing clips/snapshots or the JSMPEG live view. To view the birdseye view set this to `birdseye` and use the `frigate-jsmpeg` live provider.|
|
||||
| `view_default` | `live` | The view to show by default. See [views](#views) below.|
|
||||
| - | - | - |
|
||||
| `camera_entity` | | The optional Frigate camera entity to use in the `frigate` live provider view. Also used to automatically detect the value of `frigate.camera_name`.|
|
||||
|
||||
### Optional
|
||||
### Frigate server options
|
||||
|
||||
All variables listed are under a `frigate:` section.
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | --------------------------------------------- | - |
|
||||
| `live_provider` | `frigate` | The means through which the live camera view is displayed. See [Live Provider](#live-provider) below.|
|
||||
| `frigate_client_id` | `frigate` | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://blakeblackshear.github.io/frigate/usage/home-assistant/#multiple-instance-support).|
|
||||
| `view_timeout` | | 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.|
|
||||
| `frigate_url` | | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. |
|
||||
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
||||
| `live_preload` | `false` | Whether or not to preload the live view. Preloading causes the live view to render in the background so it's instantly available when requested. This consumes additional network/CPU resources continually.|
|
||||
| `event_viewer.lazy_load` | `true` | Whether or not to lazily load media in the event viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. |
|
||||
| - | - | - |
|
||||
| `camera_name` | Autodetected from `camera_entity` if that is specified. | The Frigate camera name to use when communicating with the Frigate server, e.g. for viewing clips/snapshots or the JSMPEG live view. To view the birdseye view set this to `birdseye` and use the `frigate-jsmpeg` live provider.|
|
||||
| `url` | | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. |
|
||||
| `label` | | A Frigate label / object filter used to filter events (clips & snapshots), e.g. 'person'.|
|
||||
| `zone` | | A Frigate zone used to filter events (clips & snapshots), e.g. 'front_door'.|
|
||||
| `client_id` | `frigate` | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://blakeblackshear.github.io/frigate/usage/home-assistant/#multiple-instance-support).|
|
||||
|
||||
#### Live Provider
|
||||
### View options
|
||||
|
||||
All variables listed are under a `view:` section.
|
||||
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| `default` | `live` | The view to show in the card by default. See [views](#views) below.|
|
||||
| `timeout` | | 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.|
|
||||
|
||||
### Menu options
|
||||
|
||||
All variables listed are under a `menu:` section.
|
||||
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| `mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
||||
| `button_size` | `40px` | The size of the menu buttons (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].|
|
||||
| `buttons.{frigate, live, clips, snapshots, image, download, frigate_ui, fullscreen}` | `true`, except for `image` | Whether or not to show these builtin actions in the card menu. |
|
||||
|
||||
### Live options
|
||||
|
||||
All variables listed are under a `live:` section.
|
||||
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| `preload` | `false` | Whether or not to preload the live view. Preloading causes the live view to render in the background so it's instantly available when requested. This consumes additional network/CPU resources continually.|
|
||||
| `provider` | `frigate` | The means through which the live camera view is displayed. See [Live Provider](#live-provider) below.|
|
||||
| `webrtc.url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).|
|
||||
| `webrtc.entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). |
|
||||
| `webrtc.*`| | Any other options in a `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.|
|
||||
|
||||
#### Available Live Providers
|
||||
|
||||
|Live Provider|Latency|Frame Rate|Installation|Description|
|
||||
| -- | -- | -- | -- | -- |
|
||||
@@ -93,26 +122,33 @@ lovelace:
|
||||
|`frigate-jsmpeg`|Lower|Low|Builtin|Stream the JSMPEG stream from Frigate (proxied via the Frigate integration). See [note below on the required integration version](#jsmpeg-troubleshooting) for this live provider to function. This is the only live provider that can view the Frigate `birdseye` view.|
|
||||
|`webrtc`|Lowest|High|Separate installation required|Uses [WebRTC](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc).|
|
||||
|
||||
### Appearance
|
||||
### Event Viewer options
|
||||
|
||||
All variables listed are under a `event_viewer:` section.
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | --------------------------------------------- | - |
|
||||
| `menu_mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
||||
| `menu_buttons.{frigate, live, clips, snapshots, download, frigate_ui, fullscreen}` | `true` | Whether or not to show these builtin actions in the card menu. |
|
||||
| `controls.nextprev` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
||||
| `dimensions.aspect_ratio_mode` | `dynamic` | The aspect ratio mode to use. Acceptable values: `dynamic`, `static`, `unconstrained`. See [aspect ratios](#aspect-ratios) below.|
|
||||
| `dimensions.aspect_ratio` | `16:9` | The aspect ratio to use. Acceptable values: `<W>:<H>` or `<W>/<H>`. See [aspect ratios](#aspect-ratios) below.|
|
||||
| `image` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).|
|
||||
| - | - | - |
|
||||
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
||||
| `lazy_load` | `true` | Whether or not to lazily load media in the event viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. |
|
||||
| `controls.next_previous.style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
||||
| `controls.next_previous.size` | `48px` | The size of the next/previous controls (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].|
|
||||
|
||||
<a name="aspect-ratios"></a>
|
||||
### Image options
|
||||
|
||||
#### Aspect Ratio
|
||||
All variables listed are under a `image:` section.
|
||||
|
||||
The card can show live cameras, stored events (clip or snapshot) and an event gallery (clips or snapshots). Of these [views](#views), the gallery views have no intrinsic aspect-ratio, whereas the other views have the aspect-ratio of the media.
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| `src` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).|
|
||||
|
||||
The card aspect ratio can be changed with the `dimensions.aspect_ratio_mode` and `dimensions.aspect_ratio` appearance options.
|
||||
### Dimension options
|
||||
|
||||
If no aspect ratio is specified or available, but one is needed then `16:9` will be used by default.
|
||||
All variables listed are under a `dimensions:` section.
|
||||
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| `aspect_ratio_mode` | `dynamic` | The aspect ratio mode to use. Acceptable values: `dynamic`, `static`, `unconstrained`. See [aspect ratios](#aspect-ratios) below.|
|
||||
| `aspect_ratio` | `16:9` | The aspect ratio to use. Acceptable values: `<W>:<H>` or `<W>/<H>`. See [aspect ratios](#aspect-ratios) below.|
|
||||
|
||||
#### `dimensions.aspect_ratio_mode`:
|
||||
|
||||
@@ -128,6 +164,22 @@ If no aspect ratio is specified or available, but one is needed then `16:9` will
|
||||
* `4 / 3` or `4:3`: Default fullscreen ratio.
|
||||
* `<W>/<H>` or `<W>:<H>`: Any arbitrary aspect-ratio.
|
||||
|
||||
|
||||
<a name="aspect-ratios"></a>
|
||||
|
||||
#### Aspect Ratio
|
||||
|
||||
The card can show live cameras, stored events (clip or snapshot) and an event
|
||||
gallery (clips or snapshots). Of these [views](#views), the gallery views have
|
||||
no intrinsic aspect-ratio, whereas the other views have the aspect-ratio of the
|
||||
media.
|
||||
|
||||
The card aspect ratio can be changed with the `dimensions.aspect_ratio_mode` and
|
||||
`dimensions.aspect_ratio` options described above.
|
||||
|
||||
If no aspect ratio is specified or available, but one is needed then `16:9` will
|
||||
be used by default.
|
||||
|
||||
#### Example aspect ratio configuration
|
||||
|
||||
Have the card aspect-ratio dynamically follow the last loaded media, but use `4:3` as the default when there is no such media:
|
||||
@@ -138,19 +190,17 @@ dimensions:
|
||||
aspect_ratio: '4:3'
|
||||
```
|
||||
|
||||
<a name="advanced-options"></a>
|
||||
<a name="other-options"></a>
|
||||
|
||||
### Advanced Options
|
||||
### Other Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | - | --------------------------------------------- |
|
||||
| `label` | | A label used to filter events (clips & snapshots), e.g. 'person'.|
|
||||
| `zone` | | A zone used to filter events (clips & snapshots), e.g. 'front_door'.|
|
||||
| - | - | - |
|
||||
| `update_entities` | | A list of entity ids that should cause the whole card to re-render, this can be useful in the `clip` or `snapshot` mode to (for example) cause a motion sensor to trigger a card refresh. Configurable in YAML only. Entities used in picture elements / included in the menu do not need to be explicitly included here to be kept updated. |
|
||||
|
||||
<a name="webrtc"></a>
|
||||
|
||||
### WebRTC Options
|
||||
### Using WebRTC
|
||||
|
||||
WebRTC support blends the use of the ultra-realtime [WebRTC live
|
||||
view](https://github.com/AlexxIT/WebRTC) with convenient access to Frigate
|
||||
@@ -158,11 +208,6 @@ events/snapshots/UI. A perfect combination!
|
||||
|
||||
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/webrtc.png" alt="Live viewing" width="400px">
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | - | -------------------------------------------- |
|
||||
| `webrtc.url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).|
|
||||
| `webrtc.entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). |
|
||||
| `webrtc.*`| | Any other options in a `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.|
|
||||
|
||||
**Note**: WebRTC must be installed and configured separately (see [details](https://github.com/AlexxIT/WebRTC)) before it can be used with this card.
|
||||
|
||||
@@ -180,6 +225,7 @@ specify the WebRTC source camera:
|
||||
|
||||
```yaml
|
||||
[rest of Frigate card configuration]
|
||||
live:
|
||||
webrtc:
|
||||
entity: 'camera.front_door_rstp`
|
||||
```
|
||||
@@ -190,27 +236,13 @@ webrtc:
|
||||
|
||||
```yaml
|
||||
[rest of Frigate card configuration]
|
||||
live:
|
||||
webrtc:
|
||||
url: 'rtsp://USERNAME:PASSWORD@CAMERA:554/RTSP_PATH'
|
||||
```
|
||||
|
||||
See [WebRTC configuration](https://github.com/AlexxIT/WebRTC#configuration) for full configuration options.
|
||||
|
||||
#### Example
|
||||
|
||||
This example allows access to the detection, recordings and snapshots switches
|
||||
from the menu. It also enables a different entity to trigger a card update (but
|
||||
without appearing in the menu).
|
||||
|
||||
```yaml
|
||||
entities:
|
||||
- entity: switch.front_door_recordings
|
||||
- entity: switch.front_door_snapshots
|
||||
- entity: switch.front_door_detect
|
||||
- entity: binary_sensor.front_door_person_motion
|
||||
show: false
|
||||
```
|
||||
|
||||
## Picture Elements / Menu customizations
|
||||
|
||||
This card supports the [Picture Elements configuration
|
||||
@@ -436,7 +468,7 @@ update_entities:
|
||||
- binary_sensor.office_person_motion
|
||||
```
|
||||
|
||||
See the [advanced options](#advanced-options) above.
|
||||
See the [other options](#other-options) above.
|
||||
|
||||
### Getting from a snapshot to a clip
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 152 KiB |
@@ -19,6 +19,8 @@
|
||||
"@material/image-list": "^12.0.0",
|
||||
"custom-card-helpers": "^1.8.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"dlv": "github:developit/dlv",
|
||||
"dset": "^3.1.1",
|
||||
"embla-carousel": "^5.0.1",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"lit": "^2.0.2",
|
||||
|
||||
+67
-91
@@ -20,7 +20,12 @@ import {
|
||||
import screenfull from 'screenfull';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { entitySchema, frigateCardConfigSchema, MenuInteraction } from './types.js';
|
||||
import {
|
||||
entitySchema,
|
||||
frigateCardConfigSchema,
|
||||
MenuInteraction,
|
||||
RawFrigateCardConfig,
|
||||
} from './types.js';
|
||||
import type {
|
||||
BrowseMediaQueryParameters,
|
||||
Entity,
|
||||
@@ -58,6 +63,7 @@ import './patches/ha-hls-player.js';
|
||||
import cardStyle from './scss/card.scss';
|
||||
import { ResolvedMediaCache } from './resolved-media.js';
|
||||
import { BrowseMediaUtil } from './browse-media-util.js';
|
||||
import { copyConfig, isConfigUpgradeable, upgradeConfig } from './config-mgmt.js';
|
||||
|
||||
/** A note on media callbacks:
|
||||
*
|
||||
@@ -192,72 +198,72 @@ export class FrigateCard extends LitElement {
|
||||
protected _getMenuButtons(): MenuButton[] {
|
||||
const buttons: MenuButton[] = [];
|
||||
|
||||
if (this.config.menu_buttons?.frigate ?? true) {
|
||||
if (this.config.menu.buttons.frigate) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'frigate',
|
||||
title: localize('menu.frigate'),
|
||||
title: localize('config.menu.buttons.frigate'),
|
||||
});
|
||||
}
|
||||
if (this.config.menu_buttons?.live ?? true) {
|
||||
if (this.config.menu.buttons.live) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'live',
|
||||
title: localize('menu.live'),
|
||||
title: localize('config.view.views.live'),
|
||||
icon: 'mdi:cctv',
|
||||
emphasize: this._view.is('live'),
|
||||
});
|
||||
}
|
||||
if (this.config.menu_buttons?.clips ?? true) {
|
||||
if (this.config.menu.buttons.clips) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'clips',
|
||||
hold_action: 'clip',
|
||||
title: localize('menu.clips'),
|
||||
title: localize('config.view.views.clips'),
|
||||
icon: 'mdi:filmstrip',
|
||||
emphasize: this._view.is('clips'),
|
||||
});
|
||||
}
|
||||
if (this.config.menu_buttons?.snapshots ?? true) {
|
||||
if (this.config.menu.buttons.snapshots) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'snapshots',
|
||||
hold_action: 'snapshot',
|
||||
title: localize('menu.snapshots'),
|
||||
title: localize('config.view.views.snapshots'),
|
||||
icon: 'mdi:camera',
|
||||
emphasize: this._view.is('snapshots'),
|
||||
});
|
||||
}
|
||||
if (this.config.menu_buttons?.image ?? false) {
|
||||
if (this.config.menu.buttons.image) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'image',
|
||||
title: localize('menu.image'),
|
||||
title: localize('config.view.views.image'),
|
||||
icon: 'mdi:image',
|
||||
emphasize: this._view.is('image'),
|
||||
});
|
||||
}
|
||||
if (this._view.isViewerView() && (this.config.menu_buttons?.download ?? true)) {
|
||||
if (this.config.menu.buttons.download && this._view.isViewerView()) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'download',
|
||||
title: localize('menu.download'),
|
||||
title: localize('config.menu.buttons.download'),
|
||||
icon: 'mdi:download',
|
||||
});
|
||||
}
|
||||
if ((this.config.menu_buttons?.frigate_ui ?? true) && this.config.frigate_url) {
|
||||
if (this.config.menu.buttons.frigate_ui && this.config.frigate.url) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'frigate_ui',
|
||||
title: localize('menu.frigate_ui'),
|
||||
title: localize('config.menu.buttons.frigate_ui'),
|
||||
icon: 'mdi:web',
|
||||
});
|
||||
}
|
||||
if ((this.config.menu_buttons?.fullscreen ?? true) && screenfull.isEnabled) {
|
||||
if (this.config.menu.buttons.fullscreen && screenfull.isEnabled) {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
tap_action: 'fullscreen',
|
||||
title: localize('menu.fullscreen'),
|
||||
title: localize('config.menu.buttons.fullscreen'),
|
||||
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
||||
});
|
||||
}
|
||||
@@ -300,8 +306,8 @@ export class FrigateCard extends LitElement {
|
||||
}
|
||||
|
||||
// Option 1: Name specified in config -> done!
|
||||
if (this.config.frigate_camera_name) {
|
||||
return this.config.frigate_camera_name;
|
||||
if (this.config.frigate.camera_name) {
|
||||
return this.config.frigate.camera_name;
|
||||
}
|
||||
|
||||
if (this.config.camera_entity) {
|
||||
@@ -401,15 +407,21 @@ export class FrigateCard extends LitElement {
|
||||
* Set the card configuration.
|
||||
* @param inputConfig The card configuration.
|
||||
*/
|
||||
public setConfig(inputConfig: FrigateCardConfig): void {
|
||||
public setConfig(inputConfig: RawFrigateCardConfig): void {
|
||||
if (!inputConfig) {
|
||||
throw new Error(localize('error.invalid_configuration:'));
|
||||
throw new Error(localize('error.invalid_configuration'));
|
||||
}
|
||||
|
||||
const configUpgradeable = isConfigUpgradeable(inputConfig);
|
||||
const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
|
||||
if (!parseResult.success) {
|
||||
const hint = this._getParseErrorPaths(parseResult.error);
|
||||
let upgradeMessage = '';
|
||||
if (configUpgradeable && getLovelace().mode !== 'yaml') {
|
||||
upgradeMessage = `${localize('error.upgrade_available')}. `;
|
||||
}
|
||||
throw new Error(
|
||||
upgradeMessage +
|
||||
`${localize('error.invalid_configuration')}: ` +
|
||||
(hint.length
|
||||
? JSON.stringify(hint, null, ' ')
|
||||
@@ -436,7 +448,7 @@ export class FrigateCard extends LitElement {
|
||||
this._message = null;
|
||||
|
||||
if (view === undefined) {
|
||||
this._view = new View({ view: this.config.view_default });
|
||||
this._view = new View({ view: this.config.view.default });
|
||||
} else {
|
||||
this._view = view;
|
||||
}
|
||||
@@ -505,7 +517,7 @@ export class FrigateCard extends LitElement {
|
||||
}
|
||||
|
||||
const path =
|
||||
`/api/frigate/${this.config.frigate_client_id}` +
|
||||
`/api/frigate/${this.config.frigate.client_id}` +
|
||||
`/notifications/${event_id}/` +
|
||||
`${this._view.isClipRelatedView() ? 'clip.mp4' : 'snapshot.jpg'}` +
|
||||
`?download=true`;
|
||||
@@ -595,22 +607,22 @@ export class FrigateCard extends LitElement {
|
||||
* @returns The URL or null if unavailable.
|
||||
*/
|
||||
protected _getFrigateURLFromContext(): string | null {
|
||||
if (!this.config.frigate_url) {
|
||||
if (!this.config.frigate.url) {
|
||||
return null;
|
||||
}
|
||||
if (!this._frigateCameraName) {
|
||||
return this.config.frigate_url;
|
||||
return this.config.frigate.url;
|
||||
} else if (this._view.is('live')) {
|
||||
return `${this.config.frigate_url}/cameras/${this._frigateCameraName}`;
|
||||
return `${this.config.frigate.url}/cameras/${this._frigateCameraName}`;
|
||||
}
|
||||
return `${this.config.frigate_url}/events?camera=${this._frigateCameraName}`;
|
||||
return `${this.config.frigate.url}/events?camera=${this._frigateCameraName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle interaction with the card.
|
||||
*/
|
||||
protected _interactionHandler(): void {
|
||||
if (!this.config.view_timeout) {
|
||||
if (!this.config.view.timeout) {
|
||||
return;
|
||||
}
|
||||
if (this._interactionTimerID) {
|
||||
@@ -619,7 +631,7 @@ export class FrigateCard extends LitElement {
|
||||
this._interactionTimerID = window.setTimeout(() => {
|
||||
this._interactionTimerID = null;
|
||||
this._changeView();
|
||||
}, this.config.view_timeout * 1000);
|
||||
}, this.config.view.timeout * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -628,15 +640,14 @@ export class FrigateCard extends LitElement {
|
||||
*/
|
||||
protected _renderMenu(): TemplateResult | void {
|
||||
const classes = {
|
||||
'hover-menu': this.config.menu_mode.startsWith('hover-'),
|
||||
'hover-menu': this.config.menu.mode.startsWith('hover-'),
|
||||
};
|
||||
return html`
|
||||
<frigate-card-menu
|
||||
class="${classMap(classes)}"
|
||||
.hass=${this._hass}
|
||||
.menuMode=${this.config.menu_mode}
|
||||
.menuConfig=${this.config.menu}
|
||||
.buttons=${this._getMenuButtons()}
|
||||
.buttonSize=${this.config.menu_button_size}
|
||||
class="${classMap(classes)}"
|
||||
@frigate-card:menu-interaction=${this._menuInteractionHandler.bind(this)}
|
||||
></frigate-card-menu>
|
||||
`;
|
||||
@@ -655,10 +666,10 @@ export class FrigateCard extends LitElement {
|
||||
}
|
||||
return {
|
||||
mediaType: this._view.isClipRelatedView() ? 'clips' : 'snapshots',
|
||||
clientId: this.config.frigate_client_id,
|
||||
clientId: this.config.frigate.client_id,
|
||||
cameraName: this._frigateCameraName,
|
||||
label: this.config.label,
|
||||
zone: this.config.zone,
|
||||
label: this.config.frigate.label,
|
||||
zone: this.config.frigate.zone,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -760,7 +771,7 @@ export class FrigateCard extends LitElement {
|
||||
* context.
|
||||
*/
|
||||
protected _isAspectRatioEnforced(): boolean {
|
||||
const aspect_ratio_mode = this.config.dimensions?.aspect_ratio_mode ?? 'dynamic';
|
||||
const aspectRatioMode = this.config.dimensions.aspect_ratio_mode;
|
||||
|
||||
// Do not artifically constrain aspect ratio if:
|
||||
// - It's fullscreen.
|
||||
@@ -769,8 +780,8 @@ export class FrigateCard extends LitElement {
|
||||
|
||||
return !(
|
||||
(screenfull.isEnabled && screenfull.isFullscreen) ||
|
||||
aspect_ratio_mode == 'unconstrained' ||
|
||||
(aspect_ratio_mode == 'dynamic' && this._view.isMediaView())
|
||||
aspectRatioMode == 'unconstrained' ||
|
||||
(aspectRatioMode == 'dynamic' && this._view.isMediaView())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -784,14 +795,14 @@ export class FrigateCard extends LitElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
const aspect_ratio_mode = this.config.dimensions?.aspect_ratio_mode ?? 'dynamic';
|
||||
if (aspect_ratio_mode == 'dynamic' && this._mediaShowInfo) {
|
||||
const aspectRatioMode = this.config.dimensions.aspect_ratio_mode;
|
||||
if (aspectRatioMode == 'dynamic' && this._mediaShowInfo) {
|
||||
return (this._mediaShowInfo.height / this._mediaShowInfo.width) * 100;
|
||||
}
|
||||
|
||||
const default_aspect_ratio = this.config.dimensions?.aspect_ratio;
|
||||
if (default_aspect_ratio) {
|
||||
return (default_aspect_ratio[1] / default_aspect_ratio[0]) * 100;
|
||||
const defaultAspectRatio = this.config.dimensions.aspect_ratio;
|
||||
if (defaultAspectRatio) {
|
||||
return (defaultAspectRatio[1] / defaultAspectRatio[0]) * 100;
|
||||
} else {
|
||||
return (9 / 16) * 100;
|
||||
}
|
||||
@@ -801,13 +812,6 @@ export class FrigateCard extends LitElement {
|
||||
* Master render method for the card.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (this.config.show_warning) {
|
||||
return this._showWarning(localize('common.show_warning'));
|
||||
}
|
||||
if (this.config.show_error) {
|
||||
return this._showError(localize('common.show_error'));
|
||||
}
|
||||
|
||||
const padding = this._getAspectRatioPadding();
|
||||
const outerStyle = {},
|
||||
innerStyle = {};
|
||||
@@ -836,7 +840,7 @@ export class FrigateCard extends LitElement {
|
||||
window.innerWidth / window.innerHeight
|
||||
) {
|
||||
// If the menu is outside the media (i.e. above/below) allow space for it.
|
||||
const allowance = ['above', 'below'].includes(this.config.menu_mode)
|
||||
const allowance = ['above', 'below'].includes(this.config.menu.mode)
|
||||
? MENU_HEIGHT
|
||||
: 0;
|
||||
innerStyle['max-width'] = `calc(${
|
||||
@@ -850,7 +854,7 @@ export class FrigateCard extends LitElement {
|
||||
};
|
||||
|
||||
return html` <ha-card @click=${this._interactionHandler}>
|
||||
${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
|
||||
${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
|
||||
<div class="container outer" style="${styleMap(outerStyle)}">
|
||||
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
|
||||
${this._frigateCameraName == undefined
|
||||
@@ -864,7 +868,7 @@ export class FrigateCard extends LitElement {
|
||||
: this._render()}
|
||||
</div>
|
||||
</div>
|
||||
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
|
||||
${this.config.menu.mode != 'above' ? this._renderMenu() : ''}
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
@@ -891,16 +895,16 @@ export class FrigateCard extends LitElement {
|
||||
gallery: this._view.isGalleryView(),
|
||||
};
|
||||
const galleryClasses = {
|
||||
hidden: this.config.live_preload && !this._view.isGalleryView(),
|
||||
hidden: this.config.live.preload && !this._view.isGalleryView(),
|
||||
};
|
||||
const viewerClasses = {
|
||||
hidden: this.config.live_preload && !this._view.isViewerView(),
|
||||
hidden: this.config.live.preload && !this._view.isViewerView(),
|
||||
};
|
||||
const liveClasses = {
|
||||
hidden: this.config.live_preload && this._view.view != 'live',
|
||||
hidden: this.config.live.preload && this._view.view != 'live',
|
||||
};
|
||||
const imageClasses = {
|
||||
hidden: this.config.live_preload && this._view.view != 'image',
|
||||
hidden: this.config.live.preload && this._view.view != 'image',
|
||||
};
|
||||
|
||||
return html`
|
||||
@@ -908,7 +912,7 @@ export class FrigateCard extends LitElement {
|
||||
${this._message ? renderMessage(this._message) : ``}
|
||||
${!this._message && this._view.is('image')
|
||||
? html` <frigate-card-image
|
||||
.image=${this.config.image}
|
||||
.imageConfig=${this.config.image}
|
||||
class="${classMap(imageClasses)}"
|
||||
@frigate-card:media-show=${this._mediaShowHandler}
|
||||
@frigate-card:message=${this._messageHandler}
|
||||
@@ -931,11 +935,8 @@ export class FrigateCard extends LitElement {
|
||||
.hass=${this._hass}
|
||||
.view=${this._view}
|
||||
.browseMediaQueryParameters=${mediaQueryParameters}
|
||||
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
|
||||
.nextPreviousControlSize=${this.config.controls?.nextprev_size}
|
||||
.autoplayClip=${this.config.autoplay_clip}
|
||||
.viewerConfig=${this.config.event_viewer}
|
||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||
.lazyLoad=${this.config.event_viewer?.lazy_load ?? true}
|
||||
class="${classMap(viewerClasses)}"
|
||||
@frigate-card:change-view=${this._changeViewHandler}
|
||||
@frigate-card:media-show=${this._mediaShowHandler}
|
||||
@@ -947,14 +948,14 @@ export class FrigateCard extends LitElement {
|
||||
: ``}
|
||||
${
|
||||
// Note the subtle difference in condition below vs the other views in order
|
||||
// to always render the live view for live_preload mode.
|
||||
(!this._message && this._view.is('live')) || this.config.live_preload
|
||||
// to always render the live view for live.preload mode.
|
||||
(!this._message && this._view.is('live')) || this.config.live.preload
|
||||
? html`
|
||||
<frigate-card-live
|
||||
.hass=${this._hass}
|
||||
.config=${this.config}
|
||||
.frigateCameraName=${this._frigateCameraName}
|
||||
.preload=${this.config.live_preload && !this._view.is('live')}
|
||||
.preload=${this.config.live.preload && !this._view.is('live')}
|
||||
class="${classMap(liveClasses)}"
|
||||
@frigate-card:media-show=${this._mediaShowHandler}
|
||||
@frigate-card:pause=${this._pauseHandler}
|
||||
@@ -991,31 +992,6 @@ export class FrigateCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning card.
|
||||
* @param warning The warning message.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
private _showWarning(warning: string): TemplateResult {
|
||||
return html` <hui-warning> ${warning} </hui-warning> `;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error card.
|
||||
* @param error The error message.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
private _showError(error: string): TemplateResult {
|
||||
const errorCard = document.createElement('hui-error-card');
|
||||
errorCard.setConfig({
|
||||
type: 'error',
|
||||
error,
|
||||
origConfig: this.config,
|
||||
});
|
||||
|
||||
return html` ${errorCard} `;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return compiled CSS styles (thus safe to use with unsafeCSS).
|
||||
*/
|
||||
|
||||
+2
-2
@@ -214,8 +214,8 @@ export function dispatchErrorMessageEvent(element: HTMLElement, message: string)
|
||||
* @returns A boolean indicating whether or not to allow an update.
|
||||
*/
|
||||
export function shouldUpdateBasedOnHass(
|
||||
newHass: HomeAssistant | null,
|
||||
oldHass: HomeAssistant | undefined,
|
||||
newHass: HomeAssistant | undefined | null,
|
||||
oldHass: HomeAssistant | undefined | null,
|
||||
entities: string[] | null,
|
||||
): boolean {
|
||||
if (!newHass || !entities) {
|
||||
|
||||
@@ -3,17 +3,19 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import { dispatchMediaShowEvent } from '../common.js';
|
||||
|
||||
import imageStyle from '../scss/image.scss';
|
||||
import type { ImageViewConfig } from '../types.js';
|
||||
import defaultImage from '../images/frigate-bird-in-sky.jpg'
|
||||
|
||||
import imageStyle from '../scss/image.scss';
|
||||
|
||||
@customElement('frigate-card-image')
|
||||
export class FrigateCardImage extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected image?: string;
|
||||
protected imageConfig?: ImageViewConfig;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html` <img
|
||||
src=${this.image || defaultImage}
|
||||
src=${this.imageConfig?.src || defaultImage}
|
||||
@load=${(e) => {
|
||||
dispatchMediaShowEvent(this, e);
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import type { ExtendedHomeAssistant, FrigateCardConfig, MediaShowInfo } from '../types.js';
|
||||
import type { ExtendedHomeAssistant, FrigateCardConfig, MediaShowInfo, WebRTCConfig } from '../types.js';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
@@ -75,24 +75,24 @@ export class FrigateCardLive extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
return html` ${this.config.live_provider == 'frigate'
|
||||
return html` ${this.config.live.provider == 'frigate'
|
||||
? html` <frigate-card-live-frigate
|
||||
.hass=${this.hass}
|
||||
.cameraEntity=${this.config.camera_entity}
|
||||
@frigate-card:media-show=${this._mediaShowHandler}
|
||||
>
|
||||
</frigate-card-live-frigate>`
|
||||
: this.config.live_provider == 'webrtc'
|
||||
: this.config.live.provider == 'webrtc'
|
||||
? html`<frigate-card-live-webrtc
|
||||
.hass=${this.hass}
|
||||
.webRTCConfig=${this.config.webrtc || {}}
|
||||
.webRTCConfig=${this.config.live.webrtc || {}}
|
||||
@frigate-card:media-show=${this._mediaShowHandler}
|
||||
>
|
||||
</frigate-card-live-webrtc>`
|
||||
: html` <frigate-card-live-jsmpeg
|
||||
.hass=${this.hass}
|
||||
.cameraName=${this.frigateCameraName}
|
||||
.clientId=${this.config.frigate_client_id}
|
||||
.clientId=${this.config.frigate.client_id}
|
||||
@frigate-card:media-show=${this._mediaShowHandler}
|
||||
>
|
||||
</frigate-card-live-jsmpeg>`}`;
|
||||
@@ -152,7 +152,7 @@ export class FrigateCardLiveFrigate extends LitElement {
|
||||
@customElement('frigate-card-live-webrtc')
|
||||
export class FrigateCardLiveWebRTC extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected webRTCConfig?: Record<string, unknown>;
|
||||
protected webRTCConfig?: WebRTCConfig;
|
||||
|
||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||
protected _webRTCElement: HTMLElement | null = null;
|
||||
|
||||
+38
-33
@@ -16,8 +16,8 @@ import { actionHandler } from '../action-handler-directive.js';
|
||||
|
||||
import type {
|
||||
ExtendedHomeAssistant,
|
||||
FrigateMenuMode,
|
||||
MenuButton,
|
||||
MenuConfig,
|
||||
MenuInteraction,
|
||||
} from '../types.js';
|
||||
import { dispatchFrigateCardEvent, shouldUpdateBasedOnHass } from '../common.js';
|
||||
@@ -30,10 +30,16 @@ export const MENU_HEIGHT = 46;
|
||||
@customElement('frigate-card-menu')
|
||||
export class FrigateCardMenu extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass!: HomeAssistant & ExtendedHomeAssistant;
|
||||
public hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected menuMode: FrigateMenuMode = 'hidden-top';
|
||||
set menuConfig(menuConfig: MenuConfig) {
|
||||
this._menuConfig = menuConfig;
|
||||
if (menuConfig) {
|
||||
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
|
||||
}
|
||||
}
|
||||
public _menuConfig?: MenuConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected expand = false;
|
||||
@@ -41,13 +47,8 @@ export class FrigateCardMenu extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public buttons: MenuButton[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
set buttonSize(buttonSize: string) {
|
||||
this.style.setProperty('--frigate-card-menu-button-size', buttonSize);
|
||||
}
|
||||
|
||||
protected _interactionHandler(ev: CustomEvent, button: MenuButton): void {
|
||||
if (this.menuMode.startsWith('hidden-')) {
|
||||
if (this._menuConfig?.mode.startsWith('hidden-')) {
|
||||
if (button.type == 'internal-menu-icon' && button.tap_action === 'frigate') {
|
||||
this.expand = !this.expand;
|
||||
return;
|
||||
@@ -82,7 +83,7 @@ export class FrigateCardMenu extends LitElement {
|
||||
}
|
||||
|
||||
// Render a menu button.
|
||||
protected _renderButton(button: MenuButton): TemplateResult {
|
||||
protected _renderButton(button: MenuButton): TemplateResult | void {
|
||||
let state: HassEntity | null = null;
|
||||
let emphasize = false;
|
||||
let title = button.title;
|
||||
@@ -90,10 +91,13 @@ export class FrigateCardMenu extends LitElement {
|
||||
const style = ('style' in button ? button.style : {}) || {};
|
||||
|
||||
if (button.type === 'custom:frigate-card-menu-state-icon') {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
state = this.hass.states[button.entity];
|
||||
emphasize =
|
||||
!!state && button.state_color && ['on', 'active', 'home'].includes(state.state);
|
||||
title = title ?? (state.attributes.friendly_name || button.entity);
|
||||
title = title ?? (state?.attributes?.friendly_name || button.entity);
|
||||
icon = icon ?? stateIcon(state);
|
||||
} else if (button.type === 'internal-menu-icon') {
|
||||
emphasize = button.emphasize ?? false;
|
||||
@@ -133,9 +137,9 @@ export class FrigateCardMenu extends LitElement {
|
||||
}
|
||||
|
||||
// Render the Frigate menu button.
|
||||
protected _renderFrigateButton(button: MenuButton): TemplateResult {
|
||||
protected _renderFrigateButton(button: MenuButton): TemplateResult | void {
|
||||
const icon =
|
||||
this.menuMode.startsWith('hidden-') && !this.expand
|
||||
this._menuConfig?.mode.startsWith('hidden-') && !this.expand
|
||||
? 'mdi:alpha-f-box-outline'
|
||||
: 'mdi:alpha-f-box';
|
||||
|
||||
@@ -143,7 +147,12 @@ export class FrigateCardMenu extends LitElement {
|
||||
}
|
||||
|
||||
// Render the menu.
|
||||
protected render(): TemplateResult {
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._menuConfig) {
|
||||
return;
|
||||
}
|
||||
const mode = this._menuConfig.mode;
|
||||
|
||||
const isFrigateButton = function (button: MenuButton): boolean {
|
||||
return button.type === 'internal-menu-icon' && button.tap_action === 'frigate';
|
||||
};
|
||||
@@ -151,33 +160,29 @@ export class FrigateCardMenu extends LitElement {
|
||||
// If the menu is off, or if it's in hidden mode but there's no button to
|
||||
// unhide it, just show nothing.
|
||||
if (
|
||||
this.menuMode == 'none' ||
|
||||
(this.menuMode.startsWith('hidden-') && !this.buttons.find(isFrigateButton))
|
||||
mode == 'none' ||
|
||||
(mode.startsWith('hidden-') && !this.buttons.find(isFrigateButton))
|
||||
) {
|
||||
return html``;
|
||||
return;
|
||||
}
|
||||
|
||||
const classes = {
|
||||
'frigate-card-menu': true,
|
||||
'overlay-hidden':
|
||||
this.menuMode.startsWith('hidden-') ||
|
||||
this.menuMode.startsWith('overlay-') ||
|
||||
this.menuMode.startsWith('hover-'),
|
||||
mode.startsWith('hidden-') ||
|
||||
mode.startsWith('overlay-') ||
|
||||
mode.startsWith('hover-'),
|
||||
'expanded-horizontal':
|
||||
(this.menuMode.startsWith('overlay-') ||
|
||||
this.menuMode.startsWith('hover-') ||
|
||||
this.expand) &&
|
||||
(this.menuMode.endsWith('-top') || this.menuMode.endsWith('-bottom')),
|
||||
(mode.startsWith('overlay-') || mode.startsWith('hover-') || this.expand) &&
|
||||
(mode.endsWith('-top') || mode.endsWith('-bottom')),
|
||||
'expanded-vertical':
|
||||
(this.menuMode.startsWith('overlay-') ||
|
||||
this.menuMode.startsWith('hover-') ||
|
||||
this.expand) &&
|
||||
(this.menuMode.endsWith('-left') || this.menuMode.endsWith('-right')),
|
||||
full: ['above', 'below'].includes(this.menuMode),
|
||||
left: this.menuMode.endsWith('-left'),
|
||||
right: this.menuMode.endsWith('-right'),
|
||||
top: this.menuMode.endsWith('-top'),
|
||||
bottom: this.menuMode.endsWith('-bottom'),
|
||||
(mode.startsWith('overlay-') || mode.startsWith('hover-') || this.expand) &&
|
||||
(mode.endsWith('-left') || mode.endsWith('-right')),
|
||||
full: mode == 'above' || mode == 'below',
|
||||
left: mode.endsWith('-left'),
|
||||
right: mode.endsWith('-right'),
|
||||
top: mode.endsWith('-top'),
|
||||
bottom: mode.endsWith('-bottom'),
|
||||
};
|
||||
|
||||
return html`
|
||||
|
||||
@@ -2,30 +2,29 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import { NextPreviousControlStyle } from '../types.js';
|
||||
import { NextPreviousControlConfig } from '../types.js';
|
||||
|
||||
import controlStyle from '../scss/next-previous-control.scss';
|
||||
|
||||
@customElement('frigate-card-next-previous-control')
|
||||
export class FrigateCardMessage extends LitElement {
|
||||
export class FrigateCardNextPreviousControl extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected direction?: 'next' | 'previous';
|
||||
|
||||
@property({ attribute: false })
|
||||
protected controlStyle?: NextPreviousControlStyle;
|
||||
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
|
||||
if (controlConfig?.size) {
|
||||
this.style.setProperty('--frigate-card-next-prev-size', controlConfig.size);
|
||||
}
|
||||
this._controlConfig = controlConfig;
|
||||
}
|
||||
protected _controlConfig?: NextPreviousControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected thumbnail?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
set controlSize(controlSize: string | undefined) {
|
||||
if (controlSize) {
|
||||
this.style.setProperty('--frigate-card-next-prev-size', controlSize);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.controlStyle || this.controlStyle == 'none') {
|
||||
if (!this._controlConfig || this._controlConfig.style == 'none') {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -33,12 +32,12 @@ export class FrigateCardMessage extends LitElement {
|
||||
controls: true,
|
||||
previous: this.direction == 'previous',
|
||||
next: this.direction == 'next',
|
||||
thumbnails: this.controlStyle == 'thumbnails',
|
||||
chevrons: this.controlStyle == 'chevrons',
|
||||
button: this.controlStyle == 'chevrons',
|
||||
thumbnails: this._controlConfig.style == 'thumbnails',
|
||||
chevrons: this._controlConfig.style == 'chevrons',
|
||||
button: this._controlConfig.style == 'chevrons',
|
||||
};
|
||||
|
||||
if (this.controlStyle == 'chevrons') {
|
||||
if (this._controlConfig.style == 'chevrons') {
|
||||
const icon = this.direction == 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right';
|
||||
|
||||
// TODO: Upon a safe distance from the release of HA 2021.11 these
|
||||
|
||||
+39
-55
@@ -19,7 +19,7 @@ import type {
|
||||
BrowseMediaSource,
|
||||
ExtendedHomeAssistant,
|
||||
MediaShowInfo,
|
||||
NextPreviousControlStyle,
|
||||
ViewerConfig,
|
||||
} from '../types.js';
|
||||
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
|
||||
import { View } from '../view.js';
|
||||
@@ -49,24 +49,15 @@ export class FrigateCardViewer extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected view?: View;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected viewerConfig?: ViewerConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected nextPreviousControlStyle?: NextPreviousControlStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected nextPreviousControlSize?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected autoplayClip?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected lazyLoad?: boolean;
|
||||
|
||||
/**
|
||||
* Resolve all the given media for a target.
|
||||
* @param target The target to resolve media from.
|
||||
@@ -92,21 +83,23 @@ export class FrigateCardViewer extends LitElement {
|
||||
return errorFree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
return html`${until(this._render(), renderProgressIndicator())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asyncronously render the element.
|
||||
* @returns A template to render.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected async _render(): Promise<TemplateResult | void> {
|
||||
if (!this.hass || !this.view || !this.browseMediaQueryParameters) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let autoplay = true;
|
||||
|
||||
if (this.view.is('clip') || this.view.is('snapshot')) {
|
||||
let parent: BrowseMediaSource | null = null;
|
||||
try {
|
||||
@@ -129,13 +122,6 @@ export class FrigateCardViewer extends LitElement {
|
||||
}
|
||||
this.view.target = parent;
|
||||
this.view.childIndex = childIndex;
|
||||
|
||||
// In this block, no clip has been manually selected, so this is loading
|
||||
// the most recent clip on card load. In this mode, autoplay of the clip
|
||||
// may be disabled by configuration. If does not make sense to disable
|
||||
// autoplay when the user has explicitly picked an event to play in the
|
||||
// gallery.
|
||||
autoplay = this.autoplayClip ?? true;
|
||||
}
|
||||
|
||||
if (this.view.target && !(await this._resolveAllMediaForTarget(this.view.target))) {
|
||||
@@ -144,13 +130,10 @@ export class FrigateCardViewer extends LitElement {
|
||||
|
||||
return html` <frigate-card-viewer-core
|
||||
.view=${this.view}
|
||||
.nextPreviousControlStyle=${this.nextPreviousControlStyle}
|
||||
.nextPreviousControlSize=${this.nextPreviousControlSize}
|
||||
.viewerConfig=${this.viewerConfig}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
.autoplayClip=${autoplay}
|
||||
.hass=${this.hass}
|
||||
.browseMediaQueryParameters=${this.browseMediaQueryParameters}
|
||||
.lazyLoad=${this.lazyLoad}
|
||||
>
|
||||
</frigate-card-viewer-core>`;
|
||||
}
|
||||
@@ -165,29 +148,20 @@ export class FrigateCardViewer extends LitElement {
|
||||
|
||||
@customElement('frigate-card-viewer-core')
|
||||
export class FrigateCardViewerCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected view?: View;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected nextPreviousControlStyle?: NextPreviousControlStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected nextPreviousControlSize?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected autoplayClip?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||
protected viewerConfig?: ViewerConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected lazyLoad?: boolean;
|
||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
// Media carousel object.
|
||||
protected _carousel?: EmblaCarouselType;
|
||||
@@ -418,7 +392,7 @@ export class FrigateCardViewerCore extends LitElement {
|
||||
* Lazily load media in the carousel.
|
||||
*/
|
||||
protected _lazyLoadMediaHandler(): void {
|
||||
if (!this.lazyLoad || !this._carousel) {
|
||||
if (!this.viewerConfig?.lazy_load || !this._carousel) {
|
||||
return;
|
||||
}
|
||||
const slides = this._carousel.slideNodes();
|
||||
@@ -504,8 +478,7 @@ export class FrigateCardViewerCore extends LitElement {
|
||||
${neighbors && neighbors.previous
|
||||
? html`<frigate-card-next-previous-control
|
||||
.direction=${'previous'}
|
||||
.controlStyle=${this.nextPreviousControlStyle}
|
||||
.controlSize=${this.nextPreviousControlSize}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${neighbors.previous.thumbnail}
|
||||
.title=${neighbors.previous.title}
|
||||
@click=${() => this._nextPreviousHandler('previous')}
|
||||
@@ -519,8 +492,7 @@ export class FrigateCardViewerCore extends LitElement {
|
||||
${neighbors && neighbors.next
|
||||
? html`<frigate-card-next-previous-control
|
||||
.direction=${'next'}
|
||||
.controlStyle=${this.nextPreviousControlStyle}
|
||||
.controlSize=${this.nextPreviousControlSize}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${neighbors.next.thumbnail}
|
||||
.title=${neighbors.next.title}
|
||||
@click=${() => this._nextPreviousHandler('next')}
|
||||
@@ -594,7 +566,7 @@ export class FrigateCardViewerCore extends LitElement {
|
||||
): TemplateResult | void {
|
||||
// media that can be expanded (folders) cannot be resolved to a single media
|
||||
// item, skip them.
|
||||
if (!this.view || !BrowseMediaUtil.isTrueMedia(mediaToRender)) {
|
||||
if (!this.view || !this.viewerConfig || !BrowseMediaUtil.isTrueMedia(mediaToRender)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -603,20 +575,32 @@ export class FrigateCardViewerCore extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// In this block, no clip has been manually selected, so this is loading
|
||||
// the most recent clip on card load. In this mode, autoplay of the clip
|
||||
// may be disabled by configuration. If does not make sense to disable
|
||||
// autoplay when the user has explicitly picked an event to play in the
|
||||
// gallery.
|
||||
let autoplay = true;
|
||||
if (this.view.is('clip') || this.view.is('snapshot')) {
|
||||
autoplay = this.viewerConfig.autoplay_clip;
|
||||
}
|
||||
|
||||
const lazyLoad = this.viewerConfig.lazy_load;
|
||||
|
||||
return html`
|
||||
<div class="embla__slide">
|
||||
${this.view.isClipRelatedView()
|
||||
? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl'
|
||||
? html`<frigate-card-ha-hls-player
|
||||
.hass=${this.hass}
|
||||
url=${ifDefined(this.lazyLoad ? undefined : resolvedMedia.url)}
|
||||
data-url=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
|
||||
url=${ifDefined(lazyLoad ? undefined : resolvedMedia.url)}
|
||||
data-url=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
|
||||
title="${mediaToRender.title}"
|
||||
muted
|
||||
controls
|
||||
playsinline
|
||||
allow-exoplayer
|
||||
?autoplay="${this.autoplayClip}"
|
||||
?autoplay="${autoplay}"
|
||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
||||
this._mediaShowEventHandler(slideIndex, e)}
|
||||
>
|
||||
@@ -626,7 +610,7 @@ export class FrigateCardViewerCore extends LitElement {
|
||||
muted
|
||||
controls
|
||||
playsinline
|
||||
?autoplay="${this.autoplayClip}"
|
||||
?autoplay="${autoplay}"
|
||||
@loadedmetadata="${(e: Event) => {
|
||||
this._mediaShowInfoHandler(slideIndex, createMediaShowInfo(e));
|
||||
}}"
|
||||
@@ -634,14 +618,14 @@ export class FrigateCardViewerCore extends LitElement {
|
||||
@pause=${() => dispatchPauseEvent(this)}
|
||||
>
|
||||
<source
|
||||
src=${ifDefined(this.lazyLoad ? undefined : resolvedMedia.url)}
|
||||
data-src=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
|
||||
src=${ifDefined(lazyLoad ? undefined : resolvedMedia.url)}
|
||||
data-src=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
|
||||
type="${resolvedMedia.mime_type}"
|
||||
/>
|
||||
</video>`
|
||||
: html`<img
|
||||
src=${ifDefined(this.lazyLoad ? IMG_EMPTY : resolvedMedia.url)}
|
||||
data-src=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
|
||||
src=${ifDefined(lazyLoad ? IMG_EMPTY : resolvedMedia.url)}
|
||||
data-src=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
|
||||
title="${mediaToRender.title}"
|
||||
@click=${() => {
|
||||
if (this._carousel?.clickAllowed()) {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import delve from 'dlv';
|
||||
import { dset } from 'dset';
|
||||
import { RawFrigateCardConfig } from './types';
|
||||
|
||||
/**
|
||||
* Set a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param key The key to the property to set.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
export const setConfigValue = (
|
||||
obj: RawFrigateCardConfig,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
dset(obj, key, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param key The key to the property to retrieve.
|
||||
* @returns The property or undefined if not found.
|
||||
*/
|
||||
export const getConfigValue = (obj: RawFrigateCardConfig, key: string, def?: unknown): unknown => {
|
||||
return delve(obj, key, def);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param key The key to the property to delete.
|
||||
*/
|
||||
export const deleteConfigValue = (obj: RawFrigateCardConfig, key: string): void => {
|
||||
let id = key;
|
||||
let targetObj: unknown = obj;
|
||||
if (key && key.split && key.includes('.')) {
|
||||
const keys = key.split('.');
|
||||
id = keys[keys.length - 1];
|
||||
targetObj = getConfigValue(obj, keys.slice(0, -1).join('.'));
|
||||
}
|
||||
if (targetObj && typeof targetObj === 'object') {
|
||||
delete targetObj[id];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upgrade a configuration.
|
||||
* @param obj The configuration to upgrade.
|
||||
* @returns `true` if the configuration is modified.
|
||||
*/
|
||||
export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
|
||||
let upgraded = false;
|
||||
for (let i = 0; i < UPGRADES.length; i++) {
|
||||
upgraded = UPGRADES[i](obj) || upgraded;
|
||||
}
|
||||
trimConfig(obj);
|
||||
return upgraded;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if a configuration is automatically upgradeable.
|
||||
* @param obj The configuration. It is not modified.
|
||||
* @returns `true` if the configuration is upgradeable.
|
||||
*/
|
||||
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
|
||||
const newObj = JSON.parse(JSON.stringify(obj));
|
||||
return upgradeConfig(newObj);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove empty sections from a configuration.
|
||||
* @param obj Configuration object.
|
||||
*/
|
||||
export const trimConfig = function (obj: RawFrigateCardConfig): void {
|
||||
const keys = Object.keys(obj);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (typeof obj[key] === 'object' && obj[key] != null) {
|
||||
trimConfig(obj[key] as RawFrigateCardConfig);
|
||||
|
||||
if (!Object.keys(obj[key] as RawFrigateCardConfig).length) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy a configuration.
|
||||
* @param obj Configuration to copy.
|
||||
* @returns A new deeply-copied configuration.
|
||||
*/
|
||||
export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardConfig {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a property is not an object.
|
||||
* @param value The property.
|
||||
* @returns `true` is the value is not an object.
|
||||
*/
|
||||
const isNotObject = function (value: unknown) {
|
||||
return typeof value !== 'object' ? value : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a property from one location to another.
|
||||
* @param oldPath The old property path.
|
||||
* @param newPath The new property path.
|
||||
* @param transform An optional transform for the value.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
const upgradeMoveTo = function (
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
transform?: (valueIn: unknown) => unknown,
|
||||
): (obj: RawFrigateCardConfig) => boolean {
|
||||
return function (obj: RawFrigateCardConfig): boolean {
|
||||
let value = getConfigValue(obj, oldPath);
|
||||
if (transform) {
|
||||
value = transform(value);
|
||||
}
|
||||
if (typeof value !== 'undefined') {
|
||||
deleteConfigValue(obj, oldPath);
|
||||
setConfigValue(obj, newPath, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
const UPGRADES = [
|
||||
// v1.2.1 -> v2.0.0
|
||||
upgradeMoveTo('frigate_url', 'frigate.url'),
|
||||
upgradeMoveTo('frigate_client_id', 'frigate.client_id'),
|
||||
upgradeMoveTo('frigate_camera_name', 'frigate.camera_name'),
|
||||
upgradeMoveTo('label', 'frigate.label'),
|
||||
upgradeMoveTo('zone', 'frigate.zone'),
|
||||
upgradeMoveTo('view_default', 'view.default'),
|
||||
upgradeMoveTo('view_timeout', 'view.timeout'),
|
||||
upgradeMoveTo('live_provider', 'live.provider'),
|
||||
upgradeMoveTo('live_preload', 'live.preload'),
|
||||
upgradeMoveTo('webrtc', 'live.webrtc'),
|
||||
upgradeMoveTo('autoplay_clip', 'event_viewer.autoplay_clip'),
|
||||
upgradeMoveTo('controls.nextprev', 'event_viewer.controls.next_previous.style'),
|
||||
upgradeMoveTo('controls.nextprev_size', 'event_viewer.controls.next_previous.size'),
|
||||
upgradeMoveTo('menu_mode', 'menu.mode'),
|
||||
upgradeMoveTo('menu_buttons', 'menu.buttons'),
|
||||
upgradeMoveTo('menu_button_size', 'menu.button_size'),
|
||||
upgradeMoveTo('image', 'image.src', isNotObject),
|
||||
];
|
||||
+555
-334
File diff suppressed because it is too large
Load Diff
+101
-72
@@ -1,94 +1,122 @@
|
||||
{
|
||||
"common": {
|
||||
"version": "Version",
|
||||
"show_warning": "Show Warning",
|
||||
"show_error": "Show Error",
|
||||
"frigate_card": "Frigate Card",
|
||||
"frigate_card": "Frigate card",
|
||||
"frigate_card_description": "A Lovelace card for use with Frigate",
|
||||
"no_snapshots": "No snapshots",
|
||||
"no_clips": "No clips",
|
||||
"no_snapshot": "No recent snapshot",
|
||||
"no_clip": "No recent clip"
|
||||
},
|
||||
"editor": {
|
||||
"basic": "Basic",
|
||||
"basic_secondary": "Basic options for most users",
|
||||
"optional": "Optional",
|
||||
"optional_secondary": "Optional configuration to tweak card behavior",
|
||||
"appearance": "Appearance",
|
||||
"appearance_secondary": "Appearance of the card",
|
||||
"webrtc": "WebRTC",
|
||||
"webrtc_secondary": "Optional WebRTC parameters",
|
||||
"advanced": "Advanced",
|
||||
"advanced_secondary": "Advanced options",
|
||||
"camera_entity": "Camera Entity (Optional)",
|
||||
"frigate_camera_name": "Frigate camera name (Optional, autodetected from entity)",
|
||||
"default_view": "Default view (Optional)",
|
||||
"frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)",
|
||||
"view_timeout": "View timeout (seconds)",
|
||||
"frigate_url": "Frigate URL (Optional, for Frigate UI button)",
|
||||
"autoplay_clip": "Autoplay latest clip",
|
||||
"menu_mode": "Menu mode (Optional)",
|
||||
"show_button": "Show button",
|
||||
"zone": "Zone",
|
||||
"label": "Frigate label/object filter (Optional)",
|
||||
"live_provider": "Live view provider (Optional)",
|
||||
"live_preload": "Preload live view",
|
||||
"image": "Static image URL for image view (Optional)",
|
||||
"lazy_load": "Lazily load event media"
|
||||
"config": {
|
||||
"camera_entity": "Camera Entity",
|
||||
"frigate": {
|
||||
"camera_name": "Frigate camera name (Optional, autodetected from entity)",
|
||||
"client_id": "Frigate client id (For >1 Frigate server)",
|
||||
"label": "Frigate label/object filter",
|
||||
"url": "Frigate server URL",
|
||||
"zone": "Frigate zone"
|
||||
},
|
||||
"menu": {
|
||||
"frigate": "Frigate Menu / Default View",
|
||||
"live": "Live View",
|
||||
"clips": "Clips Gallery",
|
||||
"snapshots": "Snapshots Gallery",
|
||||
"clip": "Latest Clip",
|
||||
"snapshot": "Latest Snapshot",
|
||||
"frigate_ui": "Frigate User Interface",
|
||||
"fullscreen": "Fullscreen",
|
||||
"image": "Static Image",
|
||||
"download": "Download event media"
|
||||
"view": {
|
||||
"default": "Default view",
|
||||
"views": {
|
||||
"live": "Live view",
|
||||
"clip": "Most recent clip",
|
||||
"clips": "Clips gallery",
|
||||
"snapshot": "Most recent snapshot",
|
||||
"snapshots": "Snapshots gallery",
|
||||
"image": "Static image"
|
||||
},
|
||||
"control": {
|
||||
"nextprev": "Media Next & Previous Controls",
|
||||
"timeout": "View timeout secs (before returning to default, 0=never)"
|
||||
},
|
||||
"event_viewer": {
|
||||
"autoplay_clip": "Autoplay most recent clip (In 'clip' view)",
|
||||
"lazy_load": "Lazily load event media",
|
||||
"controls": {
|
||||
"next_previous": {
|
||||
"style": "Event Viewer Next & Previous Control style",
|
||||
"size": "Event Viewer Next & Previous Control size (e.g. '48px')",
|
||||
"styles": {
|
||||
"thumbnails": "Thumbnails",
|
||||
"chevrons": "Chevrons",
|
||||
"none": "None"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dimensions": {
|
||||
"aspect_ratio_mode": "Aspect Ratio Mode",
|
||||
"aspect_ratio": "Default aspect ratio (Optional)"
|
||||
},
|
||||
"aspect_ratio_mode": {
|
||||
"unconstrained": "Unconstrained aspect ratio",
|
||||
"dynamic": "Aspect ratio adjusts to media",
|
||||
"static": "Static aspect ratio"
|
||||
},
|
||||
"menu_mode": {
|
||||
"none": "No menu",
|
||||
"hidden-top": "Hidden Top",
|
||||
"hidden-left": "Hidden Left",
|
||||
"hidden-bottom": "Hidden Bottom",
|
||||
"hidden-right": "Hidden Right",
|
||||
"overlay-top": "Overlay Top",
|
||||
"overlay-left": "Overlay Left",
|
||||
"overlay-bottom": "Overlay Bottom",
|
||||
"overlay-right": "Overlay Right",
|
||||
"hover-top": "Hover Top",
|
||||
"hover-left": "Hover Left",
|
||||
"hover-bottom": "Hover Bottom",
|
||||
"hover-right": "Hover Right",
|
||||
"above": "Above",
|
||||
"below": "Below"
|
||||
},
|
||||
"live_provider": {
|
||||
"live": {
|
||||
"preload": "Preload live view",
|
||||
"provider": "Live view provider",
|
||||
"providers": {
|
||||
"frigate": "Frigate",
|
||||
"frigate-jsmpeg": "Frigate JSMpeg",
|
||||
"webrtc": "WebRTC"
|
||||
},
|
||||
"webrtc": {
|
||||
"entity": "Optional WebRTC Camera Entity (Not a Frigate camera)",
|
||||
"url": "Optional WebRTC Camera URL"
|
||||
"entity": "WebRTC Camera Entity (Not a Frigate camera)",
|
||||
"url": "WebRTC Camera URL"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"src": "Static image URL/data for image view"
|
||||
},
|
||||
"menu": {
|
||||
"buttons": {
|
||||
"frigate": "Frigate menu / Default view",
|
||||
"frigate_ui": "Frigate user Interface",
|
||||
"fullscreen": "Fullscreen",
|
||||
"download": "Download event media"
|
||||
},
|
||||
"mode": "Menu mode",
|
||||
"modes": {
|
||||
"none": "No menu",
|
||||
"hidden-top": "Hidden top",
|
||||
"hidden-left": "Hidden left",
|
||||
"hidden-bottom": "Hidden bottom",
|
||||
"hidden-right": "Hidden right",
|
||||
"overlay-top": "Overlay top",
|
||||
"overlay-left": "Overlay left",
|
||||
"overlay-bottom": "Overlay bottom",
|
||||
"overlay-right": "Overlay right",
|
||||
"hover-top": "Hover top",
|
||||
"hover-left": "Hover left",
|
||||
"hover-bottom": "Hover bottom",
|
||||
"hover-right": "Hover right",
|
||||
"above": "Above",
|
||||
"below": "Below"
|
||||
},
|
||||
"button_size": "Menu button size (e.g. '40px')"
|
||||
},
|
||||
"dimensions": {
|
||||
"aspect_ratio": "Default aspect ratio (e.g. '16:9')",
|
||||
"aspect_ratio_mode": "Aspect ratio mode",
|
||||
"aspect_ratio_modes": {
|
||||
"unconstrained": "Unconstrained aspect ratio",
|
||||
"dynamic": "Aspect ratio adjusts to media",
|
||||
"static": "Static aspect ratio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"basic": "Basic",
|
||||
"basic_secondary": "Options for most users",
|
||||
"frigate": "Frigate",
|
||||
"frigate_secondary": "Frigate server options",
|
||||
"view": "View",
|
||||
"view_secondary": "What the card should show and how to show it",
|
||||
"menu": "Menu",
|
||||
"menu_secondary": "Menu look & feel options",
|
||||
"live": "Live",
|
||||
"live_secondary": "Live camera view options",
|
||||
"event_viewer": "Event viewer",
|
||||
"event_viewer_secondary": "Snapshots & clips gallery options",
|
||||
"image": "Image",
|
||||
"image_secondary": "Static image view options",
|
||||
"dimensions": "Dimensions",
|
||||
"dimensions_secondary": "Dimensions & shape options",
|
||||
"show_button": "Show button",
|
||||
"upgrade": "Upgrade",
|
||||
"upgrade_available": "An automatic card configuration upgrade is available"
|
||||
},
|
||||
"error": {
|
||||
"empty_response": "Received empty response from Home Assistant for request",
|
||||
@@ -100,8 +128,9 @@
|
||||
"no_live_camera": "The camera_entity parameter must be set and valid for this live provider",
|
||||
"invalid_configuration": "Invalid configuration",
|
||||
"invalid_configuration_no_hint": "No location hint available (bad or missing type?)",
|
||||
"upgrade_available": "An automated card configuration upgrade is available, please visit the visual card editor",
|
||||
"missing_webrtc": "WebRTC component not found",
|
||||
"no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate_camera_name",
|
||||
"no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate.camera_name",
|
||||
"could_not_render_elements": "Could not render picture elements",
|
||||
"invalid_elements_config": "Invalid picture elements configuration",
|
||||
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
|
||||
|
||||
@@ -25,3 +25,15 @@
|
||||
ha-formfield {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
div.upgrade {
|
||||
width: auto;
|
||||
border: 1px dotted var(--primary-color);
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
div.upgrade span {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
:host {
|
||||
--video-max-height: none;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
:host {
|
||||
--video-max-height: none;
|
||||
}
|
||||
|
||||
ha-hls-player {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
img,video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
+174
-54
@@ -38,7 +38,7 @@ export type FrigateCardView =
|
||||
| typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number]
|
||||
| typeof FRIGATE_CARD_VIEWS_INTERNAL[number];
|
||||
|
||||
export const FRIGATE_MENU_MODES = [
|
||||
const FRIGATE_MENU_MODES = [
|
||||
'none',
|
||||
'hidden-top',
|
||||
'hidden-left',
|
||||
@@ -55,13 +55,8 @@ export const FRIGATE_MENU_MODES = [
|
||||
'above',
|
||||
'below',
|
||||
] as const;
|
||||
export type FrigateMenuMode = typeof FRIGATE_MENU_MODES[number];
|
||||
|
||||
export const NEXT_PREVIOUS_CONTROL_STYLES = ['none', 'thumbnails', 'chevrons'] as const;
|
||||
export type NextPreviousControlStyle = typeof NEXT_PREVIOUS_CONTROL_STYLES[number];
|
||||
|
||||
export const LIVE_PROVIDERS = ['frigate', 'frigate-jsmpeg', 'webrtc'] as const;
|
||||
export type LiveProvider = typeof LIVE_PROVIDERS[number];
|
||||
|
||||
/**
|
||||
* Action Types (for "Picture Elements" / Menu)
|
||||
@@ -122,7 +117,7 @@ const elementsBaseSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Picture Element Types
|
||||
* Picture Element Configuration.
|
||||
*
|
||||
* All picture element types are validated (not just the Frigate card custom
|
||||
* ones) as a convenience to present the user with a consistent error display
|
||||
@@ -214,7 +209,7 @@ const customSchema = z
|
||||
if (!val.match(/^custom:(?!frigate-card).+/)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Frigate-card custom elements must match specific schemas",
|
||||
message: 'Frigate-card custom elements must match specific schemas',
|
||||
fatal: true,
|
||||
});
|
||||
}
|
||||
@@ -223,7 +218,7 @@ const customSchema = z
|
||||
.passthrough();
|
||||
|
||||
/**
|
||||
* Custom Element Types
|
||||
* Custom Element Types.
|
||||
*/
|
||||
|
||||
export const menuIconSchema = iconSchema.merge(
|
||||
@@ -269,15 +264,37 @@ export type PictureElement = z.infer<typeof pictureElementSchema>;
|
||||
const pictureElementsSchema = pictureElementSchema.array().optional();
|
||||
export type PictureElements = z.infer<typeof pictureElementsSchema>;
|
||||
|
||||
export const frigateCardConfigSchema = z.object({
|
||||
camera_entity: z.string().optional(),
|
||||
|
||||
/**
|
||||
* Frigate configuration section.
|
||||
*/
|
||||
const frigateConfigDefault = {
|
||||
client_id: 'frigate' as const,
|
||||
};
|
||||
const frigateConfigDefaultSchema = z
|
||||
.object({
|
||||
// No URL validation to allow relative URLs within HA (e.g. addons).
|
||||
frigate_url: z.string().optional(),
|
||||
frigate_client_id: z.string().optional().default('frigate'),
|
||||
frigate_camera_name: z.string().optional(),
|
||||
view_default: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED).optional().default('live'),
|
||||
view_timeout: z
|
||||
url: z.string().optional(),
|
||||
client_id: z.string().optional().default(frigateConfigDefault.client_id),
|
||||
camera_name: z.string().optional(),
|
||||
label: z.string().optional(),
|
||||
zone: z.string().optional(),
|
||||
})
|
||||
.default(frigateConfigDefault);
|
||||
|
||||
/**
|
||||
* View configuration section.
|
||||
*/
|
||||
const viewConfigDefault = {
|
||||
default: 'live' as const,
|
||||
timeout: 180,
|
||||
};
|
||||
const viewConfigSchema = z
|
||||
.object({
|
||||
default: z
|
||||
.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED)
|
||||
.optional()
|
||||
.default(viewConfigDefault.default),
|
||||
timeout: z
|
||||
.number()
|
||||
.or(
|
||||
z
|
||||
@@ -286,52 +303,129 @@ export const frigateCardConfigSchema = z.object({
|
||||
.transform((val) => Number(val)),
|
||||
)
|
||||
.optional()
|
||||
.default(180),
|
||||
live_provider: z.enum(LIVE_PROVIDERS).default('frigate'),
|
||||
live_preload: z.boolean().default(false),
|
||||
image: z.string().optional(),
|
||||
webrtc: z
|
||||
.default(viewConfigDefault.timeout),
|
||||
})
|
||||
.default(viewConfigDefault);
|
||||
|
||||
/**
|
||||
* Image view configuration section.
|
||||
*/
|
||||
const imageConfigSchema = z
|
||||
.object({
|
||||
src: z.string().optional(),
|
||||
})
|
||||
.optional();
|
||||
export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
|
||||
|
||||
/**
|
||||
* Live view configuration section.
|
||||
*/
|
||||
const liveConfigDefault = {
|
||||
provider: 'frigate' as const,
|
||||
preload: false,
|
||||
};
|
||||
const webrtcConfigSchema = z
|
||||
.object({
|
||||
entity: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
label: z.string().optional(),
|
||||
zone: z.string().optional(),
|
||||
autoplay_clip: z.boolean().default(false),
|
||||
event_viewer: z
|
||||
.optional();
|
||||
export type WebRTCConfig = z.infer<typeof webrtcConfigSchema>;
|
||||
|
||||
const liveConfigSchema = z
|
||||
.object({
|
||||
lazy_load: z.boolean().default(true),
|
||||
provider: z.enum(LIVE_PROVIDERS).default(liveConfigDefault.provider),
|
||||
preload: z.boolean().default(liveConfigDefault.preload),
|
||||
webrtc: webrtcConfigSchema,
|
||||
})
|
||||
.optional(),
|
||||
menu_mode: z.enum(FRIGATE_MENU_MODES).optional().default('hidden-top'),
|
||||
menu_buttons: z
|
||||
.default(liveConfigDefault);
|
||||
|
||||
/**
|
||||
* Menu configuration section.
|
||||
*/
|
||||
const menuConfigDefault = {
|
||||
mode: 'hidden-top' as const,
|
||||
buttons: {
|
||||
frigate: true,
|
||||
live: true,
|
||||
clips: true,
|
||||
snapshots: true,
|
||||
image: false,
|
||||
download: true,
|
||||
frigate_ui: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
button_size: '40px',
|
||||
};
|
||||
const menuConfigSchema = z
|
||||
.object({
|
||||
frigate: z.boolean().default(true),
|
||||
live: z.boolean().default(true),
|
||||
clips: z.boolean().default(true),
|
||||
snapshots: z.boolean().default(true),
|
||||
image: z.boolean().default(false),
|
||||
download: z.boolean().default(true),
|
||||
frigate_ui: z.boolean().default(true),
|
||||
fullscreen: z.boolean().default(true),
|
||||
mode: z.enum(FRIGATE_MENU_MODES).optional().default(menuConfigDefault.mode),
|
||||
buttons: z
|
||||
.object({
|
||||
frigate: z.boolean().default(menuConfigDefault.buttons.frigate),
|
||||
live: z.boolean().default(menuConfigDefault.buttons.live),
|
||||
clips: z.boolean().default(menuConfigDefault.buttons.clips),
|
||||
snapshots: z.boolean().default(menuConfigDefault.buttons.snapshots),
|
||||
image: z.boolean().default(menuConfigDefault.buttons.image),
|
||||
download: z.boolean().default(menuConfigDefault.buttons.download),
|
||||
frigate_ui: z.boolean().default(menuConfigDefault.buttons.frigate_ui),
|
||||
fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen),
|
||||
})
|
||||
.optional(),
|
||||
menu_button_size: z.string().default('40px'),
|
||||
update_entities: z.string().array().optional(),
|
||||
elements: pictureElementsSchema,
|
||||
.default(menuConfigDefault.buttons),
|
||||
button_size: z.string().default(menuConfigDefault.button_size),
|
||||
})
|
||||
.default(menuConfigDefault);
|
||||
export type MenuConfig = z.infer<typeof menuConfigSchema>;
|
||||
|
||||
/**
|
||||
* Event viewer configuration section (clip, snapshot, clip-specific, snapshot-specific).
|
||||
*/
|
||||
const viewerConfigDefault = {
|
||||
autoplay_clip: false,
|
||||
lazy_load: true,
|
||||
controls: {
|
||||
next_previous: {
|
||||
size: '48px',
|
||||
style: 'thumbnails' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
const nextPreviousControlConfigSchema = z
|
||||
.object({
|
||||
style: z
|
||||
.enum(NEXT_PREVIOUS_CONTROL_STYLES)
|
||||
.default(viewerConfigDefault.controls.next_previous.style),
|
||||
size: z.string().default(viewerConfigDefault.controls.next_previous.size),
|
||||
})
|
||||
.default(viewerConfigDefault.controls.next_previous);
|
||||
export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>;
|
||||
|
||||
const viewerConfigSchema = z
|
||||
.object({
|
||||
autoplay_clip: z.boolean().default(viewerConfigDefault.autoplay_clip),
|
||||
lazy_load: z.boolean().default(viewerConfigDefault.lazy_load),
|
||||
controls: z
|
||||
.object({
|
||||
nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
|
||||
nextprev_size: z.string().default('48px'),
|
||||
next_previous: nextPreviousControlConfigSchema,
|
||||
})
|
||||
.optional(),
|
||||
dimensions: z
|
||||
.default(viewerConfigDefault.controls),
|
||||
})
|
||||
.default(viewerConfigDefault);
|
||||
export type ViewerConfig = z.infer<typeof viewerConfigSchema>;
|
||||
|
||||
/**
|
||||
* Dimensions configuration section.
|
||||
*/
|
||||
const dimensionsConfigDefault = {
|
||||
aspect_ratio_mode: 'dynamic' as const,
|
||||
aspect_ratio: [16, 9],
|
||||
};
|
||||
const dimensionsConfigSchema = z
|
||||
.object({
|
||||
aspect_ratio_mode: z
|
||||
.enum(['dynamic', 'static', 'unconstrained'])
|
||||
.default('dynamic'),
|
||||
.default(dimensionsConfigDefault.aspect_ratio_mode),
|
||||
aspect_ratio: z
|
||||
.number()
|
||||
.array()
|
||||
@@ -342,17 +436,43 @@ export const frigateCardConfigSchema = z.object({
|
||||
.regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/)
|
||||
.transform((input) => input.split(/[:\/]/).map((d) => Number(d))),
|
||||
)
|
||||
.default([16, 9]),
|
||||
.default(dimensionsConfigDefault.aspect_ratio),
|
||||
})
|
||||
.optional(),
|
||||
.default(dimensionsConfigDefault);
|
||||
|
||||
/**
|
||||
* Main card config.
|
||||
*/
|
||||
export const frigateCardConfigSchema = z.object({
|
||||
camera_entity: z.string().optional(),
|
||||
|
||||
// Main configuration sections.
|
||||
frigate: frigateConfigDefaultSchema,
|
||||
view: viewConfigSchema,
|
||||
menu: menuConfigSchema,
|
||||
live: liveConfigSchema,
|
||||
event_viewer: viewerConfigSchema,
|
||||
image: imageConfigSchema,
|
||||
elements: pictureElementsSchema,
|
||||
dimensions: dimensionsConfigSchema,
|
||||
|
||||
// Entities that should trigger a card update.
|
||||
update_entities: z.string().array().optional(),
|
||||
|
||||
// Stock lovelace card config.
|
||||
type: z.string(),
|
||||
show_warning: z.boolean().optional(),
|
||||
show_error: z.boolean().optional(),
|
||||
test_gui: z.boolean().optional(),
|
||||
});
|
||||
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
|
||||
export type RawFrigateCardConfig = Record<string, unknown>;
|
||||
|
||||
export const frigateCardConfigDefaults = {
|
||||
frigate: frigateConfigDefault,
|
||||
view: viewConfigDefault,
|
||||
menu: menuConfigDefault,
|
||||
live: liveConfigDefault,
|
||||
event_viewer: viewerConfigDefault,
|
||||
};
|
||||
|
||||
// Schema for card (non-user configured) menu icons.
|
||||
const internalMenuIconSchema = z.object({
|
||||
@@ -404,8 +524,8 @@ export interface Message {
|
||||
}
|
||||
|
||||
export interface MenuInteraction {
|
||||
interaction: "hold" | "tap" | "double_tap",
|
||||
button: MenuButton,
|
||||
interaction: 'hold' | 'tap' | 'double_tap';
|
||||
button: MenuButton;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user