Merge pull request #861 from dermotduffy/performance-one
Add `low` performance profile and `image` live provider
This commit is contained in:
@@ -35,4 +35,4 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: frigate-hass-card
|
name: frigate-hass-card
|
||||||
path: dist/frigate-hass-card.js
|
path: dist/*.js
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ name: Release
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -18,14 +19,14 @@ jobs:
|
|||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
|
|
||||||
# Upload build file to the releas as an asset.
|
# Upload build file to the release as an asset.
|
||||||
- name: Upload zip to release
|
- name: Upload zip to release
|
||||||
uses: svenstaro/upload-release-action@v1-release
|
uses: svenstaro/upload-release-action@v1-release
|
||||||
|
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
file: dist/frigate-hass-card.js
|
file: dist/*.js
|
||||||
asset_name: frigate-hass-card.js
|
file_glob: true
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
|
|||||||
@@ -113,14 +113,14 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|
|||||||
|
|
||||||
#### Available Live Providers
|
#### Available Live Providers
|
||||||
|
|
||||||
|Live Provider|Latency|Frame Rate|Installation|Description|
|
|Live Provider|Latency|Frame Rate|Loading Time|Installation|Description|
|
||||||
| -- | -- | -- | -- | -- |
|
| -- | -- | -- | -- | -- | -- |
|
||||||
|`ha` (default HA configuration)|Poor|High|Builtin|Use the built-in Home Assistant camera stream. The camera doesn't even need to be a Frigate camera! |
|
|`ha` (default HA configuration)|Poor|High|Better|Builtin|Use the built-in Home Assistant camera stream. The camera doesn't even need to be a Frigate camera! |
|
||||||
|`ha` (when configured with LL-HLS)|Better|High|Builtin|Use the built-in Home Assistant camera streams -- can be configured to use an [LL-HLS](https://www.home-assistant.io/integrations/stream/#ll-hls) feed for lower latency.|
|
|`ha` (when configured with LL-HLS)|Better|High|Better|Builtin|Use the built-in Home Assistant camera streams -- can be configured to use an [LL-HLS](https://www.home-assistant.io/integrations/stream/#ll-hls) feed for lower latency.|
|
||||||
|`ha` (Native WebRTC)|Best|High|Builtin|Use the built-in Home Assistant camera streams -- can be configured to use [native WebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) offering a very low-latency feed direct to your browser.|
|
|`ha` (Native WebRTC)|Best|High|Better|Builtin|Use the built-in Home Assistant camera streams -- can be configured to use [native WebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) offering a very low-latency feed direct to your browser.|
|
||||||
|`frigate-jsmpeg`|Better|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.|
|
|`image`|Poor|Poor|Best|Builtin|Use refreshing snapshots of the built-in Home Assistant camera streams.|
|
||||||
|`webrtc-card`|Best|High|Separate installation required|Embed's [AlexxIT's WebRTC Card](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc). Not to be confused with native Home Assistant WebRTC (use `ha` provider above).|
|
|`frigate-jsmpeg`|Better|Low|Poor|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-card`|Best|High|Better|Separate installation required|Embed's [AlexxIT's WebRTC Card](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc). Not to be confused with native Home Assistant WebRTC (use `ha` provider above).|
|
||||||
|
|
||||||
<a name="camera-frigate-configuration"></a>
|
<a name="camera-frigate-configuration"></a>
|
||||||
|
|
||||||
@@ -325,13 +325,26 @@ See the [fully expanded live configuration example](#config-expanded-live) for h
|
|||||||
| `lazy_unload` | `never` | :heavy_multiplication_x: | When to lazily **un**load lazyily-loaded cameras. `never` will never lazily-unload, `unselected` will lazy-unload a camera when it is unselected in the carousel, `hidden` will lazy-unload all cameras when the browser/tab becomes hidden or `all` on any opportunity to lazily unload (i.e. either case). This will cause a reloading delay on revisiting that camera in the carousel but will save the streaming network resources that are otherwise consumed. This option has no effect if `lazy_load` is false. Some live providers (e.g. `webrtc-card`) implement their own lazy unloading independently which may occur regardless of the value of this setting.|
|
| `lazy_unload` | `never` | :heavy_multiplication_x: | When to lazily **un**load lazyily-loaded cameras. `never` will never lazily-unload, `unselected` will lazy-unload a camera when it is unselected in the carousel, `hidden` will lazy-unload all cameras when the browser/tab becomes hidden or `all` on any opportunity to lazily unload (i.e. either case). This will cause a reloading delay on revisiting that camera in the carousel but will save the streaming network resources that are otherwise consumed. This option has no effect if `lazy_load` is false. Some live providers (e.g. `webrtc-card`) implement their own lazy unloading independently which may occur regardless of the value of this setting.|
|
||||||
| `draggable` | `true` | :heavy_multiplication_x: | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
| `draggable` | `true` | :heavy_multiplication_x: | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
||||||
| `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between live cameras. Accepted values: `slide` or `none`. |
|
| `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between live cameras. Accepted values: `slide` or `none`. |
|
||||||
| `show_image_during_load` | `true` | :white_check_mark: | If `true`, during the initial stream load, a still image will be shown instead of the loading video stream. This still image will auto-refresh every 1 second and will be replaced with the live stream once loaded. |
|
| `show_image_during_load` | `true` | :white_check_mark: | If `true`, during the initial stream load, the `image` live provider will be shown instead of the loading video stream. This still image will auto-refresh and is replaced with the live stream once loaded. |
|
||||||
| `actions` | | :white_check_mark: | Actions to use for the `live` view. See [actions](#actions) below.|
|
| `actions` | | :white_check_mark: | Actions to use for the `live` view. See [actions](#actions) below.|
|
||||||
| `controls` | | :white_check_mark: | Configuration for the `live` view controls. See below. |
|
| `controls` | | :white_check_mark: | Configuration for the `live` view controls. See below. |
|
||||||
| `jsmpeg` | | :white_check_mark: | Configuration for the `frigate-jsmpeg` live provider. See below.|
|
| `jsmpeg` | | :white_check_mark: | Configuration for the `frigate-jsmpeg` live provider. See below.|
|
||||||
| `webrtc_card` | | :white_check_mark: | Configuration for the `webrtc-card` live provider. See below.|
|
| `webrtc_card` | | :white_check_mark: | Configuration for the `webrtc-card` live provider. See below.|
|
||||||
| `layout` | | :white_check_mark: | See [media layout](#media-layout) below.|
|
| `layout` | | :white_check_mark: | See [media layout](#media-layout) below.|
|
||||||
|
|
||||||
|
#### Live Provider: Image Configuration
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
live:
|
||||||
|
image:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Overridable | Description |
|
||||||
|
| - | - | - | - |
|
||||||
|
| `refresh_seconds` | 1 | :white_check_mark: | The image will be refreshed at least every `refresh_seconds`. `0` implies no refreshing. |
|
||||||
|
|
||||||
#### Live Provider: JSMPEG Configuration
|
#### Live Provider: JSMPEG Configuration
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
@@ -557,6 +570,8 @@ See the [fully expanded image configuration example](#config-expanded-image) for
|
|||||||
| `refresh_seconds` | 0 | :white_check_mark: | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. |
|
| `refresh_seconds` | 0 | :white_check_mark: | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. |
|
||||||
| `actions` | | :white_check_mark: | Actions to use for the `image` view. See [actions](#actions) below.|
|
| `actions` | | :white_check_mark: | Actions to use for the `image` view. See [actions](#actions) below.|
|
||||||
|
|
||||||
|
**Note**: When `mode` is set to `camera` this is effectively providing the same image as the `image` live provider would show in the live camera carousel.
|
||||||
|
|
||||||
### Timeline Options
|
### Timeline Options
|
||||||
|
|
||||||
The `timeline` is used to show the timing sequence of events and recordings across cameras. You can interact with the timeline in a number of ways:
|
The `timeline` is used to show the timing sequence of events and recordings across cameras. You can interact with the timeline in a number of ways:
|
||||||
@@ -651,6 +666,88 @@ The card aspect ratio can be changed with the `dimensions.aspect_ratio_mode` and
|
|||||||
If no aspect ratio is specified or available, but one is needed then `16:9` will
|
If no aspect ratio is specified or available, but one is needed then `16:9` will
|
||||||
be used by default.
|
be used by default.
|
||||||
|
|
||||||
|
<a name="performance-options"></a>
|
||||||
|
|
||||||
|
### Performance Options
|
||||||
|
|
||||||
|
These options control the card performance settings to enable the card to run
|
||||||
|
(more) smoothly on lower end devices.
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
performance:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Overridable | Description |
|
||||||
|
| - | - | - | - |
|
||||||
|
| `profile` | `high` | :heavy_multiplication_x: | Whether the card is configured in full `high` performance mode, or `low` performance defaults for lower end devices. See [low performance profile](#performance-profile-low) below.|
|
||||||
|
|
||||||
|
#### Feature Options
|
||||||
|
|
||||||
|
Controls card-wide central functionality that may impact performance but which is not configurable elsewhere.
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
performance:
|
||||||
|
features:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Overridable | Description |
|
||||||
|
| - | - | - | - |
|
||||||
|
| `animated_progress_indicator` | `true` | :heavy_multiplication_x: | Will show the animated progress indicator 'spinner' when `true` or a simple loading icon when `false`.|
|
||||||
|
|
||||||
|
#### Style Options
|
||||||
|
|
||||||
|
Style performance options request the card minimize certain expensive CSS
|
||||||
|
stylings. This does not necessarily disable these stylings _entirely_ since that
|
||||||
|
may break the basic expected visuals of the card (e.g. menu icons need curves),
|
||||||
|
but rather avoids use of them in high item-count situations (e.g. avoiding
|
||||||
|
shadows on timeline items, or curves in the event gallery items).
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
performance:
|
||||||
|
style:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Overridable | Description |
|
||||||
|
| - | - | - | - |
|
||||||
|
| `border_radius` | `true` | :heavy_multiplication_x: | If `false` minimizes the usage of rounded corners.|
|
||||||
|
| `box_shadow` | `true` | :heavy_multiplication_x: | If `false` minimizes the usage of shadows.|
|
||||||
|
|
||||||
|
<a name="performance-profile-low"></a>
|
||||||
|
|
||||||
|
#### Performance Profile `low`
|
||||||
|
|
||||||
|
In the `low` performance profile, the card attempts to lower the CPU and network
|
||||||
|
consumption of the card by setting default option values when they have not been explicitly set by the user.
|
||||||
|
|
||||||
|
Principles used in the selection of options set by `low` profile mode:
|
||||||
|
|
||||||
|
* Get 'out of the box' performance similar to the basic "Home Assistant Picture Glance" card.
|
||||||
|
* Only change behavior that the user can case-by-case 'reset' by explicitly setting an option elsewhere.
|
||||||
|
* Do not break the visual aesthetic of the card.
|
||||||
|
|
||||||
|
**Note:**: Since the performance profile changes the _default_ value of options,
|
||||||
|
setting the `low` profile on a pre-existing card could have no effect if there are
|
||||||
|
considerable options already set by the user.
|
||||||
|
|
||||||
|
Please see <a href="src/performance.ts">the source code</a> for an exhaustive list of options set by `low` profile mode. Summary:
|
||||||
|
|
||||||
|
* The default live provider (`auto`) will resolve to the `image` live provider for cameras with a `camera_entity` specified. It will have a refresh period of 10 seconds (same as the stock Home Assistant Picture Glance card).
|
||||||
|
* No event thumbnails fetched.
|
||||||
|
* No recordings shown.
|
||||||
|
* No automated actions (e.g. mute, play, pause) except playing in live view.
|
||||||
|
* Always lazily unload anything that can be unloaded.
|
||||||
|
* Carousels are not draggable and have no 'slide' effects.
|
||||||
|
* Live image is not shown during stream loads.
|
||||||
|
* No title popups.
|
||||||
|
* Menu rendered outside the main body of the card, with reduced menu buttons.
|
||||||
|
* All optional performace features and performance styles (described above) disabled.
|
||||||
|
|
||||||
<a name="overrides"></a>
|
<a name="overrides"></a>
|
||||||
|
|
||||||
### Overrides Options
|
### Overrides Options
|
||||||
@@ -1366,6 +1463,8 @@ live:
|
|||||||
maxAudioLag: 10
|
maxAudioLag: 10
|
||||||
videoBufferSize: 524288
|
videoBufferSize: 524288
|
||||||
audioBufferSize: 131072
|
audioBufferSize: 131072
|
||||||
|
image:
|
||||||
|
refresh_seconds: 1
|
||||||
controls:
|
controls:
|
||||||
next_previous:
|
next_previous:
|
||||||
style: chevrons
|
style: chevrons
|
||||||
@@ -2085,6 +2184,23 @@ overrides:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Performance section</summary>
|
||||||
|
|
||||||
|
Reference: [Performance Options](#performance-options).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
performance:
|
||||||
|
profile: high
|
||||||
|
features:
|
||||||
|
animated_progress_indicator: true
|
||||||
|
style:
|
||||||
|
border_radius: true
|
||||||
|
box_shadow: true
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### Basic cameras configuration
|
### Basic cameras configuration
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
+14
-2
@@ -8,7 +8,7 @@ import styles from 'rollup-plugin-styles';
|
|||||||
import image from '@rollup/plugin-image';
|
import image from '@rollup/plugin-image';
|
||||||
import replace from '@rollup/plugin-replace';
|
import replace from '@rollup/plugin-replace';
|
||||||
import gitInfo from 'rollup-plugin-git-info';
|
import gitInfo from 'rollup-plugin-git-info';
|
||||||
import { visualizer } from "rollup-plugin-visualizer";
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
const watch = process.env.ROLLUP_WATCH === 'true' || process.env.ROLLUP_WATCH === '1';
|
const watch = process.env.ROLLUP_WATCH === 'true' || process.env.ROLLUP_WATCH === '1';
|
||||||
const dev = watch || process.env.DEV === 'true' || process.env.DEV === '1';
|
const dev = watch || process.env.DEV === 'true' || process.env.DEV === '1';
|
||||||
@@ -65,8 +65,20 @@ const plugins = [
|
|||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
input: 'src/card.ts',
|
input: 'src/card.ts',
|
||||||
|
preserveEntrySignatures: false,
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/frigate-hass-card.js',
|
entryFileNames: 'frigate-hass-card.js',
|
||||||
|
dir: 'dist',
|
||||||
|
chunkFileNames: (chunk) => {
|
||||||
|
// Add "lang-" to the front of the language chunk names for readability.
|
||||||
|
if (
|
||||||
|
chunk.facadeModuleId &&
|
||||||
|
chunk.facadeModuleId.match(/localize\/languages\/.*\.json/)
|
||||||
|
) {
|
||||||
|
return 'lang-[name]-[hash].js';
|
||||||
|
}
|
||||||
|
return '[name]-[hash].js';
|
||||||
|
},
|
||||||
format: 'es',
|
format: 'es',
|
||||||
...(dev && {
|
...(dev && {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { HassEntities } from 'home-assistant-js-websocket';
|
import { HassEntities } from 'home-assistant-js-websocket';
|
||||||
import { cloneDeep, merge } from 'lodash-es';
|
import merge from 'lodash-es/merge';
|
||||||
|
|
||||||
export interface ConditionState {
|
export interface ConditionState {
|
||||||
view?: string;
|
view?: string;
|
||||||
@@ -109,7 +109,7 @@ export function getOverriddenConfig(
|
|||||||
overrides: Readonly<RawOverrides> | undefined,
|
overrides: Readonly<RawOverrides> | undefined,
|
||||||
conditionState?: Readonly<ConditionState>,
|
conditionState?: Readonly<ConditionState>,
|
||||||
): RawFrigateCardConfig {
|
): RawFrigateCardConfig {
|
||||||
const output = cloneDeep(config);
|
const output = structuredClone(config);
|
||||||
let overridden = false;
|
let overridden = false;
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
for (const override of overrides) {
|
for (const override of overrides) {
|
||||||
|
|||||||
+183
-94
@@ -12,7 +12,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
import { throttle } from 'lodash-es';
|
import throttle from 'lodash-es/throttle';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { actionHandler } from './action-handler-directive.js';
|
import { actionHandler } from './action-handler-directive.js';
|
||||||
@@ -24,29 +24,20 @@ import {
|
|||||||
} from './card-condition.js';
|
} from './card-condition.js';
|
||||||
import './components/elements.js';
|
import './components/elements.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
import './components/gallery.js';
|
import type { FrigateCardImage } from './components/image.js';
|
||||||
import './components/image.js';
|
import type { FrigateCardLive } from './components/live/live.js';
|
||||||
import { FrigateCardImage } from './components/image.js';
|
|
||||||
import './components/live.js';
|
|
||||||
import { FrigateCardLive } from './components/live.js';
|
|
||||||
import './components/menu.js';
|
import './components/menu.js';
|
||||||
import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON } from './components/menu.js';
|
import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON } from './components/menu.js';
|
||||||
import './components/message.js';
|
import './components/message.js';
|
||||||
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
||||||
import './components/thumbnail-carousel.js';
|
import './components/thumbnail-carousel.js';
|
||||||
import './components/timeline.js';
|
|
||||||
import './components/viewer.js';
|
|
||||||
import { isConfigUpgradeable } from './config-mgmt.js';
|
import { isConfigUpgradeable } from './config-mgmt.js';
|
||||||
import {
|
import {
|
||||||
CAMERA_BIRDSEYE,
|
CAMERA_BIRDSEYE,
|
||||||
MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA,
|
MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA,
|
||||||
REPO_URL,
|
REPO_URL,
|
||||||
} from './const.js';
|
} from './const.js';
|
||||||
import './editor.js';
|
import { getLanguage, loadLanguages, localize } from './localize/localize.js';
|
||||||
import { getLanguage, localize } from './localize/localize.js';
|
|
||||||
import './patches/ha-camera-stream.js';
|
|
||||||
import './patches/ha-hls-player.js';
|
|
||||||
import './patches/ha-web-rtc-player.ts';
|
|
||||||
import cardStyle from './scss/card.scss';
|
import cardStyle from './scss/card.scss';
|
||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
@@ -67,6 +58,7 @@ import {
|
|||||||
MenuButton,
|
MenuButton,
|
||||||
Message,
|
Message,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
|
CardWideConfig,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
@@ -101,6 +93,7 @@ import { View } from './view.js';
|
|||||||
import pkg from '../package.json';
|
import pkg from '../package.json';
|
||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import { DataManager } from './utils/data-manager.js';
|
import { DataManager } from './utils/data-manager.js';
|
||||||
|
import { setLowPerformanceProfile, setPerformanceCSSStyles } from './performance.js';
|
||||||
|
|
||||||
/** A note on media callbacks:
|
/** A note on media callbacks:
|
||||||
*
|
*
|
||||||
@@ -165,6 +158,9 @@ export class FrigateCard extends LitElement {
|
|||||||
protected _config!: FrigateCardConfig;
|
protected _config!: FrigateCardConfig;
|
||||||
protected _rawConfig?: RawFrigateCardConfig;
|
protected _rawConfig?: RawFrigateCardConfig;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _overriddenConfig?: FrigateCardConfig;
|
protected _overriddenConfig?: FrigateCardConfig;
|
||||||
|
|
||||||
@@ -210,11 +206,17 @@ export class FrigateCard extends LitElement {
|
|||||||
protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000);
|
protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000);
|
||||||
|
|
||||||
// Whether the card has been successfully initialized.
|
// Whether the card has been successfully initialized.
|
||||||
protected _initialized = false;
|
protected _loadedHAElements = false;
|
||||||
|
protected _loadedLanguages = false;
|
||||||
|
|
||||||
protected _triggers: Map<string, Date> = new Map();
|
protected _triggers: Map<string, Date> = new Map();
|
||||||
protected _untriggerTimerID: number | null = null;
|
protected _untriggerTimerID: number | null = null;
|
||||||
|
|
||||||
|
// Whether or not to include HA state in ConditionState. Doing so increases
|
||||||
|
// CPU usage as HA state is pumped out very fast, so this is only enabled if
|
||||||
|
// the configuration is likely to consume it.
|
||||||
|
protected _needHAStateInConditionState = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the Home Assistant object.
|
* Set the Home Assistant object.
|
||||||
*/
|
*/
|
||||||
@@ -236,8 +238,10 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HA entity state is part of the condition state.
|
if (this._needHAStateInConditionState) {
|
||||||
this._generateConditionState();
|
// HA entity state is part of the condition state.
|
||||||
|
this._generateConditionState();
|
||||||
|
}
|
||||||
|
|
||||||
// Dark mode may depend on HASS.
|
// Dark mode may depend on HASS.
|
||||||
this._setLightOrDarkMode();
|
this._setLightOrDarkMode();
|
||||||
@@ -248,6 +252,7 @@ export class FrigateCard extends LitElement {
|
|||||||
* @returns A LovelaceCardEditor element.
|
* @returns A LovelaceCardEditor element.
|
||||||
*/
|
*/
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
|
await import('./editor.js');
|
||||||
return document.createElement('frigate-card-editor');
|
return document.createElement('frigate-card-editor');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +287,10 @@ export class FrigateCard extends LitElement {
|
|||||||
view: this._view?.view,
|
view: this._view?.view,
|
||||||
fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
|
fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
|
||||||
camera: this._view?.camera,
|
camera: this._view?.camera,
|
||||||
state: this._hass?.states,
|
|
||||||
mediaLoaded: !!this._currentMediaLoadedInfo,
|
mediaLoaded: !!this._currentMediaLoadedInfo,
|
||||||
|
...(this._needHAStateInConditionState && {
|
||||||
|
state: this._hass?.states,
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the components that need the new condition state. Passed directly
|
// Update the components that need the new condition state. Passed directly
|
||||||
@@ -708,24 +715,38 @@ export class FrigateCard extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = new ExtendedEntityCache();
|
const hasAutoTriggers = (config: CameraConfig): boolean => {
|
||||||
|
return config.triggers.motion || config.triggers.occupancy;
|
||||||
|
};
|
||||||
|
const hasAnyTriggers = (config: CameraConfig): boolean => {
|
||||||
|
return hasAutoTriggers(config) || !!config.triggers.entities.length;
|
||||||
|
};
|
||||||
|
const hasCameraName = (config: CameraConfig): boolean => {
|
||||||
|
return !!config.frigate?.camera_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading cameras may require a number of calls to Home Assistant.
|
||||||
|
//
|
||||||
|
// - getAllEntities: Required if any camera has auto-triggers (motion or
|
||||||
|
// occupancy sensors).
|
||||||
|
// - getExtendedEntity: Per camera entity to autodetect the Frigate camera
|
||||||
|
// name or to compute auto-triggers (motion or occupancy sensors).
|
||||||
|
// - getExtendedEntities: Per binary sensor associated with a Frigate config
|
||||||
|
// entry, to compute auto-triggers (motion or occupancy sensors).
|
||||||
|
//
|
||||||
|
// For loading performance these are only called when absolutely needed.
|
||||||
|
|
||||||
let entityList: EntityList | undefined;
|
let entityList: EntityList | undefined;
|
||||||
try {
|
const cache = new ExtendedEntityCache();
|
||||||
entityList = await getAllEntities(this._hass);
|
const loadedCameras: CameraConfig[] = [];
|
||||||
} catch (e) {
|
|
||||||
errorToConsole(e as Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cameras: Map<string, CameraConfig> = new Map();
|
const addCameraConfig = async (config: CameraConfig, index: number) => {
|
||||||
let errorFree = true;
|
|
||||||
|
|
||||||
const addCameraConfig = async (config: CameraConfig) => {
|
|
||||||
if (!this._hass) {
|
if (!this._hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let entity: ExtendedEntity | null = null;
|
let entity: ExtendedEntity | null = null;
|
||||||
if (config.camera_entity) {
|
if (config.camera_entity && (hasAnyTriggers(config) || !hasCameraName(config))) {
|
||||||
try {
|
try {
|
||||||
entity = await getExtendedEntity(this._hass, config.camera_entity, cache);
|
entity = await getExtendedEntity(this._hass, config.camera_entity, cache);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -735,77 +756,94 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.frigate.camera_name && entity) {
|
if (entity && !hasCameraName(config)) {
|
||||||
const resolvedName = this._getFrigateCameraNameFromEntity(entity);
|
const resolvedName = this._getFrigateCameraNameFromEntity(entity);
|
||||||
if (resolvedName) {
|
if (resolvedName) {
|
||||||
config.frigate.camera_name = resolvedName;
|
config.frigate.camera_name = resolvedName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity && entityList) {
|
if (entity && entityList && hasAnyTriggers(config)) {
|
||||||
// Try to find the correct entities for the motion & occupancy sensors.
|
if (hasAutoTriggers(config)) {
|
||||||
// We know they are binary_sensors, and that they'll have the same
|
// Try to find the correct entities for the motion & occupancy sensors.
|
||||||
// config entry ID as the camera. Searching via unique_id ensures this
|
// We know they are binary_sensors, and that they'll have the same
|
||||||
// search still works if the user renames the entity_id.
|
// config entry ID as the camera. Searching via unique_id ensures this
|
||||||
const binarySensorEntities = entityList.filter(
|
// search still works if the user renames the entity_id.
|
||||||
(ent) =>
|
const binarySensorEntities = entityList.filter(
|
||||||
ent.config_entry_id === entity?.config_entry_id &&
|
(ent) =>
|
||||||
!ent.disabled_by &&
|
ent.config_entry_id === entity?.config_entry_id &&
|
||||||
ent.entity_id.startsWith('binary_sensor.'),
|
!ent.disabled_by &&
|
||||||
);
|
ent.entity_id.startsWith('binary_sensor.'),
|
||||||
|
|
||||||
try {
|
|
||||||
await getExtendedEntities(
|
|
||||||
this._hass,
|
|
||||||
binarySensorEntities.map((ent) => ent.entity_id),
|
|
||||||
cache,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getExtendedEntities(
|
||||||
|
this._hass,
|
||||||
|
binarySensorEntities.map((ent) => ent.entity_id),
|
||||||
|
cache,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.triggers.motion) {
|
||||||
|
const motionEntity = this._getMotionSensor(cache, config);
|
||||||
|
if (motionEntity) {
|
||||||
|
config.triggers.entities.push(motionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.triggers.occupancy) {
|
||||||
|
const occupancyEntity = this._getOccupancySensor(cache, config);
|
||||||
|
if (occupancyEntity) {
|
||||||
|
config.triggers.entities.push(occupancyEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.triggers.entities = [...new Set(config.triggers.entities)];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedCameras[index] = config;
|
||||||
|
};
|
||||||
|
|
||||||
|
let errorFree = true;
|
||||||
|
const cameras: Map<string, CameraConfig> = new Map();
|
||||||
|
const configCameras = this._getConfig().cameras;
|
||||||
|
|
||||||
|
if (configCameras && Array.isArray(configCameras)) {
|
||||||
|
if (configCameras.some((config) => hasAutoTriggers(config))) {
|
||||||
|
try {
|
||||||
|
entityList = await getAllEntities(this._hass);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.triggers.motion) {
|
// Load all cameras in parallel, but remember the order they were provided
|
||||||
const motionEntity = this._getMotionSensor(cache, config);
|
// (they must be added to the cameraMap in this same order).
|
||||||
if (motionEntity) {
|
await Promise.all(configCameras.map((configCamera, index) => addCameraConfig(configCamera, index)))
|
||||||
config.triggers.entities.push(motionEntity);
|
|
||||||
}
|
loadedCameras.forEach((loadedCamera: CameraConfig) => {
|
||||||
|
const id = getCameraID(loadedCamera);
|
||||||
|
if (!id) {
|
||||||
|
this._setMessageAndUpdate({
|
||||||
|
message: localize('error.no_camera_id'),
|
||||||
|
type: 'error',
|
||||||
|
context: loadedCamera,
|
||||||
|
});
|
||||||
|
errorFree = false;
|
||||||
|
} else if (cameras.has(id)) {
|
||||||
|
this._setMessageAndUpdate({
|
||||||
|
message: localize('error.duplicate_camera_id'),
|
||||||
|
type: 'error',
|
||||||
|
context: loadedCamera,
|
||||||
|
});
|
||||||
|
errorFree = false;
|
||||||
|
} else {
|
||||||
|
cameras.set(id, loadedCamera);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if (config.triggers.occupancy) {
|
|
||||||
const occupancyEntity = this._getOccupancySensor(cache, config);
|
|
||||||
if (occupancyEntity) {
|
|
||||||
config.triggers.entities.push(occupancyEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.triggers.entities = [...new Set(config.triggers.entities)];
|
|
||||||
|
|
||||||
const id = getCameraID(config);
|
|
||||||
if (!id) {
|
|
||||||
this._setMessageAndUpdate({
|
|
||||||
message: localize('error.no_camera_id'),
|
|
||||||
type: 'error',
|
|
||||||
context: config,
|
|
||||||
});
|
|
||||||
errorFree = false;
|
|
||||||
} else if (cameras.has(id)) {
|
|
||||||
this._setMessageAndUpdate({
|
|
||||||
message: localize('error.duplicate_camera_id'),
|
|
||||||
type: 'error',
|
|
||||||
context: config,
|
|
||||||
});
|
|
||||||
errorFree = false;
|
|
||||||
} else {
|
|
||||||
cameras.set(id, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this._getConfig().cameras && Array.isArray(this._getConfig().cameras)) {
|
|
||||||
// Cameras are loaded sequentially rather than in parallel to preserve the
|
|
||||||
// order of the input camera array.
|
|
||||||
for (const camera of this._getConfig().cameras) {
|
|
||||||
await addCameraConfig(camera);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cameras.size) {
|
if (!cameras.size) {
|
||||||
@@ -917,9 +955,9 @@ export class FrigateCard extends LitElement {
|
|||||||
throw new Error(localize('error.invalid_configuration'));
|
throw new Error(localize('error.invalid_configuration'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const configUpgradeable = isConfigUpgradeable(inputConfig);
|
|
||||||
const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
|
const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
|
const configUpgradeable = isConfigUpgradeable(inputConfig);
|
||||||
const hint = this._getParseErrorPaths(parseResult.error);
|
const hint = this._getParseErrorPaths(parseResult.error);
|
||||||
let upgradeMessage = '';
|
let upgradeMessage = '';
|
||||||
if (configUpgradeable && getLovelace().mode !== 'yaml') {
|
if (configUpgradeable && getLovelace().mode !== 'yaml') {
|
||||||
@@ -933,7 +971,10 @@ export class FrigateCard extends LitElement {
|
|||||||
: localize('error.invalid_configuration_no_hint')),
|
: localize('error.invalid_configuration_no_hint')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const config = parseResult.data;
|
const config =
|
||||||
|
parseResult.data.performance.profile !== 'low'
|
||||||
|
? parseResult.data
|
||||||
|
: setLowPerformanceProfile(inputConfig, parseResult.data);
|
||||||
|
|
||||||
if (config.test_gui) {
|
if (config.test_gui) {
|
||||||
getLovelace().setEditMode(true);
|
getLovelace().setEditMode(true);
|
||||||
@@ -941,6 +982,10 @@ export class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
this._rawConfig = inputConfig;
|
this._rawConfig = inputConfig;
|
||||||
this._config = config;
|
this._config = config;
|
||||||
|
this._cardWideConfig = {
|
||||||
|
performance: config.performance,
|
||||||
|
};
|
||||||
|
|
||||||
this._overriddenConfig = undefined;
|
this._overriddenConfig = undefined;
|
||||||
this._cameras = undefined;
|
this._cameras = undefined;
|
||||||
this._view = undefined;
|
this._view = undefined;
|
||||||
@@ -948,6 +993,21 @@ export class FrigateCard extends LitElement {
|
|||||||
this._generateConditionState();
|
this._generateConditionState();
|
||||||
this._setLightOrDarkMode();
|
this._setLightOrDarkMode();
|
||||||
this._untrigger();
|
this._untrigger();
|
||||||
|
|
||||||
|
const hasStateCondition = (): boolean => {
|
||||||
|
for (const override of this._config.overrides ?? []) {
|
||||||
|
if (override.conditions.state?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For elements there can be arbitrary custom elements that not related to
|
||||||
|
// this card so we cannot easily determine if state is used further down
|
||||||
|
// the chain by a custom:frigate-card-conditional element. Instead, include state
|
||||||
|
// if elements are configured at all.
|
||||||
|
return !!this._config.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._needHAStateInConditionState = hasStateCondition();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1042,17 +1102,34 @@ export class FrigateCard extends LitElement {
|
|||||||
* Called before each update.
|
* Called before each update.
|
||||||
*/
|
*/
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
// Side load the necessary elements if not already initialized.
|
// Side load the necessary elements if not already initialized (do not need
|
||||||
if (!this._initialized) {
|
// to block for the loading to complete).
|
||||||
|
if (!this._loadedHAElements) {
|
||||||
sideLoadHomeAssistantElements().then((success) => {
|
sideLoadHomeAssistantElements().then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
this._initialized = true;
|
this._loadedHAElements = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._cameras && (changedProps.has('_config') || changedProps.has('_cameras'))) {
|
if (this._cameras && (changedProps.has('_config') || changedProps.has('_cameras'))) {
|
||||||
this._dataManager = new DataManager(this._cameras, this._config.timeline.media);
|
this._dataManager = new DataManager(this._cameras);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has('_cardWideConfig')) {
|
||||||
|
setPerformanceCSSStyles(this, this._cardWideConfig?.performance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._view?.is('live')) {
|
||||||
|
import('./components/live/live.js');
|
||||||
|
} else if (this._view?.isGalleryView()) {
|
||||||
|
import('./components/gallery.js');
|
||||||
|
} else if (this._view?.isViewerView()) {
|
||||||
|
import('./components/viewer.js');
|
||||||
|
} else if (this._view?.is('image')) {
|
||||||
|
import('./components/image.js');
|
||||||
|
} else if (this._view?.is('timeline')) {
|
||||||
|
import('./components/timeline.js');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,6 +1246,15 @@ export class FrigateCard extends LitElement {
|
|||||||
* @returns `true` if the element should be updated.
|
* @returns `true` if the element should be updated.
|
||||||
*/
|
*/
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
// Load the relevant languages. Cannot do anything until then.
|
||||||
|
if (!this._loadedLanguages) {
|
||||||
|
loadLanguages().then(() => {
|
||||||
|
this._loadedLanguages = true;
|
||||||
|
this.requestUpdate();
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const oldHass = changedProps.get('_hass') as HomeAssistant | undefined;
|
const oldHass = changedProps.get('_hass') as HomeAssistant | undefined;
|
||||||
let shouldUpdate = !oldHass || changedProps.size != 1;
|
let shouldUpdate = !oldHass || changedProps.size != 1;
|
||||||
|
|
||||||
@@ -1884,7 +1970,7 @@ export class FrigateCard extends LitElement {
|
|||||||
this._changeView({ resetMessage: false });
|
this._changeView({ resetMessage: false });
|
||||||
return this._render();
|
return this._render();
|
||||||
})(),
|
})(),
|
||||||
renderProgressIndicator(),
|
renderProgressIndicator({cardWideConfig: this._cardWideConfig}),
|
||||||
)
|
)
|
||||||
: // Always want to call render even if there's a message, to
|
: // Always want to call render even if there's a message, to
|
||||||
// ensure live preload is always present (even if not displayed).
|
// ensure live preload is always present (even if not displayed).
|
||||||
@@ -1954,6 +2040,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.cameras=${this._cameras}
|
.cameras=${this._cameras}
|
||||||
.galleryConfig=${this._getConfig().event_gallery}
|
.galleryConfig=${this._getConfig().event_gallery}
|
||||||
.dataManager=${this._dataManager}
|
.dataManager=${this._dataManager}
|
||||||
|
.cardWideConfig=${this._cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-gallery>`
|
</frigate-card-gallery>`
|
||||||
: ``}
|
: ``}
|
||||||
@@ -1965,6 +2052,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.viewerConfig=${this._getConfig().media_viewer}
|
.viewerConfig=${this._getConfig().media_viewer}
|
||||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
.dataManager=${this._dataManager}
|
.dataManager=${this._dataManager}
|
||||||
|
.cardWideConfig=${this._cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-viewer>`
|
</frigate-card-viewer>`
|
||||||
: ``}
|
: ``}
|
||||||
@@ -1996,6 +2084,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.liveOverrides=${getOverridesByKey(this._getConfig().overrides, 'live')}
|
.liveOverrides=${getOverridesByKey(this._getConfig().overrides, 'live')}
|
||||||
.cameras=${this._cameras}
|
.cameras=${this._cameras}
|
||||||
.dataManager=${this._dataManager}
|
.dataManager=${this._dataManager}
|
||||||
|
.cardWideConfig=${this._cardWideConfig}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
>
|
>
|
||||||
</frigate-card-live>
|
</frigate-card-live>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { throttle } from 'lodash-es';
|
import throttle from 'lodash-es/throttle';
|
||||||
import carouselStyle from '../scss/carousel.scss';
|
import carouselStyle from '../scss/carousel.scss';
|
||||||
import { TransitionEffect } from '../types';
|
import { TransitionEffect } from '../types';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import galleryStyle from '../scss/gallery.scss';
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
GalleryConfig,
|
GalleryConfig,
|
||||||
@@ -23,12 +24,23 @@ import {
|
|||||||
getFullDependentBrowseMediaQueryParametersOrDispatchError,
|
getFullDependentBrowseMediaQueryParametersOrDispatchError,
|
||||||
} from '../utils/ha/browse-media';
|
} from '../utils/ha/browse-media';
|
||||||
import { changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js';
|
import { changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js';
|
||||||
import { DataManager } from '../utils/data-manager';
|
import { DataManager } from '../utils/data-manager.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { renderProgressIndicator } from './message.js';
|
import { renderProgressIndicator } from './message.js';
|
||||||
import './thumbnail.js';
|
import './thumbnail.js';
|
||||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||||
|
|
||||||
|
interface GalleryViewContext {
|
||||||
|
// Keep track of the previous view to allow returning to a higher-level folder.
|
||||||
|
previous?: View;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'view' {
|
||||||
|
interface ViewContext {
|
||||||
|
gallery?: GalleryViewContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('frigate-card-gallery')
|
@customElement('frigate-card-gallery')
|
||||||
export class FrigateCardGallery extends LitElement {
|
export class FrigateCardGallery extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -46,6 +58,9 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public dataManager?: DataManager;
|
public dataManager?: DataManager;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -96,7 +111,7 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
browseMediaQueryParameters,
|
browseMediaQueryParameters,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return renderProgressIndicator();
|
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -192,9 +207,9 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected _showBackArrow(): boolean {
|
protected _showBackArrow(): boolean {
|
||||||
return (
|
return (
|
||||||
!!this.view?.previous &&
|
!!this.view?.context?.gallery?.previous &&
|
||||||
!!this.view.previous.target &&
|
!!this.view.context.gallery.previous.target &&
|
||||||
this.view.previous.view === this.view.view
|
this.view.context.gallery.previous.view === this.view.view
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,8 +254,8 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
${this._showBackArrow()
|
${this._showBackArrow()
|
||||||
? html` <ha-card
|
? html` <ha-card
|
||||||
@click=${(ev) => {
|
@click=${(ev) => {
|
||||||
if (this.view && this.view.previous) {
|
if (this.view && this.view.context?.gallery?.previous) {
|
||||||
this.view.previous.dispatchChangeEvent(this);
|
this.view.context.gallery.previous.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
}}
|
}}
|
||||||
@@ -262,6 +277,11 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this.view,
|
this.view,
|
||||||
child,
|
child,
|
||||||
|
{
|
||||||
|
gallery: {
|
||||||
|
previous: this.view,
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
|
|||||||
+22
-12
@@ -6,7 +6,7 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
@@ -15,13 +15,17 @@ import { CachedValueController } from '../cached-value-controller.js';
|
|||||||
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import imageStyle from '../scss/image.scss';
|
import imageStyle from '../scss/image.scss';
|
||||||
import { CameraConfig, ImageViewConfig } from '../types.js';
|
import { CameraConfig, ImageViewConfig, MediaLoadedInfo } from '../types.js';
|
||||||
import { isHassDifferent } from '../utils/ha';
|
import { isHassDifferent } from '../utils/ha';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
import {
|
||||||
|
createMediaLoadedInfo,
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
|
} from '../utils/media-info.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { dispatchErrorMessageEvent } from './message.js';
|
import { dispatchErrorMessageEvent } from './message.js';
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
|
||||||
// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py .
|
// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py .
|
||||||
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
|
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
|
||||||
@@ -48,6 +52,8 @@ export class FrigateCardImage extends LitElement {
|
|||||||
protected _cachedValueController?: CachedValueController<string>;
|
protected _cachedValueController?: CachedValueController<string>;
|
||||||
protected _boundVisibilityHandler = this._visibilityHandler.bind(this);
|
protected _boundVisibilityHandler = this._visibilityHandler.bind(this);
|
||||||
|
|
||||||
|
protected _mediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the camera entity for the current camera configuration.
|
* Get the camera entity for the current camera configuration.
|
||||||
* @returns The entity or undefined if no camera entity is available.
|
* @returns The entity or undefined if no camera entity is available.
|
||||||
@@ -234,7 +240,13 @@ export class FrigateCardImage extends LitElement {
|
|||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
src=${live(src)}
|
src=${live(src)}
|
||||||
@load=${(ev: Event) => {
|
@load=${(ev: Event) => {
|
||||||
dispatchMediaLoadedEvent(this, ev);
|
const mediaLoadedInfo = createMediaLoadedInfo(ev);
|
||||||
|
// Avoid the media being reported as repeatedly loading unless the
|
||||||
|
// media info changes.
|
||||||
|
if (mediaLoadedInfo && !isEqual(this._mediaLoadedInfo, mediaLoadedInfo)) {
|
||||||
|
this._mediaLoadedInfo = mediaLoadedInfo;
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent(this, mediaLoadedInfo);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
@error=${() => {
|
@error=${() => {
|
||||||
if (this.imageConfig?.mode === 'camera') {
|
if (this.imageConfig?.mode === 'camera') {
|
||||||
@@ -246,11 +258,9 @@ export class FrigateCardImage extends LitElement {
|
|||||||
} else if (this.imageConfig?.mode === 'url') {
|
} else if (this.imageConfig?.mode === 'url') {
|
||||||
// In url mode, the user likely specified a URL that cannot be
|
// In url mode, the user likely specified a URL that cannot be
|
||||||
// resolved. Show an error message.
|
// resolved. Show an error message.
|
||||||
dispatchErrorMessageEvent(
|
dispatchErrorMessageEvent(this, localize('error.image_load_error'), {
|
||||||
this,
|
context: this.imageConfig,
|
||||||
localize('error.image_load_error'),
|
});
|
||||||
{ context: this.imageConfig },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>`
|
/>`
|
||||||
@@ -263,7 +273,7 @@ export class FrigateCardImage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"frigate-card-image": FrigateCardImage
|
'frigate-card-image': FrigateCardImage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
|
import liveFrigateStyle from '../../scss/live-frigate.scss';
|
||||||
|
import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
|
import { getStateObjOrDispatchError } from './live.js';
|
||||||
|
import '../../patches/ha-camera-stream';
|
||||||
|
import '../../patches/ha-hls-player.js';
|
||||||
|
import '../../patches/ha-web-rtc-player.ts';
|
||||||
|
|
||||||
|
|
||||||
|
@customElement('frigate-card-live-ha')
|
||||||
|
export class FrigateCardLiveHA extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
protected _playerRef: Ref<Element & FrigateCardMediaPlayer> = createRef();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the video.
|
||||||
|
*/
|
||||||
|
public play(): void {
|
||||||
|
this._playerRef.value?.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the video.
|
||||||
|
*/
|
||||||
|
public pause(): void {
|
||||||
|
this._playerRef.value?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute the video.
|
||||||
|
*/
|
||||||
|
public mute(): void {
|
||||||
|
this._playerRef.value?.mute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmute the video.
|
||||||
|
*/
|
||||||
|
public unmute(): void {
|
||||||
|
this._playerRef.value?.unmute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
this._playerRef.value?.seek(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateObj = getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
|
||||||
|
if (!stateObj) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html` <frigate-card-ha-camera-stream
|
||||||
|
${ref(this._playerRef)}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
.controls=${true}
|
||||||
|
.muted=${true}
|
||||||
|
>
|
||||||
|
</frigate-card-ha-camera-stream>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(liveFrigateStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-live-ha': FrigateCardLiveHA;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
|
import liveFrigateStyle from '../../scss/live-frigate.scss';
|
||||||
|
import { CameraConfig, LiveImageConfig } from '../../types.js';
|
||||||
|
import { getStateObjOrDispatchError } from './live.js';
|
||||||
|
import '../image.js';
|
||||||
|
|
||||||
|
@customElement('frigate-card-live-image')
|
||||||
|
export class FrigateCardLiveImage extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public liveImageConfig?: LiveImageConfig;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _playing = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the video.
|
||||||
|
*/
|
||||||
|
public play(): void {
|
||||||
|
this._playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the video.
|
||||||
|
*/
|
||||||
|
public pause(): void {
|
||||||
|
this._playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute the video.
|
||||||
|
*/
|
||||||
|
public mute(): void {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmute the video.
|
||||||
|
*/
|
||||||
|
public unmute(): void {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public seek(_seconds: number): void {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.hass || !this.liveImageConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
|
||||||
|
|
||||||
|
return html` <frigate-card-image
|
||||||
|
.imageConfig=${{
|
||||||
|
mode: 'camera' as const,
|
||||||
|
refresh_seconds: this._playing ? this.liveImageConfig.refresh_seconds : 0,
|
||||||
|
// Don't need to pass layout options as FrigateCardLiveProvider has
|
||||||
|
// already taken care of this for us.
|
||||||
|
}}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cameraConfig=${this.cameraConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-image>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(liveFrigateStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-live-image': FrigateCardLiveImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { until } from 'lit/directives/until.js';
|
||||||
|
import { renderProgressIndicator } from '../../components/message.js';
|
||||||
|
import { localize } from '../../localize/localize.js';
|
||||||
|
import liveJSMPEGStyle from '../../scss/live-jsmpeg.scss';
|
||||||
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
CardWideConfig,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
|
JSMPEGConfig,
|
||||||
|
} from '../../types.js';
|
||||||
|
import { homeAssistantSignPath } from '../../utils/ha';
|
||||||
|
import { dispatchMediaLoadedEvent } from '../../utils/media-info.js';
|
||||||
|
import { dispatchErrorMessageEvent } from '../message.js';
|
||||||
|
import { contentsChanged, errorToConsole } from '../../utils/basic.js';
|
||||||
|
|
||||||
|
// Number of seconds a signed URL is valid for.
|
||||||
|
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
|
|
||||||
|
// Number of seconds before the expiry to trigger a refresh.
|
||||||
|
const URL_SIGN_REFRESH_THRESHOLD_SECONDS = 1 * 60 * 60;
|
||||||
|
|
||||||
|
@customElement('frigate-card-live-jsmpeg')
|
||||||
|
export class FrigateCardLiveJSMPEG extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
|
public jsmpegConfig?: JSMPEGConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
|
protected hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
protected _jsmpegCanvasElement?: HTMLCanvasElement;
|
||||||
|
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
||||||
|
protected _refreshPlayerTimerID?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the video.
|
||||||
|
*/
|
||||||
|
public play(): void {
|
||||||
|
this._jsmpegVideoPlayer?.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the video.
|
||||||
|
*/
|
||||||
|
public pause(): void {
|
||||||
|
this._jsmpegVideoPlayer?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute the video (included for completeness, JSMPEG live disables audio as
|
||||||
|
* Frigate does not encode it).
|
||||||
|
*/
|
||||||
|
public mute(): void {
|
||||||
|
const player = this._jsmpegVideoPlayer?.player;
|
||||||
|
if (player) {
|
||||||
|
player.volume = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmute the video (included for completeness, JSMPEG live disables audio as
|
||||||
|
* Frigate does not encode it).
|
||||||
|
*/
|
||||||
|
public unmute(): void {
|
||||||
|
const player = this._jsmpegVideoPlayer?.player;
|
||||||
|
if (player) {
|
||||||
|
player.volume = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video (unsupported).
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public seek(_seconds: number): void {
|
||||||
|
// JSMPEG does not support seeking.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a signed player URL.
|
||||||
|
* @returns A URL or null.
|
||||||
|
*/
|
||||||
|
protected async _getURL(): Promise<string | null> {
|
||||||
|
if (
|
||||||
|
!this.hass ||
|
||||||
|
!this.cameraConfig?.frigate.client_id ||
|
||||||
|
!this.cameraConfig?.frigate.camera_name
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: string | null | undefined;
|
||||||
|
try {
|
||||||
|
response = await homeAssistantSignPath(
|
||||||
|
this.hass,
|
||||||
|
`/api/frigate/${this.cameraConfig.frigate.client_id}` +
|
||||||
|
`/jsmpeg/${this.cameraConfig.frigate.camera_name}`,
|
||||||
|
URL_SIGN_EXPIRY_SECONDS,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!response) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response.replace(/^http/i, 'ws');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSMPEG player.
|
||||||
|
* @param url The URL for the player to connect to.
|
||||||
|
* @returns A JSMPEG player.
|
||||||
|
*/
|
||||||
|
protected async _createJSMPEGPlayer(url: string): Promise<JSMpeg.VideoElement> {
|
||||||
|
return new Promise<JSMpeg.VideoElement>((resolve) => {
|
||||||
|
let videoDecoded = false;
|
||||||
|
const player = new JSMpeg.VideoElement(
|
||||||
|
this,
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
canvas: this._jsmpegCanvasElement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The media carousel may automatically pause when the browser tab is
|
||||||
|
// inactive, JSMPEG does not need to also do so independently.
|
||||||
|
pauseWhenHidden: false,
|
||||||
|
autoplay: false,
|
||||||
|
protocols: [],
|
||||||
|
audio: false,
|
||||||
|
videoBufferSize: 1024 * 1024 * 4,
|
||||||
|
|
||||||
|
// Override with user-specified options.
|
||||||
|
...this.jsmpegConfig?.options,
|
||||||
|
|
||||||
|
// Don't allow the player to internally reconnect, as it may re-use a
|
||||||
|
// URL with a (newly) invalid signature, e.g. during a Home Assistant
|
||||||
|
// restart.
|
||||||
|
reconnectInterval: 0,
|
||||||
|
onVideoDecode: () => {
|
||||||
|
// This is the only callback that is called after the dimensions
|
||||||
|
// are available. It's called on every frame decode, so just
|
||||||
|
// ignore any subsequent calls.
|
||||||
|
if (!videoDecoded && this._jsmpegCanvasElement) {
|
||||||
|
videoDecoded = true;
|
||||||
|
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement);
|
||||||
|
resolve(player);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset / destroy the player.
|
||||||
|
*/
|
||||||
|
protected _resetPlayer(): void {
|
||||||
|
if (this._refreshPlayerTimerID) {
|
||||||
|
window.clearTimeout(this._refreshPlayerTimerID);
|
||||||
|
this._refreshPlayerTimerID = undefined;
|
||||||
|
}
|
||||||
|
if (this._jsmpegVideoPlayer) {
|
||||||
|
try {
|
||||||
|
this._jsmpegVideoPlayer.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
// Pass.
|
||||||
|
}
|
||||||
|
this._jsmpegVideoPlayer = undefined;
|
||||||
|
}
|
||||||
|
if (this._jsmpegCanvasElement) {
|
||||||
|
this._jsmpegCanvasElement.remove();
|
||||||
|
this._jsmpegCanvasElement = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component connected callback.
|
||||||
|
*/
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
if (this.isConnected) {
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component disconnected callback.
|
||||||
|
*/
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this._resetPlayer();
|
||||||
|
}
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the JSMPEG player.
|
||||||
|
*/
|
||||||
|
protected async _refreshPlayer(): Promise<void> {
|
||||||
|
this._resetPlayer();
|
||||||
|
|
||||||
|
this._jsmpegCanvasElement = document.createElement('canvas');
|
||||||
|
this._jsmpegCanvasElement.className = 'media';
|
||||||
|
|
||||||
|
if (!this.cameraConfig?.frigate.camera_name) {
|
||||||
|
return dispatchErrorMessageEvent(this, localize('error.no_camera_name'), {
|
||||||
|
context: this.cameraConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await this._getURL();
|
||||||
|
if (url) {
|
||||||
|
this._jsmpegVideoPlayer = await this._createJSMPEGPlayer(url);
|
||||||
|
this._refreshPlayerTimerID = window.setTimeout(() => {
|
||||||
|
this.requestUpdate();
|
||||||
|
}, (URL_SIGN_EXPIRY_SECONDS - URL_SIGN_REFRESH_THRESHOLD_SECONDS) * 1000);
|
||||||
|
} else {
|
||||||
|
dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_sign'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
const _render = async (): Promise<TemplateResult | void> => {
|
||||||
|
await this._refreshPlayer();
|
||||||
|
|
||||||
|
if (!this._jsmpegVideoPlayer || !this._jsmpegCanvasElement) {
|
||||||
|
return dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_player'));
|
||||||
|
}
|
||||||
|
return html`${this._jsmpegCanvasElement}`;
|
||||||
|
};
|
||||||
|
return html`${until(
|
||||||
|
_render(),
|
||||||
|
renderProgressIndicator({
|
||||||
|
cardWideConfig: this.cardWideConfig,
|
||||||
|
}),
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(liveJSMPEGStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-live-jsmpeg': FrigateCardLiveJSMPEG;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { Task } from '@lit-labs/task';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { localize } from '../../localize/localize.js';
|
||||||
|
import liveWebRTCStyle from '../../scss/live-webrtc.scss';
|
||||||
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
CardWideConfig,
|
||||||
|
FrigateCardError,
|
||||||
|
WebRTCCardConfig,
|
||||||
|
} from '../../types.js';
|
||||||
|
import { contentsChanged } from '../../utils/basic.js';
|
||||||
|
import { dispatchMediaLoadedEvent } from '../../utils/media-info.js';
|
||||||
|
import { dispatchErrorMessageEvent, renderProgressIndicator } from '../message.js';
|
||||||
|
import { renderTask } from '../../utils/task.js';
|
||||||
|
|
||||||
|
// Create a wrapper for AlexxIT's WebRTC card
|
||||||
|
// - https://github.com/AlexxIT/WebRTC
|
||||||
|
@customElement('frigate-card-live-webrtc-card')
|
||||||
|
export class FrigateCardLiveWebRTCCard extends LitElement {
|
||||||
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
|
public webRTCConfig?: WebRTCCardConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
|
protected hass?: HomeAssistant;
|
||||||
|
|
||||||
|
// A task to await the load of the WebRTC component.
|
||||||
|
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the video.
|
||||||
|
*/
|
||||||
|
public play(): void {
|
||||||
|
this._getPlayer()
|
||||||
|
?.play()
|
||||||
|
.catch(() => {
|
||||||
|
// WebRTC appears to generate additional spurious load events, which may
|
||||||
|
// result in loads after a play() call, which causes the browser to spam
|
||||||
|
// the logs unless the promise rejection is handled here.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the video.
|
||||||
|
*/
|
||||||
|
public pause(): void {
|
||||||
|
this._getPlayer()?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute the video.
|
||||||
|
*/
|
||||||
|
public mute(): void {
|
||||||
|
const player = this._getPlayer();
|
||||||
|
if (player) {
|
||||||
|
player.muted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmute the video.
|
||||||
|
*/
|
||||||
|
public unmute(): void {
|
||||||
|
const player = this._getPlayer();
|
||||||
|
if (player) {
|
||||||
|
player.muted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
const player = this._getPlayer();
|
||||||
|
if (player) {
|
||||||
|
player.currentTime = seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying video player.
|
||||||
|
* @returns The player or `null` if not found.
|
||||||
|
*/
|
||||||
|
protected _getPlayer(): HTMLVideoElement | null {
|
||||||
|
return this.renderRoot?.querySelector('#video') as HTMLVideoElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _getWebRTCCardElement(): Promise<
|
||||||
|
CustomElementConstructor | undefined
|
||||||
|
> {
|
||||||
|
await customElements.whenDefined('webrtc-camera');
|
||||||
|
return customElements.get('webrtc-camera');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the WebRTC element. May throw.
|
||||||
|
*/
|
||||||
|
protected _createWebRTC(): HTMLElement | null {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const webrtcElement = this._webrtcTask.value;
|
||||||
|
if (webrtcElement && this.hass) {
|
||||||
|
const webrtc = new webrtcElement() as HTMLElement & {
|
||||||
|
hass: HomeAssistant;
|
||||||
|
setConfig: (config: Record<string, unknown>) => void;
|
||||||
|
};
|
||||||
|
const config = { ...this.webRTCConfig };
|
||||||
|
|
||||||
|
// If the live WebRTC configuration does not specify a URL/entity to use,
|
||||||
|
// then take values from the camera configuration instead (if there are
|
||||||
|
// any).
|
||||||
|
if (!config.url) {
|
||||||
|
config.url = this.cameraConfig?.webrtc_card?.url;
|
||||||
|
}
|
||||||
|
if (!config.entity) {
|
||||||
|
config.entity = this.cameraConfig?.webrtc_card?.entity;
|
||||||
|
}
|
||||||
|
webrtc.setConfig(config);
|
||||||
|
webrtc.hass = this.hass;
|
||||||
|
return webrtc;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
const render = (): TemplateResult | void => {
|
||||||
|
let webrtcElement: HTMLElement | null;
|
||||||
|
try {
|
||||||
|
webrtcElement = this._createWebRTC();
|
||||||
|
} catch (e) {
|
||||||
|
return dispatchErrorMessageEvent(
|
||||||
|
this,
|
||||||
|
e instanceof FrigateCardError
|
||||||
|
? e.message
|
||||||
|
: localize('error.webrtc_card_reported_error') + ': ' + (e as Error).message,
|
||||||
|
{ context: (e as FrigateCardError).context },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (webrtcElement) {
|
||||||
|
// Set the id to ensure that the relevant CSS styles will have
|
||||||
|
// sufficient specifity to overcome some styles that are otherwise
|
||||||
|
// applied to <ha-card> in Safari.
|
||||||
|
webrtcElement.id = 'webrtc';
|
||||||
|
}
|
||||||
|
return html`${webrtcElement}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use a task to allow us to asynchronously wait for the WebRTC card to
|
||||||
|
// load, but yet still have the card load be followed by the updated()
|
||||||
|
// lifecycle callback (unlike just using `until`).
|
||||||
|
return renderTask(this, this._webrtcTask, render, {
|
||||||
|
inProgressFunc: () =>
|
||||||
|
renderProgressIndicator({
|
||||||
|
message: localize('error.webrtc_card_waiting'),
|
||||||
|
cardWideConfig: this.cardWideConfig,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updated lifecycle callback.
|
||||||
|
*/
|
||||||
|
public updated(): void {
|
||||||
|
// Extract the video component after it has been rendered and generate the
|
||||||
|
// media load event.
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
const video = this._getPlayer();
|
||||||
|
if (video) {
|
||||||
|
const onloadeddata = video.onloadeddata;
|
||||||
|
|
||||||
|
video.onloadeddata = (e) => {
|
||||||
|
if (onloadeddata) {
|
||||||
|
onloadeddata.call(video, e);
|
||||||
|
}
|
||||||
|
dispatchMediaLoadedEvent(this, video);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(liveWebRTCStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-live-webrtc-card': FrigateCardLiveWebRTCCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
|
||||||
import { Task } from '@lit-labs/task';
|
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import { EmblaOptionsType } from 'embla-carousel';
|
import { EmblaOptionsType } from 'embla-carousel';
|
||||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||||
import {
|
import {
|
||||||
@@ -15,66 +12,88 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { guard } from 'lit/directives/guard.js';
|
import { guard } from 'lit/directives/guard.js';
|
||||||
import { keyed } from 'lit/directives/keyed.js';
|
import { keyed } from 'lit/directives/keyed.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { ConditionState, getOverriddenConfig } from '../../card-condition.js';
|
||||||
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import { dispatchMessageEvent, renderProgressIndicator } from '../components/message.js';
|
import liveStyle from '../../scss/live.scss';
|
||||||
import { localize } from '../localize/localize.js';
|
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
||||||
import liveFrigateStyle from '../scss/live-frigate.scss';
|
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||||
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
|
|
||||||
import liveWebRTCStyle from '../scss/live-webrtc.scss';
|
|
||||||
import liveStyle from '../scss/live.scss';
|
|
||||||
import liveCarouselStyle from '../scss/live-carousel.scss';
|
|
||||||
import liveProviderStyle from '../scss/live-provider.scss';
|
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
FrigateCardError,
|
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
JSMPEGConfig,
|
|
||||||
LiveConfig,
|
LiveConfig,
|
||||||
LiveOverrides,
|
LiveOverrides,
|
||||||
LiveProvider,
|
LiveProvider,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
Message,
|
Message,
|
||||||
TransitionEffect,
|
TransitionEffect,
|
||||||
WebRTCCardConfig,
|
} from '../../types.js';
|
||||||
} from '../types.js';
|
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { contentsChanged } from '../../utils/basic.js';
|
||||||
import { contentsChanged, errorToConsole } from '../utils/basic.js';
|
import { getCameraIcon, getCameraTitle } from '../../utils/camera.js';
|
||||||
import { getCameraIcon, getCameraTitle } from '../utils/camera.js';
|
import { getFullDependentBrowseMediaQueryParameters } from '../../utils/ha/browse-media.js';
|
||||||
import { homeAssistantSignPath } from '../utils/ha';
|
|
||||||
import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js';
|
|
||||||
import {
|
import {
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
dispatchMediaLoadedEvent,
|
|
||||||
dispatchMediaUnloadedEvent,
|
dispatchMediaUnloadedEvent,
|
||||||
} from '../utils/media-info.js';
|
} from '../../utils/media-info.js';
|
||||||
import { dispatchViewContextChangeEvent, View } from '../view.js';
|
import { dispatchViewContextChangeEvent, View } from '../../view.js';
|
||||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
import { AutoMediaPlugin } from './../embla-plugins/automedia.js';
|
||||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
import { Lazyload } from './../embla-plugins/lazyload.js';
|
||||||
import {
|
import {
|
||||||
FrigateCardMediaCarousel,
|
FrigateCardMediaCarousel,
|
||||||
wrapMediaLoadedEventForCarousel,
|
wrapMediaLoadedEventForCarousel,
|
||||||
wrapMediaUnloadedEventForCarousel,
|
wrapMediaUnloadedEventForCarousel,
|
||||||
} from './media-carousel.js';
|
} from '../media-carousel.js';
|
||||||
import { dispatchErrorMessageEvent } from './message.js';
|
import '../next-prev-control.js';
|
||||||
import './next-prev-control.js';
|
import '../title-control.js';
|
||||||
import './title-control.js';
|
import '../surround.js';
|
||||||
import './surround.js';
|
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||||
import '../patches/ha-camera-stream';
|
|
||||||
import { EmblaCarouselPlugins } from './carousel.js';
|
|
||||||
import { renderTask } from '../utils/task.js';
|
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import './image';
|
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { DataManager } from '../../utils/data-manager.js';
|
||||||
import { DataManager } from '../utils/data-manager.js';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js';
|
||||||
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
|
|
||||||
// Number of seconds a signed URL is valid for.
|
/**
|
||||||
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
* Get the state object or dispatch an error. Used in `ha` and `image` live
|
||||||
|
* providers.
|
||||||
|
* @param element HTMLElement to dispatch errors from.
|
||||||
|
* @param hass Home Assistant object.
|
||||||
|
* @param cameraConfig Camera configuration.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getStateObjOrDispatchError = (
|
||||||
|
element: HTMLElement,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig?: CameraConfig,
|
||||||
|
): HassEntity | null => {
|
||||||
|
if (!cameraConfig?.camera_entity) {
|
||||||
|
dispatchErrorMessageEvent(element, localize('error.no_live_camera'), {
|
||||||
|
context: cameraConfig,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Number of seconds before the expiry to trigger a refresh.
|
const stateObj = hass.states[cameraConfig.camera_entity];
|
||||||
const URL_SIGN_REFRESH_THRESHOLD_SECONDS = 1 * 60 * 60;
|
if (!stateObj) {
|
||||||
|
dispatchErrorMessageEvent(element, localize('error.live_camera_not_found'), {
|
||||||
|
context: cameraConfig,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateObj.state === 'unavailable') {
|
||||||
|
dispatchMessageEvent(element, localize('error.live_camera_unavailable'), 'info', {
|
||||||
|
icon: 'mdi:connection',
|
||||||
|
context: getCameraTitle(hass, cameraConfig),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return stateObj;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement('frigate-card-live')
|
@customElement('frigate-card-live')
|
||||||
export class FrigateCardLive extends LitElement {
|
export class FrigateCardLive extends LitElement {
|
||||||
@@ -99,6 +118,9 @@ export class FrigateCardLive extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public dataManager?: DataManager;
|
public dataManager?: DataManager;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
// Whether or not the live view is currently in the background (i.e. preloaded
|
// Whether or not the live view is currently in the background (i.e. preloaded
|
||||||
// but not visible)
|
// but not visible)
|
||||||
@state()
|
@state()
|
||||||
@@ -250,6 +272,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
.inBackground=${this._inBackground}
|
.inBackground=${this._inBackground}
|
||||||
.conditionState=${this.conditionState}
|
.conditionState=${this.conditionState}
|
||||||
.liveOverrides=${this.liveOverrides}
|
.liveOverrides=${this.liveOverrides}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-live-carousel>
|
</frigate-card-live-carousel>
|
||||||
</frigate-card-surround>`,
|
</frigate-card-surround>`,
|
||||||
@@ -290,6 +313,9 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public conditionState?: ConditionState;
|
public conditionState?: ConditionState;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
// Index between camera name and slide number.
|
// Index between camera name and slide number.
|
||||||
protected _cameraToSlide: Record<string, number> = {};
|
protected _cameraToSlide: Record<string, number> = {};
|
||||||
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
||||||
@@ -484,7 +510,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
|
|
||||||
const liveProvider = slide?.querySelector(
|
const liveProvider = slide?.querySelector(
|
||||||
'frigate-card-live-provider',
|
'frigate-card-live-provider',
|
||||||
) as FrigateCardLiveProvider;
|
) as FrigateCardLiveProvider | null;
|
||||||
if (liveProvider) {
|
if (liveProvider) {
|
||||||
liveProvider.disabled = action !== 'load';
|
liveProvider.disabled = action !== 'load';
|
||||||
}
|
}
|
||||||
@@ -520,6 +546,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
.label=${getCameraTitle(this.hass, cameraConfig)}
|
.label=${getCameraTitle(this.hass, cameraConfig)}
|
||||||
.liveConfig=${config}
|
.liveConfig=${config}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
wrapMediaLoadedEventForCarousel(slideIndex, ev);
|
wrapMediaLoadedEventForCarousel(slideIndex, ev);
|
||||||
}}
|
}}
|
||||||
@@ -667,6 +694,9 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public label = '';
|
public label = '';
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _isVideoMediaLoaded = false;
|
protected _isVideoMediaLoaded = false;
|
||||||
|
|
||||||
@@ -719,15 +749,17 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
) {
|
) {
|
||||||
return 'webrtc-card';
|
return 'webrtc-card';
|
||||||
} else if (this.cameraConfig?.camera_entity) {
|
} else if (this.cameraConfig?.camera_entity) {
|
||||||
return 'ha';
|
if (this.cardWideConfig?.performance?.profile === 'low') {
|
||||||
|
return 'image';
|
||||||
|
} else {
|
||||||
|
return 'ha';
|
||||||
|
}
|
||||||
} else if (this.cameraConfig?.frigate.camera_name) {
|
} else if (this.cameraConfig?.frigate.camera_name) {
|
||||||
return 'frigate-jsmpeg';
|
return 'frigate-jsmpeg';
|
||||||
}
|
}
|
||||||
return frigateCardConfigDefaults.cameras.live_provider;
|
return frigateCardConfigDefaults.cameras.live_provider;
|
||||||
}
|
}
|
||||||
return (
|
return this.cameraConfig?.live_provider || 'image';
|
||||||
this.cameraConfig?.live_provider || frigateCardConfigDefaults.cameras.live_provider
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -769,6 +801,21 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
}
|
}
|
||||||
if (changedProps.has('liveConfig')) {
|
if (changedProps.has('liveConfig')) {
|
||||||
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
||||||
|
if (this.liveConfig?.show_image_during_load) {
|
||||||
|
import('./live-image.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changedProps.has('cameraConfig')) {
|
||||||
|
const provider = this._getResolvedProvider();
|
||||||
|
if (provider === 'frigate-jsmpeg') {
|
||||||
|
import('./live-jsmpeg.js');
|
||||||
|
} else if (provider === 'ha') {
|
||||||
|
import('./live-ha.js');
|
||||||
|
} else if (provider === 'webrtc-card') {
|
||||||
|
import('./live-webrtc.js');
|
||||||
|
} else if (provider === 'image') {
|
||||||
|
import('./live-image.js');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,22 +833,29 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
this.ariaLabel = this.label;
|
this.ariaLabel = this.label;
|
||||||
|
|
||||||
const provider = this._getResolvedProvider();
|
const provider = this._getResolvedProvider();
|
||||||
const showImage = !this._isVideoMediaLoaded && this._shouldShowImageDuringLoading();
|
const showImageDuringLoading =
|
||||||
|
!this._isVideoMediaLoaded && this._shouldShowImageDuringLoading();
|
||||||
const providerClasses = {
|
const providerClasses = {
|
||||||
hidden: showImage,
|
hidden: showImageDuringLoading,
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${showImage
|
${showImageDuringLoading || provider === 'image'
|
||||||
? html`<frigate-card-image
|
? html`<frigate-card-live-image
|
||||||
.imageConfig=${{
|
${ref(this._providerRef)}
|
||||||
mode: 'camera' as const,
|
|
||||||
refresh_seconds: 1,
|
|
||||||
}}
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
|
.liveImageConfig=${this.liveConfig.image}
|
||||||
|
@frigate-card:media:loaded=${() => {
|
||||||
|
if (provider === 'image') {
|
||||||
|
// Only count the media has loaded if the required provider is
|
||||||
|
// the image (not just the temporary image shown during
|
||||||
|
// loading).
|
||||||
|
this._videoMediaShowHandler();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
</frigate-card-image>`
|
</frigate-card-live-image>`
|
||||||
: html``}
|
: html``}
|
||||||
${provider === 'ha'
|
${provider === 'ha'
|
||||||
? html` <frigate-card-live-ha
|
? html` <frigate-card-live-ha
|
||||||
@@ -819,18 +873,22 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
.webRTCConfig=${this.liveConfig.webrtc_card}
|
.webRTCConfig=${this.liveConfig.webrtc_card}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-webrtc-card>`
|
</frigate-card-live-webrtc-card>`
|
||||||
: html` <frigate-card-live-jsmpeg
|
: provider === 'frigate-jsmpeg'
|
||||||
|
? html` <frigate-card-live-jsmpeg
|
||||||
${ref(this._providerRef)}
|
${ref(this._providerRef)}
|
||||||
class=${classMap(providerClasses)}
|
class=${classMap(providerClasses)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
.jsmpegConfig=${this.liveConfig.jsmpeg}
|
.jsmpegConfig=${this.liveConfig.jsmpeg}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-jsmpeg>`}
|
</frigate-card-live-jsmpeg>`
|
||||||
|
: html``}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,507 +900,8 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement('frigate-card-live-ha')
|
|
||||||
export class FrigateCardLiveFrigate extends LitElement {
|
|
||||||
@property({ attribute: false })
|
|
||||||
public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameraConfig?: CameraConfig;
|
|
||||||
|
|
||||||
protected _playerRef: Ref<FrigateCardLiveFrigate> = createRef();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play the video.
|
|
||||||
*/
|
|
||||||
public play(): void {
|
|
||||||
this._playerRef.value?.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause the video.
|
|
||||||
*/
|
|
||||||
public pause(): void {
|
|
||||||
this._playerRef.value?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mute the video.
|
|
||||||
*/
|
|
||||||
public mute(): void {
|
|
||||||
this._playerRef.value?.mute();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unmute the video.
|
|
||||||
*/
|
|
||||||
public unmute(): void {
|
|
||||||
this._playerRef.value?.unmute();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seek the video.
|
|
||||||
*/
|
|
||||||
public seek(seconds: number): void {
|
|
||||||
this._playerRef.value?.seek(seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Master render method.
|
|
||||||
* @returns A rendered template.
|
|
||||||
*/
|
|
||||||
protected render(): TemplateResult | void {
|
|
||||||
if (!this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.cameraConfig?.camera_entity) {
|
|
||||||
return dispatchErrorMessageEvent(this, localize('error.no_live_camera'), {
|
|
||||||
context: this.cameraConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
|
||||||
if (!stateObj) {
|
|
||||||
return dispatchErrorMessageEvent(this, localize('error.live_camera_not_found'), {
|
|
||||||
context: this.cameraConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateObj.state === 'unavailable') {
|
|
||||||
// Don't treat state unavailability as an error per se.
|
|
||||||
return dispatchMessageEvent(
|
|
||||||
this,
|
|
||||||
localize('error.live_camera_unavailable'),
|
|
||||||
'info',
|
|
||||||
{
|
|
||||||
icon: 'mdi:connection',
|
|
||||||
context: getCameraTitle(this.hass, this.cameraConfig),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return html` <frigate-card-ha-camera-stream
|
|
||||||
${ref(this._playerRef)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
.controls=${true}
|
|
||||||
.muted=${true}
|
|
||||||
>
|
|
||||||
</frigate-card-ha-camera-stream>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get styles.
|
|
||||||
*/
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return unsafeCSS(liveFrigateStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a wrapper for AlexxIT's WebRTC card
|
|
||||||
// - https://github.com/AlexxIT/WebRTC
|
|
||||||
@customElement('frigate-card-live-webrtc-card')
|
|
||||||
export class FrigateCardLiveWebRTCCard extends LitElement {
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
|
||||||
public webRTCConfig?: WebRTCCardConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameraConfig?: CameraConfig;
|
|
||||||
|
|
||||||
protected hass?: HomeAssistant;
|
|
||||||
|
|
||||||
// A task to await the load of the WebRTC component.
|
|
||||||
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play the video.
|
|
||||||
*/
|
|
||||||
public play(): void {
|
|
||||||
this._getPlayer()
|
|
||||||
?.play()
|
|
||||||
.catch(() => {
|
|
||||||
// WebRTC appears to generate additional spurious load events, which may
|
|
||||||
// result in loads after a play() call, which causes the browser to spam
|
|
||||||
// the logs unless the promise rejection is handled here.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause the video.
|
|
||||||
*/
|
|
||||||
public pause(): void {
|
|
||||||
this._getPlayer()?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mute the video.
|
|
||||||
*/
|
|
||||||
public mute(): void {
|
|
||||||
const player = this._getPlayer();
|
|
||||||
if (player) {
|
|
||||||
player.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unmute the video.
|
|
||||||
*/
|
|
||||||
public unmute(): void {
|
|
||||||
const player = this._getPlayer();
|
|
||||||
if (player) {
|
|
||||||
player.muted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seek the video.
|
|
||||||
*/
|
|
||||||
public seek(seconds: number): void {
|
|
||||||
const player = this._getPlayer();
|
|
||||||
if (player) {
|
|
||||||
player.currentTime = seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the underlying video player.
|
|
||||||
* @returns The player or `null` if not found.
|
|
||||||
*/
|
|
||||||
protected _getPlayer(): HTMLVideoElement | null {
|
|
||||||
return this.renderRoot?.querySelector('#video') as HTMLVideoElement | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _getWebRTCCardElement(): Promise<
|
|
||||||
CustomElementConstructor | undefined
|
|
||||||
> {
|
|
||||||
await customElements.whenDefined('webrtc-camera');
|
|
||||||
return customElements.get('webrtc-camera');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the WebRTC element. May throw.
|
|
||||||
*/
|
|
||||||
protected _createWebRTC(): HTMLElement | null {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const webrtcElement = this._webrtcTask.value;
|
|
||||||
if (webrtcElement && this.hass) {
|
|
||||||
const webrtc = new webrtcElement() as HTMLElement & {
|
|
||||||
hass: HomeAssistant;
|
|
||||||
setConfig: (config: Record<string, unknown>) => void;
|
|
||||||
};
|
|
||||||
const config = { ...this.webRTCConfig };
|
|
||||||
|
|
||||||
// If the live WebRTC configuration does not specify a URL/entity to use,
|
|
||||||
// then take values from the camera configuration instead (if there are
|
|
||||||
// any).
|
|
||||||
if (!config.url) {
|
|
||||||
config.url = this.cameraConfig?.webrtc_card?.url;
|
|
||||||
}
|
|
||||||
if (!config.entity) {
|
|
||||||
config.entity = this.cameraConfig?.webrtc_card?.entity;
|
|
||||||
}
|
|
||||||
webrtc.setConfig(config);
|
|
||||||
webrtc.hass = this.hass;
|
|
||||||
return webrtc;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Master render method.
|
|
||||||
* @returns A rendered template.
|
|
||||||
*/
|
|
||||||
protected render(): TemplateResult | void {
|
|
||||||
const render = (): TemplateResult | void => {
|
|
||||||
let webrtcElement: HTMLElement | null;
|
|
||||||
try {
|
|
||||||
webrtcElement = this._createWebRTC();
|
|
||||||
} catch (e) {
|
|
||||||
return dispatchErrorMessageEvent(
|
|
||||||
this,
|
|
||||||
e instanceof FrigateCardError
|
|
||||||
? e.message
|
|
||||||
: localize('error.webrtc_card_reported_error') + ': ' + (e as Error).message,
|
|
||||||
{ context: (e as FrigateCardError).context },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (webrtcElement) {
|
|
||||||
// Set the id to ensure that the relevant CSS styles will have
|
|
||||||
// sufficient specifity to overcome some styles that are otherwise
|
|
||||||
// applied to <ha-card> in Safari.
|
|
||||||
webrtcElement.id = 'webrtc';
|
|
||||||
}
|
|
||||||
return html`${webrtcElement}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use a task to allow us to asynchronously wait for the WebRTC card to
|
|
||||||
// load, but yet still have the card load be followed by the updated()
|
|
||||||
// lifecycle callback (unlike just using `until`).
|
|
||||||
return renderTask(this, this._webrtcTask, render, () =>
|
|
||||||
renderProgressIndicator(localize('error.webrtc_card_waiting')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updated lifecycle callback.
|
|
||||||
*/
|
|
||||||
public updated(): void {
|
|
||||||
// Extract the video component after it has been rendered and generate the
|
|
||||||
// media load event.
|
|
||||||
this.updateComplete.then(() => {
|
|
||||||
const video = this._getPlayer();
|
|
||||||
if (video) {
|
|
||||||
const onloadeddata = video.onloadeddata;
|
|
||||||
|
|
||||||
video.onloadeddata = (e) => {
|
|
||||||
if (onloadeddata) {
|
|
||||||
onloadeddata.call(video, e);
|
|
||||||
}
|
|
||||||
dispatchMediaLoadedEvent(this, video);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get styles.
|
|
||||||
*/
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return unsafeCSS(liveWebRTCStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('frigate-card-live-jsmpeg')
|
|
||||||
export class FrigateCardLiveJSMPEG extends LitElement {
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameraConfig?: CameraConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
|
||||||
public jsmpegConfig?: JSMPEGConfig;
|
|
||||||
|
|
||||||
protected hass?: ExtendedHomeAssistant;
|
|
||||||
|
|
||||||
protected _jsmpegCanvasElement?: HTMLCanvasElement;
|
|
||||||
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
|
||||||
protected _refreshPlayerTimerID?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play the video.
|
|
||||||
*/
|
|
||||||
public play(): void {
|
|
||||||
this._jsmpegVideoPlayer?.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause the video.
|
|
||||||
*/
|
|
||||||
public pause(): void {
|
|
||||||
this._jsmpegVideoPlayer?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mute the video (included for completeness, JSMPEG live disables audio as
|
|
||||||
* Frigate does not encode it).
|
|
||||||
*/
|
|
||||||
public mute(): void {
|
|
||||||
const player = this._jsmpegVideoPlayer?.player;
|
|
||||||
if (player) {
|
|
||||||
player.volume = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unmute the video (included for completeness, JSMPEG live disables audio as
|
|
||||||
* Frigate does not encode it).
|
|
||||||
*/
|
|
||||||
public unmute(): void {
|
|
||||||
const player = this._jsmpegVideoPlayer?.player;
|
|
||||||
if (player) {
|
|
||||||
player.volume = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seek the video (unsupported).
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
public seek(_seconds: number): void {
|
|
||||||
// JSMPEG does not support seeking.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a signed player URL.
|
|
||||||
* @returns A URL or null.
|
|
||||||
*/
|
|
||||||
protected async _getURL(): Promise<string | null> {
|
|
||||||
if (
|
|
||||||
!this.hass ||
|
|
||||||
!this.cameraConfig?.frigate.client_id ||
|
|
||||||
!this.cameraConfig?.frigate.camera_name
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: string | null | undefined;
|
|
||||||
try {
|
|
||||||
response = await homeAssistantSignPath(
|
|
||||||
this.hass,
|
|
||||||
`/api/frigate/${this.cameraConfig.frigate.client_id}` +
|
|
||||||
`/jsmpeg/${this.cameraConfig.frigate.camera_name}`,
|
|
||||||
URL_SIGN_EXPIRY_SECONDS,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
errorToConsole(e as Error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!response) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return response.replace(/^http/i, 'ws');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a JSMPEG player.
|
|
||||||
* @param url The URL for the player to connect to.
|
|
||||||
* @returns A JSMPEG player.
|
|
||||||
*/
|
|
||||||
protected async _createJSMPEGPlayer(url: string): Promise<JSMpeg.VideoElement> {
|
|
||||||
return new Promise<JSMpeg.VideoElement>((resolve) => {
|
|
||||||
let videoDecoded = false;
|
|
||||||
const player = new JSMpeg.VideoElement(
|
|
||||||
this,
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
canvas: this._jsmpegCanvasElement,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// The media carousel may automatically pause when the browser tab is
|
|
||||||
// inactive, JSMPEG does not need to also do so independently.
|
|
||||||
pauseWhenHidden: false,
|
|
||||||
autoplay: false,
|
|
||||||
protocols: [],
|
|
||||||
audio: false,
|
|
||||||
videoBufferSize: 1024 * 1024 * 4,
|
|
||||||
|
|
||||||
// Override with user-specified options.
|
|
||||||
...this.jsmpegConfig?.options,
|
|
||||||
|
|
||||||
// Don't allow the player to internally reconnect, as it may re-use a
|
|
||||||
// URL with a (newly) invalid signature, e.g. during a Home Assistant
|
|
||||||
// restart.
|
|
||||||
reconnectInterval: 0,
|
|
||||||
onVideoDecode: () => {
|
|
||||||
// This is the only callback that is called after the dimensions
|
|
||||||
// are available. It's called on every frame decode, so just
|
|
||||||
// ignore any subsequent calls.
|
|
||||||
if (!videoDecoded && this._jsmpegCanvasElement) {
|
|
||||||
videoDecoded = true;
|
|
||||||
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement);
|
|
||||||
resolve(player);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset / destroy the player.
|
|
||||||
*/
|
|
||||||
protected _resetPlayer(): void {
|
|
||||||
if (this._refreshPlayerTimerID) {
|
|
||||||
window.clearTimeout(this._refreshPlayerTimerID);
|
|
||||||
this._refreshPlayerTimerID = undefined;
|
|
||||||
}
|
|
||||||
if (this._jsmpegVideoPlayer) {
|
|
||||||
try {
|
|
||||||
this._jsmpegVideoPlayer.destroy();
|
|
||||||
} catch (err) {
|
|
||||||
// Pass.
|
|
||||||
}
|
|
||||||
this._jsmpegVideoPlayer = undefined;
|
|
||||||
}
|
|
||||||
if (this._jsmpegCanvasElement) {
|
|
||||||
this._jsmpegCanvasElement.remove();
|
|
||||||
this._jsmpegCanvasElement = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component connected callback.
|
|
||||||
*/
|
|
||||||
connectedCallback(): void {
|
|
||||||
super.connectedCallback();
|
|
||||||
if (this.isConnected) {
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component disconnected callback.
|
|
||||||
*/
|
|
||||||
disconnectedCallback(): void {
|
|
||||||
if (!this.isConnected) {
|
|
||||||
this._resetPlayer();
|
|
||||||
}
|
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the JSMPEG player.
|
|
||||||
*/
|
|
||||||
protected async _refreshPlayer(): Promise<void> {
|
|
||||||
this._resetPlayer();
|
|
||||||
|
|
||||||
this._jsmpegCanvasElement = document.createElement('canvas');
|
|
||||||
this._jsmpegCanvasElement.className = 'media';
|
|
||||||
|
|
||||||
if (!this.cameraConfig?.frigate.camera_name) {
|
|
||||||
return dispatchErrorMessageEvent(this, localize('error.no_camera_name'), {
|
|
||||||
context: this.cameraConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await this._getURL();
|
|
||||||
if (url) {
|
|
||||||
this._jsmpegVideoPlayer = await this._createJSMPEGPlayer(url);
|
|
||||||
this._refreshPlayerTimerID = window.setTimeout(() => {
|
|
||||||
this.requestUpdate();
|
|
||||||
}, (URL_SIGN_EXPIRY_SECONDS - URL_SIGN_REFRESH_THRESHOLD_SECONDS) * 1000);
|
|
||||||
} else {
|
|
||||||
dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_sign'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Master render method.
|
|
||||||
*/
|
|
||||||
protected render(): TemplateResult | void {
|
|
||||||
const _render = async (): Promise<TemplateResult | void> => {
|
|
||||||
await this._refreshPlayer();
|
|
||||||
|
|
||||||
if (!this._jsmpegVideoPlayer || !this._jsmpegCanvasElement) {
|
|
||||||
return dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_player'));
|
|
||||||
}
|
|
||||||
return html`${this._jsmpegCanvasElement}`;
|
|
||||||
};
|
|
||||||
return html`${until(_render(), renderProgressIndicator())}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get styles.
|
|
||||||
*/
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return unsafeCSS(liveJSMPEGStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'frigate-card-live-jsmpeg': FrigateCardLiveJSMPEG;
|
|
||||||
'frigate-card-live-webrtc-card': FrigateCardLiveWebRTCCard;
|
|
||||||
'frigate-card-live-ha': FrigateCardLiveFrigate;
|
|
||||||
'frigate-card-live-provider': FrigateCardLiveProvider;
|
'frigate-card-live-provider': FrigateCardLiveProvider;
|
||||||
'frigate-card-live-carousel': FrigateCardLiveCarousel;
|
'frigate-card-live-carousel': FrigateCardLiveCarousel;
|
||||||
'frigate-card-live': FrigateCardLive;
|
'frigate-card-live': FrigateCardLive;
|
||||||
@@ -22,7 +22,7 @@ import './next-prev-control.js';
|
|||||||
import './carousel.js';
|
import './carousel.js';
|
||||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||||
import { FrigateCardTitleControl } from './title-control.js';
|
import { FrigateCardTitleControl } from './title-control.js';
|
||||||
import { debounce } from 'lodash-es';
|
import debounce from 'lodash-es/debounce';
|
||||||
|
|
||||||
const getEmptyImageSrc = (width: number, height: number) =>
|
const getEmptyImageSrc = (width: number, height: number) =>
|
||||||
`data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
|
`data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { TROUBLESHOOTING_URL } from '../const.js';
|
import { TROUBLESHOOTING_URL } from '../const.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import messageStyle from '../scss/message.scss';
|
import messageStyle from '../scss/message.scss';
|
||||||
import { FrigateCardError, Message, MessageType } from '../types.js';
|
import { CardWideConfig, FrigateCardError, Message, MessageType } from '../types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
|
|
||||||
@customElement('frigate-card-message')
|
@customElement('frigate-card-message')
|
||||||
@@ -82,11 +82,14 @@ export class FrigateCardProgressIndicator extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public message: string | TemplateResult = '';
|
public message: string | TemplateResult = '';
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public animated = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html` <div class="message vertical">
|
return html` <div class="message vertical">
|
||||||
<span>
|
${this.animated
|
||||||
<ha-circular-progress active="true" size="large"> </ha-circular-progress>
|
? html`<ha-circular-progress active="true" size="large"> </ha-circular-progress>`
|
||||||
</span>
|
: html`<ha-icon icon="mdi:timer-sand"></ha-icon>`}
|
||||||
${this.message ? html`<span>${this.message}</span>` : html``}
|
${this.message ? html`<span>${this.message}</span>` : html``}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -112,9 +115,16 @@ export function renderMessage(message: Message): TemplateResult {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderProgressIndicator(message?: string): TemplateResult {
|
export function renderProgressIndicator(options?: {
|
||||||
|
message?: string;
|
||||||
|
cardWideConfig?: CardWideConfig;
|
||||||
|
}): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<frigate-card-progress-indicator .message=${message || ''}>
|
<frigate-card-progress-indicator
|
||||||
|
.message=${options?.message || ''}
|
||||||
|
.animated=${options?.cardWideConfig?.performance?.features
|
||||||
|
.animated_progress_indicator ?? true}
|
||||||
|
>
|
||||||
</frigate-card-progress-indicator>
|
</frigate-card-progress-indicator>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
|
|
||||||
const classes = {
|
const classes = {
|
||||||
controls: true,
|
controls: true,
|
||||||
previous: this.direction == 'previous',
|
previous: this.direction === 'previous',
|
||||||
next: this.direction == 'next',
|
next: this.direction === 'next',
|
||||||
thumbnails: this._controlConfig.style == 'thumbnails',
|
thumbnails: this._controlConfig.style === 'thumbnails',
|
||||||
icons: ['chevrons', 'icons'].includes(this._controlConfig.style),
|
icons: ['chevrons', 'icons'].includes(this._controlConfig.style),
|
||||||
button: ['chevrons', 'icons'].includes(this._controlConfig.style),
|
button: ['chevrons', 'icons'].includes(this._controlConfig.style),
|
||||||
};
|
};
|
||||||
@@ -62,7 +62,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
if (['chevrons', 'icons'].includes(this._controlConfig.style)) {
|
if (['chevrons', 'icons'].includes(this._controlConfig.style)) {
|
||||||
let icon: string;
|
let icon: string;
|
||||||
if (this._controlConfig.style === 'chevrons') {
|
if (this._controlConfig.style === 'chevrons') {
|
||||||
icon = this.direction == 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right';
|
icon = this.direction === 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right';
|
||||||
} else {
|
} else {
|
||||||
if (!this.icon) {
|
if (!this.icon) {
|
||||||
return html``;
|
return html``;
|
||||||
@@ -91,7 +91,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
aria-label="${this.label}"
|
aria-label="${this.label}"
|
||||||
/>`
|
/>`
|
||||||
: html``,
|
: html``,
|
||||||
() => html`<div class=${classMap(classes)}></div>`,
|
{ inProgressFunc: () => html`<div class=${classMap(classes)}></div>` },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { dispatchFrigateCardErrorEvent } from './message.js';
|
|||||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||||
|
|
||||||
import './surround-basic.js';
|
import './surround-basic.js';
|
||||||
import './timeline-core.js';
|
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
|
|
||||||
interface ThumbnailViewContext {
|
interface ThumbnailViewContext {
|
||||||
@@ -103,9 +102,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
?.evolve({
|
?.evolve({
|
||||||
target: parent,
|
target: parent,
|
||||||
childIndex: null,
|
childIndex: null,
|
||||||
|
|
||||||
// Don't carry over history of this 'empty' view.
|
|
||||||
previous: null,
|
|
||||||
})
|
})
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
@@ -125,6 +121,10 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
* Called before each update.
|
* Called before each update.
|
||||||
*/
|
*/
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (this.timelineConfig?.mode && this.timelineConfig.mode !== 'none') {
|
||||||
|
import('./timeline.js');
|
||||||
|
}
|
||||||
|
|
||||||
// Once the component will certainly update, dispatch a media request. Only
|
// Once the component will certainly update, dispatch a media request. Only
|
||||||
// do so if properties relevant to the request have changed (as per their
|
// do so if properties relevant to the request have changed (as per their
|
||||||
// hasChanged).
|
// hasChanged).
|
||||||
@@ -200,7 +200,7 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
>
|
>
|
||||||
</frigate-card-thumbnail-carousel>`
|
</frigate-card-thumbnail-carousel>`
|
||||||
: ''}
|
: ''}
|
||||||
${this.timelineConfig && !this.inBackground
|
${this.timelineConfig?.mode && this.timelineConfig.mode !== 'none' && !this.inBackground
|
||||||
? html` <frigate-card-timeline-core
|
? html` <frigate-card-timeline-core
|
||||||
slot=${this.timelineConfig.mode}
|
slot=${this.timelineConfig.mode}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { format, fromUnixTime } from 'date-fns';
|
import format from 'date-fns/format';
|
||||||
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -16,7 +17,7 @@ import { getEventDurationString } from '../utils/frigate.js';
|
|||||||
import { renderTask } from '../utils/task.js';
|
import { renderTask } from '../utils/task.js';
|
||||||
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { MediaSeek } from './viewer.js';
|
import type { MediaSeek } from './viewer.js';
|
||||||
import { TaskStatus } from '@lit-labs/task';
|
import { TaskStatus } from '@lit-labs/task';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -95,7 +96,7 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
|||||||
this._embedThumbnailTask,
|
this._embedThumbnailTask,
|
||||||
(embeddedThumbnail: string | null) =>
|
(embeddedThumbnail: string | null) =>
|
||||||
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
|
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
|
||||||
() => imageOff,
|
{ inProgressFunc: () => imageOff },
|
||||||
)
|
)
|
||||||
: imageOff} `;
|
: imageOff} `;
|
||||||
}
|
}
|
||||||
@@ -296,7 +297,9 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
? html`<frigate-card-thumbnail-feature-recording
|
? html`<frigate-card-thumbnail-feature-recording
|
||||||
aria-label="${label ?? ''}"
|
aria-label="${label ?? ''}"
|
||||||
title="${label ?? ''}"
|
title="${label ?? ''}"
|
||||||
.cameraTitle=${this.details || !this.cameraConfig || !this.hass ? undefined : getCameraTitle(this.hass, this.cameraConfig)}
|
.cameraTitle=${this.details || !this.cameraConfig || !this.hass
|
||||||
|
? undefined
|
||||||
|
: getCameraTitle(this.hass, this.cameraConfig)}
|
||||||
.date=${recording ? fromUnixTime(recording.start_time) : undefined}
|
.date=${recording ? fromUnixTime(recording.start_time) : undefined}
|
||||||
></frigate-card-thumbnail-feature-recording>`
|
></frigate-card-thumbnail-feature-recording>`
|
||||||
: html``}
|
: html``}
|
||||||
@@ -308,12 +311,7 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
@click=${(ev: Event) => {
|
@click=${(ev: Event) => {
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
if (event && this.hass && clientID) {
|
if (event && this.hass && clientID) {
|
||||||
retainEvent(
|
retainEvent(this.hass, clientID, event.id, !event.retain_indefinitely)
|
||||||
this.hass,
|
|
||||||
clientID,
|
|
||||||
event.id,
|
|
||||||
!event.retain_indefinitely,
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (event) {
|
if (event) {
|
||||||
event.retain_indefinitely = !event.retain_indefinitely;
|
event.retain_indefinitely = !event.retain_indefinitely;
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import {
|
import add from 'date-fns/add';
|
||||||
add,
|
import endOfHour from 'date-fns/endOfHour';
|
||||||
differenceInSeconds,
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
endOfHour,
|
import differenceInSeconds from 'date-fns/differenceInSeconds';
|
||||||
fromUnixTime,
|
import startOfHour from 'date-fns/startOfHour';
|
||||||
startOfHour,
|
import sub from 'date-fns/sub';
|
||||||
sub,
|
|
||||||
} from 'date-fns';
|
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
@@ -16,12 +14,12 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { isEqual, throttle } from 'lodash-es';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import throttle from 'lodash-es/throttle';
|
||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import { DataView, DataSet } from 'vis-data/esnext';
|
import { DataView, DataSet } from 'vis-data/esnext';
|
||||||
|
import type { DataGroupCollectionType, IdType } from 'vis-timeline/esnext';
|
||||||
import {
|
import {
|
||||||
DataGroupCollectionType,
|
|
||||||
IdType,
|
|
||||||
Timeline,
|
Timeline,
|
||||||
TimelineEventPropertiesResult,
|
TimelineEventPropertiesResult,
|
||||||
TimelineItem,
|
TimelineItem,
|
||||||
@@ -458,7 +456,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this.dataManager,
|
this.dataManager,
|
||||||
this.cameras,
|
this.cameras,
|
||||||
this.view, {
|
this.view,
|
||||||
|
{
|
||||||
cameraIDs: new Set([String(properties.group)]),
|
cameraIDs: new Set([String(properties.group)]),
|
||||||
targetTime:
|
targetTime:
|
||||||
properties.what === 'background' ? properties.time : window.end,
|
properties.what === 'background' ? properties.time : window.end,
|
||||||
@@ -471,7 +470,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this.dataManager,
|
this.dataManager,
|
||||||
this.cameras,
|
this.cameras,
|
||||||
this.view, {
|
this.view,
|
||||||
|
{
|
||||||
targetTime: window.end,
|
targetTime: window.end,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -483,12 +483,13 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this.dataManager,
|
this.dataManager,
|
||||||
this.cameras,
|
this.cameras,
|
||||||
this.view, {
|
this.view,
|
||||||
|
{
|
||||||
cameraIDs: this._getAllCameraIDs(),
|
cameraIDs: this._getAllCameraIDs(),
|
||||||
start: startOfHour(properties.time),
|
start: startOfHour(properties.time),
|
||||||
end: endOfHour(properties.time),
|
end: endOfHour(properties.time),
|
||||||
targetTime: properties.time,
|
targetTime: properties.time,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
BrowseMediaNeighbors,
|
BrowseMediaNeighbors,
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
@@ -97,6 +98,9 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public dataManager?: DataManager;
|
public dataManager?: DataManager;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -151,7 +155,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return renderProgressIndicator();
|
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` <frigate-card-surround
|
return html` <frigate-card-surround
|
||||||
@@ -169,6 +173,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
.viewerConfig=${this.viewerConfig}
|
.viewerConfig=${this.viewerConfig}
|
||||||
.browseMediaQueryParameters=${browseMediaQueryParameters}
|
.browseMediaQueryParameters=${browseMediaQueryParameters}
|
||||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-viewer-carousel>
|
</frigate-card-viewer-carousel>
|
||||||
</frigate-card-surround>`;
|
</frigate-card-surround>`;
|
||||||
@@ -206,6 +211,9 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public resolvedMediaCache?: ResolvedMediaCache;
|
public resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
||||||
|
|
||||||
// Mapping of slide # to FrigateBrowseMediaSource child #.
|
// Mapping of slide # to FrigateBrowseMediaSource child #.
|
||||||
@@ -645,7 +653,9 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
// If lazy loading is not enabled, wait for the media resolver task to
|
// If lazy loading is not enabled, wait for the media resolver task to
|
||||||
// complete and show a progress indictator until this.
|
// complete and show a progress indictator until this.
|
||||||
if (!this.viewerConfig?.lazy_load && !this._isMediaFullyResolved()) {
|
if (!this.viewerConfig?.lazy_load && !this._isMediaFullyResolved()) {
|
||||||
return renderTask(this, this._mediaResolutionTask, this._render.bind(this));
|
return renderTask(this, this._mediaResolutionTask, this._render.bind(this), {
|
||||||
|
cardWideConfig: this.cardWideConfig,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this._render();
|
return this._render();
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-5
@@ -1,4 +1,6 @@
|
|||||||
import { cloneDeep, get, isEqual, set } from 'lodash-es';
|
import get from 'lodash-es/get';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import set from 'lodash-es/set';
|
||||||
import {
|
import {
|
||||||
CONF_CAMERAS,
|
CONF_CAMERAS,
|
||||||
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
||||||
@@ -40,7 +42,7 @@ import {
|
|||||||
/**
|
/**
|
||||||
* Set a configuration value.
|
* Set a configuration value.
|
||||||
* @param obj The configuration.
|
* @param obj The configuration.
|
||||||
* @param key The key to the property to set.
|
* @param keys The key to the property to set.
|
||||||
* @param value The value to set.
|
* @param value The value to set.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -55,7 +57,8 @@ export const setConfigValue = (
|
|||||||
/**
|
/**
|
||||||
* Get a configuration value.
|
* Get a configuration value.
|
||||||
* @param obj The configuration.
|
* @param obj The configuration.
|
||||||
* @param key The key to the property to retrieve.
|
* @param keys The key to the property to retrieve.
|
||||||
|
* @param def Default if key not found.
|
||||||
* @returns The property or undefined if not found.
|
* @returns The property or undefined if not found.
|
||||||
*/
|
*/
|
||||||
export const getConfigValue = (
|
export const getConfigValue = (
|
||||||
@@ -104,7 +107,7 @@ export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
|
|||||||
* @returns `true` if the configuration is upgradeable.
|
* @returns `true` if the configuration is upgradeable.
|
||||||
*/
|
*/
|
||||||
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
|
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
|
||||||
const newObj = JSON.parse(JSON.stringify(obj));
|
const newObj = structuredClone(obj);
|
||||||
return upgradeConfig(newObj);
|
return upgradeConfig(newObj);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,7 +139,7 @@ export const trimConfig = function (obj: RawFrigateCardConfig): boolean {
|
|||||||
* @returns A new deeply-copied configuration.
|
* @returns A new deeply-copied configuration.
|
||||||
*/
|
*/
|
||||||
export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardConfig {
|
export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardConfig {
|
||||||
return cloneDeep(obj);
|
return structuredClone(obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ export const CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS =
|
|||||||
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
|
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
|
||||||
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
|
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
|
||||||
`${CONF_LIVE}.controls.title.duration_seconds` as const;
|
`${CONF_LIVE}.controls.title.duration_seconds` as const;
|
||||||
|
export const CONF_LIVE_IMAGE_REFRESH_SECONDS =
|
||||||
|
`${CONF_LIVE}.image.refresh_seconds` as const;
|
||||||
export const CONF_LIVE_LAYOUT_FIT = `${CONF_LIVE}.layout.fit` as const;
|
export const CONF_LIVE_LAYOUT_FIT = `${CONF_LIVE}.layout.fit` as const;
|
||||||
export const CONF_LIVE_LAYOUT_POSITION_X = `${CONF_LIVE}.layout.position.x` as const;
|
export const CONF_LIVE_LAYOUT_POSITION_X = `${CONF_LIVE}.layout.position.x` as const;
|
||||||
export const CONF_LIVE_LAYOUT_POSITION_Y = `${CONF_LIVE}.layout.position.y` as const;
|
export const CONF_LIVE_LAYOUT_POSITION_Y = `${CONF_LIVE}.layout.position.y` as const;
|
||||||
@@ -192,7 +194,10 @@ export const CONF_MENU_BUTTONS_FRIGATE_UI = `${CONF_MENU}.buttons.frigate_ui` as
|
|||||||
export const CONF_MENU_BUTTONS_FULLSCREEN = `${CONF_MENU}.buttons.fullscreen` as const;
|
export const CONF_MENU_BUTTONS_FULLSCREEN = `${CONF_MENU}.buttons.fullscreen` as const;
|
||||||
export const CONF_MENU_BUTTONS_IMAGE = `${CONF_MENU}.buttons.image` as const;
|
export const CONF_MENU_BUTTONS_IMAGE = `${CONF_MENU}.buttons.image` as const;
|
||||||
export const CONF_MENU_BUTTONS_LIVE = `${CONF_MENU}.buttons.live` as const;
|
export const CONF_MENU_BUTTONS_LIVE = `${CONF_MENU}.buttons.live` as const;
|
||||||
|
export const CONF_MENU_BUTTONS_MEDIA_PLAYER =
|
||||||
|
`${CONF_MENU}.buttons.media_player` as const;
|
||||||
export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const;
|
export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const;
|
||||||
|
export const CONF_MENU_BUTTONS_TIMELINE = `${CONF_MENU}.buttons.timeline` as const;
|
||||||
|
|
||||||
export const CONF_DIMENSIONS = 'dimensions' as const;
|
export const CONF_DIMENSIONS = 'dimensions' as const;
|
||||||
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
|
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
|
||||||
@@ -201,5 +206,11 @@ export const CONF_DIMENSIONS_ASPECT_RATIO_MODE =
|
|||||||
|
|
||||||
export const CONF_OVERRIDES = 'overrides' as const;
|
export const CONF_OVERRIDES = 'overrides' as const;
|
||||||
|
|
||||||
|
export const CONF_PERFORMANCE = 'performance' as const;
|
||||||
|
export const CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR = `${CONF_PERFORMANCE}.features.animated_progress_indicator`;
|
||||||
|
export const CONF_PERFORMANCE_PROFILE = `${CONF_PERFORMANCE}.profile`;
|
||||||
|
export const CONF_PERFORMANCE_STYLE_BOX_SHADOW = `${CONF_PERFORMANCE}.style.box_shadow`;
|
||||||
|
export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`;
|
||||||
|
|
||||||
// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93
|
// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93
|
||||||
export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
|
export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
|
||||||
|
|||||||
+152
-28
@@ -62,6 +62,7 @@ import {
|
|||||||
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
|
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
|
||||||
CONF_LIVE_CONTROLS_TITLE_MODE,
|
CONF_LIVE_CONTROLS_TITLE_MODE,
|
||||||
CONF_LIVE_DRAGGABLE,
|
CONF_LIVE_DRAGGABLE,
|
||||||
|
CONF_LIVE_IMAGE_REFRESH_SECONDS,
|
||||||
CONF_LIVE_LAYOUT_FIT,
|
CONF_LIVE_LAYOUT_FIT,
|
||||||
CONF_LIVE_LAYOUT_POSITION_X,
|
CONF_LIVE_LAYOUT_POSITION_X,
|
||||||
CONF_LIVE_LAYOUT_POSITION_Y,
|
CONF_LIVE_LAYOUT_POSITION_Y,
|
||||||
@@ -99,6 +100,10 @@ import {
|
|||||||
CONF_MENU_BUTTON_SIZE,
|
CONF_MENU_BUTTON_SIZE,
|
||||||
CONF_MENU_POSITION,
|
CONF_MENU_POSITION,
|
||||||
CONF_MENU_STYLE,
|
CONF_MENU_STYLE,
|
||||||
|
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||||
|
CONF_PERFORMANCE_PROFILE,
|
||||||
|
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
|
||||||
|
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
|
||||||
CONF_TIMELINE_CLUSTERING_THRESHOLD,
|
CONF_TIMELINE_CLUSTERING_THRESHOLD,
|
||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
@@ -125,6 +130,7 @@ import { localize } from './localize/localize.js';
|
|||||||
import frigate_card_editor_style from './scss/editor.scss';
|
import frigate_card_editor_style from './scss/editor.scss';
|
||||||
import {
|
import {
|
||||||
BUTTON_SIZE_MIN,
|
BUTTON_SIZE_MIN,
|
||||||
|
FrigateCardConfig,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
FRIGATE_MENU_PRIORITY_MAX,
|
FRIGATE_MENU_PRIORITY_MAX,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
@@ -136,6 +142,7 @@ import { arrayMove } from './utils/basic.js';
|
|||||||
import { getCameraID, getCameraTitle } from './utils/camera.js';
|
import { getCameraID, getCameraTitle } from './utils/camera.js';
|
||||||
import { FRIGATE_ICON_SVG_PATH } from './utils/frigate.js';
|
import { FRIGATE_ICON_SVG_PATH } from './utils/frigate.js';
|
||||||
import { getEntitiesFromHASS, sideLoadHomeAssistantElements } from './utils/ha';
|
import { getEntitiesFromHASS, sideLoadHomeAssistantElements } from './utils/ha';
|
||||||
|
import { setLowPerformanceProfile } from './performance.js';
|
||||||
|
|
||||||
const MENU_BUTTONS = 'buttons';
|
const MENU_BUTTONS = 'buttons';
|
||||||
const MENU_CAMERAS = 'cameras';
|
const MENU_CAMERAS = 'cameras';
|
||||||
@@ -150,6 +157,7 @@ const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
|
|||||||
const MENU_LIVE_CONTROLS_THUMBNAILS = 'live.controls.thumbnails';
|
const MENU_LIVE_CONTROLS_THUMBNAILS = 'live.controls.thumbnails';
|
||||||
const MENU_LIVE_CONTROLS_TIMELINE = 'live.controls.timeline';
|
const MENU_LIVE_CONTROLS_TIMELINE = 'live.controls.timeline';
|
||||||
const MENU_LIVE_CONTROLS_TITLE = 'live.controls.title';
|
const MENU_LIVE_CONTROLS_TITLE = 'live.controls.title';
|
||||||
|
const MENU_LIVE_IMAGE = 'live.image';
|
||||||
const MENU_LIVE_LAYOUT = 'live.layout';
|
const MENU_LIVE_LAYOUT = 'live.layout';
|
||||||
const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
|
const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
|
||||||
const MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS = 'media_viewer.controls.next_previous';
|
const MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS = 'media_viewer.controls.next_previous';
|
||||||
@@ -159,6 +167,8 @@ const MENU_MEDIA_VIEWER_CONTROLS_TITLE = 'media_viewer.controls.title';
|
|||||||
const MENU_MEDIA_VIEWER_LAYOUT = 'media_viewer.layout';
|
const MENU_MEDIA_VIEWER_LAYOUT = 'media_viewer.layout';
|
||||||
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
|
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
|
||||||
const MENU_OPTIONS = 'options';
|
const MENU_OPTIONS = 'options';
|
||||||
|
const MENU_PERFORMANCE_FEATURES = 'performance.features';
|
||||||
|
const MENU_PERFORMANCE_STYLE = 'performance.style';
|
||||||
const MENU_VIEW_SCAN = 'scan';
|
const MENU_VIEW_SCAN = 'scan';
|
||||||
|
|
||||||
interface EditorOptionsSet {
|
interface EditorOptionsSet {
|
||||||
@@ -226,6 +236,11 @@ const options: EditorOptions = {
|
|||||||
name: localize('editor.dimensions'),
|
name: localize('editor.dimensions'),
|
||||||
secondary: localize('editor.dimensions_secondary'),
|
secondary: localize('editor.dimensions_secondary'),
|
||||||
},
|
},
|
||||||
|
performance: {
|
||||||
|
icon: 'speedometer',
|
||||||
|
name: localize('editor.performance'),
|
||||||
|
secondary: localize('editor.performance_secondary'),
|
||||||
|
},
|
||||||
overrides: {
|
overrides: {
|
||||||
icon: 'file-replace',
|
icon: 'file-replace',
|
||||||
name: localize('editor.overrides'),
|
name: localize('editor.overrides'),
|
||||||
@@ -237,6 +252,8 @@ const options: EditorOptions = {
|
|||||||
export class FrigateCardEditor extends LitElement implements LovelaceCardEditor {
|
export class FrigateCardEditor extends LitElement implements LovelaceCardEditor {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
@state() protected _config?: RawFrigateCardConfig;
|
@state() protected _config?: RawFrigateCardConfig;
|
||||||
|
@state() protected _defaults = structuredClone(frigateCardConfigDefaults);
|
||||||
|
|
||||||
protected _initialized = false;
|
protected _initialized = false;
|
||||||
protected _configUpgradeable = false;
|
protected _configUpgradeable = false;
|
||||||
|
|
||||||
@@ -300,6 +317,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
value: 'none',
|
value: 'none',
|
||||||
label: localize('config.common.controls.next_previous.styles.none'),
|
label: localize('config.common.controls.next_previous.styles.none'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'thumbnails',
|
||||||
|
label: localize('config.common.controls.next_previous.styles.thumbnails'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _aspectRatioModes: EditorSelectOption[] = [
|
protected _aspectRatioModes: EditorSelectOption[] = [
|
||||||
@@ -438,6 +459,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: 'below', label: localize('config.common.controls.timeline.modes.below') },
|
{ value: 'below', label: localize('config.common.controls.timeline.modes.below') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected _performanceProfiles: EditorSelectOption[] = [
|
||||||
|
{ value: '', label: '' },
|
||||||
|
{ value: 'low', label: localize('config.performance.profiles.low') },
|
||||||
|
{ value: 'high', label: localize('config.performance.profiles.high') },
|
||||||
|
];
|
||||||
|
|
||||||
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
|
||||||
@@ -445,6 +472,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
// such, RawFrigateCardConfig is used as the type.
|
// such, RawFrigateCardConfig is used as the type.
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this._configUpgradeable = isConfigUpgradeable(config);
|
this._configUpgradeable = isConfigUpgradeable(config);
|
||||||
|
|
||||||
|
let unvalidatedProfile: string | null = null;
|
||||||
|
try {
|
||||||
|
// this._config may not be a valid FrigateCardConfig as it has not been
|
||||||
|
// parsed. Attempt to pull out the performance profile.
|
||||||
|
unvalidatedProfile = (this._config as FrigateCardConfig).performance?.profile;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (unvalidatedProfile === 'high' || unvalidatedProfile === 'low') {
|
||||||
|
const defaults = structuredClone(frigateCardConfigDefaults);
|
||||||
|
if (unvalidatedProfile === 'low') {
|
||||||
|
setLowPerformanceProfile(this._config, defaults);
|
||||||
|
}
|
||||||
|
this._defaults = defaults;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -465,7 +507,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
* @param optionSetName The name of the EditorOptionsSet.
|
* @param optionSetName The name of the EditorOptionsSet.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected _renderOptionSetHeader(optionSetName: string): TemplateResult {
|
protected _renderOptionSetHeader(
|
||||||
|
optionSetName: string,
|
||||||
|
titleClass?: string,
|
||||||
|
): TemplateResult {
|
||||||
const optionSet = options[optionSetName];
|
const optionSet = options[optionSetName];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -477,7 +522,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
>
|
>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<ha-icon .icon=${`mdi:${optionSet.icon}`}></ha-icon>
|
<ha-icon .icon=${`mdi:${optionSet.icon}`}></ha-icon>
|
||||||
<div class="title">${optionSet.name}</div>
|
<div class="title ${titleClass ?? ''}">${optionSet.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="secondary">${optionSet.secondary}</div>
|
<div class="secondary">${optionSet.secondary}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -669,24 +714,24 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_VIEW_SCAN_ENABLED,
|
CONF_VIEW_SCAN_ENABLED,
|
||||||
frigateCardConfigDefaults.view.scan.enabled,
|
this._defaults.view.scan.enabled,
|
||||||
{
|
{
|
||||||
label: localize(`config.${CONF_VIEW_SCAN_ENABLED}`),
|
label: localize(`config.${CONF_VIEW_SCAN_ENABLED}`),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
|
CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
|
||||||
frigateCardConfigDefaults.view.scan.show_trigger_status,
|
this._defaults.view.scan.show_trigger_status,
|
||||||
{
|
{
|
||||||
label: localize(`config.${CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS}`),
|
label: localize(`config.${CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS}`),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_VIEW_SCAN_UNTRIGGER_RESET,
|
CONF_VIEW_SCAN_UNTRIGGER_RESET,
|
||||||
frigateCardConfigDefaults.view.scan.untrigger_reset,
|
this._defaults.view.scan.untrigger_reset,
|
||||||
)}
|
)}
|
||||||
${this._renderNumberInput(CONF_VIEW_SCAN_UNTRIGGER_SECONDS, {
|
${this._renderNumberInput(CONF_VIEW_SCAN_UNTRIGGER_SECONDS, {
|
||||||
default: frigateCardConfigDefaults.view.scan.untrigger_seconds,
|
default: this._defaults.view.scan.untrigger_seconds,
|
||||||
})}
|
})}
|
||||||
</div>`
|
</div>`
|
||||||
: ''}
|
: ''}
|
||||||
@@ -730,7 +775,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
`${CONF_MENU_BUTTONS}.${button}.enabled`,
|
`${CONF_MENU_BUTTONS}.${button}.enabled`,
|
||||||
frigateCardConfigDefaults.menu.buttons[button]?.enabled ?? true,
|
this._defaults.menu.buttons[button]?.enabled ?? true,
|
||||||
{
|
{
|
||||||
label: localize('config.menu.buttons.enabled'),
|
label: localize('config.menu.buttons.enabled'),
|
||||||
},
|
},
|
||||||
@@ -744,7 +789,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
)}
|
)}
|
||||||
${this._renderNumberInput(`${CONF_MENU_BUTTONS}.${button}.priority`, {
|
${this._renderNumberInput(`${CONF_MENU_BUTTONS}.${button}.priority`, {
|
||||||
max: FRIGATE_MENU_PRIORITY_MAX,
|
max: FRIGATE_MENU_PRIORITY_MAX,
|
||||||
default: frigateCardConfigDefaults.menu.buttons[button]?.priority,
|
default: this._defaults.menu.buttons[button]?.priority,
|
||||||
label: localize('config.menu.buttons.priority'),
|
label: localize('config.menu.buttons.priority'),
|
||||||
})}
|
})}
|
||||||
${this._renderIconSelector(`${CONF_MENU_BUTTONS}.${button}.icon`, {
|
${this._renderIconSelector(`${CONF_MENU_BUTTONS}.${button}.icon`, {
|
||||||
@@ -882,6 +927,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
configPathClusteringThreshold: string,
|
configPathClusteringThreshold: string,
|
||||||
configPathTimelineMedia: string,
|
configPathTimelineMedia: string,
|
||||||
configPathShowRecordings: string,
|
configPathShowRecordings: string,
|
||||||
|
showRecordingsDefault: boolean,
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
return this._putInSubmenu(
|
return this._putInSubmenu(
|
||||||
domain,
|
domain,
|
||||||
@@ -896,7 +942,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
configPathClusteringThreshold,
|
configPathClusteringThreshold,
|
||||||
configPathTimelineMedia,
|
configPathTimelineMedia,
|
||||||
configPathShowRecordings,
|
configPathShowRecordings,
|
||||||
frigateCardConfigDefaults.mini_timeline.show_recordings,
|
showRecordingsDefault,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -912,6 +958,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
domain: string,
|
domain: string,
|
||||||
configPathStyle: string,
|
configPathStyle: string,
|
||||||
configPathSize: string,
|
configPathSize: string,
|
||||||
|
options?: {
|
||||||
|
allowIcons?: boolean;
|
||||||
|
allowThumbnails?: boolean;
|
||||||
|
},
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
return this._putInSubmenu(
|
return this._putInSubmenu(
|
||||||
domain,
|
domain,
|
||||||
@@ -919,9 +969,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
'config.common.controls.next_previous.editor_label',
|
'config.common.controls.next_previous.editor_label',
|
||||||
{ name: 'mdi:arrow-right-bold-circle' },
|
{ name: 'mdi:arrow-right-bold-circle' },
|
||||||
html`
|
html`
|
||||||
${this._renderOptionSelector(configPathStyle, this._nextPreviousControlStyles, {
|
${this._renderOptionSelector(
|
||||||
label: localize('config.common.controls.next_previous.style'),
|
configPathStyle,
|
||||||
})}
|
this._nextPreviousControlStyles.filter(
|
||||||
|
(item) =>
|
||||||
|
(!!options?.allowThumbnails || item.value !== 'thumbnails') &&
|
||||||
|
(!!options?.allowIcons || item.value !== 'icons'),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
label: localize('config.common.controls.next_previous.style'),
|
||||||
|
},
|
||||||
|
)}
|
||||||
${this._renderNumberInput(configPathSize, {
|
${this._renderNumberInput(configPathSize, {
|
||||||
min: BUTTON_SIZE_MIN,
|
min: BUTTON_SIZE_MIN,
|
||||||
label: localize('config.common.controls.next_previous.size'),
|
label: localize('config.common.controls.next_previous.size'),
|
||||||
@@ -983,21 +1041,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
})}
|
})}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
configPathShowDetails,
|
configPathShowDetails,
|
||||||
frigateCardConfigDefaults.live.controls.thumbnails.show_details,
|
this._defaults.live.controls.thumbnails.show_details,
|
||||||
{
|
{
|
||||||
label: localize('config.common.controls.thumbnails.show_details'),
|
label: localize('config.common.controls.thumbnails.show_details'),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
configPathShowFavoriteControl,
|
configPathShowFavoriteControl,
|
||||||
frigateCardConfigDefaults.live.controls.thumbnails.show_favorite_control,
|
this._defaults.live.controls.thumbnails.show_favorite_control,
|
||||||
{
|
{
|
||||||
label: localize('config.common.controls.thumbnails.show_favorite_control'),
|
label: localize('config.common.controls.thumbnails.show_favorite_control'),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
configPathShowTimelineControl,
|
configPathShowTimelineControl,
|
||||||
frigateCardConfigDefaults.live.controls.thumbnails.show_timeline_control,
|
this._defaults.live.controls.thumbnails.show_timeline_control,
|
||||||
{
|
{
|
||||||
label: localize('config.common.controls.thumbnails.show_timeline_control'),
|
label: localize('config.common.controls.thumbnails.show_timeline_control'),
|
||||||
},
|
},
|
||||||
@@ -1051,6 +1109,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
|
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
|
||||||
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
|
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
|
||||||
|
{
|
||||||
|
value: 'image',
|
||||||
|
label: localize('config.cameras.live_providers.image'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'frigate-jsmpeg',
|
value: 'frigate-jsmpeg',
|
||||||
label: localize('config.cameras.live_providers.frigate-jsmpeg'),
|
label: localize('config.cameras.live_providers.frigate-jsmpeg'),
|
||||||
@@ -1231,7 +1293,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
|
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
|
||||||
cameraIndex,
|
cameraIndex,
|
||||||
),
|
),
|
||||||
frigateCardConfigDefaults.cameras.dependencies.all_cameras,
|
this._defaults.cameras.dependencies.all_cameras,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
getArrayConfigPath(
|
getArrayConfigPath(
|
||||||
@@ -1251,11 +1313,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ name: 'mdi:magnify-scan' },
|
{ name: 'mdi:magnify-scan' },
|
||||||
html` ${this._renderSwitch(
|
html` ${this._renderSwitch(
|
||||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, cameraIndex),
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, cameraIndex),
|
||||||
frigateCardConfigDefaults.cameras.triggers.occupancy,
|
this._defaults.cameras.triggers.occupancy,
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_MOTION, cameraIndex),
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_MOTION, cameraIndex),
|
||||||
frigateCardConfigDefaults.cameras.triggers.motion,
|
this._defaults.cameras.triggers.motion,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES, cameraIndex),
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES, cameraIndex),
|
||||||
@@ -1365,7 +1427,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults = frigateCardConfigDefaults;
|
|
||||||
const entities = getEntitiesFromHASS(this.hass);
|
const entities = getEntitiesFromHASS(this.hass);
|
||||||
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
|
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
|
||||||
[]) as RawFrigateCardConfigArray;
|
[]) as RawFrigateCardConfigArray;
|
||||||
@@ -1415,10 +1476,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)}
|
${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)}
|
||||||
${this._renderNumberInput(CONF_VIEW_TIMEOUT_SECONDS)}
|
${this._renderNumberInput(CONF_VIEW_TIMEOUT_SECONDS)}
|
||||||
${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)}
|
${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)}
|
||||||
${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
|
${this._renderSwitch(
|
||||||
|
CONF_VIEW_UPDATE_FORCE,
|
||||||
|
this._defaults.view.update_force,
|
||||||
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_VIEW_UPDATE_CYCLE_CAMERA,
|
CONF_VIEW_UPDATE_CYCLE_CAMERA,
|
||||||
defaults.view.update_cycle_camera,
|
this._defaults.view.update_cycle_camera,
|
||||||
)}
|
)}
|
||||||
${this._renderViewScanMenu()}
|
${this._renderViewScanMenu()}
|
||||||
</div>
|
</div>
|
||||||
@@ -1453,9 +1517,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._expandedMenus[MENU_OPTIONS] === 'live'
|
${this._expandedMenus[MENU_OPTIONS] === 'live'
|
||||||
? html`
|
? html`
|
||||||
<div class="values">
|
<div class="values">
|
||||||
${this._renderSwitch(CONF_LIVE_PRELOAD, defaults.live.preload)}
|
${this._renderSwitch(CONF_LIVE_PRELOAD, this._defaults.live.preload)}
|
||||||
${this._renderSwitch(CONF_LIVE_DRAGGABLE, defaults.live.draggable)}
|
${this._renderSwitch(CONF_LIVE_DRAGGABLE, this._defaults.live.draggable)}
|
||||||
${this._renderSwitch(CONF_LIVE_LAZY_LOAD, defaults.live.lazy_load)}
|
${this._renderSwitch(CONF_LIVE_LAZY_LOAD, this._defaults.live.lazy_load)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_LIVE_LAZY_UNLOAD,
|
CONF_LIVE_LAZY_UNLOAD,
|
||||||
this._mediaActionNegativeConditions,
|
this._mediaActionNegativeConditions,
|
||||||
@@ -1482,7 +1546,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
|
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
|
||||||
defaults.live.show_image_during_load,
|
this._defaults.live.show_image_during_load,
|
||||||
)}
|
)}
|
||||||
${this._putInSubmenu(
|
${this._putInSubmenu(
|
||||||
MENU_LIVE_CONTROLS,
|
MENU_LIVE_CONTROLS,
|
||||||
@@ -1494,6 +1558,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
MENU_LIVE_CONTROLS_NEXT_PREVIOUS,
|
MENU_LIVE_CONTROLS_NEXT_PREVIOUS,
|
||||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
{
|
||||||
|
allowIcons: true,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderThumbnailsControls(
|
${this._renderThumbnailsControls(
|
||||||
MENU_LIVE_CONTROLS_THUMBNAILS,
|
MENU_LIVE_CONTROLS_THUMBNAILS,
|
||||||
@@ -1518,6 +1585,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
|
CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
|
||||||
CONF_LIVE_CONTROLS_TIMELINE_MEDIA,
|
CONF_LIVE_CONTROLS_TIMELINE_MEDIA,
|
||||||
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||||
|
this._defaults.live.controls.timeline.show_recordings,
|
||||||
)}
|
)}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
@@ -1528,6 +1596,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_LIVE_LAYOUT_POSITION_X,
|
CONF_LIVE_LAYOUT_POSITION_X,
|
||||||
CONF_LIVE_LAYOUT_POSITION_Y,
|
CONF_LIVE_LAYOUT_POSITION_Y,
|
||||||
)}
|
)}
|
||||||
|
${this._putInSubmenu(
|
||||||
|
MENU_LIVE_IMAGE,
|
||||||
|
true,
|
||||||
|
'config.live.image.editor_label',
|
||||||
|
{ name: 'mdi:image-sync-outline' },
|
||||||
|
html` ${this._renderNumberInput(CONF_LIVE_IMAGE_REFRESH_SECONDS)} `,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
@@ -1564,11 +1639,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_MEDIA_VIEWER_DRAGGABLE,
|
CONF_MEDIA_VIEWER_DRAGGABLE,
|
||||||
defaults.media_viewer.draggable,
|
this._defaults.media_viewer.draggable,
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_MEDIA_VIEWER_LAZY_LOAD,
|
CONF_MEDIA_VIEWER_LAZY_LOAD,
|
||||||
defaults.media_viewer.lazy_load,
|
this._defaults.media_viewer.lazy_load,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||||
@@ -1584,6 +1659,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS,
|
MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS,
|
||||||
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
{
|
||||||
|
allowThumbnails: true,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderThumbnailsControls(
|
${this._renderThumbnailsControls(
|
||||||
MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS,
|
MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS,
|
||||||
@@ -1607,6 +1685,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
|
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
|
||||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MEDIA,
|
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MEDIA,
|
||||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||||
|
this._defaults.media_viewer.controls.timeline.show_recordings,
|
||||||
)}
|
)}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
@@ -1642,7 +1721,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_TIMELINE_CLUSTERING_THRESHOLD,
|
CONF_TIMELINE_CLUSTERING_THRESHOLD,
|
||||||
CONF_TIMELINE_MEDIA,
|
CONF_TIMELINE_MEDIA,
|
||||||
CONF_TIMELINE_SHOW_RECORDINGS,
|
CONF_TIMELINE_SHOW_RECORDINGS,
|
||||||
defaults.timeline.show_recordings,
|
this._defaults.timeline.show_recordings,
|
||||||
)}
|
)}
|
||||||
${this._renderThumbnailsControls(
|
${this._renderThumbnailsControls(
|
||||||
MENU_TIMELINE_CONTROLS_THUMBNAILS,
|
MENU_TIMELINE_CONTROLS_THUMBNAILS,
|
||||||
@@ -1666,6 +1745,51 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._renderStringInput(CONF_DIMENSIONS_ASPECT_RATIO)}
|
${this._renderStringInput(CONF_DIMENSIONS_ASPECT_RATIO)}
|
||||||
</div>`
|
</div>`
|
||||||
: ''}
|
: ''}
|
||||||
|
${this._renderOptionSetHeader(
|
||||||
|
'performance',
|
||||||
|
getConfigValue(this._config, CONF_PERFORMANCE_PROFILE) === 'low'
|
||||||
|
? 'warning'
|
||||||
|
: undefined,
|
||||||
|
)}
|
||||||
|
${this._expandedMenus[MENU_OPTIONS] === 'performance'
|
||||||
|
? html` <div class="values">
|
||||||
|
${getConfigValue(this._config, CONF_PERFORMANCE_PROFILE) === 'low'
|
||||||
|
? this._renderInfo(localize('config.performance.warning'))
|
||||||
|
: html``}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
CONF_PERFORMANCE_PROFILE,
|
||||||
|
this._performanceProfiles,
|
||||||
|
)}
|
||||||
|
${this._putInSubmenu(
|
||||||
|
MENU_PERFORMANCE_FEATURES,
|
||||||
|
true,
|
||||||
|
'config.performance.features.editor_label',
|
||||||
|
{ name: 'mdi:feature-search' },
|
||||||
|
html`
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||||
|
this._defaults.performance.features.animated_progress_indicator,
|
||||||
|
)}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
${this._putInSubmenu(
|
||||||
|
MENU_PERFORMANCE_STYLE,
|
||||||
|
true,
|
||||||
|
'config.performance.style.editor_label',
|
||||||
|
{ name: 'mdi:palette-swatch-variant' },
|
||||||
|
html`
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
|
||||||
|
this._defaults.performance.style.border_radius,
|
||||||
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
|
||||||
|
this._defaults.performance.style.box_shadow,
|
||||||
|
)}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
${this._config['overrides'] !== undefined
|
${this._config['overrides'] !== undefined
|
||||||
? html` ${this._renderOptionSetHeader('overrides')}
|
? html` ${this._renderOptionSetHeader('overrides')}
|
||||||
${this._expandedMenus[MENU_OPTIONS] === 'overrides'
|
${this._expandedMenus[MENU_OPTIONS] === 'overrides'
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"live_providers": {
|
"live_providers": {
|
||||||
"auto": "Automatic",
|
"auto": "Automatic",
|
||||||
"frigate-jsmpeg": "Frigate JSMpeg",
|
"frigate-jsmpeg": "Frigate JSMpeg",
|
||||||
"ha": "Home Assistant (i.e. HLS, LL-HLS, WebRTC native)",
|
"ha": "Home Assistant video stream (i.e. HLS, LL-HLS, WebRTC native)",
|
||||||
|
"image": "Home Assistant images",
|
||||||
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
|
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
|
||||||
},
|
},
|
||||||
"title": "Title for this camera (Autodetected from entity)",
|
"title": "Title for this camera (Autodetected from entity)",
|
||||||
@@ -89,7 +90,8 @@
|
|||||||
"styles": {
|
"styles": {
|
||||||
"chevrons": "Chevrons",
|
"chevrons": "Chevrons",
|
||||||
"icons": "Icons",
|
"icons": "Icons",
|
||||||
"none": "None"
|
"none": "None",
|
||||||
|
"thumbnails": "Thumbnails"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
@@ -164,6 +166,10 @@
|
|||||||
"editor_label": "Live Controls"
|
"editor_label": "Live Controls"
|
||||||
},
|
},
|
||||||
"draggable": "Live cameras view can be dragged/swiped",
|
"draggable": "Live cameras view can be dragged/swiped",
|
||||||
|
"image": {
|
||||||
|
"editor_label": "Image Live Provider Options",
|
||||||
|
"refresh_seconds": "Number of seconds after which to refresh live image (0=never)"
|
||||||
|
},
|
||||||
"layout": "Live Layout",
|
"layout": "Live Layout",
|
||||||
"lazy_load": "Live cameras are lazily loaded",
|
"lazy_load": "Live cameras are lazily loaded",
|
||||||
"lazy_unload": "Live cameras are lazily unloaded",
|
"lazy_unload": "Live cameras are lazily unloaded",
|
||||||
@@ -238,6 +244,23 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"info": "This card configuration has manually specified overrides configured which may override values shown in the visual editor, please consult the code editor to view/modify these overrides"
|
"info": "This card configuration has manually specified overrides configured which may override values shown in the visual editor, please consult the code editor to view/modify these overrides"
|
||||||
},
|
},
|
||||||
|
"performance": {
|
||||||
|
"warning": "This card is in low profile mode so defaults have changed to optimize performance",
|
||||||
|
"features": {
|
||||||
|
"editor_label": "Feature Options",
|
||||||
|
"animated_progress_indicator": "Animated Progress Indicator"
|
||||||
|
},
|
||||||
|
"profile": "Performance profile",
|
||||||
|
"profiles": {
|
||||||
|
"low": "Low performance",
|
||||||
|
"high": "High/full performance"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"editor_label": "Style Options",
|
||||||
|
"box_shadow": "Shadows",
|
||||||
|
"border_radius": "Curves"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"camera_select": "View for newly selected cameras",
|
"camera_select": "View for newly selected cameras",
|
||||||
"dark_mode": "Dark mode",
|
"dark_mode": "Dark mode",
|
||||||
@@ -295,6 +318,8 @@
|
|||||||
"move_up": "Move up",
|
"move_up": "Move up",
|
||||||
"overrides": "Overrides are active",
|
"overrides": "Overrides are active",
|
||||||
"overrides_secondary": "Dynamic configuration overrides detected",
|
"overrides_secondary": "Dynamic configuration overrides detected",
|
||||||
|
"performance": "Performance",
|
||||||
|
"performance_secondary": "Card performance options",
|
||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"timeline_secondary": "Event timeline options",
|
"timeline_secondary": "Event timeline options",
|
||||||
"upgrade": "Upgrade",
|
"upgrade": "Upgrade",
|
||||||
|
|||||||
@@ -88,7 +88,8 @@
|
|||||||
"styles": {
|
"styles": {
|
||||||
"chevrons": "Chevrons",
|
"chevrons": "Chevrons",
|
||||||
"icons": "Icone",
|
"icons": "Icone",
|
||||||
"none": "Icone"
|
"none": "Nessuno",
|
||||||
|
"thumbnails": "Miniature"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
|
|||||||
@@ -88,7 +88,8 @@
|
|||||||
"styles": {
|
"styles": {
|
||||||
"chevrons": "Setas",
|
"chevrons": "Setas",
|
||||||
"icons": "Ícones",
|
"icons": "Ícones",
|
||||||
"none": "Nenhum"
|
"none": "Nenhum",
|
||||||
|
"thumbnails": "Miniaturas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import * as en from './languages/en.json';
|
import * as en from './languages/en.json';
|
||||||
import * as pt_BR from './languages/pt-BR.json';
|
|
||||||
import * as it from './languages/it.json';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const languages: Record<string, any> = {
|
const languages: Record<string, any> = {
|
||||||
|
// English as always loaded as it's the fallback language that will be used
|
||||||
|
// when translations are not found or before they are loaded (via
|
||||||
|
// loadLanguages()).
|
||||||
en: en,
|
en: en,
|
||||||
pt_BR: pt_BR,
|
}
|
||||||
it: it,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configured language.
|
||||||
|
*/
|
||||||
export function getLanguage(): string {
|
export function getLanguage(): string {
|
||||||
const canonicalizeLanguage = (language?: string | null): string | null => {
|
const canonicalizeLanguage = (language?: string | null): string | null => {
|
||||||
if (!language) {
|
if (!language) {
|
||||||
@@ -39,14 +41,32 @@ export function getLanguage(): string {
|
|||||||
return lang || 'en';
|
return lang || 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load required languages.
|
||||||
|
*/
|
||||||
|
export const loadLanguages = async (): Promise<void> => {
|
||||||
|
const lang = getLanguage();
|
||||||
|
if (lang === 'it') {
|
||||||
|
languages['it'] = await import('./languages/it.json');
|
||||||
|
} else if (lang === 'pt_BR') {
|
||||||
|
languages['pt_BR'] = await import('./languages/pt-BR.json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a localized version of a given string key.
|
||||||
|
* @param string The key.
|
||||||
|
* @param search An optional search key to be used with 'replace'.
|
||||||
|
* @param replace An optional replacement text to be used with 'search'.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function localize(string: string, search = '', replace = ''): string {
|
export function localize(string: string, search = '', replace = ''): string {
|
||||||
const lang = getLanguage();
|
const lang = getLanguage();
|
||||||
let translated: string;
|
let translated = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
translated = string.split('.').reduce((o, i) => o[i], languages[lang]);
|
translated = string.split('.').reduce((o, i) => o[i], languages[lang]);
|
||||||
} catch (e) {
|
} catch (_) {
|
||||||
translated = string.split('.').reduce((o, i) => o[i], languages['en']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!translated) {
|
if (!translated) {
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { deepRemoveDefaults } from './utils/zod.js';
|
||||||
|
import {
|
||||||
|
frigateCardConfigSchema,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
PerformanceConfig,
|
||||||
|
} from './types';
|
||||||
|
import { getArrayConfigPath, getConfigValue, setConfigValue } from './config-mgmt.js';
|
||||||
|
import {
|
||||||
|
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||||
|
CONF_LIVE_AUTO_MUTE,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||||
|
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||||
|
CONF_LIVE_CONTROLS_TITLE_MODE,
|
||||||
|
CONF_LIVE_DRAGGABLE,
|
||||||
|
CONF_LIVE_IMAGE_REFRESH_SECONDS,
|
||||||
|
CONF_LIVE_LAZY_UNLOAD,
|
||||||
|
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
|
||||||
|
CONF_LIVE_TRANSITION_EFFECT,
|
||||||
|
CONF_MEDIA_VIEWER_AUTO_MUTE,
|
||||||
|
CONF_MEDIA_VIEWER_AUTO_PAUSE,
|
||||||
|
CONF_MEDIA_VIEWER_AUTO_PLAY,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
|
||||||
|
CONF_MEDIA_VIEWER_DRAGGABLE,
|
||||||
|
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||||
|
CONF_MENU_BUTTONS_FRIGATE,
|
||||||
|
CONF_MENU_BUTTONS_MEDIA_PLAYER,
|
||||||
|
CONF_MENU_BUTTONS_TIMELINE,
|
||||||
|
CONF_MENU_STYLE,
|
||||||
|
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||||
|
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
|
||||||
|
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
|
||||||
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||||
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||||
|
CONF_TIMELINE_SHOW_RECORDINGS,
|
||||||
|
} from './const.js';
|
||||||
|
|
||||||
|
// Caution: These values are applied after parsing (since we cannot know the
|
||||||
|
// performance profile until afterwards), so there is no validation on these
|
||||||
|
// defaults.
|
||||||
|
const LOW_PROFILE_DEFAULTS = {
|
||||||
|
// Disable thumbnail carousels.
|
||||||
|
[CONF_LIVE_CONTROLS_THUMBNAILS_MODE]: 'none' as const,
|
||||||
|
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE]: 'none' as const,
|
||||||
|
[CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE]: 'none' as const,
|
||||||
|
|
||||||
|
// Do not show recordings on timelines.
|
||||||
|
[CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS]: false,
|
||||||
|
[CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS]: false,
|
||||||
|
[CONF_TIMELINE_SHOW_RECORDINGS]: false,
|
||||||
|
|
||||||
|
// Take no automatic media actions.
|
||||||
|
[CONF_LIVE_AUTO_MUTE]: 'never' as const,
|
||||||
|
[CONF_MEDIA_VIEWER_AUTO_PLAY]: 'never' as const,
|
||||||
|
[CONF_MEDIA_VIEWER_AUTO_PAUSE]: 'never' as const,
|
||||||
|
[CONF_MEDIA_VIEWER_AUTO_MUTE]: 'never' as const,
|
||||||
|
|
||||||
|
// Always unload resources that are lazily loaded.
|
||||||
|
[CONF_LIVE_LAZY_UNLOAD]: 'all' as const,
|
||||||
|
|
||||||
|
// Media carousels do not drag.
|
||||||
|
[CONF_LIVE_DRAGGABLE]: false,
|
||||||
|
[CONF_MEDIA_VIEWER_DRAGGABLE]: false,
|
||||||
|
|
||||||
|
// Media carousels have no effects.
|
||||||
|
[CONF_LIVE_TRANSITION_EFFECT]: 'none' as const,
|
||||||
|
[CONF_MEDIA_VIEWER_TRANSITION_EFFECT]: 'none' as const,
|
||||||
|
|
||||||
|
// Do not show image during load.
|
||||||
|
[CONF_LIVE_SHOW_IMAGE_DURING_LOAD]: false,
|
||||||
|
|
||||||
|
// Media player next/previous are chevrons.
|
||||||
|
[CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE]: 'chevrons' as const,
|
||||||
|
|
||||||
|
[CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE]: 'none' as const,
|
||||||
|
[CONF_LIVE_CONTROLS_TITLE_MODE]: 'none' as const,
|
||||||
|
|
||||||
|
// Move the menu to outside to remove the need to interact with it with open.
|
||||||
|
[CONF_MENU_STYLE]: 'outside',
|
||||||
|
|
||||||
|
// Hide several buttons that are otherwise visible by default.
|
||||||
|
[`${CONF_MENU_BUTTONS_FRIGATE}.enabled`]: false,
|
||||||
|
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
|
||||||
|
[`${CONF_MENU_BUTTONS_MEDIA_PLAYER}.enabled`]: false,
|
||||||
|
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
|
||||||
|
|
||||||
|
// Disable all options in thumbnails.
|
||||||
|
[CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||||
|
[CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||||
|
[CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||||
|
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||||
|
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||||
|
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||||
|
|
||||||
|
// Refresh the live camera image every 10 seconds (same as stock Home
|
||||||
|
// Assistant Picture Glance).
|
||||||
|
[CONF_LIVE_IMAGE_REFRESH_SECONDS]: 10,
|
||||||
|
|
||||||
|
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||||
|
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||||
|
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||||
|
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||||
|
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||||
|
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||||
|
|
||||||
|
// Disable all optional performance related features.
|
||||||
|
[CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR]: false,
|
||||||
|
|
||||||
|
// Disable all expensive CSS features.
|
||||||
|
[CONF_PERFORMANCE_STYLE_BORDER_RADIUS]: false,
|
||||||
|
[CONF_PERFORMANCE_STYLE_BOX_SHADOW]: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOW_PROFILE_CAMERA_DEFAULTS = {
|
||||||
|
[CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY]: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set low performance profile mode. Sets flags as defined in
|
||||||
|
* LOW_PROFILE_DEFAULTS unless they are explicitly overriden in the
|
||||||
|
* configuration.
|
||||||
|
* @param inputConfig The raw unparsed input configuration.
|
||||||
|
* @param outputConfig The output config to write to.
|
||||||
|
* @returns A changed (in-place) parsed input configuration.
|
||||||
|
*/
|
||||||
|
export const setLowPerformanceProfile = <T extends RawFrigateCardConfig>(
|
||||||
|
inputConfig: RawFrigateCardConfig,
|
||||||
|
outputConfig: T,
|
||||||
|
): T => {
|
||||||
|
const setIfNotSpecified = (
|
||||||
|
defaultLessConfig: RawFrigateCardConfig,
|
||||||
|
outputConfig: T,
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
) => {
|
||||||
|
if (getConfigValue(defaultLessConfig, key) === undefined) {
|
||||||
|
setConfigValue(outputConfig, key, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultLessParseResult = deepRemoveDefaults(frigateCardConfigSchema).safeParse(
|
||||||
|
inputConfig,
|
||||||
|
);
|
||||||
|
if (defaultLessParseResult.success) {
|
||||||
|
const defaultLessConfig = defaultLessParseResult.data;
|
||||||
|
Object.entries(LOW_PROFILE_DEFAULTS).forEach(([k, v]: [string, unknown]) =>
|
||||||
|
setIfNotSpecified(defaultLessConfig, outputConfig, k, v),
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(LOW_PROFILE_CAMERA_DEFAULTS).forEach(
|
||||||
|
([rawKey, v]: [string, unknown]) => {
|
||||||
|
defaultLessConfig.cameras.forEach((_, index: number) => {
|
||||||
|
const indexedKey = getArrayConfigPath(rawKey, index);
|
||||||
|
setIfNotSpecified(defaultLessConfig, outputConfig, indexedKey, v);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return outputConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STYLE_DISABLE_MAP = {
|
||||||
|
box_shadow: 'none',
|
||||||
|
border_radius: '0px',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set card-wide CSS variables for performance.
|
||||||
|
* @param element The element to set the variables on.
|
||||||
|
* @param performance The performance configuration.
|
||||||
|
*/
|
||||||
|
export const setPerformanceCSSStyles = (
|
||||||
|
element: HTMLElement,
|
||||||
|
performance?: PerformanceConfig,
|
||||||
|
): void => {
|
||||||
|
const styles = performance?.style ?? {};
|
||||||
|
for (const configKey of Object.keys(styles)) {
|
||||||
|
const CSSKey = `--frigate-card-css-${configKey.replaceAll('_', '-')}`;
|
||||||
|
if (styles[configKey] === false) {
|
||||||
|
element.style.setProperty(CSSKey, STYLE_DISABLE_MAP[configKey]);
|
||||||
|
} else {
|
||||||
|
element.style.removeProperty(CSSKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -27,16 +27,16 @@
|
|||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([location=right]) #d {
|
:host([location='right']) #d {
|
||||||
// Position to the right.
|
// Position to the right.
|
||||||
left: unset;
|
left: unset;
|
||||||
right: 0;
|
right: 0;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([location=right][open]) #d {
|
:host([location='right'][open]) #d {
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: 0px 0px 25px 0px black;
|
box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 25px 0px black);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifs {
|
#ifs {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
margin-top: -6px;
|
margin-top: -6px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
.title.warning {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
.secondary {
|
.secondary {
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
@@ -101,7 +104,7 @@ div.upgrade span {
|
|||||||
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
|
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
|
||||||
}
|
}
|
||||||
span.info {
|
span.info {
|
||||||
padding: 4px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-selector {
|
ha-selector {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--primary-text-color, white);
|
color: var(--primary-text-color, white);
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: var(--ha-card-border-radius, 4px);
|
border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
|
||||||
|
|
||||||
// Folder background color should match the thumbnail element background
|
// Folder background color should match the thumbnail element background
|
||||||
// color.
|
// color.
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ div.message div.icon {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message ha-icon, ha-circular-progress {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.dotdotdot:after {
|
.dotdotdot:after {
|
||||||
@keyframes dots {
|
@keyframes dots {
|
||||||
0%,
|
0%,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: var(--frigate-card-next-prev-size);
|
height: var(--frigate-card-next-prev-size);
|
||||||
top: calc(50% - (var(--frigate-card-next-prev-size) / 2));
|
top: calc(50% - (var(--frigate-card-next-prev-size) / 2));
|
||||||
box-shadow: 0px 0px 20px 5px black;
|
box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 20px 5px black);
|
||||||
transition: all 0.2s ease-out;
|
transition: all 0.2s ease-out;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ ha-icon {
|
|||||||
// Safari will occasionally not load thumbnails correctly with display block.
|
// Safari will occasionally not load thumbnails correctly with display block.
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
border-radius: var(--ha-card-border-radius, 4px);
|
border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
|
||||||
|
|
||||||
max-width: var(--frigate-card-thumbnail-size);
|
max-width: var(--frigate-card-thumbnail-size);
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
border: 1px solid var(--secondary-color);
|
border: 1px solid var(--secondary-color);
|
||||||
background-color: var(--secondary-background-color);
|
background-color: var(--secondary-background-color);
|
||||||
border-radius: var(--ha-card-border-radius, 4px);
|
border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
:host([details]) {
|
:host([details]) {
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: var(--ha-card-border-radius, 4px);
|
border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|
||||||
// When details are enabled, use a background color so that the details have
|
// When details are enabled, use a background color so that the details have
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ div.timeline {
|
|||||||
.vis-item.vis-selected {
|
.vis-item.vis-selected {
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
box-shadow: 0px 0px 5px 1px var(--primary-color);
|
box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 5px 1px var(--primary-color));
|
||||||
}
|
}
|
||||||
.vis-item.vis-background {
|
.vis-item.vis-background {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
@@ -88,7 +88,10 @@ div.timeline {
|
|||||||
border-style: dotted;
|
border-style: dotted;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
background-color: var(--primary-background-color);
|
background-color: var(--primary-background-color);
|
||||||
box-shadow: 0px 0px 5px 1px var(--primary-color);
|
box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 5px 1px var(--primary-color));
|
||||||
|
}
|
||||||
|
.vis-item.vis-range {
|
||||||
|
border-radius: var(--frigate-card-css-border-radius, unset);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vis-time-axis .vis-grid.vis-minor {
|
.vis-time-axis .vis-grid.vis-minor {
|
||||||
@@ -127,7 +130,7 @@ div.vis-tooltip {
|
|||||||
.target_bar {
|
.target_bar {
|
||||||
border-left: 2px solid var(--primary-color);
|
border-left: 2px solid var(--primary-color);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
box-shadow: 0px 0px 3px 1px var(--primary-color);
|
box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 3px 1px var(--primary-color));
|
||||||
|
|
||||||
// Prevent the mouse interacting with the custom time.
|
// Prevent the mouse interacting with the custom time.
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
+61
-7
@@ -59,7 +59,7 @@ const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS;
|
|||||||
export const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
|
export const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
|
||||||
export const FRIGATE_MENU_PRIORITY_MAX = 100;
|
export const FRIGATE_MENU_PRIORITY_MAX = 100;
|
||||||
|
|
||||||
const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
|
const LIVE_PROVIDERS = ['auto', 'image', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
|
||||||
export type LiveProvider = typeof LIVE_PROVIDERS[number];
|
export type LiveProvider = typeof LIVE_PROVIDERS[number];
|
||||||
|
|
||||||
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
|
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
|
||||||
@@ -206,7 +206,7 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
|||||||
'menu_toggle',
|
'menu_toggle',
|
||||||
'diagnostics',
|
'diagnostics',
|
||||||
'recording',
|
'recording',
|
||||||
'recordings'
|
'recordings',
|
||||||
] as const;
|
] as const;
|
||||||
const FRIGATE_CARD_ACTIONS = [
|
const FRIGATE_CARD_ACTIONS = [
|
||||||
...FRIGATE_CARD_GENERAL_ACTIONS,
|
...FRIGATE_CARD_GENERAL_ACTIONS,
|
||||||
@@ -696,8 +696,12 @@ const timelineCoreConfigSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type TimelineCoreConfig = z.infer<typeof timelineCoreConfigSchema>;
|
export type TimelineCoreConfig = z.infer<typeof timelineCoreConfigSchema>;
|
||||||
|
|
||||||
|
const miniTimelineConfigDefault = {
|
||||||
|
...timelineCoreConfigDefault,
|
||||||
|
mode: 'none' as const,
|
||||||
|
}
|
||||||
const miniTimelineConfigSchema = timelineCoreConfigSchema.extend({
|
const miniTimelineConfigSchema = timelineCoreConfigSchema.extend({
|
||||||
mode: z.enum(['none', 'above', 'below']),
|
mode: z.enum(['none', 'above', 'below']).default(miniTimelineConfigDefault.mode),
|
||||||
});
|
});
|
||||||
export type MiniTimelineControlConfig = z.infer<typeof miniTimelineConfigSchema>;
|
export type MiniTimelineControlConfig = z.infer<typeof miniTimelineConfigSchema>;
|
||||||
|
|
||||||
@@ -735,6 +739,11 @@ export type TitleControlConfig = z.infer<typeof titleControlConfigSchema>;
|
|||||||
/**
|
/**
|
||||||
* Live view configuration section.
|
* Live view configuration section.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const liveImageConfigDefault = {
|
||||||
|
refresh_seconds: 1,
|
||||||
|
};
|
||||||
|
|
||||||
const liveConfigDefault = {
|
const liveConfigDefault = {
|
||||||
auto_play: 'all' as const,
|
auto_play: 'all' as const,
|
||||||
auto_pause: 'never' as const,
|
auto_pause: 'never' as const,
|
||||||
@@ -746,6 +755,7 @@ const liveConfigDefault = {
|
|||||||
draggable: true,
|
draggable: true,
|
||||||
transition_effect: 'slide' as const,
|
transition_effect: 'slide' as const,
|
||||||
show_image_during_load: true,
|
show_image_during_load: true,
|
||||||
|
image: liveImageConfigDefault,
|
||||||
controls: {
|
controls: {
|
||||||
next_previous: {
|
next_previous: {
|
||||||
size: 48,
|
size: 48,
|
||||||
@@ -759,6 +769,7 @@ const liveConfigDefault = {
|
|||||||
show_timeline_control: true,
|
show_timeline_control: true,
|
||||||
mode: 'left' as const,
|
mode: 'left' as const,
|
||||||
},
|
},
|
||||||
|
timeline: miniTimelineConfigDefault,
|
||||||
title: {
|
title: {
|
||||||
mode: 'popup-bottom-right' as const,
|
mode: 'popup-bottom-right' as const,
|
||||||
duration_seconds: 2,
|
duration_seconds: 2,
|
||||||
@@ -766,6 +777,11 @@ const liveConfigDefault = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const liveImageConfigSchema = z.object({
|
||||||
|
refresh_seconds: z.number().min(0).default(liveConfigDefault.image.refresh_seconds),
|
||||||
|
});
|
||||||
|
export type LiveImageConfig = z.infer<typeof liveImageConfigSchema>;
|
||||||
|
|
||||||
const webrtcCardConfigSchema = webrtcCardCameraConfigSchema.passthrough().optional();
|
const webrtcCardConfigSchema = webrtcCardCameraConfigSchema.passthrough().optional();
|
||||||
export type WebRTCCardConfig = z.infer<typeof webrtcCardConfigSchema>;
|
export type WebRTCCardConfig = z.infer<typeof webrtcCardConfigSchema>;
|
||||||
|
|
||||||
@@ -794,8 +810,9 @@ export type JSMPEGConfig = z.infer<typeof jsmpegConfigSchema>;
|
|||||||
|
|
||||||
const liveOverridableConfigSchema = z
|
const liveOverridableConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
webrtc_card: webrtcCardConfigSchema,
|
image: liveImageConfigSchema.default(liveConfigDefault.image),
|
||||||
jsmpeg: jsmpegConfigSchema,
|
jsmpeg: jsmpegConfigSchema,
|
||||||
|
webrtc_card: webrtcCardConfigSchema,
|
||||||
controls: z
|
controls: z
|
||||||
.object({
|
.object({
|
||||||
next_previous: nextPreviousControlConfigSchema
|
next_previous: nextPreviousControlConfigSchema
|
||||||
@@ -833,7 +850,7 @@ const liveOverridableConfigSchema = z
|
|||||||
.default(liveConfigDefault.controls.thumbnails.media),
|
.default(liveConfigDefault.controls.thumbnails.media),
|
||||||
})
|
})
|
||||||
.default(liveConfigDefault.controls.thumbnails),
|
.default(liveConfigDefault.controls.thumbnails),
|
||||||
timeline: miniTimelineConfigSchema.optional(),
|
timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline),
|
||||||
title: titleControlConfigSchema
|
title: titleControlConfigSchema
|
||||||
.extend({
|
.extend({
|
||||||
mode: titleControlConfigSchema.shape.mode.default(
|
mode: titleControlConfigSchema.shape.mode.default(
|
||||||
@@ -975,6 +992,7 @@ const viewerConfigDefault = {
|
|||||||
show_timeline_control: true,
|
show_timeline_control: true,
|
||||||
mode: 'left' as const,
|
mode: 'left' as const,
|
||||||
},
|
},
|
||||||
|
timeline: miniTimelineConfigDefault,
|
||||||
title: {
|
title: {
|
||||||
mode: 'popup-bottom-right' as const,
|
mode: 'popup-bottom-right' as const,
|
||||||
duration_seconds: 2,
|
duration_seconds: 2,
|
||||||
@@ -1038,7 +1056,7 @@ const viewerConfigSchema = z
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
.default(viewerConfigDefault.controls.thumbnails),
|
.default(viewerConfigDefault.controls.thumbnails),
|
||||||
timeline: miniTimelineConfigSchema.optional(),
|
timeline: miniTimelineConfigSchema.default(viewerConfigDefault.controls.timeline),
|
||||||
title: titleControlConfigSchema
|
title: titleControlConfigSchema
|
||||||
.extend({
|
.extend({
|
||||||
mode: titleControlConfigSchema.shape.mode.default(
|
mode: titleControlConfigSchema.shape.mode.default(
|
||||||
@@ -1206,6 +1224,41 @@ const liveOverridesSchema = z
|
|||||||
.optional();
|
.optional();
|
||||||
export type LiveOverrides = z.infer<typeof liveOverridesSchema>;
|
export type LiveOverrides = z.infer<typeof liveOverridesSchema>;
|
||||||
|
|
||||||
|
const performanceConfigDefault = {
|
||||||
|
profile: 'high' as const,
|
||||||
|
features: {
|
||||||
|
animated_progress_indicator: true,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
border_radius: true,
|
||||||
|
box_shadow: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const performanceConfigSchema = z
|
||||||
|
.object({
|
||||||
|
profile: z.enum(['low', 'high']).default(performanceConfigDefault.profile),
|
||||||
|
features: z
|
||||||
|
.object({
|
||||||
|
animated_progress_indicator: z
|
||||||
|
.boolean()
|
||||||
|
.default(performanceConfigDefault.features.animated_progress_indicator),
|
||||||
|
})
|
||||||
|
.default(performanceConfigDefault.features),
|
||||||
|
style: z
|
||||||
|
.object({
|
||||||
|
border_radius: z.boolean().default(performanceConfigDefault.style.border_radius),
|
||||||
|
box_shadow: z.boolean().default(performanceConfigDefault.style.box_shadow),
|
||||||
|
})
|
||||||
|
.default(performanceConfigDefault.style),
|
||||||
|
})
|
||||||
|
.default(performanceConfigDefault);
|
||||||
|
export type PerformanceConfig = z.infer<typeof performanceConfigSchema>;
|
||||||
|
|
||||||
|
export interface CardWideConfig {
|
||||||
|
performance?: PerformanceConfig;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main card config.
|
* Main card config.
|
||||||
*/
|
*/
|
||||||
@@ -1221,6 +1274,7 @@ export const frigateCardConfigSchema = z.object({
|
|||||||
elements: pictureElementsSchema,
|
elements: pictureElementsSchema,
|
||||||
dimensions: dimensionsConfigSchema,
|
dimensions: dimensionsConfigSchema,
|
||||||
timeline: timelineConfigSchema,
|
timeline: timelineConfigSchema,
|
||||||
|
performance: performanceConfigSchema,
|
||||||
|
|
||||||
// Configuration overrides.
|
// Configuration overrides.
|
||||||
overrides: overridesSchema,
|
overrides: overridesSchema,
|
||||||
@@ -1245,7 +1299,7 @@ export const frigateCardConfigDefaults = {
|
|||||||
event_gallery: galleryConfigDefault,
|
event_gallery: galleryConfigDefault,
|
||||||
image: imageConfigDefault,
|
image: imageConfigDefault,
|
||||||
timeline: timelineConfigDefault,
|
timeline: timelineConfigDefault,
|
||||||
mini_timeline: timelineCoreConfigDefault,
|
performance: performanceConfigDefault,
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuButtonSchema = z.discriminatedUnion('type', [
|
const menuButtonSchema = z.discriminatedUnion('type', [
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { format } from 'date-fns';
|
import format from 'date-fns/format';
|
||||||
import { isEqual } from 'lodash-es';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { FrigateCardError } from '../types';
|
import { FrigateCardError } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { DataSet, DataView } from 'vis-data/esnext';
|
import { DataSet, DataView } from 'vis-data/esnext';
|
||||||
import { IdType, TimelineItem } from 'vis-timeline/esnext';
|
import type { IdType, TimelineItem } from 'vis-timeline/esnext';
|
||||||
import { CAMERA_BIRDSEYE } from '../const.js';
|
import { CAMERA_BIRDSEYE } from '../const.js';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from './frigate.js';
|
} from './frigate.js';
|
||||||
import { dispatchFrigateCardErrorEvent } from '../components/message.js';
|
import { dispatchFrigateCardErrorEvent } from '../components/message.js';
|
||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
import { throttle } from 'lodash-es';
|
import throttle from 'lodash-es/throttle';
|
||||||
|
|
||||||
const RECORDING_SEGMENT_TOLERANCE = 60;
|
const RECORDING_SEGMENT_TOLERANCE = 60;
|
||||||
const DATA_MANAGER_MAX_AGE_SECONDS = 10;
|
const DATA_MANAGER_MAX_AGE_SECONDS = 10;
|
||||||
@@ -104,7 +104,6 @@ export class DataManager {
|
|||||||
protected _maxAgeSeconds: number = DATA_MANAGER_MAX_AGE_SECONDS;
|
protected _maxAgeSeconds: number = DATA_MANAGER_MAX_AGE_SECONDS;
|
||||||
|
|
||||||
protected _cameras: Map<string, CameraConfig>;
|
protected _cameras: Map<string, CameraConfig>;
|
||||||
protected _mediaType: TimelineMediaType;
|
|
||||||
|
|
||||||
// Garbage collect segments at most once an hour.
|
// Garbage collect segments at most once an hour.
|
||||||
protected _throttledSegmentGarbageCollector = throttle(
|
protected _throttledSegmentGarbageCollector = throttle(
|
||||||
@@ -115,9 +114,8 @@ export class DataManager {
|
|||||||
{ trailing: true },
|
{ trailing: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(cameras: Map<string, CameraConfig>, mediaType: TimelineMediaType) {
|
constructor(cameras: Map<string, CameraConfig>) {
|
||||||
this._cameras = cameras;
|
this._cameras = cameras;
|
||||||
this._mediaType = mediaType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the last event fetch date.
|
// Get the last event fetch date.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { ViewContext } from 'view';
|
||||||
import { homeAssistantWSRequest } from '.';
|
import { homeAssistantWSRequest } from '.';
|
||||||
import {
|
import {
|
||||||
dispatchErrorMessageEvent,
|
dispatchErrorMessageEvent,
|
||||||
@@ -346,6 +347,7 @@ export const fetchChildMediaAndDispatchViewChange = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
view: Readonly<View>,
|
view: Readonly<View>,
|
||||||
child: Readonly<FrigateBrowseMediaSource>,
|
child: Readonly<FrigateBrowseMediaSource>,
|
||||||
|
context?: ViewContext,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
let parent: FrigateBrowseMediaSource;
|
let parent: FrigateBrowseMediaSource;
|
||||||
try {
|
try {
|
||||||
@@ -358,6 +360,7 @@ export const fetchChildMediaAndDispatchViewChange = async (
|
|||||||
.evolve({
|
.evolve({
|
||||||
target: parent,
|
target: parent,
|
||||||
})
|
})
|
||||||
|
.mergeInContext(context)
|
||||||
.dispatchChangeEvent(element);
|
.dispatchChangeEvent(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+21
-21
@@ -1,4 +1,9 @@
|
|||||||
import { add, endOfHour, fromUnixTime, getUnixTime, startOfHour, sub } from 'date-fns';
|
import add from 'date-fns/add';
|
||||||
|
import endOfHour from 'date-fns/endOfHour';
|
||||||
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
|
import getUnixTime from 'date-fns/getUnixTime';
|
||||||
|
import startOfHour from 'date-fns/startOfHour';
|
||||||
|
import sub from 'date-fns/sub';
|
||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import { dispatchMessageEvent } from '../components/message';
|
import { dispatchMessageEvent } from '../components/message';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
@@ -6,7 +11,11 @@ import { CameraConfig, ExtendedHomeAssistant, FrigateBrowseMediaSource } from '.
|
|||||||
import { View } from '../view';
|
import { View } from '../view';
|
||||||
import { formatDateAndTime, prettifyTitle } from './basic';
|
import { formatDateAndTime, prettifyTitle } from './basic';
|
||||||
import { getRecordingMediaContentID } from './frigate';
|
import { getRecordingMediaContentID } from './frigate';
|
||||||
import { createChild, createEventParentForChildren, sortYoungestToOldest } from './ha/browse-media';
|
import {
|
||||||
|
createChild,
|
||||||
|
createEventParentForChildren,
|
||||||
|
sortYoungestToOldest,
|
||||||
|
} from './ha/browse-media';
|
||||||
import {
|
import {
|
||||||
RecordingSegmentsItem,
|
RecordingSegmentsItem,
|
||||||
sortOldestToYoungest,
|
sortOldestToYoungest,
|
||||||
@@ -23,7 +32,7 @@ import { getAllDependentCameras, getTrueCameras } from './camera.js';
|
|||||||
* @param view The current view.
|
* @param view The current view.
|
||||||
* @param options A set of cameraIDs to fetch recordings for, and a targetView to dispatch to.
|
* @param options A set of cameraIDs to fetch recordings for, and a targetView to dispatch to.
|
||||||
*/
|
*/
|
||||||
export const changeViewToRecentRecordingForCameraAndDependents = async (
|
export const changeViewToRecentRecordingForCameraAndDependents = async (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: ExtendedHomeAssistant,
|
hass: ExtendedHomeAssistant,
|
||||||
dataManager: DataManager,
|
dataManager: DataManager,
|
||||||
@@ -35,22 +44,15 @@ import { getAllDependentCameras, getTrueCameras } from './camera.js';
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
await changeViewToRecording(
|
await changeViewToRecording(element, hass, dataManager, cameras, view, {
|
||||||
element,
|
...options,
|
||||||
hass,
|
|
||||||
dataManager,
|
|
||||||
cameras,
|
|
||||||
view,
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
|
|
||||||
// Fetch 1 days worth of recordings (including recordings that are for the current hour).
|
// Fetch 1 days worth of recordings (including recordings that are for the current hour).
|
||||||
cameraIDs: getAllDependentCameras(cameras, view.camera),
|
cameraIDs: getAllDependentCameras(cameras, view.camera),
|
||||||
start: sub(now, { days: 1 }),
|
start: sub(now, { days: 1 }),
|
||||||
end: add(now, { hours: 1 }),
|
end: add(now, { hours: 1 }),
|
||||||
}
|
});
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the view to a recording.
|
* Change the view to a recording.
|
||||||
@@ -132,9 +134,7 @@ const createRecordingChildren = (
|
|||||||
): FrigateBrowseMediaSource[] => {
|
): FrigateBrowseMediaSource[] => {
|
||||||
const children: FrigateBrowseMediaSource[] = [];
|
const children: FrigateBrowseMediaSource[] = [];
|
||||||
|
|
||||||
for (const cameraID of getTrueCameras(
|
for (const cameraID of getTrueCameras(cameras, cameraIDs)) {
|
||||||
cameras, cameraIDs
|
|
||||||
)) {
|
|
||||||
const config = cameras.get(cameraID) ?? null;
|
const config = cameras.get(cameraID) ?? null;
|
||||||
const recordingSummary = dataManager.getRecordingSummaryForCamera(cameraID);
|
const recordingSummary = dataManager.getRecordingSummaryForCamera(cameraID);
|
||||||
if (!config?.frigate.camera_name || !recordingSummary) {
|
if (!config?.frigate.camera_name || !recordingSummary) {
|
||||||
|
|||||||
+12
-3
@@ -4,6 +4,7 @@ import {
|
|||||||
dispatchFrigateCardErrorEvent,
|
dispatchFrigateCardErrorEvent,
|
||||||
renderProgressIndicator,
|
renderProgressIndicator,
|
||||||
} from '../components/message';
|
} from '../components/message';
|
||||||
|
import { CardWideConfig } from '../types';
|
||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from './basic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,11 +19,19 @@ export const renderTask = <R>(
|
|||||||
host: EventTarget,
|
host: EventTarget,
|
||||||
task: Task<unknown[], R>,
|
task: Task<unknown[], R>,
|
||||||
completeFunc: (result: R) => TemplateResult | void,
|
completeFunc: (result: R) => TemplateResult | void,
|
||||||
inProgressFunc?: () => TemplateResult | void,
|
options?: {
|
||||||
|
cardWideConfig?: CardWideConfig;
|
||||||
|
inProgressFunc?: () => TemplateResult | void;
|
||||||
|
},
|
||||||
): TemplateResult => {
|
): TemplateResult => {
|
||||||
|
const progressConfig = {
|
||||||
|
...(options?.cardWideConfig && { cardWideConfig: options.cardWideConfig }),
|
||||||
|
};
|
||||||
return html` ${task.render({
|
return html` ${task.render({
|
||||||
initial: () => inProgressFunc?.() ?? renderProgressIndicator(),
|
initial: () =>
|
||||||
pending: () => inProgressFunc?.() ?? renderProgressIndicator(),
|
options?.inProgressFunc?.() ?? renderProgressIndicator(progressConfig),
|
||||||
|
pending: () =>
|
||||||
|
options?.inProgressFunc?.() ?? renderProgressIndicator(progressConfig),
|
||||||
error: (e: unknown) => {
|
error: (e: unknown) => {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
dispatchFrigateCardErrorEvent(host, e as Error);
|
dispatchFrigateCardErrorEvent(host, e as Error);
|
||||||
|
|||||||
+1
-9
@@ -13,7 +13,6 @@ export interface ViewEvolveParameters {
|
|||||||
camera?: string;
|
camera?: string;
|
||||||
target?: FrigateBrowseMediaSource | null;
|
target?: FrigateBrowseMediaSource | null;
|
||||||
childIndex?: number | null;
|
childIndex?: number | null;
|
||||||
previous?: View | null;
|
|
||||||
context?: ViewContext | null;
|
context?: ViewContext | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ export class View {
|
|||||||
public camera: string;
|
public camera: string;
|
||||||
public target: FrigateBrowseMediaSource | null;
|
public target: FrigateBrowseMediaSource | null;
|
||||||
public childIndex: number | null;
|
public childIndex: number | null;
|
||||||
public previous: View | null;
|
|
||||||
public context: ViewContext | null;
|
public context: ViewContext | null;
|
||||||
|
|
||||||
constructor(params: ViewParameters) {
|
constructor(params: ViewParameters) {
|
||||||
@@ -35,7 +33,6 @@ export class View {
|
|||||||
this.camera = params.camera;
|
this.camera = params.camera;
|
||||||
this.target = params.target ?? null;
|
this.target = params.target ?? null;
|
||||||
this.childIndex = params.childIndex ?? null;
|
this.childIndex = params.childIndex ?? null;
|
||||||
this.previous = params.previous ?? null;
|
|
||||||
this.context = params.context ?? null;
|
this.context = params.context ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +87,6 @@ export class View {
|
|||||||
camera: this.camera,
|
camera: this.camera,
|
||||||
target: this.target,
|
target: this.target,
|
||||||
childIndex: this.childIndex,
|
childIndex: this.childIndex,
|
||||||
previous: this.previous,
|
|
||||||
context: this.context,
|
context: this.context,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -107,10 +103,6 @@ export class View {
|
|||||||
target: params.target !== undefined ? params.target : this.target,
|
target: params.target !== undefined ? params.target : this.target,
|
||||||
childIndex: params.childIndex !== undefined ? params.childIndex : this.childIndex,
|
childIndex: params.childIndex !== undefined ? params.childIndex : this.childIndex,
|
||||||
context: params.context !== undefined ? params.context : this.context,
|
context: params.context !== undefined ? params.context : this.context,
|
||||||
|
|
||||||
// Special case: Set the previous to this of the evolved view (rather than
|
|
||||||
// the previous of this).
|
|
||||||
previous: params.previous !== undefined ? params.previous : this,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +111,7 @@ export class View {
|
|||||||
* @param context The context to merge in.
|
* @param context The context to merge in.
|
||||||
* @returns This view.
|
* @returns This view.
|
||||||
*/
|
*/
|
||||||
public mergeInContext(context: ViewContext): View {
|
public mergeInContext(context?: ViewContext): View {
|
||||||
this.context = { ...this.context, ...context };
|
this.context = { ...this.context, ...context };
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2017",
|
"target": "es2021",
|
||||||
"module": "es2020",
|
"module": "es2020",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"lib": ["es2017", "dom", "dom.iterable"],
|
"lib": ["es2021", "dom", "dom.iterable"],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user