Merge pull request #1312 from dermotduffy/ptz-revamp
Add out-of-the-box Frigate PTZ support
This commit is contained in:
@@ -55,7 +55,7 @@ That's it!
|
|||||||
|
|
||||||
### Manual Resource Management
|
### Manual Resource Management
|
||||||
|
|
||||||
For most users, HACS should automatically add the necessary resources. Should this auto-registration does not work, you will need to complete one additional step.
|
For most users, HACS should automatically add the necessary resources. Should this auto-registration not work you will need to complete one additional step.
|
||||||
|
|
||||||
#### Lovelace in "Storage mode" (default)
|
#### Lovelace in "Storage mode" (default)
|
||||||
|
|
||||||
@@ -167,11 +167,11 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|
|||||||
|
|
||||||
##### Engine Capabilities
|
##### Engine Capabilities
|
||||||
|
|
||||||
|Engine|Live|Supports clips|Supports Snapshots|Supports Recordings|Supports Timeline|Favorite events|Favorite recordings|
|
|Engine|Live|Supports clips|Supports Snapshots|Supports Recordings|Supports Timeline|Supports PTZ out of the box|Supports manually configured PTZ|Favorite events|Favorite recordings|
|
||||||
| - | - | - | - | - | - | - | - |
|
| - | - | - | - | - | - | - | - | - | - |
|
||||||
|`frigate`| :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: |
|
|`frigate`| :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: |
|
||||||
|`generic`| :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: |
|
|`generic`| :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: |
|
||||||
|`motioneye`| :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: |
|
|`motioneye`| :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: |
|
||||||
|
|
||||||
##### Live providers supported per Engine
|
##### Live providers supported per Engine
|
||||||
|
|
||||||
@@ -485,6 +485,7 @@ menu:
|
|||||||
| `timeline` | :white_check_mark: | The `timeline` menu button: show the event timeline. |
|
| `timeline` | :white_check_mark: | The `timeline` menu button: show the event timeline. |
|
||||||
| `media_player` | :white_check_mark: | The `media_player` menu button: sends the visible media to a remote media player. Supports Frigate clips, snapshots and live camera (only for cameras that specify a `camera_entity` and only using the default HA stream (equivalent to the `ha` live provider). `jsmpeg` or `webrtc-card` are not supported, although live can still be played as long as `camera_entity` is specified. In the player list, a `tap` will send the media to the player, a `hold` will stop the media on the player. |
|
| `media_player` | :white_check_mark: | The `media_player` menu button: sends the visible media to a remote media player. Supports Frigate clips, snapshots and live camera (only for cameras that specify a `camera_entity` and only using the default HA stream (equivalent to the `ha` live provider). `jsmpeg` or `webrtc-card` are not supported, although live can still be played as long as `camera_entity` is specified. In the player list, a `tap` will send the media to the player, a `hold` will stop the media on the player. |
|
||||||
| `microphone` | :white_check_mark: | The `microphone` button allows usage of 2-way audio in certain configurations. See [Using 2-way audio](#using-2-way-audio). |
|
| `microphone` | :white_check_mark: | The `microphone` button allows usage of 2-way audio in certain configurations. See [Using 2-way audio](#using-2-way-audio). |
|
||||||
|
| `show_ptz` | :white_check_mark: | The `show_ptz` button shows or hide the PTZ controls. |
|
||||||
|
|
||||||
##### Configuration on each button
|
##### Configuration on each button
|
||||||
|
|
||||||
@@ -628,6 +629,30 @@ live:
|
|||||||
|
|
||||||
See [Using 2-way audio](#using-2-way-audio) for more information about the very particular requirements that must be followed for 2-way audio to work.
|
See [Using 2-way audio](#using-2-way-audio) for more information about the very particular requirements that must be followed for 2-way audio to work.
|
||||||
|
|
||||||
|
<a name="frigate-card-ptz"></a>
|
||||||
|
|
||||||
|
#### Live: PTZ
|
||||||
|
|
||||||
|
Controls a PTZ (Pan Tilt Zoom) controller overlay. All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
live:
|
||||||
|
ptz:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Overridable | Description |
|
||||||
|
| - | - | - | - |
|
||||||
|
| `mode` | `on` | :white_check_mark: | When `on` will show a PTZ control if so configured (manually, or by the camera engine), if `off` will not show any control. |
|
||||||
|
| `position` | `bottom-right` | :white_check_mark: | Whether to position the control on the `top-left`, `top-right`, `bottom-left` or `bottom-right`. This may be overridden by using the `style` parameter to precisely control placement. |
|
||||||
|
| `actions_left`, `actions_right`, `actions_up`, `actions_down`, `actions_zoom_in`, `actions_zoom_out`, `actions_home` | Default is set by camera engine of the selected camera | :white_check_mark: | The [Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) to call when this icon is interacted with. |
|
||||||
|
| `orientation` | `horizontal` | :white_check_mark: | Whether to show a `vertical` or `horizontal` PTZ control. |
|
||||||
|
| `hide_pan_tilt` | `false` | :white_check_mark: | When `true` the Pan & Tilt buttons of the control is hidden |
|
||||||
|
| `hide_zoom` | `false` | :white_check_mark: | When `true` the Zoom button of the control is hidden |
|
||||||
|
| `hide_home` | `false` | :white_check_mark: | When `true` the Home button of the control is hidden |
|
||||||
|
| `data_left`, `data_right`, `data_up`, `data_down`, `data_zoom_in`, `data_zoom_out`, `data_home` | | :white_check_mark: | Shorthand for a `tap_action` that calls the `service` with the data provided in this argument. Internally, this is just translated into the longer-form `actions_[button]`. If both `actions_X` and `data_X` are specified, `actions_X` takes priority. This is compatible with [AlexxIT's WebRTC Card PTZ configuration](https://github.com/AlexxIT/WebRTC/wiki/PTZ-Config-Examples). |
|
||||||
|
| `service` | | :white_check_mark: | An optional Home Assistant service to call when the `data_` parameters are used. |
|
||||||
|
| `style` | | :white_check_mark: | Optionally position and style the element using CSS. Similar to [Picture Element styling](https://www.home-assistant.io/dashboards/picture-elements/#how-to-use-the-style-object), except without any default, e.g. `left: 42%` |
|
||||||
|
|
||||||
<a name="live-display"></a>
|
<a name="live-display"></a>
|
||||||
|
|
||||||
#### Live: Display
|
#### Live: Display
|
||||||
@@ -1315,8 +1340,6 @@ elements to add special Frigate card functionality.
|
|||||||
| `custom:frigate-card-menu-submenu` | Add a configurable submenu dropdown. See [configuration below](#frigate-card-menu-submenu).|
|
| `custom:frigate-card-menu-submenu` | Add a configurable submenu dropdown. See [configuration below](#frigate-card-menu-submenu).|
|
||||||
| `custom:frigate-card-menu-submenu-select` | Add a submenu based on a `select` or `input_select`. See [configuration below](#frigate-card-submenu-select).|
|
| `custom:frigate-card-menu-submenu-select` | Add a submenu based on a `select` or `input_select`. See [configuration below](#frigate-card-submenu-select).|
|
||||||
| `custom:frigate-card-conditional` | Restrict a set of elements to only render when the card is showing particular a particular [view](#views). See [configuration below](#frigate-card-conditional).|
|
| `custom:frigate-card-conditional` | Restrict a set of elements to only render when the card is showing particular a particular [view](#views). See [configuration below](#frigate-card-conditional).|
|
||||||
| `custom:frigate-card-ptz` | Add a PTZ (Pan Tilt Zoom) controller overlay. See [configuration below](#frigate-card-ptz).|
|
|
||||||
|
|
||||||
|
|
||||||
**Note**: ℹ️ Manual positioning of custom menu icons or submenus via the `style`
|
**Note**: ℹ️ Manual positioning of custom menu icons or submenus via the `style`
|
||||||
parameter is not supported as the menu buttons displayed are context sensitive
|
parameter is not supported as the menu buttons displayed are context sensitive
|
||||||
@@ -1377,19 +1400,6 @@ Parameters for the `custom:frigate-card-conditional` element:
|
|||||||
`elements` | The elements to render. Can be any supported element, include additional condition or custom elements. |
|
`elements` | The elements to render. Can be any supported element, include additional condition or custom elements. |
|
||||||
| `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. See [Frigate Card Conditions](#frigate-card-conditions). |
|
| `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. See [Frigate Card Conditions](#frigate-card-conditions). |
|
||||||
|
|
||||||
#### `custom:frigate-card-ptz`
|
|
||||||
|
|
||||||
Parameters for the `custom:frigate-card-ptz` element:
|
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
|
||||||
| ------------- | - | -------------------------------------------- |
|
|
||||||
| `type` | | Must be `custom:frigate-card-ptz`. |
|
|
||||||
| `style` | `translate(-50%, -50%)` | Position and style the element using CSS. See [Picture Element styling](https://www.home-assistant.io/dashboards/picture-elements/#how-to-use-the-style-object). |
|
|
||||||
| `orientation` | `vertical` | Whether to show a `vertical` or `horizontal` PTZ control. |
|
|
||||||
| `actions_left`, `actions_right`, `actions_up`, `actions_down`, `actions_zoom_in`, `actions_zoom_out`, `actions_home` | The [Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) to call when this icon is interacted with. |
|
|
||||||
| `data_left`, `data_right`, `data_up`, `data_down`, `data_zoom_in`, `data_zoom_out`, `data_home` | Shorthand for a `tap_action` that calls the `service` with the data provided in this argument. Internally, this is just translated into the longer-form `actions_[button]`. If both `actions_X` and `data_X` are specified, `actions_X` takes priority. This is compatible with [AlexxIT's WebRTC Card PTZ configuration](https://github.com/AlexxIT/WebRTC/wiki/PTZ-Config-Examples). |
|
|
||||||
| `service` | | An optional Home Assistant service to call when the `data_` parameters are used. |
|
|
||||||
|
|
||||||
<a name="frigate-card-actions"></a>
|
<a name="frigate-card-actions"></a>
|
||||||
|
|
||||||
### Special Actions
|
### Special Actions
|
||||||
@@ -1399,7 +1409,7 @@ Parameters for the `custom:frigate-card-ptz` element:
|
|||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
| - | - |
|
| - | - |
|
||||||
| `action` | Must be `custom:frigate-card-action`. |
|
| `action` | Must be `custom:frigate-card-action`. |
|
||||||
| `frigate_card_action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `recording`, `recordings`, `snapshot`, `snapshots`, `download`, `timeline`, `camera_ui`, `fullscreen`, `camera_select`, `menu_toggle`, `media_player`, `live_substream_on`, `live_substream_off`, `live_substream_select`, `expand`, `microphone_mute`, `microphone_unmute`, `mute`, `unmute`, `play`, `pause`, `screenshot`|
|
| `frigate_card_action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `recording`, `recordings`, `snapshot`, `snapshots`, `download`, `timeline`, `camera_ui`, `fullscreen`, `camera_select`, `menu_toggle`, `media_player`, `live_substream_on`, `live_substream_off`, `live_substream_select`, `expand`, `microphone_mute`, `microphone_unmute`, `mute`, `unmute`, `play`, `pause`, `screenshot`, `show_ptz`, `ptz`|
|
||||||
|
|
||||||
<a name="custom-actions"></a>
|
<a name="custom-actions"></a>
|
||||||
|
|
||||||
@@ -1421,6 +1431,8 @@ Parameters for the `custom:frigate-card-ptz` element:
|
|||||||
|`mute`, `unmute`| Mute or unmute the loaded media. |
|
|`mute`, `unmute`| Mute or unmute the loaded media. |
|
||||||
|`play`, `pause`| Play or pause the loaded media. |
|
|`play`, `pause`| Play or pause the loaded media. |
|
||||||
|`screenshot`| Take a screenshot of the loaded media (e.g. a still from a video). |
|
|`screenshot`| Take a screenshot of the loaded media (e.g. a still from a video). |
|
||||||
|
|`show_ptz`| Show or hide the PTZ controls. Takes a `show_ptz` boolean parameter to indicate whether the controls show be shown or not. |
|
||||||
|
|`ptz`| Execute a native PTZ action (only for native out-of-the-box PTZ camera engines, e.g. Frigate). Takes a required `ptz_action` parameter that is one of `left`, `right`, `up`, `down`, `zoom_in`, `zoom_out` or `preset`. Takes an optional `ptz_phase` parameter that is one of `start` or `stop` to start or stop the movement discretely. Takes an optional `ptz_preset` parameter as the preset to execute when the `ptz_action` parameter is `preset`. |
|
||||||
|
|
||||||
<a name="views"></a>
|
<a name="views"></a>
|
||||||
|
|
||||||
@@ -1438,6 +1450,7 @@ This card supports several different views:
|
|||||||
|`recordings`|Shows a gallery of recent (last day) recordings for this camera and its dependents.|
|
|`recordings`|Shows a gallery of recent (last day) recordings for this camera and its dependents.|
|
||||||
|`recording`|Shows a viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon.|
|
|`recording`|Shows a viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon.|
|
||||||
|`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.timeout_seconds`).|
|
|`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.timeout_seconds`).|
|
||||||
|
|`timeline`|Shows an event timeline.|
|
||||||
|
|
||||||
### Navigating From A Snapshot To A Clip
|
### Navigating From A Snapshot To A Clip
|
||||||
|
|
||||||
@@ -2012,6 +2025,11 @@ menu:
|
|||||||
enabled: false
|
enabled: false
|
||||||
alignment: matching
|
alignment: matching
|
||||||
icon: mdi:play
|
icon: mdi:play
|
||||||
|
show_ptz:
|
||||||
|
priority: 50
|
||||||
|
enabled: false
|
||||||
|
alignment: matching
|
||||||
|
icon: mdi:pan
|
||||||
button_size: 40
|
button_size: 40
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
@@ -2066,6 +2084,29 @@ live:
|
|||||||
microphone:
|
microphone:
|
||||||
always_connected: false
|
always_connected: false
|
||||||
disconnect_seconds: 60
|
disconnect_seconds: 60
|
||||||
|
ptz:
|
||||||
|
mode: on
|
||||||
|
position: bottom-right
|
||||||
|
orientation: horizontal
|
||||||
|
hide_pan_tilt: false
|
||||||
|
hide_zoom: false
|
||||||
|
hide_home: false
|
||||||
|
style:
|
||||||
|
# Optionally override the default style.
|
||||||
|
right: 5%
|
||||||
|
# Manually specifying actions.
|
||||||
|
actions_left:
|
||||||
|
tap_action:
|
||||||
|
action: call-service
|
||||||
|
service: sonoff.send_command
|
||||||
|
service_data:
|
||||||
|
device: '048123'
|
||||||
|
cmd: left
|
||||||
|
# Equivalent short form PTZ actions (only right button shown)
|
||||||
|
service: sonoff.send_command
|
||||||
|
data_right:
|
||||||
|
device: '048123'
|
||||||
|
cmd: right
|
||||||
display:
|
display:
|
||||||
mode: single
|
mode: single
|
||||||
grid_selected_width_factor: 2
|
grid_selected_width_factor: 2
|
||||||
@@ -2432,31 +2473,6 @@ elements:
|
|||||||
state: on
|
state: on
|
||||||
state_not: off
|
state_not: off
|
||||||
media_loaded: true
|
media_loaded: true
|
||||||
# Full form PTZ actions (only left button shown).
|
|
||||||
- type: custom:frigate-card-ptz
|
|
||||||
orientation: vertical
|
|
||||||
style:
|
|
||||||
transform: none
|
|
||||||
right: 5%
|
|
||||||
top: 50%
|
|
||||||
actions_left:
|
|
||||||
tap_action:
|
|
||||||
action: call-service
|
|
||||||
service: sonoff.send_command
|
|
||||||
service_data:
|
|
||||||
device: '048123'
|
|
||||||
cmd: left
|
|
||||||
# Equivalent short form PTZ actions (only left button shown)
|
|
||||||
- type: custom:frigate-card-ptz
|
|
||||||
orientation: vertical
|
|
||||||
style:
|
|
||||||
transform: none
|
|
||||||
right: 20px
|
|
||||||
top: 180px
|
|
||||||
service: sonoff.send_command
|
|
||||||
data_left:
|
|
||||||
device: '048123'
|
|
||||||
cmd: left
|
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -2562,6 +2578,21 @@ elements:
|
|||||||
tap_action:
|
tap_action:
|
||||||
action: custom:frigate-card-action
|
action: custom:frigate-card-action
|
||||||
frigate_card_action: screenshot
|
frigate_card_action: screenshot
|
||||||
|
- type: custom:frigate-card-menu-icon
|
||||||
|
icon: mdi:alpha-p-circle
|
||||||
|
title: Show PTZ
|
||||||
|
tap_action:
|
||||||
|
action: custom:frigate-card-action
|
||||||
|
frigate_card_action: show_ptz
|
||||||
|
show_ptz: true
|
||||||
|
- type: custom:frigate-card-menu-icon
|
||||||
|
icon: mdi:alpha-q-circle
|
||||||
|
title: Native PTZ Preset
|
||||||
|
tap_action:
|
||||||
|
action: custom:frigate-card-action
|
||||||
|
frigate_card_action: ptz
|
||||||
|
ptz_action: preset
|
||||||
|
ptz_preset: 'doorway
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -3416,31 +3447,20 @@ elements:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Using a PTZ picture element
|
### Using a PTZ control
|
||||||
|
|
||||||
The card supports a custom PTZ element (`custom:frigate-card-ptz`) to conveniently control pan, tilt and zoom for cameras.
|
The card supports using PTZ controls to conveniently control pan, tilt and zoom for cameras.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand: Using the native PTZ picture element</summary>
|
<summary>Expand: Using the PTZ controls</summary>
|
||||||
|
|
||||||
This example shows the native PTZ element when the `live` or `image` view is displayed and the stream (media) has loaded.
|
This example shows the PTZ controls of the `live` view. Note that if your camera engine supports it (e.g. `frigate`) this will just work out of the box with no configuration at all.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
[...]
|
[...]
|
||||||
elements:
|
live:
|
||||||
- type: custom:frigate-card-conditional
|
ptz:
|
||||||
conditions:
|
|
||||||
media_loaded: true
|
|
||||||
view:
|
|
||||||
- live
|
|
||||||
- image
|
|
||||||
elements:
|
|
||||||
- type: custom:frigate-card-ptz
|
|
||||||
orientation: horizontal
|
orientation: horizontal
|
||||||
style:
|
|
||||||
transform: none
|
|
||||||
right: 20px
|
|
||||||
top: 180px
|
|
||||||
service: sonoff.send_command
|
service: sonoff.send_command
|
||||||
data_left:
|
data_left:
|
||||||
device: '048123'
|
device: '048123'
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
protected doubleClickTimer = new Timer();
|
protected doubleClickTimer = new Timer();
|
||||||
|
|
||||||
protected held = false;
|
protected held = false;
|
||||||
|
protected started = false;
|
||||||
|
|
||||||
public connectedCallback(): void {
|
public connectedCallback(): void {
|
||||||
[
|
[
|
||||||
@@ -83,7 +84,12 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
this.held = true;
|
this.held = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Without this check we get double start_tap events from touchstart and
|
||||||
|
// mousedown events (on Android).
|
||||||
|
if (!this.started) {
|
||||||
|
this.started = true;
|
||||||
fireEvent(element, 'action', { action: 'start_tap' });
|
fireEvent(element, 'action', { action: 'start_tap' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const end = (ev: Event): void => {
|
const end = (ev: Event): void => {
|
||||||
@@ -105,6 +111,7 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
|
|
||||||
this.holdTimer.stop();
|
this.holdTimer.stop();
|
||||||
|
|
||||||
|
this.started = false;
|
||||||
fireEvent(element, 'action', { action: 'end_tap' });
|
fireEvent(element, 'action', { action: 'end_tap' });
|
||||||
|
|
||||||
if (options?.hasHold && this.held) {
|
if (options?.hasHold && this.held) {
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ import { Entity } from '../../utils/ha/entity-registry/types';
|
|||||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
import { RequestCache } from '../cache';
|
import { RequestCache } from '../cache';
|
||||||
|
import { Camera } from '../camera';
|
||||||
import { CameraManagerEngine } from '../engine';
|
import { CameraManagerEngine } from '../engine';
|
||||||
import { CameraInitializationError } from '../error';
|
import { CameraInitializationError } from '../error';
|
||||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||||
import { rangesOverlap } from '../range';
|
import { rangesOverlap } from '../range';
|
||||||
|
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||||
import {
|
import {
|
||||||
CameraConfigs,
|
|
||||||
CameraEndpoint,
|
CameraEndpoint,
|
||||||
CameraManagerCameraCapabilities,
|
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
@@ -141,7 +141,7 @@ export class BrowseMediaCameraManagerEngine
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistryManager: EntityRegistryManager,
|
entityRegistryManager: EntityRegistryManager,
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): Promise<CameraConfig> {
|
): Promise<Camera> {
|
||||||
const entity = cameraConfig.camera_entity
|
const entity = cameraConfig.camera_entity
|
||||||
? await entityRegistryManager.getEntity(hass, cameraConfig.camera_entity)
|
? await entityRegistryManager.getEntity(hass, cameraConfig.camera_entity)
|
||||||
: null;
|
: null;
|
||||||
@@ -152,11 +152,20 @@ export class BrowseMediaCameraManagerEngine
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._cameraEntities.set(cameraConfig.camera_entity, entity);
|
this._cameraEntities.set(cameraConfig.camera_entity, entity);
|
||||||
return cameraConfig;
|
|
||||||
|
return new Camera(cameraConfig, this, {
|
||||||
|
canFavoriteEvents: false,
|
||||||
|
canFavoriteRecordings: false,
|
||||||
|
canSeek: false,
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: false,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
supportsTimeline: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultEventQuery(
|
public generateDefaultEventQuery(
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialEventQuery,
|
query: PartialEventQuery,
|
||||||
): EventQuery[] | null {
|
): EventQuery[] | null {
|
||||||
@@ -191,21 +200,6 @@ export class BrowseMediaCameraManagerEngine
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameraCapabilities(
|
|
||||||
cameraConfig: CameraConfig,
|
|
||||||
): CameraManagerCameraCapabilities | null {
|
|
||||||
const parentCapabilities = super.getCameraCapabilities(cameraConfig);
|
|
||||||
if (!parentCapabilities) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...parentCapabilities,
|
|
||||||
supportsClips: true,
|
|
||||||
supportsSnapshots: true,
|
|
||||||
supportsTimeline: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities {
|
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { CameraConfig } from '../config/types';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
|
import { CameraManagerEngine } from './engine';
|
||||||
|
import { CameraNoIDError } from './error';
|
||||||
|
import { CameraManagerCameraCapabilities } from './types';
|
||||||
|
|
||||||
|
export class Camera {
|
||||||
|
protected _config: CameraConfig;
|
||||||
|
protected _engine: CameraManagerEngine;
|
||||||
|
protected _capabilities: CameraManagerCameraCapabilities;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: CameraConfig,
|
||||||
|
engine: CameraManagerEngine,
|
||||||
|
capabilities: CameraManagerCameraCapabilities,
|
||||||
|
) {
|
||||||
|
this._config = config;
|
||||||
|
this._engine = engine;
|
||||||
|
this._capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfig(): CameraConfig {
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setID(cameraID: string): void {
|
||||||
|
this._config.id = cameraID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getID(): string {
|
||||||
|
if (this._config.id) {
|
||||||
|
return this._config.id;
|
||||||
|
}
|
||||||
|
throw new CameraNoIDError(localize('error.no_camera_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEngine(): CameraManagerEngine {
|
||||||
|
return this._engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCapabilities(): CameraManagerCameraCapabilities {
|
||||||
|
return this._capabilities;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import { CameraConfig } from '../config/types';
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
PTZAction,
|
||||||
|
PTZPhase,
|
||||||
|
} from '../config/types';
|
||||||
import { ExtendedHomeAssistant } from '../types';
|
import { ExtendedHomeAssistant } from '../types';
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { Camera } from './camera';
|
||||||
|
import { CameraManagerReadOnlyConfigStore } from './store';
|
||||||
import {
|
import {
|
||||||
CameraConfigs,
|
|
||||||
CameraEndpoint,
|
CameraEndpoint,
|
||||||
CameraEndpoints,
|
CameraEndpoints,
|
||||||
CameraEndpointsContext,
|
CameraEndpointsContext,
|
||||||
CameraManagerCameraCapabilities,
|
|
||||||
CameraManagerCameraMetadata,
|
CameraManagerCameraMetadata,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
@@ -37,57 +41,57 @@ export interface CameraManagerEngine {
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistryManager: EntityRegistryManager,
|
entityRegistryManager: EntityRegistryManager,
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): Promise<CameraConfig>;
|
): Promise<Camera>;
|
||||||
|
|
||||||
generateDefaultEventQuery(
|
generateDefaultEventQuery(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialEventQuery,
|
query: PartialEventQuery,
|
||||||
): EventQuery[] | null;
|
): EventQuery[] | null;
|
||||||
|
|
||||||
generateDefaultRecordingQuery(
|
generateDefaultRecordingQuery(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingQuery,
|
query: PartialRecordingQuery,
|
||||||
): RecordingQuery[] | null;
|
): RecordingQuery[] | null;
|
||||||
|
|
||||||
generateDefaultRecordingSegmentsQuery(
|
generateDefaultRecordingSegmentsQuery(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingSegmentsQuery,
|
query: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery[] | null;
|
): RecordingSegmentsQuery[] | null;
|
||||||
|
|
||||||
getEvents(
|
getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<EventQueryResultsMap | null>;
|
): Promise<EventQueryResultsMap | null>;
|
||||||
|
|
||||||
getRecordings(
|
getRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingQueryResultsMap | null>;
|
): Promise<RecordingQueryResultsMap | null>;
|
||||||
|
|
||||||
getRecordingSegments(
|
getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: RecordingSegmentsQuery,
|
query: RecordingSegmentsQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingSegmentsQueryResultsMap | null>;
|
): Promise<RecordingSegmentsQueryResultsMap | null>;
|
||||||
|
|
||||||
generateMediaFromEvents(
|
generateMediaFromEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
results: QueryReturnType<EventQuery>,
|
results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null;
|
): ViewMedia[] | null;
|
||||||
|
|
||||||
generateMediaFromRecordings(
|
generateMediaFromRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
results: QueryReturnType<RecordingQuery>,
|
results: QueryReturnType<RecordingQuery>,
|
||||||
): ViewMedia[] | null;
|
): ViewMedia[] | null;
|
||||||
@@ -109,7 +113,7 @@ export interface CameraManagerEngine {
|
|||||||
|
|
||||||
getMediaSeekTime(
|
getMediaSeekTime(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
target: Date,
|
target: Date,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
@@ -117,7 +121,7 @@ export interface CameraManagerEngine {
|
|||||||
|
|
||||||
getMediaMetadata(
|
getMediaMetadata(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: MediaMetadataQuery,
|
query: MediaMetadataQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<MediaMetadataQueryResultsMap | null>;
|
): Promise<MediaMetadataQueryResultsMap | null>;
|
||||||
@@ -127,14 +131,20 @@ export interface CameraManagerEngine {
|
|||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): CameraManagerCameraMetadata;
|
): CameraManagerCameraMetadata;
|
||||||
|
|
||||||
getCameraCapabilities(
|
|
||||||
cameraConfig: CameraConfig,
|
|
||||||
): CameraManagerCameraCapabilities | null;
|
|
||||||
|
|
||||||
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
|
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
|
||||||
|
|
||||||
getCameraEndpoints(
|
getCameraEndpoints(
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
context?: CameraEndpointsContext,
|
context?: CameraEndpointsContext,
|
||||||
): CameraEndpoints | null;
|
): CameraEndpoints | null;
|
||||||
|
|
||||||
|
executePTZAction(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
action: PTZAction,
|
||||||
|
options?: {
|
||||||
|
phase?: PTZPhase,
|
||||||
|
preset?: string,
|
||||||
|
}
|
||||||
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import { FrigateCardError } from '../types.js';
|
import { FrigateCardError } from '../types.js';
|
||||||
|
|
||||||
export class CameraInitializationError extends FrigateCardError {}
|
export class CameraInitializationError extends FrigateCardError {}
|
||||||
|
export class CameraNoIDError extends FrigateCardError {}
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import orderBy from 'lodash-es/orderBy';
|
|||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import uniq from 'lodash-es/uniq';
|
import uniq from 'lodash-es/uniq';
|
||||||
import uniqWith from 'lodash-es/uniqWith';
|
import uniqWith from 'lodash-es/uniqWith';
|
||||||
import { CameraConfig } from '../../config/types';
|
import { CameraConfig, PTZAction, PTZPhase } from '../../config/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import { ExtendedHomeAssistant } from '../../types';
|
import { ExtendedHomeAssistant } from '../../types';
|
||||||
import {
|
import {
|
||||||
allPromises,
|
allPromises,
|
||||||
|
errorToConsole,
|
||||||
formatDate,
|
formatDate,
|
||||||
prettifyTitle,
|
prettifyTitle,
|
||||||
runWhenIdleIfSupported,
|
runWhenIdleIfSupported,
|
||||||
@@ -24,6 +25,7 @@ import { Entity } from '../../utils/ha/entity-registry/types';
|
|||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||||
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
||||||
|
import { Camera } from '../camera';
|
||||||
import {
|
import {
|
||||||
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||||
CameraManagerEngine,
|
CameraManagerEngine,
|
||||||
@@ -31,12 +33,11 @@ import {
|
|||||||
import { CameraInitializationError } from '../error';
|
import { CameraInitializationError } from '../error';
|
||||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||||
import { DateRange } from '../range';
|
import { DateRange } from '../range';
|
||||||
|
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||||
import {
|
import {
|
||||||
CameraConfigs,
|
|
||||||
CameraEndpoint,
|
CameraEndpoint,
|
||||||
CameraEndpoints,
|
CameraEndpoints,
|
||||||
CameraEndpointsContext,
|
CameraEndpointsContext,
|
||||||
CameraManagerCameraCapabilities,
|
|
||||||
CameraManagerCameraMetadata,
|
CameraManagerCameraMetadata,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
@@ -51,6 +52,8 @@ import {
|
|||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialRecordingQuery,
|
PartialRecordingQuery,
|
||||||
PartialRecordingSegmentsQuery,
|
PartialRecordingSegmentsQuery,
|
||||||
|
PTZCapabilities,
|
||||||
|
PTZMovementType,
|
||||||
QueryResults,
|
QueryResults,
|
||||||
QueryResultsType,
|
QueryResultsType,
|
||||||
QueryReturnType,
|
QueryReturnType,
|
||||||
@@ -74,12 +77,14 @@ import {
|
|||||||
getRecordingSegments,
|
getRecordingSegments,
|
||||||
getRecordingsSummary,
|
getRecordingsSummary,
|
||||||
retainEvent,
|
retainEvent,
|
||||||
|
getPTZInfo,
|
||||||
} from './requests';
|
} from './requests';
|
||||||
import {
|
import {
|
||||||
FrigateEventQueryResults,
|
FrigateEventQueryResults,
|
||||||
FrigateRecording,
|
FrigateRecording,
|
||||||
FrigateRecordingQueryResults,
|
FrigateRecordingQueryResults,
|
||||||
FrigateRecordingSegmentsQueryResults,
|
FrigateRecordingSegmentsQueryResults,
|
||||||
|
PTZInfo,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||||
@@ -144,7 +149,7 @@ export class FrigateCameraManagerEngine
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistryManager: EntityRegistryManager,
|
entityRegistryManager: EntityRegistryManager,
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): Promise<CameraConfig> {
|
): Promise<Camera> {
|
||||||
const hasCameraName = !!cameraConfig.frigate?.camera_name;
|
const hasCameraName = !!cameraConfig.frigate?.camera_name;
|
||||||
const hasAutoTriggers =
|
const hasAutoTriggers =
|
||||||
cameraConfig.triggers.motion || cameraConfig.triggers.occupancy;
|
cameraConfig.triggers.motion || cameraConfig.triggers.occupancy;
|
||||||
@@ -207,7 +212,64 @@ export class FrigateCameraManagerEngine
|
|||||||
cameraConfig.triggers.entities = uniq(cameraConfig.triggers.entities);
|
cameraConfig.triggers.entities = uniq(cameraConfig.triggers.entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cameraConfig;
|
const ptz = await this._getPTZCapabilities(hass, cameraConfig);
|
||||||
|
|
||||||
|
const isBirdseye = this._isBirdseye(cameraConfig);
|
||||||
|
return new Camera(cameraConfig, this, {
|
||||||
|
canFavoriteEvents: !isBirdseye,
|
||||||
|
canFavoriteRecordings: !isBirdseye,
|
||||||
|
canSeek: true,
|
||||||
|
supportsClips: !isBirdseye,
|
||||||
|
supportsSnapshots: !isBirdseye,
|
||||||
|
supportsRecordings: !isBirdseye,
|
||||||
|
supportsTimeline: !isBirdseye,
|
||||||
|
|
||||||
|
...(ptz && { ptz: ptz }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _getPTZCapabilities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): Promise<PTZCapabilities | null> {
|
||||||
|
if (!cameraConfig.frigate.camera_name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ptzInfo: PTZInfo | null = null;
|
||||||
|
try {
|
||||||
|
ptzInfo = await getPTZInfo(
|
||||||
|
hass,
|
||||||
|
cameraConfig.frigate.client_id,
|
||||||
|
cameraConfig.frigate.camera_name,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panTilt: PTZMovementType[] = [
|
||||||
|
...(ptzInfo.features?.includes('pt') ? ['continuous' as const] : []),
|
||||||
|
...(ptzInfo.features?.includes('pt-r') ? ['relative' as const] : []),
|
||||||
|
];
|
||||||
|
const zoom: PTZMovementType[] = [
|
||||||
|
...(ptzInfo.features?.includes('zoom') ? ['continuous' as const] : []),
|
||||||
|
...(ptzInfo.features?.includes('zoom-r') ? ['relative' as const] : []),
|
||||||
|
];
|
||||||
|
const presets = ptzInfo.presets;
|
||||||
|
|
||||||
|
if (panTilt.length || zoom.length || presets?.length) {
|
||||||
|
return {
|
||||||
|
...(panTilt && { panTilt: panTilt }),
|
||||||
|
...(zoom && { zoom: zoom }),
|
||||||
|
...(presets && { presets: presets }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isBirdseye(cameraConfig: CameraConfig): boolean {
|
||||||
|
return cameraConfig.frigate.camera_name === CAMERA_BIRDSEYE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,13 +392,11 @@ export class FrigateCameraManagerEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultEventQuery(
|
public generateDefaultEventQuery(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query?: PartialEventQuery,
|
query?: PartialEventQuery,
|
||||||
): EventQuery[] | null {
|
): EventQuery[] | null {
|
||||||
const relevantCameraConfigs = Array.from(cameraIDs).map((cameraID) =>
|
const relevantCameraConfigs = [...store.getCameraConfigs(cameraIDs)];
|
||||||
cameras.get(cameraID),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If all cameras specify exactly the same zones or labels (incl. none), we
|
// If all cameras specify exactly the same zones or labels (incl. none), we
|
||||||
// can use a single batch query which will be better performance wise,
|
// can use a single batch query which will be better performance wise,
|
||||||
@@ -365,7 +425,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
const output: EventQuery[] = [];
|
const output: EventQuery[] = [];
|
||||||
for (const cameraID of cameraIDs) {
|
for (const cameraID of cameraIDs) {
|
||||||
const cameraConfig = cameras.get(cameraID);
|
const cameraConfig = store.getCameraConfig(cameraID);
|
||||||
if (cameraConfig) {
|
if (cameraConfig) {
|
||||||
output.push({
|
output.push({
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
@@ -384,7 +444,7 @@ export class FrigateCameraManagerEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingQuery(
|
public generateDefaultRecordingQuery(
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query?: PartialRecordingQuery,
|
query?: PartialRecordingQuery,
|
||||||
): RecordingQuery[] {
|
): RecordingQuery[] {
|
||||||
@@ -398,7 +458,7 @@ export class FrigateCameraManagerEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingSegmentsQuery(
|
public generateDefaultRecordingSegmentsQuery(
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingSegmentsQuery,
|
query: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery[] | null {
|
): RecordingSegmentsQuery[] | null {
|
||||||
@@ -431,12 +491,12 @@ export class FrigateCameraManagerEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _buildInstanceToCameraIDMapFromQuery(
|
protected _buildInstanceToCameraIDMapFromQuery(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
): Map<string, Set<string>> {
|
): Map<string, Set<string>> {
|
||||||
const output: Map<string, Set<string>> = new Map();
|
const output: Map<string, Set<string>> = new Map();
|
||||||
for (const cameraID of cameraIDs) {
|
for (const cameraID of cameraIDs) {
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||||
const clientID = cameraConfig?.frigate.client_id;
|
const clientID = cameraConfig?.frigate.client_id;
|
||||||
if (clientID) {
|
if (clientID) {
|
||||||
if (!output.has(clientID)) {
|
if (!output.has(clientID)) {
|
||||||
@@ -449,12 +509,12 @@ export class FrigateCameraManagerEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getFrigateCameraNamesForCameraIDs(
|
protected _getFrigateCameraNamesForCameraIDs(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
): Set<string> {
|
): Set<string> {
|
||||||
const output = new Set<string>();
|
const output = new Set<string>();
|
||||||
for (const cameraID of cameraIDs) {
|
for (const cameraID of cameraIDs) {
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||||
if (cameraConfig?.frigate.camera_name) {
|
if (cameraConfig?.frigate.camera_name) {
|
||||||
output.add(cameraConfig.frigate.camera_name);
|
output.add(cameraConfig.frigate.camera_name);
|
||||||
}
|
}
|
||||||
@@ -464,7 +524,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
public async getEvents(
|
public async getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<EventQueryResultsMap | null> {
|
): Promise<EventQueryResultsMap | null> {
|
||||||
@@ -487,7 +547,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
const nativeQuery: NativeFrigateEventQuery = {
|
const nativeQuery: NativeFrigateEventQuery = {
|
||||||
instance_id: instanceID,
|
instance_id: instanceID,
|
||||||
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs)),
|
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(store, cameraIDs)),
|
||||||
...(query.what && { labels: Array.from(query.what) }),
|
...(query.what && { labels: Array.from(query.what) }),
|
||||||
...(query.where && { zones: Array.from(query.where) }),
|
...(query.where && { zones: Array.from(query.where) }),
|
||||||
...(query.tags && { sub_labels: Array.from(query.tags) }),
|
...(query.tags && { sub_labels: Array.from(query.tags) }),
|
||||||
@@ -518,10 +578,7 @@ export class FrigateCameraManagerEngine
|
|||||||
// Frigate allows multiple cameras to be searched for events in a single
|
// Frigate allows multiple cameras to be searched for events in a single
|
||||||
// query. Break them down into groups of cameras per Frigate instance, then
|
// query. Break them down into groups of cameras per Frigate instance, then
|
||||||
// query once per instance for all cameras in that instance.
|
// query once per instance for all cameras in that instance.
|
||||||
const instances = this._buildInstanceToCameraIDMapFromQuery(
|
const instances = this._buildInstanceToCameraIDMapFromQuery(store, query.cameraIDs);
|
||||||
cameras,
|
|
||||||
query.cameraIDs,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(instances.keys()).map((instanceID) =>
|
Array.from(instances.keys()).map((instanceID) =>
|
||||||
@@ -533,7 +590,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
public async getRecordings(
|
public async getRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingQueryResultsMap | null> {
|
): Promise<RecordingQueryResultsMap | null> {
|
||||||
@@ -551,7 +608,7 @@ export class FrigateCameraManagerEngine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||||
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -619,7 +676,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
public async getRecordingSegments(
|
public async getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: RecordingSegmentsQuery,
|
query: RecordingSegmentsQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||||
@@ -630,7 +687,7 @@ export class FrigateCameraManagerEngine
|
|||||||
cameraID: string,
|
cameraID: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const query = { ...baseQuery, cameraIDs: new Set([cameraID]) };
|
const query = { ...baseQuery, cameraIDs: new Set([cameraID]) };
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||||
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -687,12 +744,12 @@ export class FrigateCameraManagerEngine
|
|||||||
Array.from(query.cameraIDs).map((cameraID) => processQuery(query, cameraID)),
|
Array.from(query.cameraIDs).map((cameraID) => processQuery(query, cameraID)),
|
||||||
);
|
);
|
||||||
|
|
||||||
runWhenIdleIfSupported(() => this._throttledSegmentGarbageCollector(hass, cameras));
|
runWhenIdleIfSupported(() => this._throttledSegmentGarbageCollector(hass, store));
|
||||||
return output.size ? output : null;
|
return output.size ? output : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getCameraIDMatch(
|
protected _getCameraIDMatch(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: DataQuery,
|
query: DataQuery,
|
||||||
instanceID: string,
|
instanceID: string,
|
||||||
cameraName: string,
|
cameraName: string,
|
||||||
@@ -704,7 +761,7 @@ export class FrigateCameraManagerEngine
|
|||||||
if (query.cameraIDs.size === 1) {
|
if (query.cameraIDs.size === 1) {
|
||||||
return [...query.cameraIDs][0];
|
return [...query.cameraIDs][0];
|
||||||
}
|
}
|
||||||
for (const [cameraID, cameraConfig] of cameras.entries()) {
|
for (const [cameraID, cameraConfig] of store.getCameraConfigEntries()) {
|
||||||
if (
|
if (
|
||||||
cameraConfig.frigate.client_id === instanceID &&
|
cameraConfig.frigate.client_id === instanceID &&
|
||||||
cameraConfig.frigate.camera_name === cameraName
|
cameraConfig.frigate.camera_name === cameraName
|
||||||
@@ -717,7 +774,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
public generateMediaFromEvents(
|
public generateMediaFromEvents(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
results: QueryReturnType<EventQuery>,
|
results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -728,7 +785,7 @@ export class FrigateCameraManagerEngine
|
|||||||
const output: ViewMedia[] = [];
|
const output: ViewMedia[] = [];
|
||||||
for (const event of results.events) {
|
for (const event of results.events) {
|
||||||
const cameraID = this._getCameraIDMatch(
|
const cameraID = this._getCameraIDMatch(
|
||||||
cameras,
|
store,
|
||||||
query,
|
query,
|
||||||
results.instanceID,
|
results.instanceID,
|
||||||
event.camera,
|
event.camera,
|
||||||
@@ -736,7 +793,7 @@ export class FrigateCameraManagerEngine
|
|||||||
if (!cameraID) {
|
if (!cameraID) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||||
if (!cameraConfig) {
|
if (!cameraConfig) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -771,7 +828,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
public generateMediaFromRecordings(
|
public generateMediaFromRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: RecordingQuery,
|
_query: RecordingQuery,
|
||||||
results: QueryReturnType<RecordingQuery>,
|
results: QueryReturnType<RecordingQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -781,7 +838,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
const output: ViewMedia[] = [];
|
const output: ViewMedia[] = [];
|
||||||
for (const recording of results.recordings) {
|
for (const recording of results.recordings) {
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, recording.cameraID);
|
const cameraConfig = this._getQueryableCameraConfig(store, recording.cameraID);
|
||||||
if (!cameraConfig) {
|
if (!cameraConfig) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -809,7 +866,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
public async getMediaSeekTime(
|
public async getMediaSeekTime(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
target: Date,
|
target: Date,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
@@ -828,7 +885,7 @@ export class FrigateCameraManagerEngine
|
|||||||
type: QueryType.RecordingSegments,
|
type: QueryType.RecordingSegments,
|
||||||
};
|
};
|
||||||
|
|
||||||
const results = await this.getRecordingSegments(hass, cameras, query, engineOptions);
|
const results = await this.getRecordingSegments(hass, store, query, engineOptions);
|
||||||
|
|
||||||
if (results) {
|
if (results) {
|
||||||
return this._getSeekTimeInSegments(
|
return this._getSeekTimeInSegments(
|
||||||
@@ -843,11 +900,11 @@ export class FrigateCameraManagerEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getQueryableCameraConfig(
|
protected _getQueryableCameraConfig(
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
): CameraConfig | null {
|
): CameraConfig | null {
|
||||||
const cameraConfig = cameras.get(cameraID);
|
const cameraConfig = store.getCameraConfig(cameraID);
|
||||||
if (!cameraConfig || cameraConfig.frigate.camera_name == CAMERA_BIRDSEYE) {
|
if (!cameraConfig || this._isBirdseye(cameraConfig)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return cameraConfig;
|
return cameraConfig;
|
||||||
@@ -865,7 +922,7 @@ export class FrigateCameraManagerEngine
|
|||||||
|
|
||||||
public async getMediaMetadata(
|
public async getMediaMetadata(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: MediaMetadataQuery,
|
query: MediaMetadataQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<MediaMetadataQueryResultsMap | null> {
|
): Promise<MediaMetadataQueryResultsMap | null> {
|
||||||
@@ -885,16 +942,13 @@ export class FrigateCameraManagerEngine
|
|||||||
const days: Set<string> = new Set();
|
const days: Set<string> = new Set();
|
||||||
const tags: Set<string> = new Set();
|
const tags: Set<string> = new Set();
|
||||||
|
|
||||||
const instances = this._buildInstanceToCameraIDMapFromQuery(
|
const instances = this._buildInstanceToCameraIDMapFromQuery(store, query.cameraIDs);
|
||||||
cameras,
|
|
||||||
query.cameraIDs,
|
|
||||||
);
|
|
||||||
|
|
||||||
const processEventSummary = async (
|
const processEventSummary = async (
|
||||||
instanceID: string,
|
instanceID: string,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const cameraNames = this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs);
|
const cameraNames = this._getFrigateCameraNamesForCameraIDs(store, cameraIDs);
|
||||||
for (const entry of await getEventSummary(hass, instanceID)) {
|
for (const entry of await getEventSummary(hass, instanceID)) {
|
||||||
if (!cameraNames.has(entry.camera)) {
|
if (!cameraNames.has(entry.camera)) {
|
||||||
// If this entry applies to a camera that *is* in this Frigate
|
// If this entry applies to a camera that *is* in this Frigate
|
||||||
@@ -919,7 +973,7 @@ export class FrigateCameraManagerEngine
|
|||||||
const processRecordings = async (cameraIDs: Set<string>): Promise<void> => {
|
const processRecordings = async (cameraIDs: Set<string>): Promise<void> => {
|
||||||
const recordings = await this.getRecordings(
|
const recordings = await this.getRecordings(
|
||||||
hass,
|
hass,
|
||||||
cameras,
|
store,
|
||||||
{
|
{
|
||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
cameraIDs: cameraIDs,
|
cameraIDs: cameraIDs,
|
||||||
@@ -977,7 +1031,7 @@ export class FrigateCameraManagerEngine
|
|||||||
*/
|
*/
|
||||||
protected async _garbageCollectSegments(
|
protected async _garbageCollectSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
||||||
const recordingQuery: RecordingQuery = {
|
const recordingQuery: RecordingQuery = {
|
||||||
@@ -992,7 +1046,7 @@ export class FrigateCameraManagerEngine
|
|||||||
return `${cameraID}/${startTime.getDate()}/${startTime.getHours()}`;
|
return `${cameraID}/${startTime.getDate()}/${startTime.getHours()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const results = await this.getRecordings(hass, cameras, recordingQuery);
|
const results = await this.getRecordings(hass, store, recordingQuery);
|
||||||
if (!results) {
|
if (!results) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1053,21 +1107,6 @@ export class FrigateCameraManagerEngine
|
|||||||
return seekMilliseconds / 1000;
|
return seekMilliseconds / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameraCapabilities(
|
|
||||||
cameraConfig: CameraConfig,
|
|
||||||
): CameraManagerCameraCapabilities {
|
|
||||||
const isBirdseye = cameraConfig.frigate.camera_name === CAMERA_BIRDSEYE;
|
|
||||||
return {
|
|
||||||
canFavoriteEvents: !isBirdseye,
|
|
||||||
canFavoriteRecordings: !isBirdseye,
|
|
||||||
canSeek: true,
|
|
||||||
supportsClips: !isBirdseye,
|
|
||||||
supportsSnapshots: !isBirdseye,
|
|
||||||
supportsRecordings: !isBirdseye,
|
|
||||||
supportsTimeline: !isBirdseye,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities {
|
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities {
|
||||||
return {
|
return {
|
||||||
canFavorite: ViewMediaClassifier.isEvent(media),
|
canFavorite: ViewMediaClassifier.isEvent(media),
|
||||||
@@ -1189,4 +1228,44 @@ export class FrigateCameraManagerEngine
|
|||||||
...(webrtcCard && { webrtcCard: webrtcCard }),
|
...(webrtcCard && { webrtcCard: webrtcCard }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async executePTZAction(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
action: PTZAction,
|
||||||
|
options?: {
|
||||||
|
phase?: PTZPhase;
|
||||||
|
preset?: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const cameraEntity = cameraConfig.camera_entity;
|
||||||
|
|
||||||
|
if (action === 'preset' && !options?.preset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Awkward translation between card action and service parameters:
|
||||||
|
// https://github.com/blakeblackshear/frigate-hass-integration/blob/dev/custom_components/frigate/services.yaml
|
||||||
|
await hass.callService('frigate', 'ptz', {
|
||||||
|
entity_id: cameraEntity,
|
||||||
|
action:
|
||||||
|
options?.phase === 'stop'
|
||||||
|
? 'stop'
|
||||||
|
: action === 'zoom_in' || action === 'zoom_out'
|
||||||
|
? 'zoom'
|
||||||
|
: action === 'preset'
|
||||||
|
? 'preset'
|
||||||
|
: 'move',
|
||||||
|
...(options?.phase !== 'stop' && {
|
||||||
|
argument:
|
||||||
|
action === 'zoom_in'
|
||||||
|
? 'in'
|
||||||
|
: action === 'zoom_out'
|
||||||
|
? 'out'
|
||||||
|
: action === 'preset'
|
||||||
|
? options?.preset
|
||||||
|
: action,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
eventSummarySchema,
|
eventSummarySchema,
|
||||||
FrigateEvent,
|
FrigateEvent,
|
||||||
frigateEventsSchema,
|
frigateEventsSchema,
|
||||||
|
PTZInfo,
|
||||||
|
ptzInfoSchema,
|
||||||
recordingSegmentsSchema,
|
recordingSegmentsSchema,
|
||||||
RecordingSummary,
|
RecordingSummary,
|
||||||
recordingSummarySchema,
|
recordingSummarySchema,
|
||||||
@@ -151,3 +153,20 @@ export const getEventSummary = async (
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPTZInfo = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
clientID: string,
|
||||||
|
cameraName: string,
|
||||||
|
): Promise<PTZInfo> => {
|
||||||
|
return await homeAssistantWSRequest(
|
||||||
|
hass,
|
||||||
|
ptzInfoSchema,
|
||||||
|
{
|
||||||
|
type: 'frigate/ptz/info',
|
||||||
|
instance_id: clientID,
|
||||||
|
camera: cameraName,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ export const eventSummarySchema = z
|
|||||||
.array();
|
.array();
|
||||||
export type EventSummary = z.infer<typeof eventSummarySchema>;
|
export type EventSummary = z.infer<typeof eventSummarySchema>;
|
||||||
|
|
||||||
|
export const ptzInfoSchema = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
features: z.string().array().optional(),
|
||||||
|
presets: z.string().array().optional(),
|
||||||
|
});
|
||||||
|
export type PTZInfo = z.infer<typeof ptzInfoSchema>;
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// Frigate concrete query results
|
// Frigate concrete query results
|
||||||
// ==============================
|
// ==============================
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import { CameraConfig } from '../../config/types';
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
PTZAction,
|
||||||
|
PTZPhase
|
||||||
|
} from '../../config/types';
|
||||||
import { ExtendedHomeAssistant } from '../../types';
|
import { ExtendedHomeAssistant } from '../../types';
|
||||||
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
|
import { Camera } from '../camera';
|
||||||
import { CameraManagerEngine } from '../engine';
|
import { CameraManagerEngine } from '../engine';
|
||||||
|
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||||
import {
|
import {
|
||||||
CameraConfigs,
|
|
||||||
CameraEndpoint,
|
CameraEndpoint,
|
||||||
CameraEndpoints,
|
CameraEndpoints,
|
||||||
CameraEndpointsContext,
|
CameraEndpointsContext,
|
||||||
CameraManagerCameraCapabilities,
|
|
||||||
CameraManagerCameraMetadata,
|
CameraManagerCameraMetadata,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
@@ -29,7 +33,7 @@ import {
|
|||||||
RecordingQuery,
|
RecordingQuery,
|
||||||
RecordingQueryResultsMap,
|
RecordingQueryResultsMap,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getCameraEntityFromConfig, getDefaultGo2RTCEndpoint } from '../utils.js';
|
import { getCameraEntityFromConfig, getDefaultGo2RTCEndpoint } from '../utils.js';
|
||||||
|
|
||||||
@@ -42,12 +46,20 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_entityRegistryManager: EntityRegistryManager,
|
_entityRegistryManager: EntityRegistryManager,
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): Promise<CameraConfig> {
|
): Promise<Camera> {
|
||||||
return cameraConfig;
|
return new Camera(cameraConfig, this, {
|
||||||
|
canFavoriteEvents: false,
|
||||||
|
canFavoriteRecordings: false,
|
||||||
|
canSeek: false,
|
||||||
|
supportsClips: false,
|
||||||
|
supportsRecordings: false,
|
||||||
|
supportsSnapshots: false,
|
||||||
|
supportsTimeline: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultEventQuery(
|
public generateDefaultEventQuery(
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_cameraIDs: Set<string>,
|
_cameraIDs: Set<string>,
|
||||||
_query: PartialEventQuery,
|
_query: PartialEventQuery,
|
||||||
): EventQuery[] | null {
|
): EventQuery[] | null {
|
||||||
@@ -55,7 +67,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingQuery(
|
public generateDefaultRecordingQuery(
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_cameraIDs: Set<string>,
|
_cameraIDs: Set<string>,
|
||||||
_query: PartialRecordingQuery,
|
_query: PartialRecordingQuery,
|
||||||
): RecordingQuery[] | null {
|
): RecordingQuery[] | null {
|
||||||
@@ -63,7 +75,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingSegmentsQuery(
|
public generateDefaultRecordingSegmentsQuery(
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_cameraIDs: Set<string>,
|
_cameraIDs: Set<string>,
|
||||||
_query: PartialRecordingSegmentsQuery,
|
_query: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery[] | null {
|
): RecordingSegmentsQuery[] | null {
|
||||||
@@ -72,7 +84,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getEvents(
|
public async getEvents(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: EventQuery,
|
_query: EventQuery,
|
||||||
_engineOptions?: EngineOptions,
|
_engineOptions?: EngineOptions,
|
||||||
): Promise<EventQueryResultsMap | null> {
|
): Promise<EventQueryResultsMap | null> {
|
||||||
@@ -81,7 +93,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getRecordings(
|
public async getRecordings(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: RecordingQuery,
|
_query: RecordingQuery,
|
||||||
_engineOptions?: EngineOptions,
|
_engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingQueryResultsMap | null> {
|
): Promise<RecordingQueryResultsMap | null> {
|
||||||
@@ -90,7 +102,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getRecordingSegments(
|
public async getRecordingSegments(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: RecordingSegmentsQuery,
|
_query: RecordingSegmentsQuery,
|
||||||
_engineOptions?: EngineOptions,
|
_engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||||
@@ -99,7 +111,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public generateMediaFromEvents(
|
public generateMediaFromEvents(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: EventQuery,
|
_query: EventQuery,
|
||||||
_results: QueryReturnType<EventQuery>,
|
_results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -108,7 +120,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public generateMediaFromRecordings(
|
public generateMediaFromRecordings(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: RecordingQuery,
|
_query: RecordingQuery,
|
||||||
_results: QueryReturnType<RecordingQuery>,
|
_results: QueryReturnType<RecordingQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -138,7 +150,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getMediaSeekTime(
|
public async getMediaSeekTime(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_media: ViewMedia,
|
_media: ViewMedia,
|
||||||
_target: Date,
|
_target: Date,
|
||||||
_engineOptions?: EngineOptions,
|
_engineOptions?: EngineOptions,
|
||||||
@@ -148,7 +160,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getMediaMetadata(
|
public async getMediaMetadata(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: MediaMetadataQuery,
|
_query: MediaMetadataQuery,
|
||||||
_engineOptions?: EngineOptions,
|
_engineOptions?: EngineOptions,
|
||||||
): Promise<MediaMetadataQueryResultsMap | null> {
|
): Promise<MediaMetadataQueryResultsMap | null> {
|
||||||
@@ -173,20 +185,6 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameraCapabilities(
|
|
||||||
_cameraConfig: CameraConfig,
|
|
||||||
): CameraManagerCameraCapabilities | null {
|
|
||||||
return {
|
|
||||||
canFavoriteEvents: false,
|
|
||||||
canFavoriteRecordings: false,
|
|
||||||
canSeek: false,
|
|
||||||
supportsClips: false,
|
|
||||||
supportsRecordings: false,
|
|
||||||
supportsSnapshots: false,
|
|
||||||
supportsTimeline: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities | null {
|
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -202,4 +200,16 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async executePTZAction(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameraConfig: CameraConfig,
|
||||||
|
_action: PTZAction,
|
||||||
|
_options?: {
|
||||||
|
phase?: PTZPhase;
|
||||||
|
preset?: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
// Pass.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
|
||||||
import add from 'date-fns/add';
|
import add from 'date-fns/add';
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
import merge from 'lodash-es/merge.js';
|
import merge from 'lodash-es/merge.js';
|
||||||
import sum from 'lodash-es/sum';
|
import sum from 'lodash-es/sum';
|
||||||
import { CardCameraAPI } from '../card-controller/types.js';
|
import { CardCameraAPI } from '../card-controller/types.js';
|
||||||
import { CameraConfig, CamerasConfig } from '../config/types.js';
|
import { CameraConfig, CamerasConfig, PTZAction, PTZPhase } from '../config/types.js';
|
||||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||||
import { getCameraID } from '../utils/camera.js';
|
import { getCameraID } from '../utils/camera.js';
|
||||||
import { log } from '../utils/debug.js';
|
import { log } from '../utils/debug.js';
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
|
||||||
import { ViewMedia } from '../view/media.js';
|
import { ViewMedia } from '../view/media.js';
|
||||||
import { CameraManagerEngineFactory } from './engine-factory.js';
|
import { CameraManagerEngineFactory } from './engine-factory.js';
|
||||||
import { CameraManagerEngine } from './engine.js';
|
import { CameraManagerEngine } from './engine.js';
|
||||||
@@ -49,7 +47,7 @@ import {
|
|||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResults,
|
RecordingSegmentsQueryResults,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
ResultsMap,
|
ResultsMap
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { sortMedia } from './utils.js';
|
import { sortMedia } from './utils.js';
|
||||||
|
|
||||||
@@ -102,31 +100,26 @@ export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
|||||||
results: ViewMedia[];
|
results: ViewMedia[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitializedCamera {
|
|
||||||
inputConfig: CameraConfig;
|
|
||||||
initializedConfig: CameraConfig;
|
|
||||||
engine: CameraManagerEngine;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CameraManager {
|
export class CameraManager {
|
||||||
protected _api: CardCameraAPI;
|
protected _api: CardCameraAPI;
|
||||||
protected _engineFactory: CameraManagerEngineFactory;
|
protected _engineFactory: CameraManagerEngineFactory;
|
||||||
protected _store = new CameraManagerStore();
|
protected _store: CameraManagerStore;
|
||||||
|
|
||||||
constructor(api: CardCameraAPI) {
|
constructor(api: CardCameraAPI, store?: CameraManagerStore) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
this._engineFactory = new CameraManagerEngineFactory(
|
this._engineFactory = new CameraManagerEngineFactory(
|
||||||
this._api.getEntityRegistryManager(),
|
this._api.getEntityRegistryManager(),
|
||||||
this._api.getResolvedMediaCache(),
|
this._api.getResolvedMediaCache(),
|
||||||
);
|
);
|
||||||
|
this._store = store ?? new CameraManagerStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initializeCamerasFromConfig(): Promise<void> {
|
public async initializeCamerasFromConfig(): Promise<boolean> {
|
||||||
const config = this._api.getConfigManager().getConfig();
|
const config = this._api.getConfigManager().getConfig();
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
if (!config || !hass) {
|
if (!config || !hass) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._store.reset();
|
this._store.reset();
|
||||||
@@ -143,7 +136,9 @@ export class CameraManager {
|
|||||||
await this._initializeCameras(cameras);
|
await this._initializeCameras(cameras);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _getEnginesForCameras(
|
protected async _getEnginesForCameras(
|
||||||
@@ -172,7 +167,9 @@ export class CameraManager {
|
|||||||
if (!engine || !engineType) {
|
if (!engine || !engineType) {
|
||||||
throw new CameraInitializationError(
|
throw new CameraInitializationError(
|
||||||
localize('error.no_camera_engine'),
|
localize('error.no_camera_engine'),
|
||||||
cameraConfig,
|
// Camera initialization may modify the configuration. Keep the
|
||||||
|
// original config unchanged.
|
||||||
|
cloneDeep(cameraConfig),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
engines.set(engineType, engine);
|
engines.set(engineType, engine);
|
||||||
@@ -181,27 +178,6 @@ export class CameraManager {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _initializeCamera(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
engine: CameraManagerEngine,
|
|
||||||
entityRegistryManager: EntityRegistryManager,
|
|
||||||
inputCameraConfig: CameraConfig,
|
|
||||||
): Promise<InitializedCamera> {
|
|
||||||
const initializedConfig = await engine.initializeCamera(
|
|
||||||
hass,
|
|
||||||
entityRegistryManager,
|
|
||||||
// Camera initialization may modify the configuration. Keep the original
|
|
||||||
// for display in error messages to avoid user confusion.
|
|
||||||
cloneDeep(inputCameraConfig),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
inputConfig: inputCameraConfig,
|
|
||||||
initializedConfig: initializedConfig,
|
|
||||||
engine: engine,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _initializeCameras(camerasConfig: CamerasConfig): Promise<void> {
|
protected async _initializeCameras(camerasConfig: CamerasConfig): Promise<void> {
|
||||||
const initializationStartTime = new Date();
|
const initializationStartTime = new Date();
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
@@ -229,12 +205,11 @@ export class CameraManager {
|
|||||||
const engineByConfig = await this._getEnginesForCameras(camerasConfig);
|
const engineByConfig = await this._getEnginesForCameras(camerasConfig);
|
||||||
|
|
||||||
// Configuration is initialized in parallel.
|
// Configuration is initialized in parallel.
|
||||||
const results = await allPromises(
|
const cameras = await allPromises(
|
||||||
engineByConfig.entries(),
|
engineByConfig.entries(),
|
||||||
async ([cameraConfig, engine]) =>
|
async ([cameraConfig, engine]) =>
|
||||||
await this._initializeCamera(
|
await engine.initializeCamera(
|
||||||
hass,
|
hass,
|
||||||
engine,
|
|
||||||
this._api.getEntityRegistryManager(),
|
this._api.getEntityRegistryManager(),
|
||||||
cameraConfig,
|
cameraConfig,
|
||||||
),
|
),
|
||||||
@@ -242,27 +217,26 @@ export class CameraManager {
|
|||||||
|
|
||||||
// Do the additions based off the result-order, to ensure the map order is
|
// Do the additions based off the result-order, to ensure the map order is
|
||||||
// preserved.
|
// preserved.
|
||||||
results.forEach((result) => {
|
cameras.forEach((camera) => {
|
||||||
const id = getCameraID(result.initializedConfig);
|
const cameraID = getCameraID(camera.getConfig());
|
||||||
|
|
||||||
if (!id) {
|
if (!cameraID) {
|
||||||
throw new CameraInitializationError(
|
throw new CameraInitializationError(
|
||||||
localize('error.no_camera_id'),
|
localize('error.no_camera_id'),
|
||||||
result.inputConfig,
|
camera.getConfig(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._store.hasCameraID(id)) {
|
if (this._store.hasCameraID(cameraID)) {
|
||||||
throw new CameraInitializationError(
|
throw new CameraInitializationError(
|
||||||
localize('error.duplicate_camera_id'),
|
localize('error.duplicate_camera_id'),
|
||||||
result.inputConfig,
|
camera.getConfig(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always ensure the actual ID used in the card is in the configuration itself.
|
// Always ensure the actual ID used in the card is in the configuration itself.
|
||||||
result.initializedConfig.id = id;
|
camera.setID(cameraID);
|
||||||
|
this._store.addCamera(camera);
|
||||||
this._store.addCamera(id, result.initializedConfig, result.engine);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this._store.getVisibleCameraCount()) {
|
if (!this._store.getVisibleCameraCount()) {
|
||||||
@@ -372,20 +346,16 @@ export class CameraManager {
|
|||||||
for (const [engine, cameraIDs] of engines) {
|
for (const [engine, cameraIDs] of engines) {
|
||||||
let queries: DataQuery[] | null = null;
|
let queries: DataQuery[] | null = null;
|
||||||
if (QueryClassifier.isEventQuery(partialQuery)) {
|
if (QueryClassifier.isEventQuery(partialQuery)) {
|
||||||
queries = engine.generateDefaultEventQuery(
|
queries = engine.generateDefaultEventQuery(this._store, cameraIDs, partialQuery);
|
||||||
this._store.getVisibleCameras(),
|
|
||||||
cameraIDs,
|
|
||||||
partialQuery,
|
|
||||||
);
|
|
||||||
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
||||||
queries = engine.generateDefaultRecordingQuery(
|
queries = engine.generateDefaultRecordingQuery(
|
||||||
this._store.getVisibleCameras(),
|
this._store,
|
||||||
cameraIDs,
|
cameraIDs,
|
||||||
partialQuery,
|
partialQuery,
|
||||||
);
|
);
|
||||||
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||||
queries = engine.generateDefaultRecordingSegmentsQuery(
|
queries = engine.generateDefaultRecordingSegmentsQuery(
|
||||||
this._store.getVisibleCameras(),
|
this._store,
|
||||||
cameraIDs,
|
cameraIDs,
|
||||||
partialQuery,
|
partialQuery,
|
||||||
);
|
);
|
||||||
@@ -596,7 +566,7 @@ export class CameraManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await engine.getMediaSeekTime(hass, this._store.getCameras(), media, target);
|
return await engine.getMediaSeekTime(hass, this._store, media, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _handleQuery<QT extends DataQuery>(
|
protected async _handleQuery<QT extends DataQuery>(
|
||||||
@@ -624,28 +594,28 @@ export class CameraManager {
|
|||||||
if (QueryClassifier.isEventQuery(query)) {
|
if (QueryClassifier.isEventQuery(query)) {
|
||||||
engineResult = (await engine.getEvents(
|
engineResult = (await engine.getEvents(
|
||||||
hass,
|
hass,
|
||||||
this._store.getCameras(),
|
this._store,
|
||||||
query,
|
query,
|
||||||
engineOptions,
|
engineOptions,
|
||||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
} else if (QueryClassifier.isRecordingQuery(query)) {
|
} else if (QueryClassifier.isRecordingQuery(query)) {
|
||||||
engineResult = (await engine.getRecordings(
|
engineResult = (await engine.getRecordings(
|
||||||
hass,
|
hass,
|
||||||
this._store.getCameras(),
|
this._store,
|
||||||
query,
|
query,
|
||||||
engineOptions,
|
engineOptions,
|
||||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
||||||
engineResult = (await engine.getRecordingSegments(
|
engineResult = (await engine.getRecordingSegments(
|
||||||
hass,
|
hass,
|
||||||
this._store.getCameras(),
|
this._store,
|
||||||
query,
|
query,
|
||||||
engineOptions,
|
engineOptions,
|
||||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
} else if (QueryClassifier.isMediaMetadataQuery(query)) {
|
} else if (QueryClassifier.isMediaMetadataQuery(query)) {
|
||||||
engineResult = (await engine.getMediaMetadata(
|
engineResult = (await engine.getMediaMetadata(
|
||||||
hass,
|
hass,
|
||||||
this._store.getCameras(),
|
this._store,
|
||||||
query,
|
query,
|
||||||
engineOptions,
|
engineOptions,
|
||||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
@@ -710,22 +680,12 @@ export class CameraManager {
|
|||||||
QueryClassifier.isEventQuery(query) &&
|
QueryClassifier.isEventQuery(query) &&
|
||||||
QueryResultClassifier.isEventQueryResult(result)
|
QueryResultClassifier.isEventQueryResult(result)
|
||||||
) {
|
) {
|
||||||
media = engine.generateMediaFromEvents(
|
media = engine.generateMediaFromEvents(hass, this._store, query, result);
|
||||||
hass,
|
|
||||||
this._store.getCameras(),
|
|
||||||
query,
|
|
||||||
result,
|
|
||||||
);
|
|
||||||
} else if (
|
} else if (
|
||||||
QueryClassifier.isRecordingQuery(query) &&
|
QueryClassifier.isRecordingQuery(query) &&
|
||||||
QueryResultClassifier.isRecordingQuery(result)
|
QueryResultClassifier.isRecordingQuery(result)
|
||||||
) {
|
) {
|
||||||
media = engine.generateMediaFromRecordings(
|
media = engine.generateMediaFromRecordings(hass, this._store, query, result);
|
||||||
hass,
|
|
||||||
this._store.getCameras(),
|
|
||||||
query,
|
|
||||||
result,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (media) {
|
if (media) {
|
||||||
mediaArray.push(...media);
|
mediaArray.push(...media);
|
||||||
@@ -761,12 +721,7 @@ export class CameraManager {
|
|||||||
public getCameraCapabilities(
|
public getCameraCapabilities(
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
): CameraManagerCameraCapabilities | null {
|
): CameraManagerCameraCapabilities | null {
|
||||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
return this._store.getCamera(cameraID)?.getCapabilities() ?? null;
|
||||||
const engine = this._store.getEngineForCameraID(cameraID);
|
|
||||||
if (!cameraConfig || !engine) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return engine.getCameraCapabilities(cameraConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAggregateCameraCapabilities(
|
public getAggregateCameraCapabilities(
|
||||||
@@ -787,6 +742,26 @@ export class CameraManager {
|
|||||||
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
||||||
supportsSnapshots: perCameraCapabilities.some((cap) => cap?.supportsSnapshots),
|
supportsSnapshots: perCameraCapabilities.some((cap) => cap?.supportsSnapshots),
|
||||||
supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline),
|
supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline),
|
||||||
|
|
||||||
|
supportsPTZ: perCameraCapabilities.some((cap) => !!cap?.ptz),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async executePTZAction(
|
||||||
|
cameraID: string,
|
||||||
|
action: PTZAction,
|
||||||
|
options: {
|
||||||
|
phase?: PTZPhase;
|
||||||
|
preset?: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
const engine = this._store.getEngineForCameraID(cameraID);
|
||||||
|
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||||
|
|
||||||
|
if (!engine || !cameraConfig || !hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return engine.executePTZAction(hass, cameraConfig, action, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,25 +8,23 @@ import { CameraConfig } from '../../config/types';
|
|||||||
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
||||||
import {
|
import {
|
||||||
BrowseMediaStep,
|
BrowseMediaStep,
|
||||||
BrowseMediaTarget,
|
BrowseMediaTarget
|
||||||
} from '../../utils/ha/browse-media/browse-media-manager';
|
} from '../../utils/ha/browse-media/browse-media-manager';
|
||||||
import {
|
import {
|
||||||
BROWSE_MEDIA_CACHE_SECONDS,
|
BrowseMedia, BROWSE_MEDIA_CACHE_SECONDS, MEDIA_CLASS_IMAGE,
|
||||||
BrowseMedia,
|
|
||||||
MEDIA_CLASS_IMAGE,
|
|
||||||
MEDIA_CLASS_VIDEO,
|
MEDIA_CLASS_VIDEO,
|
||||||
RichBrowseMedia,
|
RichBrowseMedia
|
||||||
} from '../../utils/ha/browse-media/types';
|
} from '../../utils/ha/browse-media/types';
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
import {
|
import {
|
||||||
BrowseMediaCameraManagerEngine,
|
BrowseMediaCameraManagerEngine,
|
||||||
getViewMediaFromBrowseMediaArray,
|
getViewMediaFromBrowseMediaArray,
|
||||||
isMediaWithinDates,
|
isMediaWithinDates
|
||||||
} from '../browse-media/engine-browse-media';
|
} from '../browse-media/engine-browse-media';
|
||||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||||
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
||||||
|
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||||
import {
|
import {
|
||||||
CameraConfigs,
|
|
||||||
CameraEndpoint,
|
CameraEndpoint,
|
||||||
CameraEndpoints,
|
CameraEndpoints,
|
||||||
CameraEndpointsContext,
|
CameraEndpointsContext,
|
||||||
@@ -41,7 +39,7 @@ import {
|
|||||||
MediaMetadataQueryResultsMap,
|
MediaMetadataQueryResultsMap,
|
||||||
QueryResults,
|
QueryResults,
|
||||||
QueryResultsType,
|
QueryResultsType,
|
||||||
QueryReturnType,
|
QueryReturnType
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import motioneyeLogo from './assets/motioneye-logo.svg';
|
import motioneyeLogo from './assets/motioneye-logo.svg';
|
||||||
import { MotionEyeEventQueryResults } from './types';
|
import { MotionEyeEventQueryResults } from './types';
|
||||||
@@ -126,7 +124,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
// Get media directories that match a given criteria.
|
// Get media directories that match a given criteria.
|
||||||
protected async _getMatchingDirectories(
|
protected async _getMatchingDirectories(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
matchOptions?: {
|
matchOptions?: {
|
||||||
start?: Date;
|
start?: Date;
|
||||||
@@ -136,11 +134,11 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
} | null,
|
} | null,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<RichBrowseMedia<BrowseMediaMetadata>[] | null> {
|
): Promise<RichBrowseMedia<BrowseMediaMetadata>[] | null> {
|
||||||
const cameraEntityID = cameras.get(cameraID)?.camera_entity;
|
const cameraConfig = store.getCameraConfig(cameraID);
|
||||||
|
const cameraEntityID = cameraConfig?.camera_entity;
|
||||||
const entity = cameraEntityID ? this._cameraEntities.get(cameraEntityID) : null;
|
const entity = cameraEntityID ? this._cameraEntities.get(cameraEntityID) : null;
|
||||||
const configID = entity?.config_entry_id;
|
const configID = entity?.config_entry_id;
|
||||||
const deviceID = entity?.device_id;
|
const deviceID = entity?.device_id;
|
||||||
const cameraConfig = cameras.get(cameraID);
|
|
||||||
|
|
||||||
if (!configID || !deviceID || !cameraConfig) {
|
if (!configID || !deviceID || !cameraConfig) {
|
||||||
return null;
|
return null;
|
||||||
@@ -206,7 +204,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
|
|
||||||
public async getEvents(
|
public async getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<EventQueryResultsMap | null> {
|
): Promise<EventQueryResultsMap | null> {
|
||||||
@@ -225,14 +223,14 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cameraConfig = cameras.get(cameraID);
|
const cameraConfig = store.getCameraConfig(cameraID);
|
||||||
if (!cameraConfig) {
|
if (!cameraConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const directories = await this._getMatchingDirectories(
|
const directories = await this._getMatchingDirectories(
|
||||||
hass,
|
hass,
|
||||||
cameras,
|
store,
|
||||||
cameraID,
|
cameraID,
|
||||||
perCameraQuery,
|
perCameraQuery,
|
||||||
engineOptions,
|
engineOptions,
|
||||||
@@ -309,7 +307,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
|
|
||||||
public generateMediaFromEvents(
|
public generateMediaFromEvents(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
_cameras: CameraConfigs,
|
_store: CameraManagerReadOnlyConfigStore,
|
||||||
_query: EventQuery,
|
_query: EventQuery,
|
||||||
results: QueryReturnType<EventQuery>,
|
results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -321,7 +319,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
|
|
||||||
public async getMediaMetadata(
|
public async getMediaMetadata(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: CameraConfigs,
|
store: CameraManagerReadOnlyConfigStore,
|
||||||
query: MediaMetadataQuery,
|
query: MediaMetadataQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<MediaMetadataQueryResultsMap | null> {
|
): Promise<MediaMetadataQueryResultsMap | null> {
|
||||||
@@ -340,7 +338,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
const getDaysForCamera = async (cameraID: string): Promise<void> => {
|
const getDaysForCamera = async (cameraID: string): Promise<void> => {
|
||||||
const directories = await this._getMatchingDirectories(
|
const directories = await this._getMatchingDirectories(
|
||||||
hass,
|
hass,
|
||||||
cameras,
|
store,
|
||||||
cameraID,
|
cameraID,
|
||||||
null,
|
null,
|
||||||
engineOptions,
|
engineOptions,
|
||||||
|
|||||||
+82
-38
@@ -1,7 +1,8 @@
|
|||||||
import { CameraConfig } from '../config/types';
|
import { CameraConfig } from '../config/types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { Camera } from './camera';
|
||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
import { CameraConfigs, Engine } from './types';
|
import { Engine } from './types';
|
||||||
|
|
||||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||||
|
|
||||||
@@ -10,74 +11,82 @@ export interface CameraManagerReadOnlyConfigStore {
|
|||||||
getCameraConfigForMedia(media: ViewMedia): CameraConfig | null;
|
getCameraConfigForMedia(media: ViewMedia): CameraConfig | null;
|
||||||
|
|
||||||
hasCameraID(cameraID: string): boolean;
|
hasCameraID(cameraID: string): boolean;
|
||||||
hasVisibleCameraID(cameraID: string): boolean;
|
|
||||||
|
|
||||||
|
getCamera(cameraID: string): Camera | null;
|
||||||
getCameraCount(): number;
|
getCameraCount(): number;
|
||||||
getVisibleCameraCount(): number;
|
getVisibleCameraCount(): number;
|
||||||
|
|
||||||
getCameras(): CameraConfigs;
|
getCameraConfigs(cameraIDs?: Iterable<string>): IterableIterator<CameraConfig>;
|
||||||
getVisibleCameras(): CameraConfigs;
|
getCameraConfigEntries(
|
||||||
|
cameraIDs?: Iterable<string>,
|
||||||
|
): IterableIterator<[string, CameraConfig]>;
|
||||||
|
|
||||||
getCameraIDs(): Set<string>;
|
getCameraIDs(): Set<string>;
|
||||||
getVisibleCameraIDs(): Set<string>;
|
getVisibleCameraIDs(): Set<string>;
|
||||||
|
|
||||||
|
getAllDependentCameras(cameraID: string): Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||||
protected _allConfigs: Map<string, CameraConfig> = new Map();
|
protected _cameras: Map<string, Camera> = new Map();
|
||||||
protected _visibleConfigs: Map<string, CameraConfig> = new Map();
|
|
||||||
protected _enginesByCamera: Map<string, CameraManagerEngine> = new Map();
|
|
||||||
protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
|
protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
|
||||||
|
|
||||||
public addCamera(
|
public addCamera(camera: Camera): void {
|
||||||
cameraID: string,
|
this._cameras.set(camera.getID(), camera);
|
||||||
cameraConfig: CameraConfig,
|
this._enginesByType.set(camera.getEngine().getEngineType(), camera.getEngine());
|
||||||
engine: CameraManagerEngine,
|
|
||||||
): void {
|
|
||||||
if (!cameraConfig.hide) {
|
|
||||||
this._visibleConfigs.set(cameraID, cameraConfig);
|
|
||||||
}
|
}
|
||||||
this._allConfigs.set(cameraID, cameraConfig);
|
|
||||||
this._enginesByCamera.set(cameraID, engine);
|
|
||||||
this._enginesByType.set(engine.getEngineType(), engine);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset(): void {
|
public reset(): void {
|
||||||
this._allConfigs.clear();
|
this._cameras.clear();
|
||||||
this._visibleConfigs.clear();
|
|
||||||
this._enginesByCamera.clear();
|
|
||||||
this._enginesByType.clear();
|
this._enginesByType.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCamera(cameraID: string): Camera | null {
|
||||||
|
return this._cameras.get(cameraID) ?? null;
|
||||||
|
}
|
||||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||||
return this._allConfigs.get(cameraID) ?? null;
|
return this._cameras.get(cameraID)?.getConfig() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasCameraID(cameraID: string): boolean {
|
public hasCameraID(cameraID: string): boolean {
|
||||||
return this._allConfigs.has(cameraID);
|
return this._cameras.has(cameraID);
|
||||||
}
|
|
||||||
public hasVisibleCameraID(cameraID: string): boolean {
|
|
||||||
return this._visibleConfigs.has(cameraID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameraCount(): number {
|
public getCameraCount(): number {
|
||||||
return this._allConfigs.size;
|
return this._cameras.size;
|
||||||
}
|
}
|
||||||
public getVisibleCameraCount(): number {
|
public getVisibleCameraCount(): number {
|
||||||
return this._visibleConfigs.size;
|
return this.getVisibleCameraIDs().size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameras(): CameraConfigs {
|
public getCameras(): Map<string, Camera> {
|
||||||
return this._allConfigs;
|
return this._cameras;
|
||||||
|
}
|
||||||
|
|
||||||
|
public *getCameraConfigs(
|
||||||
|
cameraIDs?: Iterable<string>,
|
||||||
|
): IterableIterator<CameraConfig> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
for (const [_cameraID, config] of this.getCameraConfigEntries(cameraIDs)) {
|
||||||
|
yield config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public *getCameraConfigEntries(
|
||||||
|
cameraIDs?: Iterable<string>,
|
||||||
|
): IterableIterator<[string, CameraConfig]> {
|
||||||
|
for (const cameraID of cameraIDs ?? this._cameras.keys()) {
|
||||||
|
const config = this.getCameraConfig(cameraID);
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
yield [cameraID, config];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public getVisibleCameras(): CameraConfigs {
|
|
||||||
return this._visibleConfigs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameraIDs(): Set<string> {
|
public getCameraIDs(): Set<string> {
|
||||||
return new Set(this._allConfigs.keys());
|
return new Set(this._cameras.keys());
|
||||||
}
|
}
|
||||||
public getVisibleCameraIDs(): Set<string> {
|
public getVisibleCameraIDs(): Set<string> {
|
||||||
return new Set(this._visibleConfigs.keys());
|
return this._getMatchingCameraIDs((camera) => !camera.getConfig().hide);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||||
@@ -89,7 +98,7 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getEngineForCameraID(cameraID: string): CameraManagerEngine | null {
|
public getEngineForCameraID(cameraID: string): CameraManagerEngine | null {
|
||||||
return this._enginesByCamera.get(cameraID) ?? null;
|
return this._cameras.get(cameraID)?.getEngine() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEnginesForCameraIDs(
|
public getEnginesForCameraIDs(
|
||||||
@@ -114,7 +123,42 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
return this.getEngineForCameraID(media.getCameraID());
|
return this.getEngineForCameraID(media.getCameraID());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllEngines(): CameraManagerEngine[] {
|
/**
|
||||||
return [...this._enginesByType.values()];
|
* Get all cameras that depend on a given camera.
|
||||||
|
* @param cameraManager The camera manager.
|
||||||
|
* @param cameraID ID of the target camera.
|
||||||
|
* @returns A set of dependent cameraIDs or null (since JS sets guarantee order,
|
||||||
|
* the first item in the set is guaranteed to be the cameraID itself).
|
||||||
|
*/
|
||||||
|
public getAllDependentCameras(cameraID: string): Set<string> {
|
||||||
|
const cameraIDs: Set<string> = new Set();
|
||||||
|
const getDependentCameras = (cameraID: string): void => {
|
||||||
|
const cameraConfig = this.getCameraConfig(cameraID);
|
||||||
|
if (cameraConfig) {
|
||||||
|
cameraIDs.add(cameraID);
|
||||||
|
const dependentCameras: Set<string> = new Set();
|
||||||
|
cameraConfig.dependencies.cameras.forEach((item) => dependentCameras.add(item));
|
||||||
|
if (cameraConfig.dependencies.all_cameras) {
|
||||||
|
this.getCameraIDs().forEach((cameraID) => dependentCameras.add(cameraID));
|
||||||
|
}
|
||||||
|
for (const eventCameraID of dependentCameras) {
|
||||||
|
if (!cameraIDs.has(eventCameraID)) {
|
||||||
|
getDependentCameras(eventCameraID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getDependentCameras(cameraID);
|
||||||
|
return cameraIDs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getMatchingCameraIDs(func: (camera: Camera) => boolean): Set<string> {
|
||||||
|
const output = new Set<string>();
|
||||||
|
for (const [cameraID, camera] of this._cameras.entries()) {
|
||||||
|
if (func(camera)) {
|
||||||
|
output.add(cameraID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CameraConfig, FrigateCardView } from '../config/types';
|
import { FrigateCardView } from '../config/types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
|
|
||||||
// ====
|
// ====
|
||||||
@@ -91,6 +91,14 @@ export interface MediaMetadata {
|
|||||||
what?: Set<string>;
|
what?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PTZMovementType = 'relative' | 'continuous';
|
||||||
|
|
||||||
|
export interface PTZCapabilities {
|
||||||
|
panTilt?: PTZMovementType[];
|
||||||
|
zoom?: PTZMovementType[];
|
||||||
|
presets?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface BaseCapabilities {
|
interface BaseCapabilities {
|
||||||
canFavoriteEvents: boolean;
|
canFavoriteEvents: boolean;
|
||||||
canFavoriteRecordings: boolean;
|
canFavoriteRecordings: boolean;
|
||||||
@@ -102,8 +110,14 @@ interface BaseCapabilities {
|
|||||||
supportsTimeline: boolean;
|
supportsTimeline: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CameraManagerCapabilities = BaseCapabilities;
|
export interface CameraManagerCapabilities extends BaseCapabilities {
|
||||||
export type CameraManagerCameraCapabilities = BaseCapabilities;
|
supportsPTZ: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CameraManagerCameraCapabilities extends BaseCapabilities {
|
||||||
|
ptz?: PTZCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CameraManagerMediaCapabilities {
|
export interface CameraManagerMediaCapabilities {
|
||||||
canFavorite: boolean;
|
canFavorite: boolean;
|
||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
@@ -132,8 +146,6 @@ export interface CameraEndpoints {
|
|||||||
webrtcCard?: CameraEndpoint;
|
webrtcCard?: CameraEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CameraConfigs = Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
export interface EngineOptions {
|
export interface EngineOptions {
|
||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,22 @@ export class ActionsManager {
|
|||||||
.getViewManager()
|
.getViewManager()
|
||||||
.setViewWithNewDisplayMode(frigateCardAction.display_mode);
|
.setViewWithNewDisplayMode(frigateCardAction.display_mode);
|
||||||
break;
|
break;
|
||||||
|
case 'ptz':
|
||||||
|
const cameraID = this._api.getViewManager().getView()?.camera;
|
||||||
|
if (cameraID) {
|
||||||
|
this._api
|
||||||
|
.getCameraManager()
|
||||||
|
.executePTZAction(cameraID, frigateCardAction.ptz_action, {
|
||||||
|
phase: frigateCardAction.ptz_phase,
|
||||||
|
preset: frigateCardAction.ptz_preset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'show_ptz':
|
||||||
|
this._api
|
||||||
|
.getViewManager()
|
||||||
|
.setViewWithNewContext({ live: { ptzVisible: frigateCardAction.show_ptz } });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`Frigate card received unknown card action: ${action}`);
|
console.warn(`Frigate card received unknown card action: ${action}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export enum InitializationAspect {
|
|||||||
|
|
||||||
export class InitializationManager {
|
export class InitializationManager {
|
||||||
protected _api: CardInitializerAPI;
|
protected _api: CardInitializerAPI;
|
||||||
protected _initializer;
|
protected _initializer: Initializer;
|
||||||
|
|
||||||
constructor(api: CardInitializerAPI, initializer?: Initializer) {
|
constructor(api: CardInitializerAPI, initializer?: Initializer) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
@@ -21,11 +21,15 @@ export class InitializationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isInitializedMandatory(): boolean {
|
public isInitializedMandatory(): boolean {
|
||||||
return this._initializer.isInitializedMultiple([
|
return (
|
||||||
|
this._initializer.isInitializedMultiple([
|
||||||
InitializationAspect.LANGUAGES,
|
InitializationAspect.LANGUAGES,
|
||||||
InitializationAspect.SIDE_LOAD_ELEMENTS,
|
InitializationAspect.SIDE_LOAD_ELEMENTS,
|
||||||
InitializationAspect.CAMERAS,
|
InitializationAspect.CAMERAS,
|
||||||
]);
|
]) &&
|
||||||
|
// If there's no view, re-initialize (e.g. config changes).
|
||||||
|
this._api.getViewManager().hasView()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +84,6 @@ export class InitializationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export class MediaPlayerManager {
|
|||||||
return this._mediaPlayers.length > 0;
|
return this._mediaPlayers.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initialize(): Promise<void> {
|
public async initialize(): Promise<boolean> {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
if (!hass) {
|
if (!hass) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidMediaPlayer = (entityID: string): boolean => {
|
const isValidMediaPlayer = (entityID: string): boolean => {
|
||||||
@@ -66,6 +66,8 @@ export class MediaPlayerManager {
|
|||||||
const entity = mediaPlayerEntities?.get(entityID);
|
const entity = mediaPlayerEntities?.get(entityID);
|
||||||
return !entity || !entity.hidden_by;
|
return !entity || !entity.hidden_by;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(mediaPlayer: string): Promise<void> {
|
public async stop(mediaPlayer: string): Promise<void> {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class MicrophoneManager {
|
|||||||
this._api = api;
|
this._api = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect(): Promise<void> {
|
public async connect(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
this._stream = await navigator.mediaDevices.getUserMedia({
|
this._stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
@@ -24,15 +24,19 @@ export class MicrophoneManager {
|
|||||||
});
|
});
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
|
|
||||||
this._stream = null;
|
this._stream = null;
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
this._setMute();
|
this._setMute();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disconnect(): Promise<void> {
|
public async disconnect(): Promise<void> {
|
||||||
this._stream?.getTracks().forEach((track) => track.stop());
|
this._stream?.getTracks().forEach((track) => track.stop());
|
||||||
this._stream = undefined;
|
|
||||||
|
|
||||||
|
this._stream = undefined;
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FrigateCardCustomAction, FrigateCardViewAction } from '../config/types';
|
import { FrigateCardCustomAction, FrigateCardViewAction } from '../config/types';
|
||||||
import { createFrigateCardCustomAction } from '../utils/action.js';
|
import { createFrigateCardCameraAction, createFrigateCardSimpleAction } from '../utils/action.js';
|
||||||
import { CardQueryStringAPI } from './types';
|
import { CardQueryStringAPI } from './types';
|
||||||
import { ViewManagerSetViewParameters } from './view-manager';
|
import { ViewManagerSetViewParameters } from './view-manager';
|
||||||
|
|
||||||
@@ -139,8 +139,7 @@ export class QueryStringManager {
|
|||||||
case 'camera_select':
|
case 'camera_select':
|
||||||
case 'live_substream_select':
|
case 'live_substream_select':
|
||||||
if (value) {
|
if (value) {
|
||||||
customAction = createFrigateCardCustomAction(action, {
|
customAction = createFrigateCardCameraAction(action, value, {
|
||||||
camera: value,
|
|
||||||
cardID: cardID,
|
cardID: cardID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -160,7 +159,7 @@ export class QueryStringManager {
|
|||||||
case 'snapshot':
|
case 'snapshot':
|
||||||
case 'snapshots':
|
case 'snapshots':
|
||||||
case 'timeline':
|
case 'timeline':
|
||||||
customAction = createFrigateCardCustomAction(action, {
|
customAction = createFrigateCardSimpleAction(action, {
|
||||||
cardID: cardID,
|
cardID: cardID,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ export class TriggersManager {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
let triggerChanges = false;
|
let triggerChanges = false;
|
||||||
|
|
||||||
const cameras = this._api.getCameraManager().getStore().getVisibleCameras();
|
const visibleCameraIDs = this._api
|
||||||
for (const [cameraID, config] of cameras?.entries()) {
|
.getCameraManager()
|
||||||
|
.getStore()
|
||||||
|
.getVisibleCameraIDs();
|
||||||
|
for (const [cameraID, config] of this._api
|
||||||
|
.getCameraManager()
|
||||||
|
.getStore()
|
||||||
|
.getCameraConfigEntries(visibleCameraIDs)) {
|
||||||
const triggerEntities = config.triggers.entities;
|
const triggerEntities = config.triggers.entities;
|
||||||
const diffs = getHassDifferences(hass, oldHass, triggerEntities, {
|
const diffs = getHassDifferences(hass, oldHass, triggerEntities, {
|
||||||
stateOnly: true,
|
stateOnly: true,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import { FrigateCardConfig, FrigateCardView, ViewDisplayMode } from '../config/types';
|
import { FrigateCardConfig, FrigateCardView, ViewDisplayMode } from '../config/types';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { getAllDependentCameras } from '../utils/camera';
|
|
||||||
import { log } from '../utils/debug';
|
import { log } from '../utils/debug';
|
||||||
import { executeMediaQueryForView } from '../utils/media-to-view';
|
import { executeMediaQueryForView } from '../utils/media-to-view';
|
||||||
import { CardViewAPI } from './types';
|
import { CardViewAPI } from './types';
|
||||||
@@ -28,6 +27,10 @@ export class ViewManager {
|
|||||||
return this._view;
|
return this._view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasView(): boolean {
|
||||||
|
return !!this.getView();
|
||||||
|
}
|
||||||
|
|
||||||
public setView(view: View): void {
|
public setView(view: View): void {
|
||||||
this._setView(view);
|
this._setView(view);
|
||||||
}
|
}
|
||||||
@@ -255,7 +258,7 @@ export class ViewManager {
|
|||||||
|
|
||||||
protected _createViewWithNextStream(baseView: View): View {
|
protected _createViewWithNextStream(baseView: View): View {
|
||||||
const dependencies = [
|
const dependencies = [
|
||||||
...getAllDependentCameras(this._api.getCameraManager(), baseView.camera),
|
...this._api.getCameraManager().getStore().getAllDependentCameras(baseView.camera),
|
||||||
];
|
];
|
||||||
if (dependencies.length <= 1) {
|
if (dependencies.length <= 1) {
|
||||||
return baseView.clone();
|
return baseView.clone();
|
||||||
|
|||||||
+12
@@ -163,6 +163,12 @@ class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected shouldUpdate(): boolean {
|
protected shouldUpdate(): boolean {
|
||||||
|
// Always allow messages to render, as a message may be generated during
|
||||||
|
// initialization.
|
||||||
|
if (this._controller.getMessageManager().hasMessage()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this._controller.getInitializationManager().isInitializedMandatory()) {
|
if (!this._controller.getInitializationManager().isInitializedMandatory()) {
|
||||||
this._controller.getInitializationManager().initializeMandatory();
|
this._controller.getInitializationManager().initializeMandatory();
|
||||||
return false;
|
return false;
|
||||||
@@ -295,6 +301,12 @@ class FrigateCard extends LitElement {
|
|||||||
.view=${this._controller.getViewManager().getView()}
|
.view=${this._controller.getViewManager().getView()}
|
||||||
.cameraManager=${cameraManager}
|
.cameraManager=${cameraManager}
|
||||||
.resolvedMediaCache=${this._controller.getResolvedMediaCache()}
|
.resolvedMediaCache=${this._controller.getResolvedMediaCache()}
|
||||||
|
.nonOverriddenConfig=${this._controller
|
||||||
|
.getConfigManager()
|
||||||
|
.getNonOverriddenConfig()}
|
||||||
|
.overriddenConfig=${this._controller.getConfigManager().getConfig()}
|
||||||
|
.cardWideConfig=${this._controller.getConfigManager().getCardWideConfig()}
|
||||||
|
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
|
||||||
.configManager=${this._controller.getConfigManager()}
|
.configManager=${this._controller.getConfigManager()}
|
||||||
.conditionsManagerEpoch=${this._controller
|
.conditionsManagerEpoch=${this._controller
|
||||||
.getConditionsManager()
|
.getConditionsManager()
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import { StyleInfo } from 'lit/directives/style-map';
|
import { StyleInfo } from 'lit/directives/style-map';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
||||||
|
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||||
import {
|
import {
|
||||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
MenuItem
|
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
|
MenuItem,
|
||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
import { FRIGATE_BUTTON_MENU_ICON } from '../const';
|
import { FRIGATE_BUTTON_MENU_ICON } from '../const';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
|
import { MediaLoadedInfo } from '../types';
|
||||||
import {
|
import {
|
||||||
MediaLoadedInfo,
|
createFrigateCardCameraAction,
|
||||||
} from '../types';
|
createFrigateCardDisplayModeAction,
|
||||||
import { View } from '../view/view';
|
createFrigateCardMediaPlayerAction,
|
||||||
import { createFrigateCardCustomAction } from '../utils/action';
|
createFrigateCardShowPTZAction,
|
||||||
import { getAllDependentCameras } from '../utils/camera';
|
createFrigateCardSimpleAction,
|
||||||
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
} from '../utils/action';
|
||||||
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
|
||||||
import { getEntityIcon, getEntityTitle } from '../utils/ha';
|
import { getEntityIcon, getEntityTitle } from '../utils/ha';
|
||||||
|
import { hasUsablePTZ } from '../utils/ptz';
|
||||||
import { hasSubstream } from '../utils/substream';
|
import { hasSubstream } from '../utils/substream';
|
||||||
|
import { View } from '../view/view';
|
||||||
|
|
||||||
export interface MenuButtonControllerOptions {
|
export interface MenuButtonControllerOptions {
|
||||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
showCameraUIButton?: boolean;
|
showCameraUIButton?: boolean;
|
||||||
@@ -55,15 +60,19 @@ export class MenuButtonController {
|
|||||||
view: View,
|
view: View,
|
||||||
options?: MenuButtonControllerOptions,
|
options?: MenuButtonControllerOptions,
|
||||||
): MenuItem[] {
|
): MenuItem[] {
|
||||||
const visibleCameras = cameraManager.getStore().getVisibleCameras();
|
const visibleCameraIDs = cameraManager.getStore().getVisibleCameraIDs();
|
||||||
const selectedCameraID = view.camera;
|
const selectedCameraID = view.camera;
|
||||||
const selectedCameraConfig = cameraManager
|
const selectedCameraConfig = cameraManager
|
||||||
.getStore()
|
.getStore()
|
||||||
.getCameraConfig(selectedCameraID);
|
.getCameraConfig(selectedCameraID);
|
||||||
const allSelectedCameraIDs = getAllDependentCameras(cameraManager, selectedCameraID);
|
const allSelectedCameraIDs = cameraManager
|
||||||
|
.getStore()
|
||||||
|
.getAllDependentCameras(selectedCameraID);
|
||||||
const selectedMedia = view.queryResults?.getSelectedResult();
|
const selectedMedia = view.queryResults?.getSelectedResult();
|
||||||
|
|
||||||
const cameraCapabilities =
|
const selectedCameraCapabilities =
|
||||||
|
cameraManager.getCameraCapabilities(selectedCameraID);
|
||||||
|
const aggregateCapabilities =
|
||||||
cameraManager.getAggregateCameraCapabilities(allSelectedCameraIDs);
|
cameraManager.getAggregateCameraCapabilities(allSelectedCameraIDs);
|
||||||
const mediaCapabilities = selectedMedia
|
const mediaCapabilities = selectedMedia
|
||||||
? cameraManager?.getMediaCapabilities(selectedMedia)
|
? cameraManager?.getMediaCapabilities(selectedMedia)
|
||||||
@@ -79,18 +88,18 @@ export class MenuButtonController {
|
|||||||
title: localize('config.menu.buttons.frigate'),
|
title: localize('config.menu.buttons.frigate'),
|
||||||
tap_action:
|
tap_action:
|
||||||
config.menu?.style === 'hidden'
|
config.menu?.style === 'hidden'
|
||||||
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
|
? (createFrigateCardSimpleAction('menu_toggle') as FrigateCardCustomAction)
|
||||||
: (createFrigateCardCustomAction('default') as FrigateCardCustomAction),
|
: (createFrigateCardSimpleAction('default') as FrigateCardCustomAction),
|
||||||
hold_action: createFrigateCardCustomAction(
|
hold_action: createFrigateCardSimpleAction(
|
||||||
'diagnostics',
|
'diagnostics',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (visibleCameras.size) {
|
if (visibleCameraIDs.size) {
|
||||||
const menuItems = Array.from(visibleCameras, ([cameraID, config]) => {
|
const menuItems = Array.from(
|
||||||
const action = createFrigateCardCustomAction('camera_select', {
|
cameraManager.getStore().getCameraConfigEntries(visibleCameraIDs),
|
||||||
camera: cameraID,
|
([cameraID, config]) => {
|
||||||
});
|
const action = createFrigateCardCameraAction('camera_select', cameraID);
|
||||||
const metadata = cameraManager.getCameraMetadata(cameraID);
|
const metadata = cameraManager.getCameraMetadata(cameraID);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -102,7 +111,8 @@ export class MenuButtonController {
|
|||||||
selected: selectedCameraID === cameraID,
|
selected: selectedCameraID === cameraID,
|
||||||
...(action && { tap_action: action }),
|
...(action && { tap_action: action }),
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:video-switch',
|
icon: 'mdi:video-switch',
|
||||||
@@ -127,15 +137,16 @@ export class MenuButtonController {
|
|||||||
title: localize('config.menu.buttons.substreams'),
|
title: localize('config.menu.buttons.substreams'),
|
||||||
...config.menu.buttons.substreams,
|
...config.menu.buttons.substreams,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
} else if (dependencies.length > 2) {
|
} else if (dependencies.length > 2) {
|
||||||
const menuItems = Array.from(dependencies, (cameraID) => {
|
const menuItems = Array.from(dependencies, (cameraID) => {
|
||||||
const action = createFrigateCardCustomAction('live_substream_select', {
|
const action = createFrigateCardCameraAction(
|
||||||
camera: cameraID,
|
'live_substream_select',
|
||||||
});
|
cameraID,
|
||||||
|
);
|
||||||
const metadata = cameraManager.getCameraMetadata(cameraID) ?? undefined;
|
const metadata = cameraManager.getCameraMetadata(cameraID) ?? undefined;
|
||||||
const cameraConfig = cameraManager.getStore().getCameraConfig(cameraID);
|
const cameraConfig = cameraManager.getStore().getCameraConfig(cameraID);
|
||||||
return {
|
return {
|
||||||
@@ -169,48 +180,48 @@ export class MenuButtonController {
|
|||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.view.views.live'),
|
title: localize('config.view.views.live'),
|
||||||
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cameraCapabilities?.supportsClips) {
|
if (aggregateCapabilities?.supportsClips) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:filmstrip',
|
icon: 'mdi:filmstrip',
|
||||||
...config.menu.buttons.clips,
|
...config.menu.buttons.clips,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.view.views.clips'),
|
title: localize('config.view.views.clips'),
|
||||||
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction('clips') as FrigateCardCustomAction,
|
||||||
hold_action: createFrigateCardCustomAction('clip') as FrigateCardCustomAction,
|
hold_action: createFrigateCardSimpleAction('clip') as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cameraCapabilities?.supportsSnapshots) {
|
if (aggregateCapabilities?.supportsSnapshots) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:camera',
|
icon: 'mdi:camera',
|
||||||
...config.menu.buttons.snapshots,
|
...config.menu.buttons.snapshots,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.view.views.snapshots'),
|
title: localize('config.view.views.snapshots'),
|
||||||
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
'snapshots',
|
'snapshots',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
hold_action: createFrigateCardCustomAction(
|
hold_action: createFrigateCardSimpleAction(
|
||||||
'snapshot',
|
'snapshot',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cameraCapabilities?.supportsRecordings) {
|
if (aggregateCapabilities?.supportsRecordings) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:album',
|
icon: 'mdi:album',
|
||||||
...config.menu.buttons.recordings,
|
...config.menu.buttons.recordings,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.view.views.recordings'),
|
title: localize('config.view.views.recordings'),
|
||||||
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
'recordings',
|
'recordings',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
hold_action: createFrigateCardCustomAction(
|
hold_action: createFrigateCardSimpleAction(
|
||||||
'recording',
|
'recording',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
@@ -222,19 +233,19 @@ export class MenuButtonController {
|
|||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.view.views.image'),
|
title: localize('config.view.views.image'),
|
||||||
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Don't show the timeline button unless there's at least one non-birdseye
|
// Don't show the timeline button unless there's at least one non-birdseye
|
||||||
// camera with a Frigate camera name.
|
// camera with a Frigate camera name.
|
||||||
if (cameraCapabilities?.supportsTimeline) {
|
if (aggregateCapabilities?.supportsTimeline) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:chart-gantt',
|
icon: 'mdi:chart-gantt',
|
||||||
...config.menu.buttons.timeline,
|
...config.menu.buttons.timeline,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.view.views.timeline'),
|
title: localize('config.view.views.timeline'),
|
||||||
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction('timeline') as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +255,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.download,
|
...config.menu.buttons.download,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.download'),
|
title: localize('config.menu.buttons.download'),
|
||||||
tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction('download') as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +265,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.camera_ui,
|
...config.menu.buttons.camera_ui,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.camera_ui'),
|
title: localize('config.menu.buttons.camera_ui'),
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
'camera_ui',
|
'camera_ui',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
@@ -279,16 +290,16 @@ export class MenuButtonController {
|
|||||||
style: forbidden || muted ? {} : this._getEmphasizedStyle(true),
|
style: forbidden || muted ? {} : this._getEmphasizedStyle(true),
|
||||||
...(!forbidden &&
|
...(!forbidden &&
|
||||||
buttonType === 'momentary' && {
|
buttonType === 'momentary' && {
|
||||||
start_tap_action: createFrigateCardCustomAction(
|
start_tap_action: createFrigateCardSimpleAction(
|
||||||
'microphone_unmute',
|
'microphone_unmute',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
end_tap_action: createFrigateCardCustomAction(
|
end_tap_action: createFrigateCardSimpleAction(
|
||||||
'microphone_mute',
|
'microphone_mute',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
}),
|
}),
|
||||||
...(!forbidden &&
|
...(!forbidden &&
|
||||||
buttonType === 'toggle' && {
|
buttonType === 'toggle' && {
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
options.microphoneManager.isMuted()
|
options.microphoneManager.isMuted()
|
||||||
? 'microphone_unmute'
|
? 'microphone_unmute'
|
||||||
: 'microphone_mute',
|
: 'microphone_mute',
|
||||||
@@ -303,7 +314,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.fullscreen,
|
...config.menu.buttons.fullscreen,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.fullscreen'),
|
title: localize('config.menu.buttons.fullscreen'),
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
style: options?.inFullscreenMode ? this._getEmphasizedStyle() : {},
|
style: options?.inFullscreenMode ? this._getEmphasizedStyle() : {},
|
||||||
@@ -315,7 +326,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.expand,
|
...config.menu.buttons.expand,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.expand'),
|
title: localize('config.menu.buttons.expand'),
|
||||||
tap_action: createFrigateCardCustomAction('expand') as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction('expand') as FrigateCardCustomAction,
|
||||||
style: options?.inExpandedMode ? this._getEmphasizedStyle() : {},
|
style: options?.inExpandedMode ? this._getEmphasizedStyle() : {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -328,14 +339,8 @@ export class MenuButtonController {
|
|||||||
.map((playerEntityID) => {
|
.map((playerEntityID) => {
|
||||||
const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
|
const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
|
||||||
const state = hass.states[playerEntityID];
|
const state = hass.states[playerEntityID];
|
||||||
const playAction = createFrigateCardCustomAction('media_player', {
|
const playAction = createFrigateCardMediaPlayerAction(playerEntityID, 'play');
|
||||||
media_player: playerEntityID,
|
const stopAction = createFrigateCardMediaPlayerAction(playerEntityID, 'stop');
|
||||||
media_player_action: 'play',
|
|
||||||
});
|
|
||||||
const stopAction = createFrigateCardCustomAction('media_player', {
|
|
||||||
media_player: playerEntityID,
|
|
||||||
media_player_action: 'stop',
|
|
||||||
});
|
|
||||||
const disabled = !state || state.state === 'unavailable';
|
const disabled = !state || state.state === 'unavailable';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -368,7 +373,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.play,
|
...config.menu.buttons.play,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.play'),
|
title: localize('config.menu.buttons.play'),
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
paused ? 'play' : 'pause',
|
paused ? 'play' : 'pause',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
@@ -381,7 +386,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.mute,
|
...config.menu.buttons.mute,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.mute'),
|
title: localize('config.menu.buttons.mute'),
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
muted ? 'unmute' : 'mute',
|
muted ? 'unmute' : 'mute',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
@@ -394,29 +399,39 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.screenshot,
|
...config.menu.buttons.screenshot,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.screenshot'),
|
title: localize('config.menu.buttons.screenshot'),
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardSimpleAction(
|
||||||
'screenshot',
|
'screenshot',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (view.supportsMultipleDisplayModes() && visibleCameras.size > 1) {
|
if (view.supportsMultipleDisplayModes() && visibleCameraIDs.size > 1) {
|
||||||
const isGrid = view.isGrid();
|
const isGrid = view.isGrid();
|
||||||
const action = createFrigateCardCustomAction('display_mode_select', {
|
|
||||||
display_mode: isGrid ? 'single' : 'grid',
|
|
||||||
});
|
|
||||||
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
|
||||||
if (action) {
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: isGrid ? 'mdi:grid-off' : 'mdi:grid',
|
icon: isGrid ? 'mdi:grid-off' : 'mdi:grid',
|
||||||
...config.menu.buttons.display_mode,
|
...config.menu.buttons.display_mode,
|
||||||
|
style: isGrid ? this._getEmphasizedStyle() : {},
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: isGrid
|
title: isGrid
|
||||||
? localize('display_modes.single')
|
? localize('display_modes.single')
|
||||||
: localize('display_modes.grid'),
|
: localize('display_modes.grid'),
|
||||||
tap_action: action,
|
tap_action: createFrigateCardDisplayModeAction(isGrid ? 'single' : 'grid'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasUsablePTZ(selectedCameraCapabilities, config.live.controls.ptz)) {
|
||||||
|
const isOn =
|
||||||
|
view.context?.live?.ptzVisible === false
|
||||||
|
? false
|
||||||
|
: config.live.controls.ptz.mode === 'on';
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:pan',
|
||||||
|
...config.menu.buttons.ptz,
|
||||||
|
style: isOn ? this._getEmphasizedStyle() : {},
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.ptz'),
|
||||||
|
tap_action: createFrigateCardShowPTZAction(!isOn),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import {
|
||||||
|
Actions,
|
||||||
|
ActionsConfig,
|
||||||
|
FrigateCardPTZAction,
|
||||||
|
FrigateCardPTZActions,
|
||||||
|
FrigateCardPTZConfig,
|
||||||
|
PTZAction,
|
||||||
|
PTZControlAction,
|
||||||
|
PTZ_CONTROL_ACTIONS,
|
||||||
|
} from '../config/types';
|
||||||
|
import {
|
||||||
|
frigateCardHandleActionConfig,
|
||||||
|
getActionConfigGivenAction,
|
||||||
|
} from '../utils/action';
|
||||||
|
|
||||||
|
export class PTZController {
|
||||||
|
private _host: HTMLElement;
|
||||||
|
|
||||||
|
private _config: FrigateCardPTZConfig | null = null;
|
||||||
|
private _hass: HomeAssistant | null = null;
|
||||||
|
private _cameraManager: CameraManager | null = null;
|
||||||
|
private _cameraID: string | null = null;
|
||||||
|
private _actions: FrigateCardPTZActions | null = null;
|
||||||
|
private _forceVisibility?: boolean;
|
||||||
|
|
||||||
|
constructor(host: HTMLElement) {
|
||||||
|
this._host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config?: FrigateCardPTZConfig) {
|
||||||
|
this._config = config ?? null;
|
||||||
|
|
||||||
|
this._host.setAttribute('data-orientation', config?.orientation ?? 'horizontal');
|
||||||
|
this._host.setAttribute('data-position', config?.position ?? 'bottom-right');
|
||||||
|
this._host.setAttribute(
|
||||||
|
'style',
|
||||||
|
Object.entries(config?.style ?? {})
|
||||||
|
.map(([k, v]) => `${k}:${v}`)
|
||||||
|
.join(';'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfig(): FrigateCardPTZConfig | null {
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setHASS(hass?: HomeAssistant) {
|
||||||
|
this._hass = hass ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCamera(cameraManager?: CameraManager, cameraID?: string) {
|
||||||
|
this._cameraManager = cameraManager ?? null;
|
||||||
|
this._cameraID = cameraID ?? null;
|
||||||
|
|
||||||
|
this._calculateActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setForceVisibility(forceVisibility?: boolean): void {
|
||||||
|
this._forceVisibility = forceVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleAction(
|
||||||
|
ev: HASSDomEvent<{ action: string }>,
|
||||||
|
config?: ActionsConfig | null,
|
||||||
|
): void {
|
||||||
|
// Nothing else has the configuration for this action, so don't let it
|
||||||
|
// propagate further.
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const interaction: string = ev.detail.action;
|
||||||
|
const action = getActionConfigGivenAction(interaction, config);
|
||||||
|
if (config && action && this._hass) {
|
||||||
|
frigateCardHandleActionConfig(this._host, this._hass, config, interaction, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPTZActions(actionName: PTZControlAction): Actions | null {
|
||||||
|
const propertyName = 'actions_' + actionName;
|
||||||
|
return this._config?.[propertyName] ?? this._actions?.[propertyName] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hasAnyAction(): boolean {
|
||||||
|
for (const actionName of PTZ_CONTROL_ACTIONS) {
|
||||||
|
if ('actions_' + actionName in (this._actions ?? {})) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldDisplay(): boolean {
|
||||||
|
return this._forceVisibility === false
|
||||||
|
? false
|
||||||
|
: this._config?.mode === 'on' && this._hasAnyAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _calculateActions(): void {
|
||||||
|
const getDefaultAction = (
|
||||||
|
ptzAction: PTZAction,
|
||||||
|
options?: {
|
||||||
|
phase?: 'start' | 'stop';
|
||||||
|
preset?: string;
|
||||||
|
},
|
||||||
|
): FrigateCardPTZAction => ({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'ptz',
|
||||||
|
ptz_action: ptzAction,
|
||||||
|
...(options?.phase && { ptz_phase: options.phase }),
|
||||||
|
...(options?.preset && { ptz_preset: options.preset }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDefaultActions = (
|
||||||
|
ptzAction: PTZAction,
|
||||||
|
continuous: boolean,
|
||||||
|
preset?: string,
|
||||||
|
): Actions =>
|
||||||
|
continuous
|
||||||
|
? {
|
||||||
|
start_tap_action: getDefaultAction(ptzAction, {
|
||||||
|
phase: 'start',
|
||||||
|
preset: preset,
|
||||||
|
}),
|
||||||
|
end_tap_action: getDefaultAction(ptzAction, {
|
||||||
|
phase: 'stop',
|
||||||
|
preset: preset,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
tap_action: getDefaultAction(ptzAction, { preset: preset }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this._cameraManager || !this._cameraID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ptzCapabilities = this._cameraManager.getCameraCapabilities(
|
||||||
|
this._cameraID,
|
||||||
|
)?.ptz;
|
||||||
|
|
||||||
|
const defaultActions: FrigateCardPTZActions = {};
|
||||||
|
const panTilt = ptzCapabilities?.panTilt;
|
||||||
|
const zoom = ptzCapabilities?.zoom;
|
||||||
|
const presets = ptzCapabilities?.presets;
|
||||||
|
|
||||||
|
if (panTilt?.length) {
|
||||||
|
const continuous = panTilt.includes('continuous');
|
||||||
|
defaultActions.actions_up = getDefaultActions('up', continuous);
|
||||||
|
defaultActions.actions_down = getDefaultActions('down', continuous);
|
||||||
|
defaultActions.actions_left = getDefaultActions('left', continuous);
|
||||||
|
defaultActions.actions_right = getDefaultActions('right', continuous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoom?.length) {
|
||||||
|
const continuous = zoom.includes('continuous');
|
||||||
|
defaultActions.actions_zoom_in = getDefaultActions('zoom_in', continuous);
|
||||||
|
defaultActions.actions_zoom_out = getDefaultActions('zoom_out', continuous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presets?.length) {
|
||||||
|
defaultActions.actions_home = getDefaultActions('preset', false, presets[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._actions = {
|
||||||
|
...defaultActions,
|
||||||
|
...this._config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,7 +69,14 @@ export class FrigateCardCarousel extends LitElement {
|
|||||||
this.setAttribute('direction', this.direction);
|
this.setAttribute('direction', this.direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
const destroyProperties = ['direction', 'dragFree', 'transitionEffect'] as const;
|
const destroyProperties = [
|
||||||
|
'direction',
|
||||||
|
'dragEnabled',
|
||||||
|
'dragFree',
|
||||||
|
'loop',
|
||||||
|
'plugins',
|
||||||
|
'transitionEffect',
|
||||||
|
] as const;
|
||||||
if (destroyProperties.some((prop) => changedProps.has(prop))) {
|
if (destroyProperties.some((prop) => changedProps.has(prop))) {
|
||||||
this._carousel?.destroy();
|
this._carousel?.destroy();
|
||||||
this._carousel = null;
|
this._carousel = null;
|
||||||
|
|||||||
+8
-128
@@ -1,19 +1,18 @@
|
|||||||
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
html,
|
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
|
||||||
import {
|
import {
|
||||||
Actions,
|
ConditionsManagerEpoch,
|
||||||
ActionsConfig,
|
evaluateConditionViaEvent,
|
||||||
FrigateCardPTZConfig,
|
} from '../card-controller/conditions-manager.js';
|
||||||
|
import {
|
||||||
FrigateConditional,
|
FrigateConditional,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -23,19 +22,9 @@ import {
|
|||||||
PictureElements,
|
PictureElements,
|
||||||
} from '../config/types.js';
|
} from '../config/types.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import ptzStyle from '../scss/elements-ptz.scss';
|
|
||||||
import elementsStyle from '../scss/elements.scss';
|
import elementsStyle from '../scss/elements.scss';
|
||||||
import { FrigateCardError } from '../types.js';
|
import { FrigateCardError } from '../types.js';
|
||||||
import {
|
import { dispatchFrigateCardEvent, errorToConsole } from '../utils/basic.js';
|
||||||
frigateCardHandleActionConfig,
|
|
||||||
frigateCardHasAction,
|
|
||||||
getActionConfigGivenAction,
|
|
||||||
} from '../utils/action.js';
|
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
|
||||||
import {
|
|
||||||
ConditionsManagerEpoch,
|
|
||||||
evaluateConditionViaEvent,
|
|
||||||
} from '../card-controller/conditions-manager.js';
|
|
||||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||||
|
|
||||||
/* A note on picture element rendering:
|
/* A note on picture element rendering:
|
||||||
@@ -122,7 +111,7 @@ export class FrigateCardElementsCore extends LitElement {
|
|||||||
try {
|
try {
|
||||||
element.setConfig(config);
|
element.setConfig(config);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
errorToConsole(e as Error, console.error);
|
||||||
throw new FrigateCardError(localize('error.invalid_elements_config'));
|
throw new FrigateCardError(localize('error.invalid_elements_config'));
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
@@ -342,114 +331,6 @@ export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuI
|
|||||||
@customElement('frigate-card-menu-submenu-select')
|
@customElement('frigate-card-menu-submenu-select')
|
||||||
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
|
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
|
||||||
|
|
||||||
@customElement('frigate-card-ptz')
|
|
||||||
export class FrigateCardPTZ extends LitElement {
|
|
||||||
@property({ attribute: false })
|
|
||||||
public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
protected _config: FrigateCardPTZConfig | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the card config.
|
|
||||||
* @param config The configuration.
|
|
||||||
*/
|
|
||||||
public setConfig(config: FrigateCardPTZConfig): void {
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before each update.
|
|
||||||
*/
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
|
||||||
if (changedProps.has('_config')) {
|
|
||||||
this.setAttribute('data-orientation', this._config?.orientation ?? 'vertical');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a PTZ action.
|
|
||||||
* @param ev The actionHandler event.
|
|
||||||
* @param config The action configuration.
|
|
||||||
*/
|
|
||||||
protected _actionHandler(
|
|
||||||
ev: HASSDomEvent<{ action: string }>,
|
|
||||||
config?: ActionsConfig,
|
|
||||||
): void {
|
|
||||||
// Nothing else has the configuration for this action, so don't let it
|
|
||||||
// propagate further.
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
const interaction: string = ev.detail.action;
|
|
||||||
const action = getActionConfigGivenAction(interaction, config);
|
|
||||||
if (config && action && this.hass) {
|
|
||||||
frigateCardHandleActionConfig(this, this.hass, config, interaction, action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the elements.
|
|
||||||
* @returns A rendered template or void.
|
|
||||||
*/
|
|
||||||
protected render(): TemplateResult | void {
|
|
||||||
if (!this._config) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const renderIcon = (
|
|
||||||
name: string,
|
|
||||||
icon: string,
|
|
||||||
actions?: Actions,
|
|
||||||
): TemplateResult => {
|
|
||||||
const hasHold = frigateCardHasAction(actions?.hold_action);
|
|
||||||
const hasDoubleClick = frigateCardHasAction(actions?.double_tap_action);
|
|
||||||
const classes = {
|
|
||||||
[name]: true,
|
|
||||||
disabled: !actions,
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`<ha-icon
|
|
||||||
class=${classMap(classes)}
|
|
||||||
icon=${icon}
|
|
||||||
.actionHandler=${actionHandler({
|
|
||||||
hasHold: hasHold,
|
|
||||||
hasDoubleClick: hasDoubleClick,
|
|
||||||
})}
|
|
||||||
.title=${localize(`elements.ptz.${name}`)}
|
|
||||||
@action=${(ev) => this._actionHandler(ev, actions)}
|
|
||||||
></ha-icon>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return html` <div class="ptz">
|
|
||||||
<div class="ptz-move">
|
|
||||||
${renderIcon('right', 'mdi:arrow-right', this._config.actions_right)}
|
|
||||||
${renderIcon('left', 'mdi:arrow-left', this._config.actions_left)}
|
|
||||||
${renderIcon('up', 'mdi:arrow-up', this._config.actions_up)}
|
|
||||||
${renderIcon('down', 'mdi:arrow-down', this._config.actions_down)}
|
|
||||||
</div>
|
|
||||||
${this._config.actions_zoom_in || this._config.actions_zoom_out
|
|
||||||
? html` <div class="ptz-zoom">
|
|
||||||
${renderIcon('zoom_in', 'mdi:plus', this._config.actions_zoom_in)}
|
|
||||||
${renderIcon('zoom_out', 'mdi:minus', this._config.actions_zoom_out)}
|
|
||||||
</div>`
|
|
||||||
: html``}
|
|
||||||
${this._config.actions_home
|
|
||||||
? html`
|
|
||||||
<div class="ptz-home">
|
|
||||||
${renderIcon('home', 'mdi:home', this._config.actions_home)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html``}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return compiled CSS styles.
|
|
||||||
*/
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return unsafeCSS(ptzStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'frigate-card-conditional': FrigateCardElementsConditional;
|
'frigate-card-conditional': FrigateCardElementsConditional;
|
||||||
@@ -459,6 +340,5 @@ declare global {
|
|||||||
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
|
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
|
||||||
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
|
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
|
||||||
'frigate-card-elements-core': FrigateCardElementsCore;
|
'frigate-card-elements-core': FrigateCardElementsCore;
|
||||||
'frigate-card-ptz': FrigateCardPTZ;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-39
@@ -13,7 +13,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
|||||||
import { keyed } from 'lit/directives/keyed.js';
|
import { keyed } from 'lit/directives/keyed.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { CameraConfigs, CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
import {
|
import {
|
||||||
ConditionsManagerEpoch,
|
ConditionsManagerEpoch,
|
||||||
getOverriddenConfig,
|
getOverriddenConfig,
|
||||||
@@ -45,6 +45,7 @@ import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy
|
|||||||
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
||||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||||
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
||||||
|
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
||||||
import {
|
import {
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
dispatchMediaUnloadedEvent,
|
dispatchMediaUnloadedEvent,
|
||||||
@@ -55,18 +56,20 @@ import { dispatchViewContextChangeEvent, View } from '../../view/view.js';
|
|||||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||||
import { renderMessage } from '../message.js';
|
import { renderMessage } from '../message.js';
|
||||||
import '../next-prev-control.js';
|
import '../next-prev-control.js';
|
||||||
|
import '../ptz.js';
|
||||||
|
import { FrigateCardPTZ } from '../ptz.js';
|
||||||
import '../surround.js';
|
import '../surround.js';
|
||||||
import '../title-control.js';
|
import '../title-control.js';
|
||||||
import {
|
import {
|
||||||
FrigateCardTitleControl,
|
FrigateCardTitleControl,
|
||||||
getDefaultTitleConfigForView,
|
getDefaultTitleConfigForView,
|
||||||
} from '../title-control.js';
|
} from '../title-control.js';
|
||||||
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
|
||||||
|
|
||||||
interface LiveViewContext {
|
interface LiveViewContext {
|
||||||
// A cameraID override (used for dependencies/substreams to force a different
|
// A cameraID override (used for dependencies/substreams to force a different
|
||||||
// camera to be live rather than the camera selected in the view).
|
// camera to be live rather than the camera selected in the view).
|
||||||
overrides?: Map<string, string>;
|
overrides?: Map<string, string>;
|
||||||
|
ptzVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'view' {
|
declare module 'view' {
|
||||||
@@ -387,6 +390,10 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
// 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 _refTitleControl: Ref<FrigateCardTitleControl> = createRef();
|
protected _refTitleControl: Ref<FrigateCardTitleControl> = createRef();
|
||||||
|
protected _refPTZControl: Ref<FrigateCardPTZ> = createRef();
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _mediaHasLoaded = false;
|
||||||
|
|
||||||
protected _getTransitionEffect(): TransitionEffect {
|
protected _getTransitionEffect(): TransitionEffect {
|
||||||
return (
|
return (
|
||||||
@@ -449,25 +456,20 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getSlides(): [TemplateResult[], Record<string, number>] {
|
protected _getSlides(): [TemplateResult[], Record<string, number>] {
|
||||||
let cameras: CameraConfigs | null = null;
|
if (!this.cameraManager) {
|
||||||
if (this.viewFilterCameraID) {
|
|
||||||
const config = this.cameraManager
|
|
||||||
?.getStore()
|
|
||||||
.getCameraConfig(this.viewFilterCameraID);
|
|
||||||
if (config) {
|
|
||||||
cameras = new Map([[this.viewFilterCameraID, config]]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cameras = this.cameraManager?.getStore().getVisibleCameras() ?? null;
|
|
||||||
}
|
|
||||||
if (!cameras) {
|
|
||||||
return [[], {}];
|
return [[], {}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cameraIDs = this.viewFilterCameraID
|
||||||
|
? new Set([this.viewFilterCameraID])
|
||||||
|
: this.cameraManager.getStore().getVisibleCameraIDs();
|
||||||
|
|
||||||
const slides: TemplateResult[] = [];
|
const slides: TemplateResult[] = [];
|
||||||
const cameraToSlide: Record<string, number> = {};
|
const cameraToSlide: Record<string, number> = {};
|
||||||
|
|
||||||
for (const [cameraID, cameraConfig] of cameras) {
|
for (const [cameraID, cameraConfig] of this.cameraManager
|
||||||
|
.getStore()
|
||||||
|
.getCameraConfigEntries(cameraIDs)) {
|
||||||
const liveCameraID =
|
const liveCameraID =
|
||||||
this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
||||||
const liveCameraConfig =
|
const liveCameraConfig =
|
||||||
@@ -487,9 +489,9 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _setViewHandler(ev: CustomEvent<CarouselSelected>): void {
|
protected _setViewHandler(ev: CustomEvent<CarouselSelected>): void {
|
||||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||||
if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
|
if (cameraIDs && ev.detail.index !== this._getSelectedCameraIndex()) {
|
||||||
this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]);
|
this._setViewCameraID([...cameraIDs][ev.detail.index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,22 +577,23 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
|
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
|
||||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
const cameraIDs = this.cameraManager
|
||||||
if (this.viewFilterCameraID || !cameras || !this.view || !this.hass) {
|
? [...this.cameraManager.getStore().getVisibleCameraIDs()]
|
||||||
|
: [];
|
||||||
|
if (this.viewFilterCameraID || cameraIDs.length <= 1 || !this.view || !this.hass) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cameraID = this.viewFilterCameraID ?? this.view.camera;
|
const cameraID = this.viewFilterCameraID ?? this.view.camera;
|
||||||
const keys = Array.from(cameras.keys());
|
const currentIndex = cameraIDs.indexOf(cameraID);
|
||||||
const currentIndex = keys.indexOf(cameraID);
|
|
||||||
|
|
||||||
if (currentIndex < 0 || cameras.size <= 1) {
|
if (currentIndex < 0) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
keys[currentIndex > 0 ? currentIndex - 1 : cameras.size - 1],
|
cameraIDs[currentIndex > 0 ? currentIndex - 1 : cameraIDs.length - 1],
|
||||||
keys[currentIndex + 1 < cameras.size ? currentIndex + 1 : 0],
|
cameraIDs[currentIndex + 1 < cameraIDs.length ? currentIndex + 1 : 0],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,18 +611,19 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
const hasMultipleCameras = slides.length > 1;
|
const hasMultipleCameras = slides.length > 1;
|
||||||
const [prevID, nextID] = this._getCameraIDsOfNeighbors();
|
const [prevID, nextID] = this._getCameraIDsOfNeighbors();
|
||||||
|
|
||||||
const overrideCameraID = (cameraID: string): string => {
|
const getOverrideCameraID = (cameraID: string): string => {
|
||||||
return this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
return this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cameraMetadataPrevious = prevID
|
const cameraMetadataPrevious = prevID
|
||||||
? this.cameraManager.getCameraMetadata(overrideCameraID(prevID))
|
? this.cameraManager.getCameraMetadata(getOverrideCameraID(prevID))
|
||||||
: null;
|
: null;
|
||||||
|
const cameraID = this.viewFilterCameraID ?? this.view.camera;
|
||||||
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
|
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
|
||||||
overrideCameraID(this.viewFilterCameraID ?? this.view.camera),
|
getOverrideCameraID(cameraID),
|
||||||
);
|
);
|
||||||
const cameraMetadataNext = nextID
|
const cameraMetadataNext = nextID
|
||||||
? this.cameraManager.getCameraMetadata(overrideCameraID(nextID))
|
? this.cameraManager.getCameraMetadata(getOverrideCameraID(nextID))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const titleConfig = getDefaultTitleConfigForView(
|
const titleConfig = getDefaultTitleConfigForView(
|
||||||
@@ -656,6 +660,10 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
if (this._refTitleControl.value) {
|
if (this._refTitleControl.value) {
|
||||||
this._refTitleControl.value.show();
|
this._refTitleControl.value.show();
|
||||||
}
|
}
|
||||||
|
this._mediaHasLoaded = true;
|
||||||
|
}}
|
||||||
|
@frigate-card:media:unloaded=${() => {
|
||||||
|
this._mediaHasLoaded = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
@@ -688,6 +696,14 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
>
|
>
|
||||||
</frigate-card-next-previous-control>
|
</frigate-card-next-previous-control>
|
||||||
</frigate-card-carousel>
|
</frigate-card-carousel>
|
||||||
|
<frigate-card-ptz
|
||||||
|
.hass=${this.hass}
|
||||||
|
.config=${this.overriddenLiveConfig.controls.ptz}
|
||||||
|
.cameraManager=${this.cameraManager}
|
||||||
|
.cameraID=${cameraID}
|
||||||
|
.forceVisibility=${this._mediaHasLoaded && this.view.context?.live?.ptzVisible}
|
||||||
|
>
|
||||||
|
</frigate-card-ptz>
|
||||||
${cameraMetadataCurrent && titleConfig
|
${cameraMetadataCurrent && titleConfig
|
||||||
? html`<frigate-card-title-control
|
? html`<frigate-card-title-control
|
||||||
${ref(this._refTitleControl)}
|
${ref(this._refTitleControl)}
|
||||||
@@ -846,9 +862,6 @@ export class FrigateCardLiveProvider
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Component disconnected callback.
|
|
||||||
*/
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._isVideoMediaLoaded = false;
|
this._isVideoMediaLoaded = false;
|
||||||
}
|
}
|
||||||
@@ -860,9 +873,6 @@ export class FrigateCardLiveProvider
|
|||||||
this._isVideoMediaLoaded = true;
|
this._isVideoMediaLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before each update.
|
|
||||||
*/
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('load')) {
|
if (changedProps.has('load')) {
|
||||||
if (!this.load) {
|
if (!this.load) {
|
||||||
@@ -915,10 +925,6 @@ export class FrigateCardLiveProvider
|
|||||||
: template;
|
: template;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Master render method.
|
|
||||||
* @returns A rendered template.
|
|
||||||
*/
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.load || !this.hass || !this.liveConfig || !this.cameraConfig) {
|
if (!this.load || !this.hass || !this.liveConfig || !this.cameraConfig) {
|
||||||
return;
|
return;
|
||||||
@@ -941,6 +947,8 @@ export class FrigateCardLiveProvider
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stateObj.state === 'unavailable') {
|
if (stateObj.state === 'unavailable') {
|
||||||
|
dispatchMediaUnloadedEvent(this);
|
||||||
|
|
||||||
// An unavailable camera gets a message rendered in place vs dispatched,
|
// An unavailable camera gets a message rendered in place vs dispatched,
|
||||||
// as this may be a common occurrence (e.g. Frigate cameras that stop
|
// as this may be a common occurrence (e.g. Frigate cameras that stop
|
||||||
// receiving frames). Otherwise a single temporarily unavailable camera
|
// receiving frames). Otherwise a single temporarily unavailable camera
|
||||||
|
|||||||
@@ -167,8 +167,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_ev: CustomEvent<{ value: unknown }>,
|
_ev: CustomEvent<{ value: unknown }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
const visibleCameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||||
if (!this.hass || !cameras || !this.cameraManager || !this.view) {
|
if (!this.hass || !visibleCameraIDs || !this.cameraManager || !this.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cameraIDs =
|
const cameraIDs =
|
||||||
getArrayValueAsSet(this._refCamera.value?.value) ?? new Set(cameras.keys());
|
getArrayValueAsSet(this._refCamera.value?.value) ?? visibleCameraIDs;
|
||||||
const mediaType = this._refMediaType.value?.value as
|
const mediaType = this._refMediaType.value?.value as
|
||||||
| MediaFilterMediaType
|
| MediaFilterMediaType
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -268,9 +268,9 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('cameraManager')) {
|
if (changedProps.has('cameraManager')) {
|
||||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
const cameras = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||||
if (cameras) {
|
if (cameras) {
|
||||||
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
|
this._cameraOptions = [...cameras].map((cameraID) => ({
|
||||||
value: cameraID,
|
value: cameraID,
|
||||||
label: this.hass
|
label: this.hass
|
||||||
? this.cameraManager?.getCameraMetadata(cameraID)?.title ?? ''
|
? this.cameraManager?.getCameraMetadata(cameraID)?.title ?? ''
|
||||||
@@ -319,8 +319,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
|
|
||||||
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
|
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
|
||||||
const queries = this.view?.query?.getQueries();
|
const queries = this.view?.query?.getQueries();
|
||||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
const visibleCameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||||
if (!this.view || !queries || !cameras) {
|
if (!this.view || !queries || !visibleCameraIDs) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
);
|
);
|
||||||
// Special note: If all visible cameras are selected, this is the same as no
|
// Special note: If all visible cameras are selected, this is the same as no
|
||||||
// selector at all.
|
// selector at all.
|
||||||
if (cameraIDSets.length === 1 && !isEqual(queries[0].cameraIDs, cameras)) {
|
if (cameraIDSets.length === 1 && !isEqual(queries[0].cameraIDs, visibleCameraIDs)) {
|
||||||
cameraIDs = [...queries[0].cameraIDs];
|
cameraIDs = [...queries[0].cameraIDs];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
* @param action The action to check.
|
* @param action The action to check.
|
||||||
* @returns `true` if the action toggles the menu, `false` otherwise.
|
* @returns `true` if the action toggles the menu, `false` otherwise.
|
||||||
*/
|
*/
|
||||||
protected _isMenuToggleAction(action: ActionType | undefined): boolean {
|
protected _isMenuToggleAction(action: ActionType | null): boolean {
|
||||||
// Determine if this action is a Frigate card action, if so handle it
|
// Determine if this action is a Frigate card action, if so handle it
|
||||||
// internally.
|
// internally.
|
||||||
if (!action) {
|
if (!action) {
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
unsafeCSS
|
||||||
|
} from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
|
import { PTZController } from '../components-lib/ptz-controller.js';
|
||||||
|
import { Actions, FrigateCardPTZConfig } from '../config/types.js';
|
||||||
|
import { localize } from '../localize/localize.js';
|
||||||
|
import ptzStyle from '../scss/ptz.scss';
|
||||||
|
import { frigateCardHasAction } from '../utils/action.js';
|
||||||
|
|
||||||
|
@customElement('frigate-card-ptz')
|
||||||
|
export class FrigateCardPTZ extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public config?: FrigateCardPTZConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraID?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public forceVisibility?: boolean;
|
||||||
|
|
||||||
|
protected _controller = new PTZController(this);
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has('config')) {
|
||||||
|
this._controller.setConfig(this.config);
|
||||||
|
}
|
||||||
|
if (changedProps.has('hass')) {
|
||||||
|
this._controller.setHASS(this.hass);
|
||||||
|
}
|
||||||
|
if (changedProps.has('cameraManager') || changedProps.has('cameraID')) {
|
||||||
|
this._controller.setCamera(this.cameraManager, this.cameraID);
|
||||||
|
}
|
||||||
|
if (changedProps.has('forceVisibility')) {
|
||||||
|
this._controller.setForceVisibility(this.forceVisibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this._controller.shouldDisplay()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderIcon = (
|
||||||
|
name: string,
|
||||||
|
icon: string,
|
||||||
|
actions: Actions | null,
|
||||||
|
): TemplateResult => {
|
||||||
|
const classes = {
|
||||||
|
[name]: true,
|
||||||
|
disabled: !actions,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`<ha-icon
|
||||||
|
class=${classMap(classes)}
|
||||||
|
icon=${icon}
|
||||||
|
.actionHandler=${actionHandler({
|
||||||
|
hasHold: frigateCardHasAction(actions?.hold_action),
|
||||||
|
hasDoubleClick: frigateCardHasAction(actions?.double_tap_action),
|
||||||
|
})}
|
||||||
|
.title=${localize(`elements.ptz.${name}`)}
|
||||||
|
@action=${(ev: HASSDomEvent<{ action: string }>) =>
|
||||||
|
this._controller.handleAction(ev, actions)}
|
||||||
|
></ha-icon>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = this._controller.getConfig();
|
||||||
|
const actionsZoomIn = this._controller.getPTZActions('zoom_in');
|
||||||
|
const actionsZoomOut = this._controller.getPTZActions('zoom_out');
|
||||||
|
const actionsHome = this._controller.getPTZActions('home');
|
||||||
|
|
||||||
|
return html` <div class="ptz">
|
||||||
|
${!config?.hide_pan_tilt
|
||||||
|
? html`<div class="ptz-move">
|
||||||
|
${renderIcon(
|
||||||
|
'right',
|
||||||
|
'mdi:arrow-right',
|
||||||
|
this._controller.getPTZActions('right'),
|
||||||
|
)}
|
||||||
|
${renderIcon(
|
||||||
|
'left',
|
||||||
|
'mdi:arrow-left',
|
||||||
|
this._controller.getPTZActions('left'),
|
||||||
|
)}
|
||||||
|
${renderIcon('up', 'mdi:arrow-up', this._controller.getPTZActions('up'))}
|
||||||
|
${renderIcon(
|
||||||
|
'down',
|
||||||
|
'mdi:arrow-down',
|
||||||
|
this._controller.getPTZActions('down'),
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
${!config?.hide_zoom && (actionsZoomIn || actionsZoomOut)
|
||||||
|
? html` <div class="ptz-zoom">
|
||||||
|
${renderIcon('zoom_in', 'mdi:plus', actionsZoomIn)}
|
||||||
|
${renderIcon('zoom_out', 'mdi:minus', actionsZoomOut)}
|
||||||
|
</div>`
|
||||||
|
: html``}
|
||||||
|
${!config?.hide_home && actionsHome
|
||||||
|
? html`
|
||||||
|
<div class="ptz-home">${renderIcon('home', 'mdi:home', actionsHome)}</div>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(ptzStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-ptz': FrigateCardPTZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import basicBlockStyle from '../scss/basic-block.scss';
|
import basicBlockStyle from '../scss/basic-block.scss';
|
||||||
import { ClipsOrSnapshotsOrAll, ExtendedHomeAssistant } from '../types.js';
|
import { ClipsOrSnapshotsOrAll, ExtendedHomeAssistant } from '../types.js';
|
||||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { getAllDependentCameras } from '../utils/camera.js';
|
|
||||||
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
|
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import './surround-basic.js';
|
import './surround-basic.js';
|
||||||
@@ -140,7 +139,8 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
if (this.view?.is('live')) {
|
if (this.view?.is('live')) {
|
||||||
return this.view.isGrid()
|
return this.view.isGrid()
|
||||||
? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
|
? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
|
||||||
: getAllDependentCameras(this.cameraManager, this.view.camera);
|
: this.cameraManager?.getStore().getAllDependentCameras(this.view.camera) ??
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
if (this.view.isViewerView()) {
|
if (this.view.isViewerView()) {
|
||||||
return this.view.query?.getQueryCameraIDs() ?? null;
|
return this.view.query?.getQueryCameraIDs() ?? null;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ExtendedHomeAssistant } from '../types.js';
|
|||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { CarouselDirection } from '../utils/embla/carousel-controller.js';
|
import { CarouselDirection } from '../utils/embla/carousel-controller.js';
|
||||||
|
import AutoSize from '../utils/embla/plugins/auto-size/auto-size.js';
|
||||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import './carousel.js';
|
import './carousel.js';
|
||||||
@@ -136,6 +137,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
|||||||
|
|
||||||
return html`<frigate-card-carousel
|
return html`<frigate-card-carousel
|
||||||
direction=${direction}
|
direction=${direction}
|
||||||
|
.plugins=${[AutoSize()]}
|
||||||
.selected=${this._getSelectedSlide() ?? 0}
|
.selected=${this._getSelectedSlide() ?? 0}
|
||||||
.dragFree=${true}
|
.dragFree=${true}
|
||||||
>
|
>
|
||||||
|
|||||||
+48
-29
@@ -9,10 +9,17 @@ import {
|
|||||||
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';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch, getOverridesByKey } from '../card-controller/conditions-manager.js';
|
import {
|
||||||
|
ConditionsManagerEpoch,
|
||||||
|
getOverridesByKey,
|
||||||
|
} from '../card-controller/conditions-manager.js';
|
||||||
|
import {
|
||||||
|
CardWideConfig,
|
||||||
|
FrigateCardConfig,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
} from '../config/types.js';
|
||||||
import viewsStyle from '../scss/views.scss';
|
import viewsStyle from '../scss/views.scss';
|
||||||
import { ExtendedHomeAssistant } from '../types.js';
|
import { ExtendedHomeAssistant } from '../types.js';
|
||||||
import { ConfigManager } from '../card-controller/config-manager.js';
|
|
||||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
@@ -32,7 +39,16 @@ export class FrigateCardViews extends LitElement {
|
|||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public configManager?: ConfigManager;
|
public nonOverriddenConfig?: FrigateCardConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public overriddenConfig?: FrigateCardConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public rawConfig?: RawFrigateCardConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public resolvedMediaCache?: ResolvedMediaCache;
|
public resolvedMediaCache?: ResolvedMediaCache;
|
||||||
@@ -94,21 +110,21 @@ export class FrigateCardViews extends LitElement {
|
|||||||
return (
|
return (
|
||||||
// Special case: Never preload for diagnostics -- we want that to be as
|
// Special case: Never preload for diagnostics -- we want that to be as
|
||||||
// minimal as possible.
|
// minimal as possible.
|
||||||
!!this.configManager?.getConfig()?.live.preload && !this.view?.is('diagnostics')
|
!!this.overriddenConfig?.live.preload && !this.view?.is('diagnostics')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
const config = this.configManager?.getConfig();
|
|
||||||
const nonOverriddenConfig = this.configManager?.getNonOverriddenConfig();
|
|
||||||
const cardWideConfig = this.configManager?.getCardWideConfig();
|
|
||||||
const rawConfig = this.configManager?.getRawConfig();
|
|
||||||
|
|
||||||
// Only essential items should be added to the below list, since we want the
|
// Only essential items should be added to the below list, since we want the
|
||||||
// overall views pane to render in ~almost all cases (e.g. for a camera
|
// overall views pane to render in ~almost all cases (e.g. for a camera
|
||||||
// initialization error to display, `view` and `cameraConfig` may both be
|
// initialization error to display, `view` and `cameraConfig` may both be
|
||||||
// undefined, but we still want to render).
|
// undefined, but we still want to render).
|
||||||
if (!this.hass || !config || !nonOverriddenConfig || !cardWideConfig) {
|
if (
|
||||||
|
!this.hass ||
|
||||||
|
!this.overriddenConfig ||
|
||||||
|
!this.nonOverriddenConfig ||
|
||||||
|
!this.cardWideConfig
|
||||||
|
) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,17 +138,17 @@ export class FrigateCardViews extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const thumbnailConfig = this.view?.is('live')
|
const thumbnailConfig = this.view?.is('live')
|
||||||
? config.live.controls.thumbnails
|
? this.overriddenConfig.live.controls.thumbnails
|
||||||
: this.view?.isViewerView()
|
: this.view?.isViewerView()
|
||||||
? config.media_viewer.controls.thumbnails
|
? this.overriddenConfig.media_viewer.controls.thumbnails
|
||||||
: this.view?.is('timeline')
|
: this.view?.is('timeline')
|
||||||
? config.timeline.controls.thumbnails
|
? this.overriddenConfig.timeline.controls.thumbnails
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const miniTimelineConfig = this.view?.is('live')
|
const miniTimelineConfig = this.view?.is('live')
|
||||||
? config.live.controls.timeline
|
? this.overriddenConfig.live.controls.timeline
|
||||||
: this.view?.isViewerView()
|
: this.view?.isViewerView()
|
||||||
? config.media_viewer.controls.timeline
|
? this.overriddenConfig.media_viewer.controls.timeline
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const cameraConfig = this.view
|
const cameraConfig = this.view
|
||||||
@@ -144,16 +160,16 @@ export class FrigateCardViews extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.fetchMedia=${this.view?.is('live')
|
.fetchMedia=${this.view?.is('live')
|
||||||
? config.live.controls.thumbnails.media
|
? this.overriddenConfig.live.controls.thumbnails.media
|
||||||
: undefined}
|
: undefined}
|
||||||
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
||||||
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
${!this.hide && this.view?.is('image') && cameraConfig
|
${!this.hide && this.view?.is('image') && cameraConfig
|
||||||
? html` <frigate-card-image
|
? html` <frigate-card-image
|
||||||
.imageConfig=${config.image}
|
.imageConfig=${this.overriddenConfig.image}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
@@ -165,9 +181,9 @@ export class FrigateCardViews extends LitElement {
|
|||||||
? html` <frigate-card-gallery
|
? html` <frigate-card-gallery
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.galleryConfig=${config.media_gallery}
|
.galleryConfig=${this.overriddenConfig.media_gallery}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-gallery>`
|
</frigate-card-gallery>`
|
||||||
: ``}
|
: ``}
|
||||||
@@ -176,10 +192,10 @@ export class FrigateCardViews extends LitElement {
|
|||||||
<frigate-card-viewer
|
<frigate-card-viewer
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.viewerConfig=${config.media_viewer}
|
.viewerConfig=${this.overriddenConfig.media_viewer}
|
||||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-viewer>
|
</frigate-card-viewer>
|
||||||
`
|
`
|
||||||
@@ -188,16 +204,16 @@ export class FrigateCardViews extends LitElement {
|
|||||||
? html` <frigate-card-timeline
|
? html` <frigate-card-timeline
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.timelineConfig=${config.timeline}
|
.timelineConfig=${this.overriddenConfig.timeline}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-timeline>`
|
</frigate-card-timeline>`
|
||||||
: ``}
|
: ``}
|
||||||
${!this.hide && this.view?.is('diagnostics')
|
${!this.hide && this.view?.is('diagnostics')
|
||||||
? html` <frigate-card-diagnostics
|
? html` <frigate-card-diagnostics
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.rawConfig=${rawConfig}
|
.rawConfig=${this.rawConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-diagnostics>`
|
</frigate-card-diagnostics>`
|
||||||
: ``}
|
: ``}
|
||||||
@@ -213,12 +229,15 @@ export class FrigateCardViews extends LitElement {
|
|||||||
<frigate-card-live
|
<frigate-card-live
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.nonOverriddenLiveConfig=${nonOverriddenConfig.live}
|
.nonOverriddenLiveConfig=${this.nonOverriddenConfig.live}
|
||||||
.overriddenLiveConfig=${config.live}
|
.overriddenLiveConfig=${this.overriddenConfig.live}
|
||||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||||
.liveOverrides=${getOverridesByKey('live', config.overrides)}
|
.liveOverrides=${getOverridesByKey(
|
||||||
|
'live',
|
||||||
|
this.overriddenConfig.overrides,
|
||||||
|
)}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.microphoneStream=${this.microphoneStream}
|
.microphoneStream=${this.microphoneStream}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
>
|
>
|
||||||
|
|||||||
+68
-2
@@ -3,7 +3,7 @@ import get from 'lodash-es/get';
|
|||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import set from 'lodash-es/set';
|
import set from 'lodash-es/set';
|
||||||
import unset from 'lodash-es/unset';
|
import unset from 'lodash-es/unset';
|
||||||
import { RawFrigateCardConfig } from './config/types';
|
import { RawFrigateCardConfig, RawFrigateCardConfigArray } from './config/types';
|
||||||
import {
|
import {
|
||||||
CONF_CAMERAS,
|
CONF_CAMERAS,
|
||||||
CONF_CAMERAS_GLOBAL_IMAGE,
|
CONF_CAMERAS_GLOBAL_IMAGE,
|
||||||
@@ -303,7 +303,7 @@ export const deleteTransform = function (_value: unknown): number | null | undef
|
|||||||
};
|
};
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
// Upgrade Related Functions: Specific Transforms
|
// Upgrade Related Functions: Specific Transforms / Upgraders
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -360,6 +360,71 @@ const frigateUIActionTransform = (data: unknown): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform element PTZ to native live PTZ.
|
||||||
|
* @param data Input data.
|
||||||
|
* @returns `true` if the configuration was modified.
|
||||||
|
*/
|
||||||
|
const upgradePTZElementsToLive = function (): (data: unknown) => boolean {
|
||||||
|
return function (data: unknown): boolean {
|
||||||
|
if (
|
||||||
|
typeof data !== 'object' ||
|
||||||
|
!data ||
|
||||||
|
!(CONF_ELEMENTS in data) ||
|
||||||
|
!Array.isArray(data[CONF_ELEMENTS])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundPTZ = false;
|
||||||
|
const movePTZ = (element: RawFrigateCardConfig): void => {
|
||||||
|
if (!foundPTZ) {
|
||||||
|
if (!get(data, 'live.controls.ptz')) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { type: _, ...newPTZ } = element;
|
||||||
|
set(data, 'live.controls.ptz', newPTZ);
|
||||||
|
}
|
||||||
|
foundPTZ = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processElements = (
|
||||||
|
elements: RawFrigateCardConfigArray,
|
||||||
|
): RawFrigateCardConfigArray => {
|
||||||
|
const newElements: RawFrigateCardConfigArray = [];
|
||||||
|
for (const element of elements) {
|
||||||
|
if (element['type'] === 'custom:frigate-card-ptz') {
|
||||||
|
movePTZ(element);
|
||||||
|
} else if (
|
||||||
|
(element['type'] === 'conditional' ||
|
||||||
|
element['type'] === 'custom:frigate-card-conditional') &&
|
||||||
|
Array.isArray(element['elements'])
|
||||||
|
) {
|
||||||
|
const newConditionalElements = processElements(element['elements']);
|
||||||
|
if (newConditionalElements.length) {
|
||||||
|
element['elements'] = newConditionalElements;
|
||||||
|
newElements.push(element);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newElements.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newElements = processElements(data[CONF_ELEMENTS]);
|
||||||
|
|
||||||
|
if (foundPTZ) {
|
||||||
|
if (newElements.length) {
|
||||||
|
data[CONF_ELEMENTS] = newElements;
|
||||||
|
} else {
|
||||||
|
delete data[CONF_ELEMENTS];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundPTZ;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const UPGRADES = [
|
const UPGRADES = [
|
||||||
// v4.0.0 -> v4.1.0
|
// v4.0.0 -> v4.1.0
|
||||||
upgradeArrayOfObjects(
|
upgradeArrayOfObjects(
|
||||||
@@ -408,4 +473,5 @@ const UPGRADES = [
|
|||||||
typeof data === 'object' && data ? <RawFrigateCardConfig>data : {},
|
typeof data === 'object' && data ? <RawFrigateCardConfig>data : {},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
upgradePTZElementsToLive(),
|
||||||
];
|
];
|
||||||
|
|||||||
+105
-68
@@ -65,6 +65,19 @@ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
|
|||||||
export type AutoUnmuteCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
|
export type AutoUnmuteCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
|
||||||
export type AutoPlayCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
|
export type AutoPlayCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
|
||||||
|
|
||||||
|
const PTZ_BASE_ACTIONS = ['left', 'right', 'up', 'down', 'zoom_in', 'zoom_out'] as const;
|
||||||
|
|
||||||
|
// PTZ actions as used by the PTZ control (includes a 'home' button).
|
||||||
|
export const PTZ_CONTROL_ACTIONS = [...PTZ_BASE_ACTIONS, 'home'] as const;
|
||||||
|
export type PTZControlAction = (typeof PTZ_CONTROL_ACTIONS)[number];
|
||||||
|
|
||||||
|
// PTZ actions as used by the camera manager (includes generic presets).
|
||||||
|
const PTZ_ACTIONS = [...PTZ_BASE_ACTIONS, 'preset'] as const;
|
||||||
|
export type PTZAction = (typeof PTZ_ACTIONS)[number];
|
||||||
|
|
||||||
|
const PTZ_PHASES = ['start', 'stop'] as const;
|
||||||
|
export type PTZPhase = (typeof PTZ_PHASES)[number];
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
// View Display Mode
|
// View Display Mode
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
@@ -201,32 +214,22 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
|||||||
'camera_ui',
|
'camera_ui',
|
||||||
'default',
|
'default',
|
||||||
'diagnostics',
|
'diagnostics',
|
||||||
'expand',
|
|
||||||
'download',
|
'download',
|
||||||
|
'expand',
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
'menu_toggle',
|
|
||||||
'mute',
|
|
||||||
'live_substream_on',
|
|
||||||
'live_substream_off',
|
'live_substream_off',
|
||||||
|
'live_substream_on',
|
||||||
|
'menu_toggle',
|
||||||
'microphone_mute',
|
'microphone_mute',
|
||||||
'microphone_unmute',
|
'microphone_unmute',
|
||||||
'play',
|
'mute',
|
||||||
'pause',
|
'pause',
|
||||||
|
'play',
|
||||||
'screenshot',
|
'screenshot',
|
||||||
'unmute',
|
'unmute',
|
||||||
] as const;
|
] as const;
|
||||||
export type FrigateCardGeneralAction = (typeof FRIGATE_CARD_GENERAL_ACTIONS)[number];
|
export type FrigateCardGeneralAction = (typeof FRIGATE_CARD_GENERAL_ACTIONS)[number];
|
||||||
|
|
||||||
const FRIGATE_CARD_ACTIONS = [
|
|
||||||
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
|
||||||
...FRIGATE_CARD_GENERAL_ACTIONS,
|
|
||||||
'camera_select',
|
|
||||||
'live_substream_select',
|
|
||||||
'media_player',
|
|
||||||
'display_mode_select',
|
|
||||||
] as const;
|
|
||||||
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
|
|
||||||
|
|
||||||
const frigateCardViewActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
const frigateCardViewActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||||
frigate_card_action: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED),
|
frigate_card_action: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED),
|
||||||
});
|
});
|
||||||
@@ -240,9 +243,6 @@ const frigateCardCameraSelectActionSchema = frigateCardCustomActionsBaseSchema.e
|
|||||||
frigate_card_action: z.literal('camera_select'),
|
frigate_card_action: z.literal('camera_select'),
|
||||||
camera: z.string(),
|
camera: z.string(),
|
||||||
});
|
});
|
||||||
export type FrigateCardCameraSelectAction = z.infer<
|
|
||||||
typeof frigateCardCameraSelectActionSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
const frigateCardLiveDependencySelectActionSchema =
|
const frigateCardLiveDependencySelectActionSchema =
|
||||||
frigateCardCustomActionsBaseSchema.extend({
|
frigateCardCustomActionsBaseSchema.extend({
|
||||||
@@ -263,6 +263,19 @@ const frigateCardViewDisplayModeActionSchema = frigateCardCustomActionsBaseSchem
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const frigateCardPTZActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||||
|
frigate_card_action: z.literal('ptz'),
|
||||||
|
ptz_action: z.enum(PTZ_ACTIONS),
|
||||||
|
ptz_phase: z.enum(PTZ_PHASES).optional(),
|
||||||
|
ptz_preset: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type FrigateCardPTZAction = z.infer<typeof frigateCardPTZActionSchema>;
|
||||||
|
|
||||||
|
const frigateCardShowPTZActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||||
|
frigate_card_action: z.literal('show_ptz'),
|
||||||
|
show_ptz: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
export const frigateCardCustomActionSchema = z.union([
|
export const frigateCardCustomActionSchema = z.union([
|
||||||
frigateCardViewActionSchema,
|
frigateCardViewActionSchema,
|
||||||
frigateCardGeneralActionSchema,
|
frigateCardGeneralActionSchema,
|
||||||
@@ -270,6 +283,8 @@ export const frigateCardCustomActionSchema = z.union([
|
|||||||
frigateCardLiveDependencySelectActionSchema,
|
frigateCardLiveDependencySelectActionSchema,
|
||||||
frigateCardMediaPlayerActionSchema,
|
frigateCardMediaPlayerActionSchema,
|
||||||
frigateCardViewDisplayModeActionSchema,
|
frigateCardViewDisplayModeActionSchema,
|
||||||
|
frigateCardPTZActionSchema,
|
||||||
|
frigateCardShowPTZActionSchema,
|
||||||
]);
|
]);
|
||||||
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
||||||
|
|
||||||
@@ -481,53 +496,8 @@ export const frigateConditionalSchema = z.object({
|
|||||||
conditions: frigateCardConditionSchema,
|
conditions: frigateCardConditionSchema,
|
||||||
elements: z.lazy(() => pictureElementsSchema),
|
elements: z.lazy(() => pictureElementsSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
|
export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
|
||||||
|
|
||||||
// *************************************************************************
|
|
||||||
// Custom Element Configuration: PTZ
|
|
||||||
// *************************************************************************
|
|
||||||
|
|
||||||
export const frigateCardPTZSchema = z.preprocess(
|
|
||||||
// To avoid lots of YAML duplication, provide an easy way to just specify the
|
|
||||||
// service data as actions for each PTZ icon, and it will be preprocessed into
|
|
||||||
// the full form. This also provides compatability with the AlexIT/WebRTC PTZ
|
|
||||||
// configuration.
|
|
||||||
(data) => {
|
|
||||||
if (!data || typeof data !== 'object' || !data['service']) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
const out = { ...data };
|
|
||||||
['left', 'right', 'up', 'down', 'zoom_in', 'zoom_out', 'home'].forEach((name) => {
|
|
||||||
if (`data_${name}` in data && !(`actions_${name}` in data)) {
|
|
||||||
out[`actions_${name}`] = {
|
|
||||||
tap_action: {
|
|
||||||
action: 'call-service',
|
|
||||||
service: data['service'],
|
|
||||||
data: data[`data_${name}`],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
delete out[`data_${name}`];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
z.object({
|
|
||||||
type: z.literal('custom:frigate-card-ptz'),
|
|
||||||
style: z.object({}).passthrough().optional(),
|
|
||||||
orientation: z.enum(['vertical', 'horizontal']).default('vertical').optional(),
|
|
||||||
service: z.string().optional(),
|
|
||||||
actions_left: actionsBaseSchema.optional(),
|
|
||||||
actions_right: actionsBaseSchema.optional(),
|
|
||||||
actions_up: actionsBaseSchema.optional(),
|
|
||||||
actions_down: actionsBaseSchema.optional(),
|
|
||||||
actions_zoom_in: actionsBaseSchema.optional(),
|
|
||||||
actions_zoom_out: actionsBaseSchema.optional(),
|
|
||||||
actions_home: actionsBaseSchema.optional(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
export type FrigateCardPTZConfig = z.infer<typeof frigateCardPTZSchema>;
|
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
// Custom Element Configuration: Stock Picture Elements + Custom
|
// Custom Element Configuration: Stock Picture Elements + Custom
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
@@ -540,7 +510,6 @@ const pictureElementSchema = z.union([
|
|||||||
menuSubmenuSchema,
|
menuSubmenuSchema,
|
||||||
menuSubmenuSelectSchema,
|
menuSubmenuSelectSchema,
|
||||||
frigateConditionalSchema,
|
frigateConditionalSchema,
|
||||||
frigateCardPTZSchema,
|
|
||||||
stateBadgeIconSchema,
|
stateBadgeIconSchema,
|
||||||
stateIconSchema,
|
stateIconSchema,
|
||||||
stateLabelSchema,
|
stateLabelSchema,
|
||||||
@@ -818,6 +787,70 @@ const jsmpegConfigSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const frigateCardPTZActions = z.object({
|
||||||
|
actions_left: actionsBaseSchema.optional(),
|
||||||
|
actions_right: actionsBaseSchema.optional(),
|
||||||
|
actions_up: actionsBaseSchema.optional(),
|
||||||
|
actions_down: actionsBaseSchema.optional(),
|
||||||
|
actions_zoom_in: actionsBaseSchema.optional(),
|
||||||
|
actions_zoom_out: actionsBaseSchema.optional(),
|
||||||
|
actions_home: actionsBaseSchema.optional(),
|
||||||
|
});
|
||||||
|
export type FrigateCardPTZActions = z.infer<typeof frigateCardPTZActions>;
|
||||||
|
|
||||||
|
const livePTZControlsDefaults = {
|
||||||
|
orientation: 'horizontal' as const,
|
||||||
|
mode: 'on' as const,
|
||||||
|
hide_pan_tilt: false,
|
||||||
|
hide_zoom: false,
|
||||||
|
hide_home: false,
|
||||||
|
position: 'bottom-right' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const frigateCardPTZSchema = z.preprocess(
|
||||||
|
// To avoid lots of YAML duplication, provide an easy way to just specify the
|
||||||
|
// service data as actions for each PTZ icon, and it will be preprocessed into
|
||||||
|
// the full form. This also provides compatability with the AlexIT/WebRTC PTZ
|
||||||
|
// configuration.
|
||||||
|
(data) => {
|
||||||
|
if (!data || typeof data !== 'object' || !data['service']) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const out = { ...data };
|
||||||
|
PTZ_CONTROL_ACTIONS.forEach((name) => {
|
||||||
|
if (`data_${name}` in data && !(`actions_${name}` in data)) {
|
||||||
|
out[`actions_${name}`] = {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: data['service'],
|
||||||
|
data: data[`data_${name}`],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
delete out[`data_${name}`];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
frigateCardPTZActions.extend({
|
||||||
|
mode: z.enum(['off', 'on']).default(livePTZControlsDefaults.mode),
|
||||||
|
position: z
|
||||||
|
.enum(['top-left', 'top-right', 'bottom-left', 'bottom-right'])
|
||||||
|
.default(livePTZControlsDefaults.position),
|
||||||
|
|
||||||
|
orientation: z
|
||||||
|
.enum(['vertical', 'horizontal'])
|
||||||
|
.default(livePTZControlsDefaults.orientation),
|
||||||
|
|
||||||
|
hide_pan_tilt: z.boolean().default(livePTZControlsDefaults.hide_pan_tilt),
|
||||||
|
hide_zoom: z.boolean().default(livePTZControlsDefaults.hide_zoom),
|
||||||
|
hide_home: z.boolean().default(livePTZControlsDefaults.hide_home),
|
||||||
|
|
||||||
|
service: z.string().optional(),
|
||||||
|
style: z.object({}).passthrough().optional(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export type FrigateCardPTZConfig = z.infer<typeof frigateCardPTZSchema>;
|
||||||
|
|
||||||
const liveThumbnailControlsDefaults = {
|
const liveThumbnailControlsDefaults = {
|
||||||
...thumbnailControlsDefaults,
|
...thumbnailControlsDefaults,
|
||||||
media: 'all' as const,
|
media: 'all' as const,
|
||||||
@@ -842,6 +875,7 @@ const liveConfigDefault = {
|
|||||||
size: 48,
|
size: 48,
|
||||||
style: 'chevrons' as const,
|
style: 'chevrons' as const,
|
||||||
},
|
},
|
||||||
|
ptz: livePTZControlsDefaults,
|
||||||
thumbnails: liveThumbnailControlsDefaults,
|
thumbnails: liveThumbnailControlsDefaults,
|
||||||
timeline: miniTimelineConfigDefault,
|
timeline: miniTimelineConfigDefault,
|
||||||
},
|
},
|
||||||
@@ -872,6 +906,7 @@ const liveOverridableConfigSchema = z
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
.default(liveConfigDefault.controls.next_previous),
|
.default(liveConfigDefault.controls.next_previous),
|
||||||
|
ptz: frigateCardPTZSchema.default(liveConfigDefault.controls.ptz),
|
||||||
thumbnails: livethumbnailsControlSchema.default(
|
thumbnails: livethumbnailsControlSchema.default(
|
||||||
liveConfigDefault.controls.thumbnails,
|
liveConfigDefault.controls.thumbnails,
|
||||||
),
|
),
|
||||||
@@ -934,7 +969,7 @@ const castConfigDefault = {
|
|||||||
method: 'standard' as const,
|
method: 'standard' as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const castSchema = z.object({
|
const castSchema = z.object({
|
||||||
method: z.enum(['standard', 'dashboard']).default(castConfigDefault.method).optional(),
|
method: z.enum(['standard', 'dashboard']).default(castConfigDefault.method).optional(),
|
||||||
dashboard: z
|
dashboard: z
|
||||||
.object({
|
.object({
|
||||||
@@ -1170,6 +1205,7 @@ const menuConfigDefault = {
|
|||||||
recordings: hiddenButtonDefault,
|
recordings: hiddenButtonDefault,
|
||||||
screenshot: hiddenButtonDefault,
|
screenshot: hiddenButtonDefault,
|
||||||
display_mode: visibleButtonDefault,
|
display_mode: visibleButtonDefault,
|
||||||
|
ptz: hiddenButtonDefault,
|
||||||
},
|
},
|
||||||
button_size: 40,
|
button_size: 40,
|
||||||
};
|
};
|
||||||
@@ -1220,6 +1256,7 @@ const menuConfigSchema = z
|
|||||||
display_mode: visibleButtonSchema.default(
|
display_mode: visibleButtonSchema.default(
|
||||||
menuConfigDefault.buttons.display_mode,
|
menuConfigDefault.buttons.display_mode,
|
||||||
),
|
),
|
||||||
|
ptz: hiddenButtonSchema.default(menuConfigDefault.buttons.ptz),
|
||||||
})
|
})
|
||||||
.default(menuConfigDefault.buttons),
|
.default(menuConfigDefault.buttons),
|
||||||
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
|
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
|
||||||
@@ -1411,13 +1448,13 @@ const overridesSchema = z
|
|||||||
// Automation Configuration
|
// Automation Configuration
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|
||||||
const automationActionSchema = actionSchema.array().optional();
|
const automationActionSchema = actionSchema.array();
|
||||||
export type AutomationActions = z.infer<typeof automationActionSchema>;
|
export type AutomationActions = z.infer<typeof automationActionSchema>;
|
||||||
|
|
||||||
const automationSchema = z.object({
|
const automationSchema = z.object({
|
||||||
conditions: frigateCardConditionSchema,
|
conditions: frigateCardConditionSchema,
|
||||||
actions: automationActionSchema,
|
actions: automationActionSchema.optional(),
|
||||||
actions_not: automationActionSchema,
|
actions_not: automationActionSchema.optional(),
|
||||||
});
|
});
|
||||||
export type Automation = z.infer<typeof automationSchema>;
|
export type Automation = z.infer<typeof automationSchema>;
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -98,7 +98,7 @@ export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
|
|||||||
export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SIZE =
|
export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SIZE =
|
||||||
`${CONF_MEDIA_GALLERY}.controls.thumbnails.size` as const;
|
`${CONF_MEDIA_GALLERY}.controls.thumbnails.size` as const;
|
||||||
|
|
||||||
export const CONF_MEDIA_VIEWER = 'media_viewer' as const;
|
const CONF_MEDIA_VIEWER = 'media_viewer' as const;
|
||||||
export const CONF_MEDIA_VIEWER_AUTO_PLAY = `${CONF_MEDIA_VIEWER}.auto_play` as const;
|
export const CONF_MEDIA_VIEWER_AUTO_PLAY = `${CONF_MEDIA_VIEWER}.auto_play` as const;
|
||||||
export const CONF_MEDIA_VIEWER_AUTO_PAUSE = `${CONF_MEDIA_VIEWER}.auto_pause` as const;
|
export const CONF_MEDIA_VIEWER_AUTO_PAUSE = `${CONF_MEDIA_VIEWER}.auto_pause` as const;
|
||||||
export const CONF_MEDIA_VIEWER_AUTO_MUTE = `${CONF_MEDIA_VIEWER}.auto_mute` as const;
|
export const CONF_MEDIA_VIEWER_AUTO_MUTE = `${CONF_MEDIA_VIEWER}.auto_mute` as const;
|
||||||
@@ -169,6 +169,17 @@ export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE =
|
|||||||
`${CONF_LIVE}.controls.next_previous.style` as const;
|
`${CONF_LIVE}.controls.next_previous.style` as const;
|
||||||
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE =
|
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE =
|
||||||
`${CONF_LIVE}.controls.next_previous.size` as const;
|
`${CONF_LIVE}.controls.next_previous.size` as const;
|
||||||
|
export const CONF_LIVE_CONTROLS_PTZ_HIDE_HOME =
|
||||||
|
`${CONF_LIVE}.controls.ptz.hide_home` as const;
|
||||||
|
export const CONF_LIVE_CONTROLS_PTZ_HIDE_PAN_TILT =
|
||||||
|
`${CONF_LIVE}.controls.ptz.hide_pan_tilt` as const;
|
||||||
|
export const CONF_LIVE_CONTROLS_PTZ_HIDE_ZOOM =
|
||||||
|
`${CONF_LIVE}.controls.ptz.hide_zoom` as const;
|
||||||
|
export const CONF_LIVE_CONTROLS_PTZ_MODE = `${CONF_LIVE}.controls.ptz.mode` as const;
|
||||||
|
export const CONF_LIVE_CONTROLS_PTZ_ORIENTATION =
|
||||||
|
`${CONF_LIVE}.controls.ptz.orientation` as const;
|
||||||
|
export const CONF_LIVE_CONTROLS_PTZ_POSITION =
|
||||||
|
`${CONF_LIVE}.controls.ptz.position` as const;
|
||||||
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA =
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA =
|
||||||
`${CONF_LIVE}.controls.thumbnails.media` as const;
|
`${CONF_LIVE}.controls.thumbnails.media` as const;
|
||||||
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE =
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE =
|
||||||
@@ -257,17 +268,10 @@ export const CONF_MENU_STYLE = `${CONF_MENU}.style` as const;
|
|||||||
export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const;
|
export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const;
|
||||||
export const CONF_MENU_BUTTONS = `${CONF_MENU}.buttons` as const;
|
export const CONF_MENU_BUTTONS = `${CONF_MENU}.buttons` as const;
|
||||||
|
|
||||||
export const CONF_MENU_BUTTONS_CAMERAS = `${CONF_MENU}.buttons.cameras` as const;
|
|
||||||
export const CONF_MENU_BUTTONS_CLIPS = `${CONF_MENU}.buttons.clips` as const;
|
|
||||||
export const CONF_MENU_BUTTONS_DOWNLOAD = `${CONF_MENU}.buttons.download` as const;
|
|
||||||
export const CONF_MENU_BUTTONS_FRIGATE = `${CONF_MENU}.buttons.frigate` as const;
|
export const CONF_MENU_BUTTONS_FRIGATE = `${CONF_MENU}.buttons.frigate` as const;
|
||||||
export const CONF_MENU_BUTTONS_CAMERA_UI = `${CONF_MENU}.buttons.camera_ui` as const;
|
export const CONF_MENU_BUTTONS_CAMERA_UI = `${CONF_MENU}.buttons.camera_ui` 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_LIVE = `${CONF_MENU}.buttons.live` as const;
|
|
||||||
export const CONF_MENU_BUTTONS_MEDIA_PLAYER =
|
export const CONF_MENU_BUTTONS_MEDIA_PLAYER =
|
||||||
`${CONF_MENU}.buttons.media_player` as const;
|
`${CONF_MENU}.buttons.media_player` 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_MENU_BUTTONS_TIMELINE = `${CONF_MENU}.buttons.timeline` as const;
|
||||||
|
|
||||||
const CONF_DIMENSIONS = 'dimensions' as const;
|
const CONF_DIMENSIONS = 'dimensions' as const;
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ import {
|
|||||||
CONF_LIVE_CONTROLS_BUILTIN,
|
CONF_LIVE_CONTROLS_BUILTIN,
|
||||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_HIDE_HOME,
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_HIDE_PAN_TILT,
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_HIDE_ZOOM,
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_MODE,
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_ORIENTATION,
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_POSITION,
|
||||||
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
|
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
|
||||||
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
@@ -211,6 +217,7 @@ const MENU_CAMERAS_WEBRTC_CARD = 'cameras.webrtc_card';
|
|||||||
const MENU_IMAGE_LAYOUT = 'image.layout';
|
const MENU_IMAGE_LAYOUT = 'image.layout';
|
||||||
const MENU_LIVE_CONTROLS = 'live.controls';
|
const MENU_LIVE_CONTROLS = 'live.controls';
|
||||||
const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
|
const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
|
||||||
|
const MENU_LIVE_CONTROLS_PTZ = 'live.controls.ptz';
|
||||||
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';
|
||||||
@@ -575,6 +582,45 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: 'dashboard', label: localize('config.cameras.cast.methods.dashboard') },
|
{ value: 'dashboard', label: localize('config.cameras.cast.methods.dashboard') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected _ptzModes: EditorSelectOption[] = [
|
||||||
|
{ value: '', label: '' },
|
||||||
|
{ value: 'on', label: localize('config.live.controls.ptz.modes.on') },
|
||||||
|
{ value: 'off', label: localize('config.live.controls.ptz.modes.off') },
|
||||||
|
{ value: 'auto', label: localize('config.live.controls.ptz.modes.auto') },
|
||||||
|
];
|
||||||
|
|
||||||
|
protected _ptzOrientations: EditorSelectOption[] = [
|
||||||
|
{ value: '', label: '' },
|
||||||
|
{
|
||||||
|
value: 'vertical',
|
||||||
|
label: localize('config.live.controls.ptz.orientations.vertical'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'horizontal',
|
||||||
|
label: localize('config.live.controls.ptz.orientations.horizontal'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
protected _ptzPositions: EditorSelectOption[] = [
|
||||||
|
{ value: '', label: '' },
|
||||||
|
{
|
||||||
|
value: 'top-left',
|
||||||
|
label: localize('config.live.controls.ptz.positions.top-left'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'top-right',
|
||||||
|
label: localize('config.live.controls.ptz.positions.top-right'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'bottom-left',
|
||||||
|
label: localize('config.live.controls.ptz.positions.bottom-left'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'bottom-right',
|
||||||
|
label: localize('config.live.controls.ptz.positions.bottom-right'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
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
|
||||||
@@ -1886,6 +1932,8 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._renderMenuButton('play') /* */}
|
${this._renderMenuButton('play') /* */}
|
||||||
${this._renderMenuButton('mute') /* */}
|
${this._renderMenuButton('mute') /* */}
|
||||||
${this._renderMenuButton('screenshot')}
|
${this._renderMenuButton('screenshot')}
|
||||||
|
${this._renderMenuButton('display_mode')}
|
||||||
|
${this._renderMenuButton('ptz')}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
@@ -1981,6 +2029,47 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||||
this._defaults.live.controls.timeline.show_recordings,
|
this._defaults.live.controls.timeline.show_recordings,
|
||||||
)}
|
)}
|
||||||
|
${this._putInSubmenu(
|
||||||
|
MENU_LIVE_CONTROLS_PTZ,
|
||||||
|
true,
|
||||||
|
'config.live.controls.ptz.editor_label',
|
||||||
|
{ name: 'mdi:pan' },
|
||||||
|
html`
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_MODE,
|
||||||
|
this._ptzModes,
|
||||||
|
)}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_POSITION,
|
||||||
|
this._ptzPositions,
|
||||||
|
)}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_ORIENTATION,
|
||||||
|
this._ptzOrientations,
|
||||||
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_HIDE_PAN_TILT,
|
||||||
|
this._defaults.live.controls.ptz.hide_pan_tilt,
|
||||||
|
{
|
||||||
|
label: localize('config.live.controls.ptz.hide_pan_tilt'),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_HIDE_ZOOM,
|
||||||
|
this._defaults.live.controls.ptz.hide_pan_tilt,
|
||||||
|
{
|
||||||
|
label: localize('config.live.controls.ptz.hide_zoom'),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_LIVE_CONTROLS_PTZ_HIDE_HOME,
|
||||||
|
this._defaults.live.controls.ptz.hide_home,
|
||||||
|
{
|
||||||
|
label: localize('config.live.controls.ptz.hide_home'),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
${this._renderMediaLayout(
|
${this._renderMediaLayout(
|
||||||
|
|||||||
@@ -233,7 +233,31 @@
|
|||||||
"auto_play": "Automatically play live cameras",
|
"auto_play": "Automatically play live cameras",
|
||||||
"auto_unmute": "Automatically unmute live cameras",
|
"auto_unmute": "Automatically unmute live cameras",
|
||||||
"controls": {
|
"controls": {
|
||||||
"editor_label": "Live Controls"
|
"editor_label": "Live Controls",
|
||||||
|
"ptz": {
|
||||||
|
"editor_label": "PTZ",
|
||||||
|
"mode": "Mode",
|
||||||
|
"modes": {
|
||||||
|
"on": "On",
|
||||||
|
"off": "Off",
|
||||||
|
"auto": "Automatic"
|
||||||
|
},
|
||||||
|
"orientation": "Orientation",
|
||||||
|
"orientations": {
|
||||||
|
"vertical": "Vertical",
|
||||||
|
"horizontal": "Horizontal"
|
||||||
|
},
|
||||||
|
"position": "Position",
|
||||||
|
"positions": {
|
||||||
|
"top-left": "Top left",
|
||||||
|
"top-right": "Top right",
|
||||||
|
"bottom-left": "Bottom left",
|
||||||
|
"bottom-right": "Bottom right"
|
||||||
|
},
|
||||||
|
"hide_zoom": "Hide zoom control",
|
||||||
|
"hide_pan_tilt": "Hide pan & tilt control",
|
||||||
|
"hide_home": "Hide home control"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"draggable": "Live cameras view can be dragged/swiped",
|
"draggable": "Live cameras view can be dragged/swiped",
|
||||||
"layout": "Live Layout",
|
"layout": "Live Layout",
|
||||||
@@ -301,6 +325,7 @@
|
|||||||
"mute": "Mute / Unmute",
|
"mute": "Mute / Unmute",
|
||||||
"play": "Play / Pause",
|
"play": "Play / Pause",
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
|
"ptz": "Show PTZ controls",
|
||||||
"recordings": "Recordings",
|
"recordings": "Recordings",
|
||||||
"screenshot": "Screenshot",
|
"screenshot": "Screenshot",
|
||||||
"snapshots": "Snapshots",
|
"snapshots": "Snapshots",
|
||||||
|
|||||||
@@ -233,7 +233,31 @@
|
|||||||
"auto_play": "Gioca automaticamente le telecamere dal vivo",
|
"auto_play": "Gioca automaticamente le telecamere dal vivo",
|
||||||
"auto_unmute": "Riattiva automaticamente l'audio delle telecamere live",
|
"auto_unmute": "Riattiva automaticamente l'audio delle telecamere live",
|
||||||
"controls": {
|
"controls": {
|
||||||
"editor_label": "Controlli dal vivo"
|
"editor_label": "Controlli dal vivo",
|
||||||
|
"ptz": {
|
||||||
|
"editor_label": "",
|
||||||
|
"mode": "",
|
||||||
|
"modes": {
|
||||||
|
"on": "",
|
||||||
|
"off": "",
|
||||||
|
"auto": ""
|
||||||
|
},
|
||||||
|
"orientation": "",
|
||||||
|
"orientations": {
|
||||||
|
"vertical": "",
|
||||||
|
"horizontal": ""
|
||||||
|
},
|
||||||
|
"position": "",
|
||||||
|
"positions": {
|
||||||
|
"top-left": "",
|
||||||
|
"top-right": "",
|
||||||
|
"bottom-left": "",
|
||||||
|
"bottom-right": ""
|
||||||
|
},
|
||||||
|
"hide_zoom": "",
|
||||||
|
"hide_pan_tilt": "",
|
||||||
|
"hide_home": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"draggable": "Il Visualizzatore eventi può essere trascinato oppure puoi scorrere",
|
"draggable": "Il Visualizzatore eventi può essere trascinato oppure puoi scorrere",
|
||||||
"layout": "Disposizione dal vivo",
|
"layout": "Disposizione dal vivo",
|
||||||
@@ -299,6 +323,7 @@
|
|||||||
"mute": "",
|
"mute": "",
|
||||||
"play": "",
|
"play": "",
|
||||||
"priority": "Priorità",
|
"priority": "Priorità",
|
||||||
|
"ptz": "",
|
||||||
"screenshot": "",
|
"screenshot": "",
|
||||||
"snapshots": "Istantanee",
|
"snapshots": "Istantanee",
|
||||||
"substreams": "Flusso/i secondario/i",
|
"substreams": "Flusso/i secondario/i",
|
||||||
|
|||||||
@@ -233,7 +233,31 @@
|
|||||||
"auto_play": "Reproduzir câmeras ao vivo automaticamente",
|
"auto_play": "Reproduzir câmeras ao vivo automaticamente",
|
||||||
"auto_unmute": "Ativar automaticamente o som das câmeras ao vivo",
|
"auto_unmute": "Ativar automaticamente o som das câmeras ao vivo",
|
||||||
"controls": {
|
"controls": {
|
||||||
"editor_label": "Controles da visualização ao vivo"
|
"editor_label": "Controles da visualização ao vivo",
|
||||||
|
"ptz": {
|
||||||
|
"editor_label": "",
|
||||||
|
"mode": "",
|
||||||
|
"modes": {
|
||||||
|
"on": "",
|
||||||
|
"off": "",
|
||||||
|
"auto": ""
|
||||||
|
},
|
||||||
|
"orientation": "",
|
||||||
|
"orientations": {
|
||||||
|
"vertical": "",
|
||||||
|
"horizontal": ""
|
||||||
|
},
|
||||||
|
"position": "",
|
||||||
|
"positions": {
|
||||||
|
"top-left": "",
|
||||||
|
"top-right": "",
|
||||||
|
"bottom-left": "",
|
||||||
|
"bottom-right": ""
|
||||||
|
},
|
||||||
|
"hide_zoom": "",
|
||||||
|
"hide_pan_tilt": "",
|
||||||
|
"hide_home": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
|
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
|
||||||
"layout": "Layout dinâmico",
|
"layout": "Layout dinâmico",
|
||||||
@@ -300,6 +324,7 @@
|
|||||||
"mute": "",
|
"mute": "",
|
||||||
"play": "",
|
"play": "",
|
||||||
"priority": "Prioridade",
|
"priority": "Prioridade",
|
||||||
|
"ptz": "",
|
||||||
"recordings": "Gravações",
|
"recordings": "Gravações",
|
||||||
"screenshot": "",
|
"screenshot": "",
|
||||||
"snapshots": "Instantâneos",
|
"snapshots": "Instantâneos",
|
||||||
|
|||||||
@@ -226,7 +226,31 @@
|
|||||||
"auto_play": "Reproduzir câmeras ao vivo automaticamente",
|
"auto_play": "Reproduzir câmeras ao vivo automaticamente",
|
||||||
"auto_unmute": "Ativar automaticamente o som das câmeras ao vivo",
|
"auto_unmute": "Ativar automaticamente o som das câmeras ao vivo",
|
||||||
"controls": {
|
"controls": {
|
||||||
"editor_label": "Controles da visualização ao vivo"
|
"editor_label": "Controles da visualização ao vivo",
|
||||||
|
"ptz": {
|
||||||
|
"editor_label": "",
|
||||||
|
"mode": "",
|
||||||
|
"modes": {
|
||||||
|
"on": "",
|
||||||
|
"off": "",
|
||||||
|
"auto": ""
|
||||||
|
},
|
||||||
|
"orientation": "",
|
||||||
|
"orientations": {
|
||||||
|
"vertical": "",
|
||||||
|
"horizontal": ""
|
||||||
|
},
|
||||||
|
"position": "",
|
||||||
|
"positions": {
|
||||||
|
"top-left": "",
|
||||||
|
"top-right": "",
|
||||||
|
"bottom-left": "",
|
||||||
|
"bottom-right": ""
|
||||||
|
},
|
||||||
|
"hide_zoom": "",
|
||||||
|
"hide_pan_tilt": "",
|
||||||
|
"hide_home": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
|
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
|
||||||
"layout": "layout",
|
"layout": "layout",
|
||||||
@@ -292,6 +316,7 @@
|
|||||||
"mute": "",
|
"mute": "",
|
||||||
"play": "",
|
"play": "",
|
||||||
"priority": "Prioridade",
|
"priority": "Prioridade",
|
||||||
|
"ptz": "",
|
||||||
"screenshot": "",
|
"screenshot": "",
|
||||||
"snapshots": "Instantâneos",
|
"snapshots": "Instantâneos",
|
||||||
"substreams": "substreams",
|
"substreams": "substreams",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function getLanguage(hass?: HomeAssistant): string {
|
|||||||
/**
|
/**
|
||||||
* Load required languages.
|
* Load required languages.
|
||||||
*/
|
*/
|
||||||
export const loadLanguages = async (hass: HomeAssistant): Promise<void> => {
|
export const loadLanguages = async (hass: HomeAssistant): Promise<boolean> => {
|
||||||
const lang = getLanguage(hass);
|
const lang = getLanguage(hass);
|
||||||
if (lang === 'it') {
|
if (lang === 'it') {
|
||||||
languages[lang] = await import('./languages/it.json');
|
languages[lang] = await import('./languages/it.json');
|
||||||
@@ -65,6 +65,7 @@ export const loadLanguages = async (hass: HomeAssistant): Promise<void> => {
|
|||||||
if (lang) {
|
if (lang) {
|
||||||
frigateCardLanguage = lang;
|
frigateCardLanguage = lang;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
import { css, CSSResultGroup, html, TemplateResult, unsafeCSS } from 'lit';
|
import { css, CSSResultGroup, html, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { query } from 'lit/decorators/query.js';
|
import { query } from 'lit/decorators/query.js';
|
||||||
import { screenshotMedia } from '../utils/screenshot.js';
|
|
||||||
import { dispatchErrorMessageEvent } from '../components/message.js';
|
import { dispatchErrorMessageEvent } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../types.js';
|
import { FrigateCardMediaPlayer } from '../types.js';
|
||||||
import { mayHaveAudio } from '../utils/audio.js';
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
|
import { errorToConsole } from '../utils/basic.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
hideMediaControlsTemporarily, MEDIA_LOAD_CONTROLS_HIDE_SECONDS, setControlsOnVideo
|
hideMediaControlsTemporarily, MEDIA_LOAD_CONTROLS_HIDE_SECONDS, setControlsOnVideo
|
||||||
} from '../utils/media.js';
|
} from '../utils/media.js';
|
||||||
|
import { screenshotMedia } from '../utils/screenshot.js';
|
||||||
|
|
||||||
customElements.whenDefined('ha-hls-player').then(() => {
|
customElements.whenDefined('ha-hls-player').then(() => {
|
||||||
@customElement('frigate-card-ha-hls-player')
|
@customElement('frigate-card-ha-hls-player')
|
||||||
@@ -97,7 +98,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
// Use native Frigate card error handling for fatal errors.
|
// Use native Frigate card error handling for fatal errors.
|
||||||
return dispatchErrorMessageEvent(this, this._error);
|
return dispatchErrorMessageEvent(this, this._error);
|
||||||
} else {
|
} else {
|
||||||
console.error(this._error);
|
errorToConsole(this._error, console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ ha-icon {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
opacity: 50%;
|
color: var(--primary-color);
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Modified from / inspired by:
|
// Inspired by:
|
||||||
// https://github.com/AlexxIT/WebRTC/blob/master/custom_components/webrtc/www/webrtc-camera.js
|
// https://github.com/AlexxIT/WebRTC/blob/master/custom_components/webrtc/www/webrtc-camera.js
|
||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: absolute;
|
||||||
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
@@ -9,6 +9,19 @@
|
|||||||
--frigate-card-ptz-icon-size: 24px;
|
--frigate-card-ptz-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([data-position$='-left']) {
|
||||||
|
left: 5%;
|
||||||
|
}
|
||||||
|
:host([data-position$='-right']) {
|
||||||
|
right: 5%;
|
||||||
|
}
|
||||||
|
:host([data-position^='top-']) {
|
||||||
|
top: 5%;
|
||||||
|
}
|
||||||
|
:host([data-position^='bottom-']) {
|
||||||
|
bottom: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
/*****************
|
/*****************
|
||||||
* Main Containers
|
* Main Containers
|
||||||
*****************/
|
*****************/
|
||||||
+64
-43
@@ -7,10 +7,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
ActionType,
|
ActionType,
|
||||||
FrigateCardAction,
|
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
frigateCardCustomActionSchema,
|
frigateCardCustomActionSchema,
|
||||||
ViewDisplayMode,
|
FrigateCardGeneralAction,
|
||||||
|
FrigateCardUserSpecifiedView,
|
||||||
} from '../config/types.js';
|
} from '../config/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,59 +30,75 @@ export function convertActionToFrigateCardCustomAction(
|
|||||||
return parseResult.success ? parseResult.data : null;
|
return parseResult.success ? parseResult.data : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function createFrigateCardSimpleAction(
|
||||||
* Create a Frigate card custom action.
|
action: FrigateCardGeneralAction | FrigateCardUserSpecifiedView,
|
||||||
* @param action The Frigate card action string (e.g. 'fullscreen')
|
options?: {
|
||||||
* @returns A FrigateCardCustomAction for that action string or null.
|
|
||||||
*/
|
|
||||||
export function createFrigateCardCustomAction(
|
|
||||||
action: FrigateCardAction,
|
|
||||||
args?: {
|
|
||||||
cardID?: string;
|
cardID?: string;
|
||||||
camera?: string;
|
|
||||||
media_player?: string;
|
|
||||||
media_player_action?: 'play' | 'stop';
|
|
||||||
display_mode?: ViewDisplayMode;
|
|
||||||
},
|
},
|
||||||
): FrigateCardCustomAction | null {
|
): FrigateCardCustomAction | null {
|
||||||
if (action === 'camera_select' || action === 'live_substream_select') {
|
|
||||||
if (!args?.camera) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
frigate_card_action: action,
|
frigate_card_action: action,
|
||||||
camera: args.camera as string,
|
...(options?.cardID && { card_id: options.cardID }),
|
||||||
...(args.cardID && { card_id: args.cardID }),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action === 'media_player') {
|
|
||||||
if (!args?.media_player || !args.media_player_action) {
|
export function createFrigateCardCameraAction(
|
||||||
return null;
|
action: 'camera_select' | 'live_substream_select',
|
||||||
}
|
camera: string,
|
||||||
|
options?: {
|
||||||
|
cardID?: string;
|
||||||
|
},
|
||||||
|
): FrigateCardCustomAction {
|
||||||
return {
|
return {
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
frigate_card_action: action,
|
frigate_card_action: action,
|
||||||
media_player: args.media_player,
|
camera: camera,
|
||||||
media_player_action: args.media_player_action,
|
...(options?.cardID && { card_id: options.cardID }),
|
||||||
...(args.cardID && { card_id: args.cardID }),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action === 'display_mode_select') {
|
|
||||||
if (!args?.display_mode) {
|
export function createFrigateCardMediaPlayerAction(
|
||||||
return null;
|
mediaPlayer: string,
|
||||||
}
|
mediaPlayerAction: 'play' | 'stop',
|
||||||
|
options?: {
|
||||||
|
cardID?: string;
|
||||||
|
},
|
||||||
|
): FrigateCardCustomAction {
|
||||||
return {
|
return {
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
frigate_card_action: action,
|
frigate_card_action: 'media_player',
|
||||||
display_mode: args?.display_mode,
|
media_player: mediaPlayer,
|
||||||
...(args.cardID && { card_id: args.cardID }),
|
media_player_action: mediaPlayerAction,
|
||||||
|
...(options?.cardID && { card_id: options.cardID }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createFrigateCardDisplayModeAction(
|
||||||
|
displayMode: 'single' | 'grid',
|
||||||
|
options?: {
|
||||||
|
cardID?: string;
|
||||||
|
},
|
||||||
|
): FrigateCardCustomAction {
|
||||||
return {
|
return {
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
frigate_card_action: action,
|
frigate_card_action: 'display_mode_select',
|
||||||
...(args?.cardID && { card_id: args.cardID }),
|
display_mode: displayMode,
|
||||||
|
...(options?.cardID && { card_id: options.cardID }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFrigateCardShowPTZAction(
|
||||||
|
showPTZ: boolean,
|
||||||
|
options?: {
|
||||||
|
cardID?: string;
|
||||||
|
},
|
||||||
|
): FrigateCardCustomAction {
|
||||||
|
return {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'show_ptz',
|
||||||
|
show_ptz: showPTZ,
|
||||||
|
...(options?.cardID && { card_id: options.cardID }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +110,10 @@ export function createFrigateCardCustomAction(
|
|||||||
*/
|
*/
|
||||||
export function getActionConfigGivenAction(
|
export function getActionConfigGivenAction(
|
||||||
interaction?: string,
|
interaction?: string,
|
||||||
config?: Actions,
|
config?: Actions | null,
|
||||||
): ActionType | ActionType[] | undefined {
|
): ActionType | ActionType[] | null {
|
||||||
if (!interaction || !config) {
|
if (!interaction || !config) {
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
if (interaction == 'tap' && config.tap_action) {
|
if (interaction == 'tap' && config.tap_action) {
|
||||||
return config.tap_action;
|
return config.tap_action;
|
||||||
@@ -110,7 +126,7 @@ export function getActionConfigGivenAction(
|
|||||||
} else if (interaction == 'start_tap' && config.start_tap_action) {
|
} else if (interaction == 'start_tap' && config.start_tap_action) {
|
||||||
return config.start_tap_action;
|
return config.start_tap_action;
|
||||||
}
|
}
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,7 +148,7 @@ export const frigateCardHandleActionConfig = (
|
|||||||
entity?: string;
|
entity?: string;
|
||||||
},
|
},
|
||||||
action: string,
|
action: string,
|
||||||
actionConfig?: ActionType | ActionType[],
|
actionConfig?: ActionType | ActionType[] | null,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// Only allow a tap action to use a default non-config (the more-info config).
|
// Only allow a tap action to use a default non-config (the more-info config).
|
||||||
if (actionConfig || action == 'tap') {
|
if (actionConfig || action == 'tap') {
|
||||||
@@ -149,7 +165,7 @@ export const frigateCardHandleAction = (
|
|||||||
camera_image?: string;
|
camera_image?: string;
|
||||||
entity?: string;
|
entity?: string;
|
||||||
},
|
},
|
||||||
actionConfig: ActionType | ActionType[] | undefined,
|
actionConfig?: ActionType | ActionType[] | null,
|
||||||
): void => {
|
): void => {
|
||||||
// ActionConfig vs ActionType:
|
// ActionConfig vs ActionType:
|
||||||
// * There is a slight typing (but not functional) difference between
|
// * There is a slight typing (but not functional) difference between
|
||||||
@@ -161,7 +177,12 @@ export const frigateCardHandleAction = (
|
|||||||
handleActionConfig(node, hass, config, action as ActionConfig | undefined),
|
handleActionConfig(node, hass, config, action as ActionConfig | undefined),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
handleActionConfig(node, hass, config, actionConfig as ActionConfig | undefined);
|
handleActionConfig(
|
||||||
|
node,
|
||||||
|
hass,
|
||||||
|
config,
|
||||||
|
(actionConfig ?? undefined) as ActionConfig | undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CameraManager } from '../camera-manager/manager.js';
|
|
||||||
import { CameraConfig, RawFrigateCardConfig } from '../config/types.js';
|
import { CameraConfig, RawFrigateCardConfig } from '../config/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,48 +29,3 @@ export function getCameraID(
|
|||||||
''
|
''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all cameras that depend on a given camera.
|
|
||||||
* @param cameraManager The camera manager.
|
|
||||||
* @param cameraID ID of the target camera.
|
|
||||||
* @returns A set of dependent cameraIDs or null (since JS sets guarantee order,
|
|
||||||
* the first item in the set is guaranteed to be the cameraID itself).
|
|
||||||
*/
|
|
||||||
export function getAllDependentCameras(
|
|
||||||
cameraManager: CameraManager,
|
|
||||||
cameraID: string,
|
|
||||||
): Set<string>;
|
|
||||||
export function getAllDependentCameras(
|
|
||||||
cameraManager?: CameraManager,
|
|
||||||
cameraID?: string,
|
|
||||||
): Set<string> | null;
|
|
||||||
export function getAllDependentCameras(
|
|
||||||
cameraManager?: CameraManager,
|
|
||||||
cameraID?: string,
|
|
||||||
): Set<string> | null {
|
|
||||||
if (!cameraManager || !cameraID) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const cameras = cameraManager.getStore().getCameras();
|
|
||||||
|
|
||||||
const cameraIDs: Set<string> = new Set();
|
|
||||||
const getDependentCameras = (cameraID: string): void => {
|
|
||||||
const cameraConfig = cameras.get(cameraID);
|
|
||||||
if (cameraConfig) {
|
|
||||||
cameraIDs.add(cameraID);
|
|
||||||
const dependentCameras: Set<string> = new Set();
|
|
||||||
cameraConfig.dependencies.cameras.forEach((item) => dependentCameras.add(item));
|
|
||||||
if (cameraConfig.dependencies.all_cameras) {
|
|
||||||
cameras.forEach((_, key) => dependentCameras.add(key));
|
|
||||||
}
|
|
||||||
for (const eventCameraID of dependentCameras) {
|
|
||||||
if (!cameraIDs.has(eventCameraID)) {
|
|
||||||
getDependentCameras(eventCameraID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
getDependentCameras(cameraID);
|
|
||||||
return cameraIDs;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ enum InitializationState {
|
|||||||
INITIALIZED = 'initialized',
|
INITIALIZED = 'initialized',
|
||||||
}
|
}
|
||||||
|
|
||||||
type InitializationCallback = () => Promise<unknown>;
|
type InitializationCallback = () => Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages initialization state & calling initializers.
|
* Manages initialization state & calling initializers.
|
||||||
@@ -43,8 +43,11 @@ export class Initializer {
|
|||||||
if (state !== InitializationState.INITIALIZING) {
|
if (state !== InitializationState.INITIALIZING) {
|
||||||
if (initializer) {
|
if (initializer) {
|
||||||
this._state.set(aspect, InitializationState.INITIALIZING);
|
this._state.set(aspect, InitializationState.INITIALIZING);
|
||||||
await initializer();
|
if (await initializer()) {
|
||||||
this._state.set(aspect, InitializationState.INITIALIZED);
|
this._state.set(aspect, InitializationState.INITIALIZED);
|
||||||
|
} else {
|
||||||
|
this.uninitialize(aspect);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._state.set(aspect, InitializationState.INITIALIZED);
|
this._state.set(aspect, InitializationState.INITIALIZED);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from './basic';
|
||||||
import { getAllDependentCameras } from './camera.js';
|
|
||||||
|
|
||||||
type ResultSelectType = 'latest' | 'time' | 'none';
|
type ResultSelectType = 'latest' | 'time' | 'none';
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const cameraIDs = options?.allCameras
|
const cameraIDs = options?.allCameras
|
||||||
? cameraManager.getStore().getVisibleCameraIDs()
|
? cameraManager.getStore().getVisibleCameraIDs()
|
||||||
: getAllDependentCameras(cameraManager, view.camera);
|
: cameraManager.getStore().getAllDependentCameras(view.camera);
|
||||||
if (!cameraIDs.size) {
|
if (!cameraIDs.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -98,7 +97,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const cameraIDs = options?.allCameras
|
const cameraIDs = options?.allCameras
|
||||||
? cameraManager.getStore().getVisibleCameraIDs()
|
? cameraManager.getStore().getVisibleCameraIDs()
|
||||||
: getAllDependentCameras(cameraManager, view.camera);
|
: cameraManager.getStore().getAllDependentCameras(view.camera);
|
||||||
if (!cameraIDs.size) {
|
if (!cameraIDs.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { CameraManagerCameraCapabilities } from '../camera-manager/types';
|
||||||
|
import { FrigateCardPTZConfig, PTZ_CONTROL_ACTIONS } from '../config/types';
|
||||||
|
|
||||||
|
export const hasUsablePTZ = (
|
||||||
|
capabilities: CameraManagerCameraCapabilities | null,
|
||||||
|
config: FrigateCardPTZConfig,
|
||||||
|
): boolean => {
|
||||||
|
for (const actionName of PTZ_CONTROL_ACTIONS) {
|
||||||
|
if ('actions_' + actionName in config) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !!capabilities?.ptz;
|
||||||
|
};
|
||||||
+2
-1
@@ -5,6 +5,7 @@ import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
|||||||
import { MediaQueries } from './media-queries';
|
import { MediaQueries } from './media-queries';
|
||||||
import { MediaQueriesClassifier } from './media-queries-classifier.js';
|
import { MediaQueriesClassifier } from './media-queries-classifier.js';
|
||||||
import { MediaQueriesResults } from './media-queries-results';
|
import { MediaQueriesResults } from './media-queries-results';
|
||||||
|
import merge from 'lodash-es/merge';
|
||||||
|
|
||||||
interface ViewEvolveParameters {
|
interface ViewEvolveParameters {
|
||||||
view?: FrigateCardView;
|
view?: FrigateCardView;
|
||||||
@@ -177,7 +178,7 @@ export class View {
|
|||||||
* @returns This view.
|
* @returns This view.
|
||||||
*/
|
*/
|
||||||
public mergeInContext(context?: ViewContext): View {
|
public mergeInContext(context?: ViewContext): View {
|
||||||
this.context = { ...this.context, ...context };
|
this.context = merge(this.context ?? {}, this.context, context);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { Camera } from '../../src/camera-manager/camera.js';
|
||||||
|
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic.js';
|
||||||
|
import { createCameraCapabilities, createCameraConfig } from '../test-utils.js';
|
||||||
|
|
||||||
|
describe('Camera', () => {
|
||||||
|
it('should get config', async () => {
|
||||||
|
const config = createCameraConfig();
|
||||||
|
const camera = new Camera(
|
||||||
|
config,
|
||||||
|
new GenericCameraManagerEngine(),
|
||||||
|
createCameraCapabilities(),
|
||||||
|
);
|
||||||
|
expect(camera.getConfig()).toBe(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get capabilities', async () => {
|
||||||
|
const capabilities = createCameraCapabilities();
|
||||||
|
const camera = new Camera(
|
||||||
|
createCameraConfig(),
|
||||||
|
new GenericCameraManagerEngine(),
|
||||||
|
capabilities,
|
||||||
|
);
|
||||||
|
expect(camera.getCapabilities()).toBe(capabilities);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get engine', async () => {
|
||||||
|
const engine = new GenericCameraManagerEngine();
|
||||||
|
const camera = new Camera(createCameraConfig(), engine, createCameraCapabilities());
|
||||||
|
expect(camera.getEngine()).toBe(engine);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set and get id', async () => {
|
||||||
|
const config = createCameraConfig();
|
||||||
|
const camera = new Camera(
|
||||||
|
config,
|
||||||
|
new GenericCameraManagerEngine(),
|
||||||
|
createCameraCapabilities(),
|
||||||
|
);
|
||||||
|
camera.setID('foo');
|
||||||
|
expect(camera.getID()).toBe('foo');
|
||||||
|
expect(camera.getConfig().id).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw without id', async () => {
|
||||||
|
const config = createCameraConfig();
|
||||||
|
const camera = new Camera(
|
||||||
|
config,
|
||||||
|
new GenericCameraManagerEngine(),
|
||||||
|
createCameraCapabilities(),
|
||||||
|
);
|
||||||
|
expect(() => camera.getID()).toThrowError(
|
||||||
|
'Could not determine camera id for the following ' +
|
||||||
|
"camera, may need to set 'id' parameter manually",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/t
|
|||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
FrigateCardView,
|
FrigateCardView,
|
||||||
|
PTZAction,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
} from '../../../src/config/types';
|
} from '../../../src/config/types';
|
||||||
import { ViewMedia } from '../../../src/view/media';
|
import { ViewMedia } from '../../../src/view/media';
|
||||||
@@ -392,3 +393,101 @@ describe('getCameraEndpoints', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('executePTZAction', () => {
|
||||||
|
describe('preset', () => {
|
||||||
|
it('should reject without preset argument', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||||
|
|
||||||
|
createEngine().executePTZAction(hass, cameraConfig, 'preset');
|
||||||
|
|
||||||
|
expect(hass.callService).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should succeed', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||||
|
|
||||||
|
createEngine().executePTZAction(hass, cameraConfig, 'preset', {
|
||||||
|
preset: 'preset-foo',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||||
|
entity_id: 'camera.office',
|
||||||
|
action: 'preset',
|
||||||
|
argument: 'preset-foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('zoom', () => {
|
||||||
|
describe.each([['zoom_in' as const], ['zoom_out' as const]])(
|
||||||
|
'%s',
|
||||||
|
(actionName: PTZAction) => {
|
||||||
|
it('start', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||||
|
|
||||||
|
createEngine().executePTZAction(hass, cameraConfig, actionName);
|
||||||
|
|
||||||
|
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||||
|
entity_id: 'camera.office',
|
||||||
|
action: 'zoom',
|
||||||
|
argument: actionName === 'zoom_in' ? 'in' : 'out',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stop', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||||
|
|
||||||
|
createEngine().executePTZAction(hass, cameraConfig, actionName, {
|
||||||
|
phase: 'stop',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||||
|
entity_id: 'camera.office',
|
||||||
|
action: 'stop',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('move', () => {
|
||||||
|
describe.each([
|
||||||
|
['left' as const],
|
||||||
|
['right' as const],
|
||||||
|
['up' as const],
|
||||||
|
['down' as const],
|
||||||
|
])('%s', (actionName: PTZAction) => {
|
||||||
|
it('start', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||||
|
|
||||||
|
createEngine().executePTZAction(hass, cameraConfig, actionName);
|
||||||
|
|
||||||
|
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||||
|
entity_id: 'camera.office',
|
||||||
|
action: 'move',
|
||||||
|
argument: actionName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stop', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||||
|
|
||||||
|
createEngine().executePTZAction(hass, cameraConfig, actionName, {
|
||||||
|
phase: 'stop',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||||
|
entity_id: 'camera.office',
|
||||||
|
action: 'stop',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
getEvents,
|
||||||
|
getEventSummary,
|
||||||
|
getPTZInfo,
|
||||||
|
getRecordingSegments,
|
||||||
|
getRecordingsSummary,
|
||||||
|
retainEvent,
|
||||||
|
} from '../../../src/camera-manager/frigate/requests';
|
||||||
|
import {
|
||||||
|
EventSummary,
|
||||||
|
eventSummarySchema,
|
||||||
|
FrigateEvent,
|
||||||
|
frigateEventsSchema,
|
||||||
|
ptzInfoSchema,
|
||||||
|
recordingSegmentsSchema,
|
||||||
|
recordingSummarySchema,
|
||||||
|
retainResultSchema,
|
||||||
|
} from '../../../src/camera-manager/frigate/types';
|
||||||
|
import { RecordingSegment } from '../../../src/camera-manager/types';
|
||||||
|
import { homeAssistantWSRequest } from '../../../src/utils/ha';
|
||||||
|
import { createFrigateEvent, createHASS } from '../../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../../src/utils/ha');
|
||||||
|
|
||||||
|
describe('frigate requests', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
it('should get recordings summary', async () => {
|
||||||
|
const recordingSummary = {
|
||||||
|
events: 0,
|
||||||
|
hours: [],
|
||||||
|
day: new Date(),
|
||||||
|
};
|
||||||
|
const hass = createHASS();
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValue(recordingSummary);
|
||||||
|
expect(await getRecordingsSummary(hass, 'clientID', 'camera.office')).toBe(
|
||||||
|
recordingSummary,
|
||||||
|
);
|
||||||
|
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||||
|
hass,
|
||||||
|
recordingSummarySchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'frigate/recordings/summary',
|
||||||
|
instance_id: 'clientID',
|
||||||
|
camera: 'camera.office',
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get recordings segments', async () => {
|
||||||
|
const recordingSegments: RecordingSegment[] = [
|
||||||
|
{
|
||||||
|
start_time: 0,
|
||||||
|
end_time: 1,
|
||||||
|
id: 'foo',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const hass = createHASS();
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValue(recordingSegments);
|
||||||
|
expect(
|
||||||
|
await getRecordingSegments(hass, {
|
||||||
|
instance_id: 'clientID',
|
||||||
|
camera: 'camera.office',
|
||||||
|
after: 1,
|
||||||
|
before: 0,
|
||||||
|
}),
|
||||||
|
).toBe(recordingSegments);
|
||||||
|
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||||
|
hass,
|
||||||
|
recordingSegmentsSchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'frigate/recordings/get',
|
||||||
|
instance_id: 'clientID',
|
||||||
|
camera: 'camera.office',
|
||||||
|
after: 1,
|
||||||
|
before: 0,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should retain event', async () => {
|
||||||
|
it('successfully', async () => {
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
message: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
const hass = createHASS();
|
||||||
|
retainEvent(hass, 'clientID', 'eventID', true);
|
||||||
|
|
||||||
|
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||||
|
hass,
|
||||||
|
retainResultSchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'frigate/event/retain',
|
||||||
|
instance_id: 'clientID',
|
||||||
|
event_id: 'eventID',
|
||||||
|
retain: true,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unsuccessfully', async () => {
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
message: 'failed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const hass = createHASS();
|
||||||
|
await expect(retainEvent(hass, 'clientID', 'eventID', true)).rejects.toThrowError(
|
||||||
|
/Could not retain event/,
|
||||||
|
);
|
||||||
|
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||||
|
hass,
|
||||||
|
retainResultSchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'frigate/event/retain',
|
||||||
|
instance_id: 'clientID',
|
||||||
|
event_id: 'eventID',
|
||||||
|
retain: true,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get events', async () => {
|
||||||
|
const events: FrigateEvent[] = [createFrigateEvent()];
|
||||||
|
const hass = createHASS();
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValue(events);
|
||||||
|
expect(
|
||||||
|
await getEvents(hass, {
|
||||||
|
instance_id: 'clientID',
|
||||||
|
cameras: ['camera.office'],
|
||||||
|
labels: ['person'],
|
||||||
|
zones: ['zone'],
|
||||||
|
after: 0,
|
||||||
|
before: 1,
|
||||||
|
limit: 10,
|
||||||
|
has_clip: true,
|
||||||
|
has_snapshot: true,
|
||||||
|
favorites: true,
|
||||||
|
}),
|
||||||
|
).toBe(events);
|
||||||
|
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||||
|
hass,
|
||||||
|
frigateEventsSchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'frigate/events/get',
|
||||||
|
instance_id: 'clientID',
|
||||||
|
cameras: ['camera.office'],
|
||||||
|
labels: ['person'],
|
||||||
|
zones: ['zone'],
|
||||||
|
after: 0,
|
||||||
|
before: 1,
|
||||||
|
limit: 10,
|
||||||
|
has_clip: true,
|
||||||
|
has_snapshot: true,
|
||||||
|
favorites: true,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get event summary', async () => {
|
||||||
|
const eventSummary: EventSummary = [
|
||||||
|
{
|
||||||
|
camera: 'camera.office',
|
||||||
|
day: '2023-10-29',
|
||||||
|
label: 'person',
|
||||||
|
sub_label: null,
|
||||||
|
zones: ['door'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const hass = createHASS();
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValue(eventSummary);
|
||||||
|
expect(await getEventSummary(hass, 'clientID')).toBe(eventSummary);
|
||||||
|
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||||
|
hass,
|
||||||
|
eventSummarySchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'frigate/events/summary',
|
||||||
|
instance_id: 'clientID',
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get PTZ info', async () => {
|
||||||
|
const ptzInfo = [
|
||||||
|
{
|
||||||
|
name: 'camera.office',
|
||||||
|
features: ['zoom', 'zoom-r'],
|
||||||
|
presets: ['preset01', 'preset02'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const hass = createHASS();
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValue(ptzInfo);
|
||||||
|
expect(await getPTZInfo(hass, 'clientID', 'camera.office')).toBe(ptzInfo);
|
||||||
|
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||||
|
hass,
|
||||||
|
ptzInfoSchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'frigate/ptz/info',
|
||||||
|
instance_id: 'clientID',
|
||||||
|
camera: 'camera.office',
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createHASS,
|
createHASS,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
|
createStore,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
|
|
||||||
const createEngine = (): GenericCameraManagerEngine => {
|
const createEngine = (): GenericCameraManagerEngine => {
|
||||||
@@ -26,19 +27,29 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
|
|
||||||
it('should initialize camera', async () => {
|
it('should initialize camera', async () => {
|
||||||
const config = createGenericCameraConfig();
|
const config = createGenericCameraConfig();
|
||||||
expect(
|
const camera = await createEngine().initializeCamera(
|
||||||
await createEngine().initializeCamera(
|
|
||||||
createHASS(),
|
createHASS(),
|
||||||
mock<EntityRegistryManager>(),
|
mock<EntityRegistryManager>(),
|
||||||
config,
|
config,
|
||||||
),
|
);
|
||||||
).toEqual(config);
|
|
||||||
|
expect(camera.getConfig()).toEqual(config);
|
||||||
|
expect(camera.getCapabilities()).toEqual({
|
||||||
|
canFavoriteEvents: false,
|
||||||
|
canFavoriteRecordings: false,
|
||||||
|
canSeek: false,
|
||||||
|
supportsClips: false,
|
||||||
|
supportsRecordings: false,
|
||||||
|
supportsSnapshots: false,
|
||||||
|
supportsTimeline: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate default event query', () => {
|
it('should generate default event query', () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
createEngine().generateDefaultEventQuery(
|
engine.generateDefaultEventQuery(
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
new Set(['camera-1']),
|
new Set(['camera-1']),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
@@ -46,9 +57,10 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should generate default recording query', () => {
|
it('should generate default recording query', () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
createEngine().generateDefaultRecordingQuery(
|
engine.generateDefaultRecordingQuery(
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
new Set(['camera-1']),
|
new Set(['camera-1']),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
@@ -56,9 +68,10 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should generate default recording segments query', () => {
|
it('should generate default recording segments query', () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
createEngine().generateDefaultRecordingSegmentsQuery(
|
engine.generateDefaultRecordingSegmentsQuery(
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
new Set(['camera-1']),
|
new Set(['camera-1']),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
@@ -66,30 +79,33 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get events', async () => {
|
it('should get events', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
await createEngine().getEvents(
|
await engine.getEvents(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
{ type: QueryType.Event, cameraIDs: new Set(['camera-1']) },
|
{ type: QueryType.Event, cameraIDs: new Set(['camera-1']) },
|
||||||
),
|
),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get recordings', async () => {
|
it('should get recordings', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
await createEngine().getRecordings(
|
await engine.getRecordings(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
{ type: QueryType.Recording, cameraIDs: new Set(['camera-1']) },
|
{ type: QueryType.Recording, cameraIDs: new Set(['camera-1']) },
|
||||||
),
|
),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get recording segments', async () => {
|
it('should get recording segments', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
await createEngine().getRecordingSegments(
|
await engine.getRecordingSegments(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
{
|
{
|
||||||
type: QueryType.RecordingSegments,
|
type: QueryType.RecordingSegments,
|
||||||
cameraIDs: new Set(['camera-1']),
|
cameraIDs: new Set(['camera-1']),
|
||||||
@@ -101,10 +117,11 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should generate media from events', async () => {
|
it('should generate media from events', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
createEngine().generateMediaFromEvents(
|
engine.generateMediaFromEvents(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
{
|
{
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
cameraIDs: new Set(['camera-1']),
|
cameraIDs: new Set(['camera-1']),
|
||||||
@@ -118,10 +135,11 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should generate media from recordings', async () => {
|
it('should generate media from recordings', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
createEngine().generateMediaFromRecordings(
|
engine.generateMediaFromRecordings(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
{
|
{
|
||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
cameraIDs: new Set(['camera-1']),
|
cameraIDs: new Set(['camera-1']),
|
||||||
@@ -167,10 +185,11 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get media seek time', async () => {
|
it('should get media seek time', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
await createEngine().getMediaSeekTime(
|
await engine.getMediaSeekTime(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
new TestViewMedia(),
|
new TestViewMedia(),
|
||||||
new Date(),
|
new Date(),
|
||||||
),
|
),
|
||||||
@@ -178,10 +197,11 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get media metadata', async () => {
|
it('should get media metadata', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
expect(
|
expect(
|
||||||
await createEngine().getMediaMetadata(
|
await engine.getMediaMetadata(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
new Map([['camera-1', createGenericCameraConfig()]]),
|
createStore([{ cameraID: 'camera-1', engine: engine }]),
|
||||||
{ type: QueryType.MediaMetadata, cameraIDs: new Set(['camera-1']) },
|
{ type: QueryType.MediaMetadata, cameraIDs: new Set(['camera-1']) },
|
||||||
),
|
),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
@@ -264,18 +284,6 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get camera capabilities metadata', async () => {
|
|
||||||
expect(createEngine().getCameraCapabilities(createGenericCameraConfig())).toEqual({
|
|
||||||
canFavoriteEvents: false,
|
|
||||||
canFavoriteRecordings: false,
|
|
||||||
canSeek: false,
|
|
||||||
supportsClips: false,
|
|
||||||
supportsRecordings: false,
|
|
||||||
supportsSnapshots: false,
|
|
||||||
supportsTimeline: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get media capabilities', () => {
|
it('should get media capabilities', () => {
|
||||||
expect(createEngine().getMediaCapabilities(new TestViewMedia())).toBeNull();
|
expect(createEngine().getMediaCapabilities(new TestViewMedia())).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -303,4 +311,10 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should execute PTZ action', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
createEngine().executePTZAction(hass, createCameraConfig(), 'left');
|
||||||
|
expect(hass.callService).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { Camera } from '../../src/camera-manager/camera.js';
|
||||||
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
|
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
|
||||||
import { CameraManagerStore } from '../../src/camera-manager/store.js';
|
import { CameraManagerStore } from '../../src/camera-manager/store.js';
|
||||||
import { Engine } from '../../src/camera-manager/types.js';
|
import { Engine } from '../../src/camera-manager/types.js';
|
||||||
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry/index.js';
|
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry/index.js';
|
||||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
|
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
|
||||||
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
|
import {
|
||||||
|
TestViewMedia,
|
||||||
|
createCameraCapabilities,
|
||||||
|
createCameraConfig,
|
||||||
|
} from '../test-utils.js';
|
||||||
|
|
||||||
describe('CameraManagerStore', async () => {
|
describe('CameraManagerStore', async () => {
|
||||||
const config_visible = createCameraConfig();
|
const configVisible = createCameraConfig({
|
||||||
const config_hidden = createCameraConfig({
|
id: 'camera-visible',
|
||||||
|
});
|
||||||
|
const configHidden = createCameraConfig({
|
||||||
|
id: 'camera-hidden',
|
||||||
hide: true,
|
hide: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,71 +29,100 @@ describe('CameraManagerStore', async () => {
|
|||||||
const engineGeneric = await engineFactory.createEngine(Engine.Generic);
|
const engineGeneric = await engineFactory.createEngine(Engine.Generic);
|
||||||
const engineFrigate = await engineFactory.createEngine(Engine.Frigate);
|
const engineFrigate = await engineFactory.createEngine(Engine.Frigate);
|
||||||
|
|
||||||
const setupStore = async (): Promise<CameraManagerStore> => {
|
const setupStore = (): CameraManagerStore => {
|
||||||
const store = new CameraManagerStore();
|
const store = new CameraManagerStore();
|
||||||
store.addCamera('camera-visible', config_visible, engineGeneric);
|
store.addCamera(
|
||||||
store.addCamera('camera-hidden', config_hidden, engineFrigate);
|
new Camera(configVisible, engineGeneric, createCameraCapabilities()),
|
||||||
|
);
|
||||||
|
store.addCamera(new Camera(configHidden, engineFrigate, createCameraCapabilities()));
|
||||||
return store;
|
return store;
|
||||||
};
|
};
|
||||||
|
|
||||||
it('getCameraConfig', async () => {
|
it('getCameraConfig', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getCameraConfig('camera-visible')).toBe(config_visible);
|
expect(store.getCameraConfig('camera-visible')).toBe(configVisible);
|
||||||
expect(store.getCameraConfig('camera-hidden')).toBe(config_hidden);
|
expect(store.getCameraConfig('camera-hidden')).toBe(configHidden);
|
||||||
expect(store.getCameraConfig('camera-not-exist')).toBeNull();
|
expect(store.getCameraConfig('camera-not-exist')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hasCameraID', async () => {
|
it('hasCameraID', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.hasCameraID('camera-visible')).toBeTruthy();
|
expect(store.hasCameraID('camera-visible')).toBeTruthy();
|
||||||
expect(store.hasCameraID('camera-hidden')).toBeTruthy();
|
expect(store.hasCameraID('camera-hidden')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hasVisibleCameraID', async () => {
|
|
||||||
const store = await setupStore();
|
|
||||||
expect(store.hasVisibleCameraID('camera-visible')).toBeTruthy();
|
|
||||||
expect(store.hasVisibleCameraID('camera-hidden')).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getCameraCount', async () => {
|
it('getCameraCount', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getCameraCount()).toBe(2);
|
expect(store.getCameraCount()).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getVisibleCameraCount', async () => {
|
it('getVisibleCameraCount', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getVisibleCameraCount()).toBe(1);
|
expect(store.getVisibleCameraCount()).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCameras', async () => {
|
describe('getCamera', async () => {
|
||||||
const store = await setupStore();
|
it('present', async () => {
|
||||||
expect(store.getCameras()).toEqual(
|
const store = setupStore();
|
||||||
new Map([
|
expect(store.getCamera('camera-visible')?.getConfig()).toEqual(configVisible);
|
||||||
['camera-visible', config_visible],
|
|
||||||
['camera-hidden', config_hidden],
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getVisibleCameras', async () => {
|
it('absent', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getVisibleCameras()).toEqual(
|
expect(store.getCamera('not-a-camera')).toBeNull();
|
||||||
new Map([['camera-visible', config_visible]]),
|
});
|
||||||
);
|
});
|
||||||
|
|
||||||
|
describe('getCameraConfigs', async () => {
|
||||||
|
it('all', async () => {
|
||||||
|
const store = setupStore();
|
||||||
|
expect([...store.getCameraConfigs()]).toEqual([configVisible, configHidden]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('named', async () => {
|
||||||
|
const store = setupStore();
|
||||||
|
expect([...store.getCameraConfigs(['camera-visible', 'not-a-camera'])]).toEqual([
|
||||||
|
configVisible,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCameraConfigEntries', async () => {
|
||||||
|
it('all', async () => {
|
||||||
|
const store = setupStore();
|
||||||
|
expect([...store.getCameraConfigEntries()]).toEqual([
|
||||||
|
['camera-visible', configVisible],
|
||||||
|
['camera-hidden', configHidden],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('named', async () => {
|
||||||
|
const store = setupStore();
|
||||||
|
expect([
|
||||||
|
...store.getCameraConfigEntries(['camera-visible', 'not-a-camera']),
|
||||||
|
]).toEqual([['camera-visible', configVisible]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCameras', async () => {
|
||||||
|
const store = setupStore();
|
||||||
|
expect([...store.getCameras().keys()]).toEqual(['camera-visible', 'camera-hidden']);
|
||||||
|
expect(store.getCameras().get('camera-visible')?.getConfig()).toEqual(configVisible);
|
||||||
|
expect(store.getCameras().get('camera-hidden')?.getConfig()).toEqual(configHidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCameraIDs', async () => {
|
it('getCameraIDs', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getCameraIDs()).toEqual(new Set(['camera-visible', 'camera-hidden']));
|
expect(store.getCameraIDs()).toEqual(new Set(['camera-visible', 'camera-hidden']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getVisibleCameraIDs', async () => {
|
it('getVisibleCameraIDs', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getVisibleCameraIDs()).toEqual(new Set(['camera-visible']));
|
expect(store.getVisibleCameraIDs()).toEqual(new Set(['camera-visible']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reset', async () => {
|
it('reset', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
|
|
||||||
store.reset();
|
store.reset();
|
||||||
|
|
||||||
@@ -94,24 +131,24 @@ describe('CameraManagerStore', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('getCameraConfigForMedia', async () => {
|
it('getCameraConfigForMedia', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
|
|
||||||
const media_1 = new TestViewMedia({ cameraID: 'camera-visible' });
|
const media_1 = new TestViewMedia({ cameraID: 'camera-visible' });
|
||||||
expect(store.getCameraConfigForMedia(media_1)).toBe(config_visible);
|
expect(store.getCameraConfigForMedia(media_1)).toBe(configVisible);
|
||||||
|
|
||||||
const media_2 = new TestViewMedia({ cameraID: 'camera-not-exist' });
|
const media_2 = new TestViewMedia({ cameraID: 'camera-not-exist' });
|
||||||
expect(store.getCameraConfigForMedia(media_2)).toBeNull();
|
expect(store.getCameraConfigForMedia(media_2)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getEngineOfType', async () => {
|
it('getEngineOfType', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getEngineOfType(Engine.Generic)).toBe(engineGeneric);
|
expect(store.getEngineOfType(Engine.Generic)).toBe(engineGeneric);
|
||||||
expect(store.getEngineOfType(Engine.Frigate)).toBe(engineFrigate);
|
expect(store.getEngineOfType(Engine.Frigate)).toBe(engineFrigate);
|
||||||
expect(store.getEngineOfType(Engine.MotionEye)).toBeNull();
|
expect(store.getEngineOfType(Engine.MotionEye)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getEngineForCameraID', async () => {
|
it('getEngineForCameraID', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getEngineForCameraID('camera-visible')).toBe(engineGeneric);
|
expect(store.getEngineForCameraID('camera-visible')).toBe(engineGeneric);
|
||||||
expect(store.getEngineForCameraID('camera-hidden')).toBe(engineFrigate);
|
expect(store.getEngineForCameraID('camera-hidden')).toBe(engineFrigate);
|
||||||
expect(store.getEngineForCameraID('camera-not-exist')).toBeNull();
|
expect(store.getEngineForCameraID('camera-not-exist')).toBeNull();
|
||||||
@@ -119,13 +156,23 @@ describe('CameraManagerStore', async () => {
|
|||||||
|
|
||||||
describe('getEnginesForCameraIDs', async () => {
|
describe('getEnginesForCameraIDs', async () => {
|
||||||
it('empty input', async () => {
|
it('empty input', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
expect(store.getEnginesForCameraIDs(new Set())).toBeNull();
|
expect(store.getEnginesForCameraIDs(new Set())).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('multiple cameras', async () => {
|
it('multiple cameras', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
store.addCamera('camera-visible2', config_visible, engineGeneric);
|
store.addCamera(
|
||||||
|
new Camera(
|
||||||
|
{
|
||||||
|
...configVisible,
|
||||||
|
id: 'camera-visible2',
|
||||||
|
},
|
||||||
|
engineGeneric,
|
||||||
|
createCameraCapabilities(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
store.getEnginesForCameraIDs(
|
store.getEnginesForCameraIDs(
|
||||||
new Set([
|
new Set([
|
||||||
@@ -145,13 +192,61 @@ describe('CameraManagerStore', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('getEngineForMedia', async () => {
|
it('getEngineForMedia', async () => {
|
||||||
const store = await setupStore();
|
const store = setupStore();
|
||||||
const media = new TestViewMedia({ cameraID: 'camera-visible' });
|
const media = new TestViewMedia({ cameraID: 'camera-visible' });
|
||||||
expect(store.getEngineForMedia(media)).toBe(engineGeneric);
|
expect(store.getEngineForMedia(media)).toBe(engineGeneric);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getAllEngines', async () => {
|
describe('getAllDependentCameras', () => {
|
||||||
const store = await setupStore();
|
it('should return dependent cameras', () => {
|
||||||
expect(store.getAllEngines()).toEqual([engineGeneric, engineFrigate]);
|
const store = new CameraManagerStore();
|
||||||
|
store.addCamera(
|
||||||
|
new Camera(
|
||||||
|
createCameraConfig({
|
||||||
|
id: 'one',
|
||||||
|
dependencies: {
|
||||||
|
cameras: ['two', 'three'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
engineGeneric,
|
||||||
|
createCameraCapabilities(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
store.addCamera(
|
||||||
|
new Camera(
|
||||||
|
createCameraConfig({
|
||||||
|
id: 'two',
|
||||||
|
}),
|
||||||
|
engineGeneric,
|
||||||
|
createCameraCapabilities(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(store.getAllDependentCameras('one')).toEqual(new Set(['one', 'two']));
|
||||||
|
});
|
||||||
|
it('should return all cameras', () => {
|
||||||
|
const store = new CameraManagerStore();
|
||||||
|
store.addCamera(
|
||||||
|
new Camera(
|
||||||
|
createCameraConfig({
|
||||||
|
id: 'one',
|
||||||
|
dependencies: {
|
||||||
|
all_cameras: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
engineGeneric,
|
||||||
|
createCameraCapabilities(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
store.addCamera(
|
||||||
|
new Camera(
|
||||||
|
createCameraConfig({
|
||||||
|
id: 'two',
|
||||||
|
}),
|
||||||
|
engineGeneric,
|
||||||
|
createCameraCapabilities(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(store.getAllDependentCameras('one')).toEqual(new Set(['one', 'two']));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/utils/action.js');
|
vi.mock('../../src/utils/action.js');
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
const createAction = (
|
const createAction = (
|
||||||
action: Record<string, unknown>,
|
action: Record<string, unknown>,
|
||||||
@@ -708,6 +707,62 @@ describe('ActionsManager.executeAction', () => {
|
|||||||
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
|
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should handle ptz action', () => {
|
||||||
|
it('with selected camera', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({ camera: 'camera.office' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'ptz',
|
||||||
|
ptz_action: 'left',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
|
||||||
|
'camera.office',
|
||||||
|
'left',
|
||||||
|
{
|
||||||
|
phase: undefined,
|
||||||
|
preset: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without selected camera', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'ptz',
|
||||||
|
ptz_action: 'left',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle show_ptz action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'show_ptz',
|
||||||
|
show_ptz: true,
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewWithNewContext).toBeCalledWith(
|
||||||
|
expect.objectContaining({ live: { ptzVisible: true } }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle unknown action', async () => {
|
it('should handle unknown action', async () => {
|
||||||
const manager = new ActionsManager(createCardAPI());
|
const manager = new ActionsManager(createCardAPI());
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { CardController } from '../../src/card-controller/controller';
|
||||||
import { HASSManager } from '../../src/card-controller/hass-manager';
|
import { HASSManager } from '../../src/card-controller/hass-manager';
|
||||||
import { CardHASSAPI } from '../../src/card-controller/types';
|
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
@@ -8,12 +8,11 @@ import {
|
|||||||
createConfig,
|
createConfig,
|
||||||
createHASS,
|
createHASS,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
const createAPIWithoutMediaPlayers = (): CardController => {
|
||||||
|
|
||||||
const createAPIWithoutMediaPlayers = (): CardHASSAPI => {
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getMediaPlayerManager().getMediaPlayers).mockReturnValue([]);
|
vi.mocked(api.getMediaPlayerManager().getMediaPlayers).mockReturnValue([]);
|
||||||
return api;
|
return api;
|
||||||
@@ -158,20 +157,20 @@ describe('HASSManager', () => {
|
|||||||
|
|
||||||
describe('should set default view when', () => {
|
describe('should set default view when', () => {
|
||||||
it('selected camera trigger entity changes', () => {
|
it('selected camera trigger entity changes', () => {
|
||||||
const cameraManager = createCameraManager({
|
const api = createAPIWithoutMediaPlayers();
|
||||||
configs: new Map([
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
[
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
'camera.foo',
|
createStore([
|
||||||
createCameraConfig({
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig({
|
||||||
triggers: {
|
triggers: {
|
||||||
entities: ['binary_sensor.motion'],
|
entities: ['binary_sensor.motion'],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
createView({
|
createView({
|
||||||
camera: 'camera.foo',
|
camera: 'camera.foo',
|
||||||
@@ -189,20 +188,20 @@ describe('HASSManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('selected camera is unknown', () => {
|
it('selected camera is unknown', () => {
|
||||||
const cameraManager = createCameraManager({
|
const api = createAPIWithoutMediaPlayers();
|
||||||
configs: new Map([
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
[
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
'camera.foo',
|
createStore([
|
||||||
createCameraConfig({
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig({
|
||||||
triggers: {
|
triggers: {
|
||||||
entities: ['binary_sensor.motion'],
|
entities: ['binary_sensor.motion'],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
createView({
|
createView({
|
||||||
camera: 'camera.UNKNOWN',
|
camera: 'camera.UNKNOWN',
|
||||||
|
|||||||
@@ -18,11 +18,39 @@ describe('InitializationManager', () => {
|
|||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not be initialized', () => {
|
describe('should correctly determine when mandatory initialization is required', () => {
|
||||||
const manager = new InitializationManager(createCardAPI());
|
it('without aspects', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const initializer = mock<Initializer>();
|
||||||
|
const manager = new InitializationManager(api, initializer);
|
||||||
|
|
||||||
|
initializer.isInitializedMultiple.mockReturnValue(false);
|
||||||
|
|
||||||
expect(manager.isInitializedMandatory()).toBeFalsy();
|
expect(manager.isInitializedMandatory()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('without view', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const initializer = mock<Initializer>();
|
||||||
|
const manager = new InitializationManager(api, initializer);
|
||||||
|
|
||||||
|
initializer.isInitializedMultiple.mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(manager.isInitializedMandatory()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with aspects and view', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const initializer = mock<Initializer>();
|
||||||
|
const manager = new InitializationManager(api, initializer);
|
||||||
|
|
||||||
|
initializer.isInitializedMultiple.mockReturnValue(true);
|
||||||
|
vi.mocked(api.getViewManager().hasView).mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(manager.isInitializedMandatory()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('should initialize mandatory', () => {
|
describe('should initialize mandatory', () => {
|
||||||
it('without hass', async () => {
|
it('without hass', async () => {
|
||||||
const manager = new InitializationManager(createCardAPI());
|
const manager = new InitializationManager(createCardAPI());
|
||||||
@@ -41,7 +69,9 @@ describe('InitializationManager', () => {
|
|||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false);
|
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false);
|
||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false);
|
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
|
||||||
|
false,
|
||||||
|
);
|
||||||
const manager = new InitializationManager(api);
|
const manager = new InitializationManager(api);
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
expect(await manager.initializeMandatory()).toBeTruthy();
|
||||||
@@ -50,7 +80,6 @@ describe('InitializationManager', () => {
|
|||||||
expect(sideLoadHomeAssistantElements).toBeCalled();
|
expect(sideLoadHomeAssistantElements).toBeCalled();
|
||||||
expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled();
|
expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled();
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully with querystring view', async () => {
|
it('successfully with querystring view', async () => {
|
||||||
@@ -71,7 +100,9 @@ describe('InitializationManager', () => {
|
|||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false);
|
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
|
||||||
|
false,
|
||||||
|
);
|
||||||
const manager = new InitializationManager(api);
|
const manager = new InitializationManager(api);
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
expect(await manager.initializeMandatory()).toBeTruthy();
|
||||||
@@ -134,7 +165,6 @@ describe('InitializationManager', () => {
|
|||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
|
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
|
||||||
expect(api.getMediaPlayerManager().initialize).not.toBeCalled();
|
expect(api.getMediaPlayerManager().initialize).not.toBeCalled();
|
||||||
expect(api.getMicrophoneManager().connect).not.toBeCalled();
|
expect(api.getMicrophoneManager().connect).not.toBeCalled();
|
||||||
expect(api.getCardElementManager().update).not.toBeCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully with all inititalizers', async () => {
|
it('successfully with all inititalizers', async () => {
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
||||||
|
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../src/const';
|
||||||
|
import { ExtendedHomeAssistant } from '../../src/types';
|
||||||
|
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
|
||||||
import {
|
import {
|
||||||
TestViewMedia,
|
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
createHASS,
|
createHASS,
|
||||||
createRegistryEntity,
|
createRegistryEntity,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
|
createStore,
|
||||||
|
TestViewMedia,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
|
||||||
import { ExtendedHomeAssistant } from '../../src/types';
|
|
||||||
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../src/const';
|
|
||||||
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
const createHASSWithMediaPlayers = (): ExtendedHomeAssistant => {
|
const createHASSWithMediaPlayers = (): ExtendedHomeAssistant => {
|
||||||
const attributesSupported = {
|
const attributesSupported = {
|
||||||
@@ -145,9 +144,7 @@ describe('MediaPlayerManager', () => {
|
|||||||
describe('live', () => {
|
describe('live', () => {
|
||||||
it('without camera config', async () => {
|
it('without camera config', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(null);
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
@@ -159,28 +156,33 @@ describe('MediaPlayerManager', () => {
|
|||||||
describe('using standard method', () => {
|
describe('using standard method', () => {
|
||||||
it('successfully', async () => {
|
it('successfully', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
createCameraConfig({
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.foo',
|
camera_entity: 'camera.foo',
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
vi.mocked(api.getCameraManager().getCameraMetadata).mockReturnValue({
|
||||||
title: 'camera title',
|
title: 'camera title',
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
});
|
});
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||||
const hass = createHASS({
|
createHASS({
|
||||||
'camera.foo': createStateEntity({
|
'camera.foo': createStateEntity({
|
||||||
attributes: {
|
attributes: {
|
||||||
entity_picture: 'http://thumbnail',
|
entity_picture: 'http://thumbnail',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
}),
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
);
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
await manager.playLive('media_player.foo', 'camera.foo');
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
||||||
'media_player',
|
'media_player',
|
||||||
@@ -199,32 +201,41 @@ describe('MediaPlayerManager', () => {
|
|||||||
|
|
||||||
it('without camera_entity', async () => {
|
it('without camera_entity', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
|
||||||
createCameraConfig({}),
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
await manager.playLive('media_player.foo', 'camera.foo');
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
|
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without title and thumbnail', async () => {
|
it('without title and thumbnail', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
createCameraConfig({
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.foo',
|
camera_entity: 'camera.foo',
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
await manager.playLive('media_player.foo', 'camera.foo');
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
||||||
'media_player',
|
'media_player',
|
||||||
@@ -242,9 +253,12 @@ describe('MediaPlayerManager', () => {
|
|||||||
describe('using dashboard method', () => {
|
describe('using dashboard method', () => {
|
||||||
it('successfully', async () => {
|
it('successfully', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
createCameraConfig({
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.foo',
|
camera_entity: 'camera.foo',
|
||||||
cast: {
|
cast: {
|
||||||
method: 'dashboard',
|
method: 'dashboard',
|
||||||
@@ -254,13 +268,14 @@ describe('MediaPlayerManager', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getQueryStringManager().generateQueryString).mockReturnValue('');
|
vi.mocked(api.getQueryStringManager().generateQueryString).mockReturnValue('');
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
await manager.playLive('media_player.foo', 'camera.foo');
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
||||||
'cast',
|
'cast',
|
||||||
@@ -275,9 +290,12 @@ describe('MediaPlayerManager', () => {
|
|||||||
|
|
||||||
it('without hass', async () => {
|
it('without hass', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
createCameraConfig({
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.foo',
|
camera_entity: 'camera.foo',
|
||||||
cast: {
|
cast: {
|
||||||
method: 'dashboard',
|
method: 'dashboard',
|
||||||
@@ -287,12 +305,13 @@ describe('MediaPlayerManager', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
await manager.playLive('media_player.foo', 'camera.foo');
|
||||||
|
|
||||||
// No actual test can be performed here as nothing observable happens.
|
// No actual test can be performed here as nothing observable happens.
|
||||||
// This test serves only as code-coverage long-tail.
|
// This test serves only as code-coverage long-tail.
|
||||||
@@ -301,21 +320,25 @@ describe('MediaPlayerManager', () => {
|
|||||||
|
|
||||||
it('without required configuration', async () => {
|
it('without required configuration', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
createCameraConfig({
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.foo',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.foo',
|
camera_entity: 'camera.foo',
|
||||||
cast: {
|
cast: {
|
||||||
method: 'dashboard',
|
method: 'dashboard',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
await manager.playLive('media_player.foo', 'camera.foo');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
vi.mocked(api.getMessageManager().setMessageIfHigherPriority),
|
vi.mocked(api.getMessageManager().setMessageIfHigherPriority),
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ describe('MicrophoneManager', () => {
|
|||||||
const manager = new MicrophoneManager(api);
|
const manager = new MicrophoneManager(api);
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error());
|
navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error());
|
||||||
|
|
||||||
await manager.connect();
|
expect(await manager.connect()).toBeFalsy();
|
||||||
|
|
||||||
expect(manager.isConnected()).toBeFalsy();
|
expect(manager.isConnected()).toBeFalsy();
|
||||||
expect(manager.isForbidden()).toBeTruthy();
|
expect(manager.isForbidden()).toBeTruthy();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import add from 'date-fns/add';
|
import add from 'date-fns/add';
|
||||||
import { HassEntities } from 'home-assistant-js-websocket';
|
import { HassEntities } from 'home-assistant-js-websocket';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { ScanOptions } from '../../src/config/types';
|
import { CardController } from '../../src/card-controller/controller';
|
||||||
import { TriggersManager } from '../../src/card-controller/triggers-manager';
|
import { TriggersManager } from '../../src/card-controller/triggers-manager';
|
||||||
|
import { ScanOptions } from '../../src/config/types';
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
@@ -10,18 +11,17 @@ import {
|
|||||||
createConfig,
|
createConfig,
|
||||||
createHASS,
|
createHASS,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
// Creating and mocking a trigger API is a lot of boilerplate, this convenience
|
// Creating and mocking a trigger API is a lot of boilerplate, this convenience
|
||||||
// function reduces it.
|
// function reduces it.
|
||||||
const createTriggerAPI = (options?: {
|
const createTriggerAPI = (options?: {
|
||||||
config?: Partial<ScanOptions>;
|
config?: Partial<ScanOptions>;
|
||||||
hassStates?: HassEntities;
|
hassStates?: HassEntities;
|
||||||
interaction?: boolean;
|
interaction?: boolean;
|
||||||
}) => {
|
}): CardController => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
@@ -37,19 +37,18 @@ const createTriggerAPI = (options?: {
|
|||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||||
createHASS(options?.hassStates),
|
createHASS(options?.hassStates),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
createCameraManager({
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
configs: new Map([
|
createStore([
|
||||||
[
|
{
|
||||||
'camera_1',
|
cameraID: 'camera_1',
|
||||||
createCameraConfig({
|
config: createCameraConfig({
|
||||||
triggers: {
|
triggers: {
|
||||||
entities: ['binary_sensor.motion'],
|
entities: ['binary_sensor.motion'],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
]),
|
]),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(
|
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(
|
||||||
options?.interaction ?? false,
|
options?.interaction ?? false,
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { QueryType } from '../../src/camera-manager/types';
|
import { QueryType } from '../../src/camera-manager/types';
|
||||||
import { FrigateCardView } from '../../src/config/types';
|
|
||||||
import { getAllDependentCameras } from '../../src/utils/camera';
|
|
||||||
import { ViewManager } from '../../src/card-controller/view-manager';
|
import { ViewManager } from '../../src/card-controller/view-manager';
|
||||||
|
import { FrigateCardView } from '../../src/config/types';
|
||||||
|
import { executeMediaQueryForView } from '../../src/utils/media-to-view';
|
||||||
import { EventMediaQueries } from '../../src/view/media-queries';
|
import { EventMediaQueries } from '../../src/view/media-queries';
|
||||||
|
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||||
|
import { View } from '../../src/view/view';
|
||||||
import {
|
import {
|
||||||
|
createCameraCapabilities,
|
||||||
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
createConfig,
|
createConfig,
|
||||||
createHASS,
|
createHASS,
|
||||||
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
generateViewMediaArray,
|
generateViewMediaArray
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
vi.mock('../../src/utils/media-to-view');
|
||||||
vi.mock('../../src/utils/camera');
|
|
||||||
|
|
||||||
describe('ViewManager.setView', () => {
|
describe('ViewManager.setView', () => {
|
||||||
it('should set view', () => {
|
it('should set view', () => {
|
||||||
@@ -29,6 +33,7 @@ describe('ViewManager.setView', () => {
|
|||||||
manager.setView(view);
|
manager.setView(view);
|
||||||
|
|
||||||
expect(manager.getView()).toBe(view);
|
expect(manager.getView()).toBe(view);
|
||||||
|
expect(manager.hasView()).toBeTruthy();
|
||||||
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
||||||
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
||||||
expect(api.getMessageManager().reset).toBeCalled();
|
expect(api.getMessageManager().reset).toBeCalled();
|
||||||
@@ -101,6 +106,7 @@ describe('ViewManager.reset', () => {
|
|||||||
manager.reset();
|
manager.reset();
|
||||||
|
|
||||||
expect(manager.getView()).toBeNull();
|
expect(manager.getView()).toBeNull();
|
||||||
|
expect(manager.hasView()).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,6 +115,13 @@ describe('ViewManager.setViewDefault', () => {
|
|||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
manager.setViewDefault();
|
manager.setViewDefault();
|
||||||
@@ -130,12 +143,18 @@ describe('ViewManager.setViewDefault', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should cycle camera when configured', () => {
|
it('should cycle camera when configured', () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera_2',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
view: {
|
view: {
|
||||||
@@ -160,13 +179,19 @@ describe('ViewManager.setViewDefault', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should respect parameters', () => {
|
it('should respect parameters', () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera.kitchen', 'camera.office']),
|
|
||||||
);
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.office',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
|
|
||||||
manager.setViewDefault({
|
manager.setViewDefault({
|
||||||
@@ -186,60 +211,84 @@ describe('ViewManager.setViewByParameters', () => {
|
|||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
|
||||||
|
createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
manager.setViewByParameters({
|
manager.setViewByParameters({
|
||||||
cameraID: 'camera',
|
cameraID: 'camera.kitchen',
|
||||||
viewName: 'clips',
|
viewName: 'clips',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('clips');
|
expect(manager.getView()?.view).toBe('clips');
|
||||||
expect(manager.getView()?.camera).toBe('camera');
|
expect(manager.getView()?.camera).toBe('camera.kitchen');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set view by parameters using existing view if unspecified', () => {
|
it('should set view by parameters using existing view if unspecified', () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.office',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
|
||||||
|
createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
manager.setViewByParameters({
|
manager.setViewByParameters({
|
||||||
cameraID: 'camera_1',
|
cameraID: 'camera.kitchen',
|
||||||
viewName: 'clips',
|
viewName: 'clips',
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.setViewByParameters({
|
manager.setViewByParameters({
|
||||||
cameraID: 'camera_2',
|
cameraID: 'camera.office',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('clips');
|
expect(manager.getView()?.view).toBe('clips');
|
||||||
expect(manager.getView()?.camera).toBe('camera_2');
|
expect(manager.getView()?.camera).toBe('camera.office');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set view by parameters using config as fallback', () => {
|
it('should set view by parameters using config as fallback', () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
manager.setViewByParameters({
|
manager.setViewByParameters({
|
||||||
cameraID: 'camera_1',
|
cameraID: 'camera.kitchen',
|
||||||
// No prior view, and no specified view. This could happen during query
|
// No prior view, and no specified view. This could happen during query
|
||||||
// string based initialization.
|
// string based initialization.
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('live');
|
expect(manager.getView()?.view).toBe('live');
|
||||||
expect(manager.getView()?.camera).toBe('camera_1');
|
expect(manager.getView()?.camera).toBe('camera.kitchen');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set view by parameters without config', () => {
|
it('should not set view by parameters without config', () => {
|
||||||
@@ -253,12 +302,19 @@ describe('ViewManager.setViewByParameters', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not set view by parameters without visible cameras', () => {
|
it('should not set view by parameters without visible cameras', () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(new Set());
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
config: createCameraConfig({
|
||||||
|
hide: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
manager.setViewByParameters({
|
manager.setViewByParameters({
|
||||||
@@ -278,6 +334,21 @@ describe('ViewManager.setViewByParameters', () => {
|
|||||||
])('%s', (viewName: FrigateCardView) => {
|
])('%s', (viewName: FrigateCardView) => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
|
||||||
|
createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager()).getConfig.mockReturnValue(
|
vi.mocked(api.getConfigManager()).getConfig.mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
media_viewer: {
|
media_viewer: {
|
||||||
@@ -295,7 +366,7 @@ describe('ViewManager.setViewByParameters', () => {
|
|||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
|
|
||||||
manager.setViewByParameters({
|
manager.setViewByParameters({
|
||||||
cameraID: 'camera',
|
cameraID: 'camera.kitchen',
|
||||||
viewName: viewName,
|
viewName: viewName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,43 +385,31 @@ describe('ViewManager.setViewByParameters', () => {
|
|||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
|
||||||
|
createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
|
|
||||||
manager.setViewByParameters({
|
manager.setViewByParameters({
|
||||||
cameraID: 'camera',
|
cameraID: 'camera.kitchen',
|
||||||
viewName: viewName,
|
viewName: viewName,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(manager.getView()?.displayMode).toBe('single');
|
expect(manager.getView()?.displayMode).toBe('single');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set view by parameters using config as fallback', () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_1_hd']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera_1',
|
|
||||||
viewName: 'live',
|
|
||||||
substream: 'camera_1_hd',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('live');
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_1');
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
|
||||||
new Map([['camera_1', 'camera_1_hd']]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
@@ -360,6 +419,7 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
manager.setView(createView());
|
manager.setView(createView());
|
||||||
|
|
||||||
@@ -380,20 +440,40 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set display mode to grid and create new query', async () => {
|
it('should set display mode to grid and create new query', async () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.office',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
||||||
|
|
||||||
const media = generateViewMediaArray({ count: 5 });
|
const mediaArray = generateViewMediaArray({ count: 5 });
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media);
|
vi.mocked(executeMediaQueryForView).mockResolvedValue(
|
||||||
|
new View({
|
||||||
|
view: 'clip',
|
||||||
|
camera: 'camera.kitchen',
|
||||||
|
queryResults: new MediaQueriesResults({ results: mediaArray }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
const query = new EventMediaQueries([
|
const query = new EventMediaQueries([
|
||||||
@@ -410,39 +490,50 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('grid');
|
await manager.setViewWithNewDisplayMode('grid');
|
||||||
|
|
||||||
expect(manager.getView()?.queryResults?.getResults()).toBe(media);
|
expect(manager.getView()?.queryResults?.getResults()).toBe(mediaArray);
|
||||||
expect(cameraManager.executeMediaQueries).toBeCalledWith(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'event-query',
|
|
||||||
cameraIDs: new Set(['camera_1', 'camera_2']),
|
|
||||||
hasClip: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set display mode to single and create new query', async () => {
|
it('should set display mode to single and create new query', async () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.office',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
||||||
|
|
||||||
const media = generateViewMediaArray({ count: 5 });
|
const mediaArray = generateViewMediaArray({ count: 5 });
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media);
|
vi.mocked(executeMediaQueryForView).mockResolvedValue(
|
||||||
|
new View({
|
||||||
|
view: 'clip',
|
||||||
|
camera: 'camera.kitchen',
|
||||||
|
queryResults: new MediaQueriesResults({ results: mediaArray }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
const query = new EventMediaQueries([
|
const query = new EventMediaQueries([
|
||||||
{
|
{
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
cameraIDs: new Set(['camera_1', 'camera_2']),
|
cameraIDs: new Set(['camera.kitchen', 'camera.office']),
|
||||||
hasClip: true,
|
hasClip: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -450,53 +541,58 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|||||||
manager.setView(
|
manager.setView(
|
||||||
createView({
|
createView({
|
||||||
view: 'clip',
|
view: 'clip',
|
||||||
camera: 'camera_2',
|
camera: 'camera.office',
|
||||||
query: query,
|
query: query,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('single');
|
await manager.setViewWithNewDisplayMode('single');
|
||||||
|
|
||||||
expect(manager.getView()?.queryResults?.getResults()).toBe(media);
|
expect(manager.getView()?.queryResults?.getResults()).toBe(mediaArray);
|
||||||
expect(cameraManager.executeMediaQueries).toBeCalledWith(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'event-query',
|
|
||||||
cameraIDs: new Set(['camera_2']),
|
|
||||||
hasClip: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set display mode to single and handle failed new query', async () => {
|
it('should set display mode to single and handle failed new query', async () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.office',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
const query = new EventMediaQueries([
|
const query = new EventMediaQueries([
|
||||||
{
|
{
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
cameraIDs: new Set(['camera_1', 'camera_2']),
|
cameraIDs: new Set(['camera.kitchen', 'camera.office']),
|
||||||
hasClip: true,
|
hasClip: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const originalView = createView({
|
const originalView = createView({
|
||||||
view: 'clip',
|
view: 'clip',
|
||||||
camera: 'camera_2',
|
camera: 'camera.office',
|
||||||
query: query,
|
query: query,
|
||||||
});
|
});
|
||||||
manager.setView(originalView);
|
manager.setView(originalView);
|
||||||
|
|
||||||
// Query execution fails / returns null.
|
// Query execution fails / returns null.
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(null);
|
vi.mocked(executeMediaQueryForView).mockRejectedValue(null);
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('single');
|
await manager.setViewWithNewDisplayMode('single');
|
||||||
|
|
||||||
@@ -504,14 +600,28 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set display mode and handle empty new query results', async () => {
|
it('should set display mode and handle empty new query results', async () => {
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.office',
|
||||||
|
capabilities: createCameraCapabilities({
|
||||||
|
supportsClips: true,
|
||||||
|
supportsRecordings: true,
|
||||||
|
supportsSnapshots: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
@@ -519,20 +629,20 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|||||||
const query = new EventMediaQueries([
|
const query = new EventMediaQueries([
|
||||||
{
|
{
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
cameraIDs: new Set(['camera_1']),
|
cameraIDs: new Set(['camera.kitchen']),
|
||||||
hasClip: true,
|
hasClip: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const originalView = createView({
|
const originalView = createView({
|
||||||
view: 'clip',
|
view: 'clip',
|
||||||
camera: 'camera_2',
|
camera: 'camera.office',
|
||||||
query: query,
|
query: query,
|
||||||
});
|
});
|
||||||
manager.setView(originalView);
|
manager.setView(originalView);
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('grid');
|
await manager.setViewWithNewDisplayMode('grid');
|
||||||
|
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(null);
|
vi.mocked(executeMediaQueryForView).mockResolvedValue(null);
|
||||||
|
|
||||||
// Empty queries will not be executed, so view will not be changed.
|
// Empty queries will not be executed, so view will not be changed.
|
||||||
expect(manager.getView()?.displayMode).toBeNull();
|
expect(manager.getView()?.displayMode).toBeNull();
|
||||||
@@ -541,13 +651,21 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|||||||
|
|
||||||
describe('ViewManager.setViewWithSubstream', () => {
|
describe('ViewManager.setViewWithSubstream', () => {
|
||||||
it('should set new equal view with no dependencies', () => {
|
it('should set new equal view with no dependencies', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const manager = new ViewManager(api);
|
||||||
const view = createView({
|
const view = createView({
|
||||||
view: 'live',
|
view: 'live',
|
||||||
camera: 'camera',
|
camera: 'camera',
|
||||||
});
|
});
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera']));
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
manager.setView(view);
|
||||||
manager.setViewWithSubstream();
|
manager.setViewWithSubstream();
|
||||||
|
|
||||||
@@ -557,39 +675,71 @@ describe('ViewManager.setViewWithSubstream', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set new view with next substream', () => {
|
it('should set new view with next substream', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
config: createCameraConfig({
|
||||||
|
dependencies: {
|
||||||
|
cameras: ['camera.kitchen_hd'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen_hd',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const manager = new ViewManager(api);
|
||||||
const view = createView({
|
const view = createView({
|
||||||
view: 'live',
|
view: 'live',
|
||||||
camera: 'camera',
|
camera: 'camera.kitchen',
|
||||||
});
|
});
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
manager.setView(view);
|
||||||
manager.setViewWithSubstream();
|
manager.setViewWithSubstream();
|
||||||
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
||||||
new Map([['camera', 'camera2']]),
|
new Map([['camera.kitchen', 'camera.kitchen_hd']]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set new view with next substream when view has invalid substream', () => {
|
it('should set new view with next substream when view has invalid substream', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
config: createCameraConfig({
|
||||||
|
dependencies: {
|
||||||
|
cameras: ['camera.kitchen_hd'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen_hd',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const manager = new ViewManager(api);
|
||||||
const view = createView({
|
const view = createView({
|
||||||
view: 'live',
|
view: 'live',
|
||||||
camera: 'camera',
|
camera: 'camera.kitchen',
|
||||||
context: {
|
context: {
|
||||||
live: {
|
live: {
|
||||||
overrides: new Map([['camera', 'camera-that-does-not-exist']]),
|
overrides: new Map([['camera.kitchen', 'camera-that-does-not-exist']]),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
manager.setView(view);
|
||||||
manager.setViewWithSubstream();
|
manager.setViewWithSubstream();
|
||||||
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
||||||
new Map([['camera', 'camera']]),
|
new Map([['camera.kitchen', 'camera.kitchen']]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -680,8 +830,12 @@ describe('ViewManager.isViewSupportedByCamera', () => {
|
|||||||
['media' as const, false],
|
['media' as const, false],
|
||||||
])('%s', (viewName: FrigateCardView, expected: boolean) => {
|
])('%s', (viewName: FrigateCardView, expected: boolean) => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const cameraManager = createCameraManager();
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue({
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera.kitchen',
|
||||||
|
capabilities: {
|
||||||
canFavoriteEvents: false,
|
canFavoriteEvents: false,
|
||||||
canFavoriteRecordings: false,
|
canFavoriteRecordings: false,
|
||||||
canSeek: false,
|
canSeek: false,
|
||||||
@@ -689,8 +843,10 @@ describe('ViewManager.isViewSupportedByCamera', () => {
|
|||||||
supportsRecordings: false,
|
supportsRecordings: false,
|
||||||
supportsSnapshots: false,
|
supportsSnapshots: false,
|
||||||
supportsTimeline: false,
|
supportsTimeline: false,
|
||||||
});
|
},
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
|
|
||||||
expect(manager.isViewSupportedByCamera('camera', viewName)).toBe(expected);
|
expect(manager.isViewSupportedByCamera('camera', viewName)).toBe(expected);
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
} from '../../src/components-lib/menu-controller';
|
} from '../../src/components-lib/menu-controller';
|
||||||
import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types';
|
import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types';
|
||||||
import { FrigateCardMediaPlayer } from '../../src/types';
|
import { FrigateCardMediaPlayer } from '../../src/types';
|
||||||
import { createFrigateCardCustomAction } from '../../src/utils/action';
|
import { createFrigateCardSimpleAction } from '../../src/utils/action';
|
||||||
import { ViewMedia } from '../../src/view/media';
|
import { ViewMedia } from '../../src/view/media';
|
||||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||||
import { View } from '../../src/view/view';
|
import { View } from '../../src/view/view';
|
||||||
import {
|
import {
|
||||||
|
createAggregateCameraCapabilities,
|
||||||
createCameraCapabilities,
|
createCameraCapabilities,
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
@@ -26,10 +27,10 @@ import {
|
|||||||
createMediaCapabilities,
|
createMediaCapabilities,
|
||||||
createMediaLoadedInfo,
|
createMediaLoadedInfo,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
vi.mock('../../src/utils/media-player-controller.js');
|
vi.mock('../../src/utils/media-player-controller.js');
|
||||||
vi.mock('../../src/card-controller/microphone-manager.js');
|
vi.mock('../../src/card-controller/microphone-manager.js');
|
||||||
|
|
||||||
@@ -42,16 +43,15 @@ const calculateButtons = (
|
|||||||
view?: View;
|
view?: View;
|
||||||
},
|
},
|
||||||
): MenuItem[] => {
|
): MenuItem[] => {
|
||||||
|
let cameraManager: CameraManager | null = options?.cameraManager ?? null;
|
||||||
|
if (!cameraManager) {
|
||||||
|
cameraManager = createCameraManager();
|
||||||
|
}
|
||||||
|
|
||||||
return controller.calculateButtons(
|
return controller.calculateButtons(
|
||||||
options?.hass ?? createHASS(),
|
options?.hass ?? createHASS(),
|
||||||
options?.config ?? createConfig(),
|
options?.config ?? createConfig(),
|
||||||
options?.cameraManager ??
|
cameraManager,
|
||||||
createCameraManager({
|
|
||||||
configs: new Map([
|
|
||||||
['camera-1', createCameraConfig()],
|
|
||||||
['camera-2', createCameraConfig()],
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
options?.view ?? createView({ camera: 'camera-1' }),
|
options?.view ?? createView({ camera: 'camera-1' }),
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
@@ -79,8 +79,8 @@ describe('MenuButtonController', () => {
|
|||||||
priority: 50,
|
priority: 50,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: 'Frigate menu / Default view',
|
title: 'Frigate menu / Default view',
|
||||||
tap_action: createFrigateCardCustomAction('menu_toggle'),
|
tap_action: createFrigateCardSimpleAction('menu_toggle'),
|
||||||
hold_action: createFrigateCardCustomAction('diagnostics'),
|
hold_action: createFrigateCardSimpleAction('diagnostics'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,19 +94,17 @@ describe('MenuButtonController', () => {
|
|||||||
priority: 50,
|
priority: 50,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: 'Frigate menu / Default view',
|
title: 'Frigate menu / Default view',
|
||||||
tap_action: createFrigateCardCustomAction('default'),
|
tap_action: createFrigateCardSimpleAction('default'),
|
||||||
hold_action: createFrigateCardCustomAction('diagnostics'),
|
hold_action: createFrigateCardSimpleAction('diagnostics'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have cameras menu', () => {
|
it('should have cameras menu', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
['camera-1', createCameraConfig()],
|
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
|
||||||
['camera-2', createCameraConfig()],
|
);
|
||||||
]),
|
vi.mocked(cameraManager).getCameraMetadata.mockReturnValue({
|
||||||
});
|
|
||||||
mock<CameraManager>(cameraManager).getCameraMetadata.mockReturnValue({
|
|
||||||
title: 'title',
|
title: 'title',
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
});
|
});
|
||||||
@@ -150,14 +148,12 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not have a cameras menu without a visible camera', () => {
|
it('should not have a cameras menu without a visible camera', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
['camera-1', createCameraConfig()],
|
createStore([
|
||||||
['camera-2', createCameraConfig()],
|
{ cameraID: 'camera-1', config: createCameraConfig({ hide: true }) },
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
|
|
||||||
vi.mocked(cameraManager.getStore()).getVisibleCameras.mockReturnValue(new Map());
|
|
||||||
|
|
||||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||||
expect(buttons).not.toEqual(
|
expect(buttons).not.toEqual(
|
||||||
@@ -170,12 +166,16 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have substream button with single dependency', () => {
|
it('should have substream button with single dependency', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
['camera-1', createCameraConfig({ dependencies: { cameras: ['camera-2'] } })],
|
createStore([
|
||||||
['camera-2', createCameraConfig()],
|
{
|
||||||
|
cameraID: 'camera-1',
|
||||||
|
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
|
||||||
|
},
|
||||||
|
{ cameraID: 'camera-2' },
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
|
|
||||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||||
expect(buttons).toContainEqual({
|
expect(buttons).toContainEqual({
|
||||||
@@ -193,12 +193,17 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have substream button selected with single dependency', () => {
|
it('should have substream button selected with single dependency', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
['camera-1', createCameraConfig({ dependencies: { cameras: ['camera-2'] } })],
|
createStore([
|
||||||
['camera-2', createCameraConfig()],
|
{
|
||||||
|
cameraID: 'camera-1',
|
||||||
|
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
|
||||||
|
},
|
||||||
|
{ cameraID: 'camera-2' },
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
|
|
||||||
const view = createView({
|
const view = createView({
|
||||||
camera: 'camera-1',
|
camera: 'camera-1',
|
||||||
context: {
|
context: {
|
||||||
@@ -227,29 +232,31 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have substream menu without substream on with multiple dependencies', () => {
|
it('should have substream menu without substream on with multiple dependencies', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
[
|
createStore([
|
||||||
'camera-1',
|
{
|
||||||
createCameraConfig({
|
cameraID: 'camera-1',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.1',
|
camera_entity: 'camera.1',
|
||||||
dependencies: { cameras: ['camera-2', 'camera-3'] },
|
dependencies: { cameras: ['camera-2', 'camera-3'] },
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
[
|
{
|
||||||
'camera-2',
|
cameraID: 'camera-2',
|
||||||
createCameraConfig({
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.2',
|
camera_entity: 'camera.2',
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
[
|
{
|
||||||
'camera-3',
|
cameraID: 'camera-3',
|
||||||
createCameraConfig({
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.3',
|
camera_entity: 'camera.3',
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
|
|
||||||
// Return different metadata depending on the camera to test multiple code
|
// Return different metadata depending on the camera to test multiple code
|
||||||
// paths.
|
// paths.
|
||||||
mock<CameraManager>(cameraManager).getCameraMetadata.mockImplementation(
|
mock<CameraManager>(cameraManager).getCameraMetadata.mockImplementation(
|
||||||
@@ -316,29 +323,30 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have substream menu with substream on with multiple dependencies', () => {
|
it('should have substream menu with substream on with multiple dependencies', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
[
|
createStore([
|
||||||
'camera-1',
|
{
|
||||||
createCameraConfig({
|
cameraID: 'camera-1',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.1',
|
camera_entity: 'camera.1',
|
||||||
dependencies: { cameras: ['camera-2', 'camera-3'] },
|
dependencies: { cameras: ['camera-2', 'camera-3'] },
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
[
|
{
|
||||||
'camera-2',
|
cameraID: 'camera-2',
|
||||||
createCameraConfig({
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.2',
|
camera_entity: 'camera.2',
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
[
|
{
|
||||||
'camera-3',
|
cameraID: 'camera-3',
|
||||||
createCameraConfig({
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.3',
|
camera_entity: 'camera.3',
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
|
|
||||||
const view = createView({
|
const view = createView({
|
||||||
camera: 'camera-1',
|
camera: 'camera-1',
|
||||||
@@ -436,8 +444,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have styled clips menu button in clips view', () => {
|
it('should have styled clips menu button in clips view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsClips: true }),
|
createAggregateCameraCapabilities({ supportsClips: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
cameraManager: cameraManager,
|
cameraManager: cameraManager,
|
||||||
@@ -457,8 +465,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have unstyled clips menu button in non-clips view', () => {
|
it('should have unstyled clips menu button in non-clips view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsClips: true }),
|
createAggregateCameraCapabilities({ supportsClips: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||||
expect(buttons).toContainEqual({
|
expect(buttons).toContainEqual({
|
||||||
@@ -475,8 +483,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have styled snapshots menu button in snapshots view', () => {
|
it('should have styled snapshots menu button in snapshots view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsSnapshots: true }),
|
createAggregateCameraCapabilities({ supportsSnapshots: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
cameraManager: cameraManager,
|
cameraManager: cameraManager,
|
||||||
@@ -496,8 +504,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have unstyled snapshots menu button in non-snapshots view', () => {
|
it('should have unstyled snapshots menu button in non-snapshots view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsSnapshots: true }),
|
createAggregateCameraCapabilities({ supportsSnapshots: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||||
expect(buttons).toContainEqual({
|
expect(buttons).toContainEqual({
|
||||||
@@ -514,8 +522,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have styled recordings menu button in recordings view', () => {
|
it('should have styled recordings menu button in recordings view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsRecordings: true }),
|
createAggregateCameraCapabilities({ supportsRecordings: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
cameraManager: cameraManager,
|
cameraManager: cameraManager,
|
||||||
@@ -535,8 +543,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have unstyled recordings menu button in non-recordings view', () => {
|
it('should have unstyled recordings menu button in non-recordings view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsRecordings: true }),
|
createAggregateCameraCapabilities({ supportsRecordings: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||||
expect(buttons).toContainEqual({
|
expect(buttons).toContainEqual({
|
||||||
@@ -581,8 +589,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have styled timeline menu button in timeline view', () => {
|
it('should have styled timeline menu button in timeline view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsTimeline: true }),
|
createAggregateCameraCapabilities({ supportsTimeline: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
cameraManager: cameraManager,
|
cameraManager: cameraManager,
|
||||||
@@ -601,8 +609,8 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have unstyled timeline menu button in non-timeline view', () => {
|
it('should have unstyled timeline menu button in non-timeline view', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||||
createCameraCapabilities({ supportsTimeline: true }),
|
createAggregateCameraCapabilities({ supportsTimeline: true }),
|
||||||
);
|
);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
cameraManager: cameraManager,
|
cameraManager: cameraManager,
|
||||||
@@ -622,15 +630,15 @@ describe('MenuButtonController', () => {
|
|||||||
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
||||||
|
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
|
||||||
|
createMediaCapabilities({ canDownload: true }),
|
||||||
|
);
|
||||||
const view = createView({
|
const view = createView({
|
||||||
queryResults: new MediaQueriesResults({
|
queryResults: new MediaQueriesResults({
|
||||||
results: [new ViewMedia('clip', 'camera-1')],
|
results: [new ViewMedia('clip', 'camera-1')],
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
|
|
||||||
createMediaCapabilities({ canDownload: true }),
|
|
||||||
);
|
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
cameraManager: cameraManager,
|
cameraManager: cameraManager,
|
||||||
view: view,
|
view: view,
|
||||||
@@ -653,15 +661,15 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
|
||||||
|
createMediaCapabilities({ canDownload: true }),
|
||||||
|
);
|
||||||
const view = createView({
|
const view = createView({
|
||||||
queryResults: new MediaQueriesResults({
|
queryResults: new MediaQueriesResults({
|
||||||
results: [new ViewMedia('clip', 'camera-1')],
|
results: [new ViewMedia('clip', 'camera-1')],
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
|
|
||||||
createMediaCapabilities({ canDownload: true }),
|
|
||||||
);
|
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
cameraManager: cameraManager,
|
cameraManager: cameraManager,
|
||||||
view: view,
|
view: view,
|
||||||
@@ -906,16 +914,18 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have media players button', () => {
|
it('should have media players button', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
[
|
createStore([
|
||||||
'camera-1',
|
{
|
||||||
createCameraConfig({
|
cameraID: 'camera-1',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.1',
|
camera_entity: 'camera.1',
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
|
|
||||||
const mediaPlayerController = mock<MediaPlayerManager>();
|
const mediaPlayerController = mock<MediaPlayerManager>();
|
||||||
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
|
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
|
||||||
mediaPlayerController.getMediaPlayers.mockReturnValue(['media_player.tv']);
|
mediaPlayerController.getMediaPlayers.mockReturnValue(['media_player.tv']);
|
||||||
@@ -961,16 +971,17 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should disable media players button when entity not found', () => {
|
it('should disable media players button when entity not found', () => {
|
||||||
const cameraManager = createCameraManager({
|
const cameraManager = createCameraManager();
|
||||||
configs: new Map([
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
[
|
createStore([
|
||||||
'camera-1',
|
{
|
||||||
createCameraConfig({
|
cameraID: 'camera-1',
|
||||||
|
config: createCameraConfig({
|
||||||
camera_entity: 'camera.1',
|
camera_entity: 'camera.1',
|
||||||
}),
|
}),
|
||||||
],
|
},
|
||||||
]),
|
]),
|
||||||
});
|
);
|
||||||
const mediaPlayerController = mock<MediaPlayerManager>();
|
const mediaPlayerController = mock<MediaPlayerManager>();
|
||||||
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
|
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
|
||||||
mediaPlayerController.getMediaPlayers.mockReturnValue(['not_a_real_player']);
|
mediaPlayerController.getMediaPlayers.mockReturnValue(['not_a_real_player']);
|
||||||
@@ -1109,7 +1120,13 @@ describe('MenuButtonController', () => {
|
|||||||
'%s',
|
'%s',
|
||||||
(displayMode: ViewDisplayMode) => {
|
(displayMode: ViewDisplayMode) => {
|
||||||
const view = createView({ view: 'live', displayMode: displayMode });
|
const view = createView({ view: 'live', displayMode: displayMode });
|
||||||
expect(calculateButtons(controller, { view: view })).toContainEqual({
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
calculateButtons(controller, { cameraManager: cameraManager, view: view }),
|
||||||
|
).toContainEqual({
|
||||||
icon: displayMode === 'single' ? 'mdi:grid' : 'mdi:grid-off',
|
icon: displayMode === 'single' ? 'mdi:grid' : 'mdi:grid-off',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
priority: 50,
|
priority: 50,
|
||||||
@@ -1118,6 +1135,7 @@ describe('MenuButtonController', () => {
|
|||||||
displayMode === 'grid'
|
displayMode === 'grid'
|
||||||
? 'Show single media viewer'
|
? 'Show single media viewer'
|
||||||
: 'Show media viewer for each camera in a grid',
|
: 'Show media viewer for each camera in a grid',
|
||||||
|
style: displayMode === 'grid' ? { color: 'var(--primary-color, white)' } : {},
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
frigate_card_action: 'display_mode_select',
|
frigate_card_action: 'display_mode_select',
|
||||||
@@ -1128,6 +1146,85 @@ describe('MenuButtonController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should have show ptz button', () => {
|
||||||
|
it('when the selected camera is not PTZ enabled', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
|
||||||
|
createCameraCapabilities(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||||
|
expect(buttons).not.toContainEqual({
|
||||||
|
enabled: false,
|
||||||
|
icon: 'mdi:pan',
|
||||||
|
priority: 50,
|
||||||
|
style: {
|
||||||
|
color: 'var(--primary-color, white)',
|
||||||
|
},
|
||||||
|
tap_action: {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'show_ptz',
|
||||||
|
show_ptz: false,
|
||||||
|
},
|
||||||
|
title: 'Show PTZ controls',
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when the selected camera is PTZ enabled', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
|
||||||
|
createCameraCapabilities({ ptz: {} }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||||
|
expect(buttons).toContainEqual({
|
||||||
|
enabled: false,
|
||||||
|
icon: 'mdi:pan',
|
||||||
|
priority: 50,
|
||||||
|
style: {
|
||||||
|
color: 'var(--primary-color, white)',
|
||||||
|
},
|
||||||
|
tap_action: {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'show_ptz',
|
||||||
|
show_ptz: false,
|
||||||
|
},
|
||||||
|
title: 'Show PTZ controls',
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when the context has PTZ visiblity turned off', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
|
||||||
|
createCameraCapabilities({ ptz: {} }),
|
||||||
|
);
|
||||||
|
const view = createView({
|
||||||
|
camera: 'camera-1',
|
||||||
|
context: { live: { ptzVisible: false } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = calculateButtons(controller, {
|
||||||
|
cameraManager: cameraManager,
|
||||||
|
view: view,
|
||||||
|
});
|
||||||
|
expect(buttons).toContainEqual({
|
||||||
|
enabled: false,
|
||||||
|
icon: 'mdi:pan',
|
||||||
|
priority: 50,
|
||||||
|
style: {},
|
||||||
|
tap_action: {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'show_ptz',
|
||||||
|
show_ptz: true,
|
||||||
|
},
|
||||||
|
title: 'Show PTZ controls',
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle dynamic buttons', () => {
|
it('should handle dynamic buttons', () => {
|
||||||
const button: MenuItem = {
|
const button: MenuItem = {
|
||||||
...dynamicButton,
|
...dynamicButton,
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { PTZController } from '../../src/components-lib/ptz-controller';
|
||||||
|
import {
|
||||||
|
FrigateCardPTZConfig,
|
||||||
|
frigateCardPTZSchema,
|
||||||
|
PTZControlAction,
|
||||||
|
} from '../../src/config/types';
|
||||||
|
import {
|
||||||
|
frigateCardHandleActionConfig,
|
||||||
|
getActionConfigGivenAction,
|
||||||
|
} from '../../src/utils/action.js';
|
||||||
|
import {
|
||||||
|
createCameraCapabilities,
|
||||||
|
createCameraManager,
|
||||||
|
createHASS,
|
||||||
|
} from '../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../src/utils/action.js');
|
||||||
|
|
||||||
|
const createConfig = (config?: Partial<FrigateCardPTZConfig>): FrigateCardPTZConfig => {
|
||||||
|
return frigateCardPTZSchema.parse({
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('PTZController', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be creatable', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
expect(controller).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get config creatable', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
const config = createConfig();
|
||||||
|
controller.setConfig(config);
|
||||||
|
expect(controller.getConfig()).toBe(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set element attributes', () => {
|
||||||
|
describe('orientation', () => {
|
||||||
|
describe('with config', () => {
|
||||||
|
it.each([['horizontal' as const], ['vertical' as const]])(
|
||||||
|
'%s',
|
||||||
|
(orientation: 'horizontal' | 'vertical') => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const controller = new PTZController(element);
|
||||||
|
controller.setConfig(createConfig({ orientation: orientation }));
|
||||||
|
expect(element.getAttribute('data-orientation')).toBe(orientation);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('without config', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const controller = new PTZController(element);
|
||||||
|
controller.setConfig();
|
||||||
|
expect(element.getAttribute('data-orientation')).toBe('horizontal');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('position', () => {
|
||||||
|
describe('with config', () => {
|
||||||
|
it.each([
|
||||||
|
['top-left' as const],
|
||||||
|
['top-right' as const],
|
||||||
|
['bottom-left' as const],
|
||||||
|
['bottom-right' as const],
|
||||||
|
])(
|
||||||
|
'%s',
|
||||||
|
(position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const controller = new PTZController(element);
|
||||||
|
controller.setConfig(createConfig({ position: position }));
|
||||||
|
expect(element.getAttribute('data-position')).toBe(position);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('without config', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const controller = new PTZController(element);
|
||||||
|
controller.setConfig();
|
||||||
|
expect(element.getAttribute('data-position')).toBe('bottom-right');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('with style in config', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const controller = new PTZController(element);
|
||||||
|
controller.setConfig(createConfig({ style: { transform: 'none', left: '50%' } }));
|
||||||
|
expect(element.getAttribute('style')).toBe('transform:none;left:50%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should respect mode', () => {
|
||||||
|
it('off', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
expect(controller.shouldDisplay()).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('off but forced on', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setForceVisibility(true);
|
||||||
|
// No actions, no rendering, no matter what.
|
||||||
|
expect(controller.shouldDisplay()).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('on without actions', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(createConfig({ mode: 'on' }));
|
||||||
|
expect(controller.shouldDisplay()).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('on with actions', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
|
||||||
|
controller.setCamera(createCameraManager(), 'camera.office');
|
||||||
|
expect(controller.shouldDisplay()).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('on with actions but forced off', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
|
||||||
|
controller.setCamera(createCameraManager(), 'camera.office');
|
||||||
|
controller.setForceVisibility(false);
|
||||||
|
expect(controller.shouldDisplay()).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get PTZ actions', () => {
|
||||||
|
it('without config', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
expect(controller.getPTZActions('left')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('using defaults', () => {
|
||||||
|
describe('with continous PTZ', () => {
|
||||||
|
it.each([
|
||||||
|
['left' as const],
|
||||||
|
['right' as const],
|
||||||
|
['up' as const],
|
||||||
|
['down' as const],
|
||||||
|
['zoom_in' as const],
|
||||||
|
['zoom_out' as const],
|
||||||
|
])('%s', (actionName: PTZControlAction) => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(createConfig());
|
||||||
|
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||||
|
createCameraCapabilities({
|
||||||
|
ptz: {
|
||||||
|
panTilt: ['continuous'],
|
||||||
|
zoom: ['continuous'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
controller.setCamera(cameraManager, 'camera.office');
|
||||||
|
|
||||||
|
expect(controller.getPTZActions(actionName)).toEqual({
|
||||||
|
start_tap_action: {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'ptz',
|
||||||
|
ptz_action: actionName,
|
||||||
|
ptz_phase: 'start',
|
||||||
|
},
|
||||||
|
end_tap_action: {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'ptz',
|
||||||
|
ptz_action: actionName,
|
||||||
|
ptz_phase: 'stop',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with relative PTZ', () => {
|
||||||
|
it.each([
|
||||||
|
['left' as const],
|
||||||
|
['right' as const],
|
||||||
|
['up' as const],
|
||||||
|
['down' as const],
|
||||||
|
['zoom_in' as const],
|
||||||
|
['zoom_out' as const],
|
||||||
|
])('%s', (actionName: PTZControlAction) => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(createConfig());
|
||||||
|
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||||
|
createCameraCapabilities({
|
||||||
|
ptz: {
|
||||||
|
panTilt: ['relative'],
|
||||||
|
zoom: ['relative'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
controller.setCamera(cameraManager, 'camera.office');
|
||||||
|
|
||||||
|
expect(controller.getPTZActions(actionName)).toEqual({
|
||||||
|
tap_action: {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'ptz',
|
||||||
|
ptz_action: actionName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('home', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(createConfig());
|
||||||
|
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||||
|
createCameraCapabilities({
|
||||||
|
ptz: {
|
||||||
|
presets: ['preset-foo'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
controller.setCamera(cameraManager, 'camera.office');
|
||||||
|
|
||||||
|
expect(controller.getPTZActions('home')).toEqual({
|
||||||
|
tap_action: {
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'ptz',
|
||||||
|
ptz_action: 'preset',
|
||||||
|
ptz_preset: 'preset-foo',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('using config', () => {
|
||||||
|
it.each([
|
||||||
|
['left' as const],
|
||||||
|
['right' as const],
|
||||||
|
['up' as const],
|
||||||
|
['down' as const],
|
||||||
|
['zoom_in' as const],
|
||||||
|
['zoom_out' as const],
|
||||||
|
])('%s', (actionName: PTZControlAction) => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(
|
||||||
|
createConfig({
|
||||||
|
[`actions_${actionName}`]: {
|
||||||
|
argument: actionName,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
controller.setCamera(createCameraManager(), 'camera.office');
|
||||||
|
|
||||||
|
expect(controller.getPTZActions(actionName)).toEqual({
|
||||||
|
argument: actionName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('home', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setConfig(
|
||||||
|
createConfig({
|
||||||
|
actions_home: {
|
||||||
|
argument: 'home',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
controller.setCamera(createCameraManager(), 'camera.office');
|
||||||
|
|
||||||
|
expect(controller.getPTZActions('home')).toEqual({
|
||||||
|
argument: 'home',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle action', () => {
|
||||||
|
it('without hass or action', () => {
|
||||||
|
const controller = new PTZController(document.createElement('div'));
|
||||||
|
controller.setHASS();
|
||||||
|
controller.setCamera();
|
||||||
|
controller.handleAction(
|
||||||
|
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
|
||||||
|
);
|
||||||
|
expect(frigateCardHandleActionConfig).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with action', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const controller = new PTZController(element);
|
||||||
|
const hass = createHASS();
|
||||||
|
controller.setHASS(hass);
|
||||||
|
controller.setConfig(
|
||||||
|
createConfig({
|
||||||
|
[`actions_left`]: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const tapAction = {
|
||||||
|
action: 'none' as const,
|
||||||
|
};
|
||||||
|
const actionsConfig = {
|
||||||
|
tap_action: tapAction,
|
||||||
|
};
|
||||||
|
vi.mocked(getActionConfigGivenAction).mockReturnValue(tapAction);
|
||||||
|
|
||||||
|
controller.handleAction(
|
||||||
|
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
|
||||||
|
actionsConfig,
|
||||||
|
);
|
||||||
|
expect(frigateCardHandleActionConfig).toBeCalledWith(
|
||||||
|
element,
|
||||||
|
hass,
|
||||||
|
actionsConfig,
|
||||||
|
'tap',
|
||||||
|
actionsConfig.tap_action,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -739,4 +739,433 @@ describe('should handle version specific upgrades', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should move PTZ elements to live', () => {
|
||||||
|
it('case with 1 element', () => {
|
||||||
|
const config = {
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-ptz',
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
service_data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(upgradeConfig(config)).toBeTruthy();
|
||||||
|
expect(config).toEqual({
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
ptz: {
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
color: 'white',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('case with >1 element', () => {
|
||||||
|
const config = {
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-ptz',
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
service_data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'service-button',
|
||||||
|
title: 'title',
|
||||||
|
service: 'service',
|
||||||
|
service_data: {
|
||||||
|
message: "It's a trick",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(upgradeConfig(config)).toBeTruthy();
|
||||||
|
expect(config).toEqual({
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
service: 'service',
|
||||||
|
service_data: {
|
||||||
|
message: "It's a trick",
|
||||||
|
},
|
||||||
|
title: 'title',
|
||||||
|
type: 'service-button',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
ptz: {
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
color: 'white',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('case with custom conditional element with 2 PTZ but nothing else', () => {
|
||||||
|
const config = {
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-conditional',
|
||||||
|
conditions: {
|
||||||
|
fullscreen: true,
|
||||||
|
media_loaded: true,
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-ptz',
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
service_data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-ptz',
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
service_data: {
|
||||||
|
message: 'Hello 2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(upgradeConfig(config)).toBeTruthy();
|
||||||
|
expect(config).toEqual({
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
ptz: {
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
color: 'white',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('case with custom conditional element with 1 PTZ and another element', () => {
|
||||||
|
const config = {
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-conditional',
|
||||||
|
conditions: {
|
||||||
|
fullscreen: true,
|
||||||
|
media_loaded: true,
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'service-button',
|
||||||
|
title: 'title',
|
||||||
|
service: 'service',
|
||||||
|
service_data: {
|
||||||
|
message: "It's a trick",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-ptz',
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
service_data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(upgradeConfig(config)).toBeTruthy();
|
||||||
|
expect(config).toEqual({
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
ptz: {
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
color: 'white',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-conditional',
|
||||||
|
conditions: {
|
||||||
|
fullscreen: true,
|
||||||
|
media_loaded: true,
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'service-button',
|
||||||
|
title: 'title',
|
||||||
|
service: 'service',
|
||||||
|
service_data: {
|
||||||
|
message: "It's a trick",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('case with stock conditional element with 1 PTZ', () => {
|
||||||
|
const config = {
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'conditional',
|
||||||
|
conditions: [{ entity: 'light.office', state: 'on' }],
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-ptz',
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
service_data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(upgradeConfig(config)).toBeTruthy();
|
||||||
|
expect(config).toEqual({
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
ptz: {
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
data: {
|
||||||
|
message: 'Hello 1',
|
||||||
|
},
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
color: 'white',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('case when live.controls.ptz already exists', () => {
|
||||||
|
const config = {
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
ptz: {
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
data: {
|
||||||
|
message: 'Original',
|
||||||
|
},
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
color: 'white',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'custom:frigate-card-ptz',
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
service_data: {
|
||||||
|
message: 'Replacement that should be ignored',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(upgradeConfig(config)).toBeTruthy();
|
||||||
|
expect(config).toEqual({
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
ptz: {
|
||||||
|
actions_up: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
data: {
|
||||||
|
message: 'Original',
|
||||||
|
},
|
||||||
|
service: 'notify.persistent_notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orientation: 'vertical',
|
||||||
|
style: {
|
||||||
|
color: 'white',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ describe('config defaults', () => {
|
|||||||
size: 48,
|
size: 48,
|
||||||
style: 'chevrons',
|
style: 'chevrons',
|
||||||
},
|
},
|
||||||
|
ptz: {
|
||||||
|
hide_home: false,
|
||||||
|
hide_pan_tilt: false,
|
||||||
|
hide_zoom: false,
|
||||||
|
mode: 'on',
|
||||||
|
orientation: 'horizontal',
|
||||||
|
position: 'bottom-right',
|
||||||
|
},
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
media: 'all',
|
media: 'all',
|
||||||
mode: 'right',
|
mode: 'right',
|
||||||
@@ -208,6 +216,10 @@ describe('config defaults', () => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
priority: 50,
|
priority: 50,
|
||||||
},
|
},
|
||||||
|
ptz: {
|
||||||
|
enabled: false,
|
||||||
|
priority: 50,
|
||||||
|
},
|
||||||
recordings: {
|
recordings: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
priority: 50,
|
priority: 50,
|
||||||
@@ -343,7 +355,13 @@ describe('should convert webrtc card PTZ to Frigate card PTZ', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
service: 'foo',
|
service: 'foo',
|
||||||
type: 'custom:frigate-card-ptz',
|
|
||||||
|
hide_home: false,
|
||||||
|
hide_pan_tilt: false,
|
||||||
|
hide_zoom: false,
|
||||||
|
mode: 'on',
|
||||||
|
orientation: 'horizontal',
|
||||||
|
position: 'bottom-right',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+55
-57
@@ -1,27 +1,17 @@
|
|||||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { Camera } from '../src/camera-manager/camera';
|
||||||
|
import { CameraManagerEngine } from '../src/camera-manager/engine';
|
||||||
import { FrigateEvent, FrigateRecording } from '../src/camera-manager/frigate/types';
|
import { FrigateEvent, FrigateRecording } from '../src/camera-manager/frigate/types';
|
||||||
|
import { GenericCameraManagerEngine } from '../src/camera-manager/generic/engine-generic';
|
||||||
import { CameraManager } from '../src/camera-manager/manager';
|
import { CameraManager } from '../src/camera-manager/manager';
|
||||||
import { CameraManagerStore } from '../src/camera-manager/store';
|
import { CameraManagerStore } from '../src/camera-manager/store';
|
||||||
import {
|
import {
|
||||||
CameraConfigs,
|
|
||||||
CameraManagerCameraCapabilities,
|
CameraManagerCameraCapabilities,
|
||||||
|
CameraManagerCapabilities,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
QueryType,
|
|
||||||
} from '../src/camera-manager/types';
|
} from '../src/camera-manager/types';
|
||||||
import {
|
|
||||||
CameraConfig,
|
|
||||||
FrigateCardCondition,
|
|
||||||
FrigateCardConfig,
|
|
||||||
PerformanceConfig,
|
|
||||||
RawFrigateCardConfig,
|
|
||||||
cameraConfigSchema,
|
|
||||||
frigateCardConditionSchema,
|
|
||||||
frigateCardConfigSchema,
|
|
||||||
performanceConfigSchema,
|
|
||||||
} from '../src/config/types';
|
|
||||||
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
|
||||||
import { ActionsManager } from '../src/card-controller/actions-manager';
|
import { ActionsManager } from '../src/card-controller/actions-manager';
|
||||||
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
|
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
|
||||||
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
||||||
@@ -44,6 +34,18 @@ import { QueryStringManager } from '../src/card-controller/query-string-manager'
|
|||||||
import { StyleManager } from '../src/card-controller/style-manager';
|
import { StyleManager } from '../src/card-controller/style-manager';
|
||||||
import { TriggersManager } from '../src/card-controller/triggers-manager';
|
import { TriggersManager } from '../src/card-controller/triggers-manager';
|
||||||
import { ViewManager } from '../src/card-controller/view-manager';
|
import { ViewManager } from '../src/card-controller/view-manager';
|
||||||
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
cameraConfigSchema,
|
||||||
|
FrigateCardCondition,
|
||||||
|
frigateCardConditionSchema,
|
||||||
|
FrigateCardConfig,
|
||||||
|
frigateCardConfigSchema,
|
||||||
|
PerformanceConfig,
|
||||||
|
performanceConfigSchema,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
} from '../src/config/types';
|
||||||
|
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
||||||
import { Entity } from '../src/utils/ha/entity-registry/types';
|
import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||||
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
||||||
@@ -150,55 +152,51 @@ export const createViewWithMedia = (options?: Partial<ViewParameters>): View =>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createCameraManager = (options?: {
|
export const createStore = (
|
||||||
store?: CameraManagerStore;
|
cameras?: {
|
||||||
configs?: CameraConfigs;
|
cameraID: string;
|
||||||
}): CameraManager => {
|
engine?: CameraManagerEngine;
|
||||||
const cameraManager = new CameraManager(createCardAPI());
|
config?: CameraConfig;
|
||||||
let store: CameraManagerStore | undefined = options?.store;
|
capabilities?: CameraManagerCameraCapabilities;
|
||||||
if (!store) {
|
}[],
|
||||||
store = mock<CameraManagerStore>();
|
): CameraManagerStore => {
|
||||||
const configs = options?.configs ?? new Map([['camera', createCameraConfig()]]);
|
const store = new CameraManagerStore();
|
||||||
vi.mocked(store.getCameras).mockReturnValue(configs);
|
for (const cameraProps of cameras ?? []) {
|
||||||
vi.mocked(store.getVisibleCameras).mockReturnValue(configs);
|
const camera = new Camera(
|
||||||
vi.mocked(store.getVisibleCameraIDs).mockReturnValue(new Set(configs.keys()));
|
cameraProps.config ?? createCameraConfig(),
|
||||||
vi.mocked(store.hasVisibleCameraID).mockImplementation((cameraID: string) =>
|
cameraProps.engine ?? new GenericCameraManagerEngine(),
|
||||||
[...configs.keys()].includes(cameraID),
|
cameraProps.capabilities ?? createCameraCapabilities(),
|
||||||
);
|
|
||||||
vi.mocked(store.getCameraConfig).mockImplementation(
|
|
||||||
(cameraID): CameraConfig | null => {
|
|
||||||
return configs.get(cameraID) ?? null;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
camera.setID(cameraProps.cameraID);
|
||||||
|
store.addCamera(camera);
|
||||||
}
|
}
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(store);
|
return store;
|
||||||
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
|
};
|
||||||
{
|
|
||||||
cameraIDs: new Set(['camera']),
|
|
||||||
type: QueryType.Event,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue([
|
|
||||||
{
|
|
||||||
cameraIDs: new Set(['camera']),
|
|
||||||
type: QueryType.Recording,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue({
|
|
||||||
canFavoriteEvents: true,
|
|
||||||
canFavoriteRecordings: true,
|
|
||||||
canSeek: true,
|
|
||||||
supportsClips: true,
|
|
||||||
supportsRecordings: true,
|
|
||||||
supportsSnapshots: true,
|
|
||||||
supportsTimeline: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
export const createCameraManager = (): CameraManager => {
|
||||||
|
const cameraManager = mock<CameraManager>();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(createStore());
|
||||||
return cameraManager;
|
return cameraManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createAggregateCameraCapabilities = (
|
||||||
|
capabilities?: Partial<CameraManagerCapabilities>,
|
||||||
|
): CameraManagerCapabilities => {
|
||||||
|
return {
|
||||||
|
canFavoriteEvents: false,
|
||||||
|
canFavoriteRecordings: false,
|
||||||
|
canSeek: false,
|
||||||
|
supportsClips: false,
|
||||||
|
supportsRecordings: false,
|
||||||
|
supportsSnapshots: false,
|
||||||
|
supportsTimeline: false,
|
||||||
|
supportsPTZ: false,
|
||||||
|
...capabilities,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const createCameraCapabilities = (
|
export const createCameraCapabilities = (
|
||||||
options?: Partial<CameraManagerCameraCapabilities>,
|
capabilities?: Partial<CameraManagerCameraCapabilities>,
|
||||||
): CameraManagerCameraCapabilities => {
|
): CameraManagerCameraCapabilities => {
|
||||||
return {
|
return {
|
||||||
canFavoriteEvents: false,
|
canFavoriteEvents: false,
|
||||||
@@ -208,7 +206,7 @@ export const createCameraCapabilities = (
|
|||||||
supportsRecordings: false,
|
supportsRecordings: false,
|
||||||
supportsSnapshots: false,
|
supportsSnapshots: false,
|
||||||
supportsTimeline: false,
|
supportsTimeline: false,
|
||||||
...options,
|
...capabilities,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+51
-48
@@ -4,7 +4,11 @@ import { mock } from 'vitest-mock-extended';
|
|||||||
import { actionSchema } from '../../src/config/types';
|
import { actionSchema } from '../../src/config/types';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
createFrigateCardCustomAction,
|
createFrigateCardCameraAction,
|
||||||
|
createFrigateCardDisplayModeAction,
|
||||||
|
createFrigateCardMediaPlayerAction,
|
||||||
|
createFrigateCardShowPTZAction,
|
||||||
|
createFrigateCardSimpleAction,
|
||||||
frigateCardHandleAction,
|
frigateCardHandleAction,
|
||||||
frigateCardHandleActionConfig,
|
frigateCardHandleActionConfig,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
@@ -37,13 +41,24 @@ describe('convertActionToFrigateCardCustomAction', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createFrigateCardCustomAction', () => {
|
describe('createFrigateCardSimpleAction', () => {
|
||||||
it('should create camera_select', () => {
|
it('should create general action', () => {
|
||||||
expect(
|
expect(
|
||||||
createFrigateCardCustomAction('camera_select', {
|
createFrigateCardSimpleAction('clips', {
|
||||||
camera: 'camera',
|
|
||||||
cardID: 'card_id',
|
cardID: 'card_id',
|
||||||
}),
|
}),
|
||||||
|
).toEqual({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'clips',
|
||||||
|
card_id: 'card_id',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createFrigateCardCameraAction', () => {
|
||||||
|
it('should create camera_select', () => {
|
||||||
|
expect(
|
||||||
|
createFrigateCardCameraAction('camera_select', 'camera', { cardID: 'card_id' }),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
camera: 'camera',
|
camera: 'camera',
|
||||||
@@ -51,16 +66,12 @@ describe('createFrigateCardCustomAction', () => {
|
|||||||
card_id: 'card_id',
|
card_id: 'card_id',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create camera_select without camera', () => {
|
|
||||||
expect(createFrigateCardCustomAction('camera_select')).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createFrigateCardMediaPlayerAction', () => {
|
||||||
it('should create media_player', () => {
|
it('should create media_player', () => {
|
||||||
expect(
|
expect(
|
||||||
createFrigateCardCustomAction('media_player', {
|
createFrigateCardMediaPlayerAction('device', 'play', {
|
||||||
media_player: 'device',
|
|
||||||
media_player_action: 'play',
|
|
||||||
cardID: 'card_id',
|
cardID: 'card_id',
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
@@ -71,37 +82,12 @@ describe('createFrigateCardCustomAction', () => {
|
|||||||
card_id: 'card_id',
|
card_id: 'card_id',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create media_player without player or action', () => {
|
|
||||||
expect(
|
|
||||||
createFrigateCardCustomAction('media_player', {
|
|
||||||
media_player_action: 'play',
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
createFrigateCardCustomAction('media_player', {
|
|
||||||
media_player: 'device',
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create general action', () => {
|
|
||||||
expect(
|
|
||||||
createFrigateCardCustomAction('clips', {
|
|
||||||
cardID: 'card_id',
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
action: 'fire-dom-event',
|
|
||||||
frigate_card_action: 'clips',
|
|
||||||
card_id: 'card_id',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createFrigateCardDisplayModeAction', () => {
|
||||||
it('should create display mode action', () => {
|
it('should create display mode action', () => {
|
||||||
expect(
|
expect(
|
||||||
createFrigateCardCustomAction('display_mode_select', {
|
createFrigateCardDisplayModeAction('grid', {
|
||||||
display_mode: 'grid',
|
|
||||||
cardID: 'card_id',
|
cardID: 'card_id',
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
@@ -111,9 +97,20 @@ describe('createFrigateCardCustomAction', () => {
|
|||||||
card_id: 'card_id',
|
card_id: 'card_id',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not create display mode action without display mode', () => {
|
describe('createFrigateCardShowPTZAction', () => {
|
||||||
expect(createFrigateCardCustomAction('display_mode_select')).toBeNull();
|
it('should create show PTZ action', () => {
|
||||||
|
expect(
|
||||||
|
createFrigateCardShowPTZAction(true, {
|
||||||
|
cardID: 'card_id',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
frigate_card_action: 'show_ptz',
|
||||||
|
show_ptz: true,
|
||||||
|
card_id: 'card_id',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,13 +121,13 @@ describe('getActionConfigGivenAction', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not handle undefined arguments', () => {
|
it('should not handle undefined arguments', () => {
|
||||||
expect(getActionConfigGivenAction()).toBeUndefined();
|
expect(getActionConfigGivenAction()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not handle unknown interactions', () => {
|
it('should not handle unknown interactions', () => {
|
||||||
expect(
|
expect(
|
||||||
getActionConfigGivenAction('triple_poke', { triple_poke_action: action }),
|
getActionConfigGivenAction('triple_poke', { triple_poke_action: action }),
|
||||||
).toBeUndefined();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle tap actions', () => {
|
it('should handle tap actions', () => {
|
||||||
@@ -178,17 +175,23 @@ describe('frigateCardHandleActionConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle simple case', () => {
|
it('should handle simple case', () => {
|
||||||
frigateCardHandleActionConfig(element, createHASS(), {}, 'tap', action);
|
const hass = createHASS();
|
||||||
|
frigateCardHandleActionConfig(element, hass, {}, 'tap', action);
|
||||||
expect(handleActionConfig).toBeCalled();
|
expect(handleActionConfig).toBeCalled();
|
||||||
|
expect(handleActionConfig).toBeCalledWith(element, hass, {}, action);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle array case', () => {
|
it('should handle array case', () => {
|
||||||
frigateCardHandleActionConfig(element, createHASS(), {}, 'tap', [
|
const hass = createHASS();
|
||||||
action,
|
frigateCardHandleActionConfig(element, hass, {}, 'tap', [action, action, action]);
|
||||||
action,
|
|
||||||
action,
|
|
||||||
]);
|
|
||||||
expect(handleActionConfig).toBeCalledTimes(3);
|
expect(handleActionConfig).toBeCalledTimes(3);
|
||||||
|
expect(handleActionConfig).toBeCalledWith(element, hass, {}, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null case', () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
frigateCardHandleActionConfig(element, hass, {}, 'tap', null);
|
||||||
|
expect(handleActionConfig).toBeCalledWith(element, hass, {}, undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { getCameraID } from '../../src/utils/camera.js';
|
||||||
import { CameraManager } from '../../src/camera-manager/manager.js';
|
import { createCameraConfig } from '../test-utils.js';
|
||||||
import { CameraConfigs } from '../../src/camera-manager/types.js';
|
|
||||||
import { getAllDependentCameras, getCameraID } from '../../src/utils/camera.js';
|
|
||||||
import { createCameraConfig, createCameraManager } from '../test-utils.js';
|
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
describe('getCameraID', () => {
|
describe('getCameraID', () => {
|
||||||
it('should get camera id with id', () => {
|
it('should get camera id with id', () => {
|
||||||
@@ -25,7 +20,9 @@ describe('getCameraID', () => {
|
|||||||
expect(getCameraID(config)).toBe('foo');
|
expect(getCameraID(config)).toBe('foo');
|
||||||
});
|
});
|
||||||
it('should get camera id with go2rtc url and stream', () => {
|
it('should get camera id with go2rtc url and stream', () => {
|
||||||
const config = createCameraConfig({ go2rtc: { url: 'https://foo', stream: 'office' } });
|
const config = createCameraConfig({
|
||||||
|
go2rtc: { url: 'https://foo', stream: 'office' },
|
||||||
|
});
|
||||||
expect(getCameraID(config)).toBe('https://foo#office');
|
expect(getCameraID(config)).toBe('https://foo#office');
|
||||||
});
|
});
|
||||||
it('should get camera id with frigate camera_name', () => {
|
it('should get camera id with frigate camera_name', () => {
|
||||||
@@ -39,48 +36,3 @@ describe('getCameraID', () => {
|
|||||||
expect(getCameraID(config)).toBe('');
|
expect(getCameraID(config)).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAllDependentCameras', () => {
|
|
||||||
it('should return null without cameraManager', () => {
|
|
||||||
expect(getAllDependentCameras()).toBeNull();
|
|
||||||
});
|
|
||||||
it('should return null without cameraID', () => {
|
|
||||||
expect(getAllDependentCameras(mock<CameraManager>())).toBeNull();
|
|
||||||
});
|
|
||||||
it('should return dependent cameras', () => {
|
|
||||||
const cameraConfigs: CameraConfigs = new Map([
|
|
||||||
[
|
|
||||||
'one',
|
|
||||||
createCameraConfig({
|
|
||||||
dependencies: {
|
|
||||||
cameras: ['two', 'three'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
['two', createCameraConfig({})],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
|
||||||
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
|
|
||||||
new Set(['one', 'two']),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('should return all cameras', () => {
|
|
||||||
const cameraConfigs: CameraConfigs = new Map([
|
|
||||||
[
|
|
||||||
'one',
|
|
||||||
createCameraConfig({
|
|
||||||
dependencies: {
|
|
||||||
all_cameras: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
['two', createCameraConfig({})],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
|
||||||
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
|
|
||||||
new Set(['one', 'two']),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ vi.mock('../../package.json', () => ({
|
|||||||
gitDate: 'Wed, 6 Sep 2023 21:27:28 -0700',
|
gitDate: 'Wed, 6 Sep 2023 21:27:28 -0700',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
vi.mock('../../src/utils/ha');
|
vi.mock('../../src/utils/ha');
|
||||||
vi.mock('../../src/localize/localize.js');
|
vi.mock('../../src/localize/localize.js');
|
||||||
vi.mock('../../src/utils/ha/device-registry');
|
vi.mock('../../src/utils/ha/device-registry');
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { CameraManager } from '../../src/camera-manager/manager.js';
|
|
||||||
import { downloadMedia, downloadURL } from '../../src/utils/download';
|
import { downloadMedia, downloadURL } from '../../src/utils/download';
|
||||||
import { homeAssistantSignPath } from '../../src/utils/ha';
|
import { homeAssistantSignPath } from '../../src/utils/ha';
|
||||||
import { ViewMedia } from '../../src/view/media';
|
import { ViewMedia } from '../../src/view/media';
|
||||||
import { createCameraManager, createHASS } from '../test-utils';
|
import { createCameraManager, createHASS } from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
vi.mock('../../src/utils/ha');
|
vi.mock('../../src/utils/ha');
|
||||||
|
|
||||||
const media = new ViewMedia('clip', 'camera-1');
|
const media = new ViewMedia('clip', 'camera-1');
|
||||||
@@ -73,7 +71,7 @@ describe('downloadMedia', () => {
|
|||||||
|
|
||||||
it('should throw error when no media', () => {
|
it('should throw error when no media', () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue(null);
|
vi.mocked(cameraManager.getMediaDownloadPath).mockResolvedValue(null);
|
||||||
|
|
||||||
expect(downloadMedia(createHASS(), cameraManager, media)).rejects.toThrow(
|
expect(downloadMedia(createHASS(), cameraManager, media)).rejects.toThrow(
|
||||||
/No media to download/,
|
/No media to download/,
|
||||||
@@ -84,7 +82,7 @@ describe('downloadMedia', () => {
|
|||||||
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||||
|
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
||||||
sign: true,
|
sign: true,
|
||||||
endpoint: 'foo',
|
endpoint: 'foo',
|
||||||
});
|
});
|
||||||
@@ -98,7 +96,7 @@ describe('downloadMedia', () => {
|
|||||||
|
|
||||||
it('should download media', async () => {
|
it('should download media', async () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
||||||
sign: true,
|
sign: true,
|
||||||
endpoint: 'foo',
|
endpoint: 'foo',
|
||||||
});
|
});
|
||||||
@@ -111,7 +109,7 @@ describe('downloadMedia', () => {
|
|||||||
|
|
||||||
it('should download media without signing', async () => {
|
it('should download media without signing', async () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
||||||
sign: false,
|
sign: false,
|
||||||
endpoint: 'https://foo/',
|
endpoint: 'https://foo/',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import add from 'date-fns/add';
|
import add from 'date-fns/add';
|
||||||
import sub from 'date-fns/sub';
|
import sub from 'date-fns/sub';
|
||||||
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
||||||
import { CameraConfigs } from '../../src/camera-manager/types';
|
import { QueryType } from '../../src/camera-manager/types';
|
||||||
import {
|
import {
|
||||||
changeViewToRecentEventsForCameraAndDependents,
|
changeViewToRecentEventsForCameraAndDependents,
|
||||||
changeViewToRecentRecordingForCameraAndDependents,
|
changeViewToRecentRecordingForCameraAndDependents,
|
||||||
@@ -14,12 +14,11 @@ import { EventMediaQueries } from '../../src/view/media-queries';
|
|||||||
import {
|
import {
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
createPerformanceConfig,
|
createPerformanceConfig,
|
||||||
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
TestViewMedia,
|
TestViewMedia,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
const createElementListenForView = (): {
|
const createElementListenForView = (): {
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
viewHandler: Mock<any, any>;
|
viewHandler: Mock<any, any>;
|
||||||
@@ -66,11 +65,10 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
|||||||
|
|
||||||
it('should do nothing without camera config for selected camera', async () => {
|
it('should do nothing without camera config for selected camera', async () => {
|
||||||
const elementHandler = createElementListenForView();
|
const elementHandler = createElementListenForView();
|
||||||
const cameraManager = createCameraManager({ configs: new Map() });
|
|
||||||
|
|
||||||
await changeViewToRecentEventsForCameraAndDependents(
|
await changeViewToRecentEventsForCameraAndDependents(
|
||||||
elementHandler.element,
|
elementHandler.element,
|
||||||
cameraManager,
|
createCameraManager(),
|
||||||
{},
|
{},
|
||||||
createView(),
|
createView(),
|
||||||
);
|
);
|
||||||
@@ -79,11 +77,10 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
|||||||
|
|
||||||
it('should do nothing without camera configs for all cameras', async () => {
|
it('should do nothing without camera configs for all cameras', async () => {
|
||||||
const elementHandler = createElementListenForView();
|
const elementHandler = createElementListenForView();
|
||||||
const cameraManager = createCameraManager({ configs: new Map() });
|
|
||||||
|
|
||||||
await changeViewToRecentEventsForCameraAndDependents(
|
await changeViewToRecentEventsForCameraAndDependents(
|
||||||
elementHandler.element,
|
elementHandler.element,
|
||||||
cameraManager,
|
createCameraManager(),
|
||||||
{},
|
{},
|
||||||
createView(),
|
createView(),
|
||||||
{
|
{
|
||||||
@@ -113,6 +110,19 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
|||||||
it('should dispatch new view on success', async () => {
|
it('should dispatch new view on success', async () => {
|
||||||
const elementHandler = createElementListenForView();
|
const elementHandler = createElementListenForView();
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
|
||||||
|
{
|
||||||
|
type: QueryType.Event,
|
||||||
|
cameraIDs: new Set(['camera']),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const mediaArray = [new ViewMedia('clip', 'camera')];
|
const mediaArray = [new ViewMedia('clip', 'camera')];
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||||
@@ -136,6 +146,19 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
|||||||
|
|
||||||
const elementHandler = createElementListenForView();
|
const elementHandler = createElementListenForView();
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
|
||||||
|
{
|
||||||
|
type: QueryType.Event,
|
||||||
|
cameraIDs: new Set(['camera']),
|
||||||
|
},
|
||||||
|
]);
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
||||||
|
|
||||||
await changeViewToRecentEventsForCameraAndDependents(
|
await changeViewToRecentEventsForCameraAndDependents(
|
||||||
@@ -150,6 +173,13 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
|||||||
|
|
||||||
it('should respect media chunk size', async () => {
|
it('should respect media chunk size', async () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
await changeViewToRecentEventsForCameraAndDependents(
|
await changeViewToRecentEventsForCameraAndDependents(
|
||||||
createElementListenForView().element,
|
createElementListenForView().element,
|
||||||
@@ -178,6 +208,13 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
|||||||
['clips' as const, 'hasClip'],
|
['clips' as const, 'hasClip'],
|
||||||
])('%s', async (mediaType, queryParameter) => {
|
])('%s', async (mediaType, queryParameter) => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
await changeViewToRecentEventsForCameraAndDependents(
|
await changeViewToRecentEventsForCameraAndDependents(
|
||||||
createElementListenForView().element,
|
createElementListenForView().element,
|
||||||
@@ -206,12 +243,9 @@ describe('executeMediaQueryForView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not execute empty queries', async () => {
|
it('should not execute empty queries', async () => {
|
||||||
const cameraConfigs: CameraConfigs = new Map();
|
|
||||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await executeMediaQueryForView(
|
await executeMediaQueryForView(
|
||||||
cameraManager,
|
createCameraManager(),
|
||||||
createView(),
|
createView(),
|
||||||
new EventMediaQueries(),
|
new EventMediaQueries(),
|
||||||
),
|
),
|
||||||
@@ -219,24 +253,25 @@ describe('executeMediaQueryForView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on failure', async () => {
|
it('should throw on failure', async () => {
|
||||||
const cameraConfigs: CameraConfigs = new Map();
|
const cameraManager = createCameraManager();
|
||||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
executeMediaQueryForView(
|
executeMediaQueryForView(
|
||||||
cameraManager,
|
cameraManager,
|
||||||
createView(),
|
createView(),
|
||||||
new EventMediaQueries(
|
new EventMediaQueries([
|
||||||
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
|
{
|
||||||
),
|
type: QueryType.Event,
|
||||||
|
cameraIDs: new Set('camera'),
|
||||||
|
},
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
).rejects.toThrowError();
|
).rejects.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select time-based result', async () => {
|
it('should select time-based result', async () => {
|
||||||
const cameraConfigs: CameraConfigs = new Map();
|
const cameraManager = createCameraManager();
|
||||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const mediaArray = [
|
const mediaArray = [
|
||||||
@@ -249,9 +284,12 @@ describe('executeMediaQueryForView', () => {
|
|||||||
const view = await executeMediaQueryForView(
|
const view = await executeMediaQueryForView(
|
||||||
cameraManager,
|
cameraManager,
|
||||||
createView(),
|
createView(),
|
||||||
new EventMediaQueries(
|
new EventMediaQueries([
|
||||||
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
|
{
|
||||||
),
|
type: QueryType.Event,
|
||||||
|
cameraIDs: new Set('camera'),
|
||||||
|
},
|
||||||
|
]),
|
||||||
{
|
{
|
||||||
select: 'time',
|
select: 'time',
|
||||||
targetTime: add(now, { seconds: 30 }),
|
targetTime: add(now, { seconds: 30 }),
|
||||||
@@ -264,8 +302,7 @@ describe('executeMediaQueryForView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should select nothing when time-based selection does not match', async () => {
|
it('should select nothing when time-based selection does not match', async () => {
|
||||||
const cameraConfigs: CameraConfigs = new Map();
|
const cameraManager = createCameraManager();
|
||||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const mediaArray = [
|
const mediaArray = [
|
||||||
@@ -279,9 +316,12 @@ describe('executeMediaQueryForView', () => {
|
|||||||
const view = await executeMediaQueryForView(
|
const view = await executeMediaQueryForView(
|
||||||
cameraManager,
|
cameraManager,
|
||||||
createView(),
|
createView(),
|
||||||
new EventMediaQueries(
|
new EventMediaQueries([
|
||||||
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
|
{
|
||||||
),
|
type: QueryType.Event,
|
||||||
|
cameraIDs: new Set('camera'),
|
||||||
|
},
|
||||||
|
]),
|
||||||
{
|
{
|
||||||
select: 'time',
|
select: 'time',
|
||||||
targetTime: sub(now, { seconds: 30 }),
|
targetTime: sub(now, { seconds: 30 }),
|
||||||
@@ -292,6 +332,24 @@ describe('executeMediaQueryForView', () => {
|
|||||||
expect(view?.queryResults?.getSelectedIndex()).toBe(2);
|
expect(view?.queryResults?.getSelectedIndex()).toBe(2);
|
||||||
expect(view?.queryResults?.getResults()).toBe(mediaArray);
|
expect(view?.queryResults?.getResults()).toBe(mediaArray);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should select nothing when query returns null', async () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await executeMediaQueryForView(
|
||||||
|
cameraManager,
|
||||||
|
createView(),
|
||||||
|
new EventMediaQueries([
|
||||||
|
{
|
||||||
|
type: QueryType.Event,
|
||||||
|
cameraIDs: new Set('camera'),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
@@ -302,11 +360,10 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
|
|||||||
|
|
||||||
it('should do nothing without camera config for selected camera', async () => {
|
it('should do nothing without camera config for selected camera', async () => {
|
||||||
const elementHandler = createElementListenForView();
|
const elementHandler = createElementListenForView();
|
||||||
const cameraManager = createCameraManager({ configs: new Map() });
|
|
||||||
|
|
||||||
await changeViewToRecentRecordingForCameraAndDependents(
|
await changeViewToRecentRecordingForCameraAndDependents(
|
||||||
elementHandler.element,
|
elementHandler.element,
|
||||||
cameraManager,
|
createCameraManager(),
|
||||||
{},
|
{},
|
||||||
createView(),
|
createView(),
|
||||||
);
|
);
|
||||||
@@ -315,11 +372,10 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
|
|||||||
|
|
||||||
it('should do nothing without camera configs for all cameras', async () => {
|
it('should do nothing without camera configs for all cameras', async () => {
|
||||||
const elementHandler = createElementListenForView();
|
const elementHandler = createElementListenForView();
|
||||||
const cameraManager = createCameraManager({ configs: new Map() });
|
|
||||||
|
|
||||||
await changeViewToRecentRecordingForCameraAndDependents(
|
await changeViewToRecentRecordingForCameraAndDependents(
|
||||||
elementHandler.element,
|
elementHandler.element,
|
||||||
cameraManager,
|
createCameraManager(),
|
||||||
{},
|
{},
|
||||||
createView(),
|
createView(),
|
||||||
{
|
{
|
||||||
@@ -346,9 +402,22 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
|
|||||||
it('should dispatch new view on success', async () => {
|
it('should dispatch new view on success', async () => {
|
||||||
const elementHandler = createElementListenForView();
|
const elementHandler = createElementListenForView();
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const mediaArray = [new ViewMedia('recording', 'camera')];
|
const mediaArray = [new ViewMedia('recording', 'camera')];
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||||
|
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue([
|
||||||
|
{
|
||||||
|
type: QueryType.Recording,
|
||||||
|
cameraIDs: new Set(['camera']),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
await changeViewToRecentRecordingForCameraAndDependents(
|
await changeViewToRecentRecordingForCameraAndDependents(
|
||||||
elementHandler.element,
|
elementHandler.element,
|
||||||
@@ -366,6 +435,13 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
|
|||||||
|
|
||||||
it('should respect media chunk size', async () => {
|
it('should respect media chunk size', async () => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
await changeViewToRecentRecordingForCameraAndDependents(
|
await changeViewToRecentRecordingForCameraAndDependents(
|
||||||
createElementListenForView().element,
|
createElementListenForView().element,
|
||||||
@@ -402,9 +478,7 @@ describe('executeMediaQueryForViewWithErrorDispatching', () => {
|
|||||||
elementHandler.element,
|
elementHandler.element,
|
||||||
cameraManager,
|
cameraManager,
|
||||||
createView(),
|
createView(),
|
||||||
new EventMediaQueries(
|
new EventMediaQueries([{ type: QueryType.Event, cameraIDs: new Set(['camera']) }]),
|
||||||
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { FrigateCardPTZConfig, frigateCardPTZSchema } from '../../src/config/types';
|
||||||
|
import { hasUsablePTZ } from '../../src/utils/ptz';
|
||||||
|
import { createCameraCapabilities } from '../test-utils';
|
||||||
|
|
||||||
|
const createPTZConfig = (
|
||||||
|
config?: Partial<FrigateCardPTZConfig>,
|
||||||
|
): FrigateCardPTZConfig => {
|
||||||
|
return frigateCardPTZSchema.parse(config ?? {});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('hasUsablePTZ', () => {
|
||||||
|
it('should return true with manual actions', () => {
|
||||||
|
expect(
|
||||||
|
hasUsablePTZ(
|
||||||
|
createCameraCapabilities(),
|
||||||
|
createPTZConfig({
|
||||||
|
actions_left: {},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true with capabilities', () => {
|
||||||
|
expect(
|
||||||
|
hasUsablePTZ(
|
||||||
|
createCameraCapabilities({
|
||||||
|
ptz: {},
|
||||||
|
}),
|
||||||
|
createPTZConfig(),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return false with manual actions or capabilities', () => {
|
||||||
|
expect(hasUsablePTZ(createCameraCapabilities(), createPTZConfig())).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user