Add interaction and triggered conditions.
This commit is contained in:
@@ -400,8 +400,9 @@ See the [fully expanded view configuration example](#config-expanded-view) for h
|
||||
| - | - | - | - |
|
||||
| `default` | `live` | :white_check_mark: | The view to show in the card by default. The default camera is the first one listed. See [views](#views) below.|
|
||||
| `camera_select` | `current` | :white_check_mark: | The view to show when a new camera is selected (e.g. in the camera menu). If `current` the view is unchanged when a new camera is selected. Other acceptable values may be seen at [views](#views) below.|
|
||||
| `dark_mode` | `off` | :white_check_mark: | Whether or not to turn dark mode `on`, `off` or `auto` to automatically turn on if the card `timeout_seconds` has expired (i.e. card has been left unattended for that period of time) or if dark mode is enabled in the HA profile theme setting. Dark mode dims the brightness by `25%`.|
|
||||
| `timeout_seconds` | `300` | :white_check_mark: | A numbers of seconds of inactivity after user interaction, after which the card will reset to the default configured view (i.e. 'screensaver' functionality). Inactivity is defined as lack of mouse/touch interaction with the Frigate card. If the default view occurs sooner (e.g. via `update_seconds` or manually) the timer will be stopped. `0` means disable this functionality. |
|
||||
| `dark_mode` | `off` | :white_check_mark: | Whether or not to turn dark mode `on`, `off` or `auto` to automatically turn on if the card `interaction_seconds` has expired (i.e. card has been left unattended for that period of time) or if dark mode is enabled in the HA profile theme setting. Dark mode dims the brightness by `25%`.|
|
||||
| `interaction_seconds` | `300` | :white_check_mark: | After a mouse/touch interaction with the Frigate card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used in conditions with the `interaction` parameter of a [Frigate card condition](#frigate-card-condition) or with `reset_after_interaction` (below). `0` means no interactions are reported / acted upon. |
|
||||
| `reset_after_interaction` | `true` | :white_check_mark: | If `true` the card will reset to the default configured view (i.e. 'screensaver' functionality) after `interaction_seconds` has elapsed after user interaction. |
|
||||
| `update_seconds` | `0` | :white_check_mark: | A number of seconds after which to automatically update/refresh the default view. See [card updates](#card-updates) below for behavior and usecases. If the default view occurs sooner (e.g. manually) the timer will start over. `0` disables this functionality.|
|
||||
| `update_force` | `false` | :white_check_mark: | Whether automated card updates/refreshes should ignore user interaction. See [card updates](#card-updates) below for behavior and usecases.|
|
||||
| `update_entities` | | :white_check_mark: | **YAML only**: A card-wide list of entities that should cause the view to reset to the default (if the entity only pertains to a particular camera use `triggers` for the selected camera instead, see [Trigger Configuration](#camera-triggers-configuration)). See [card updates](#card-updates) below for behavior and usecases.|
|
||||
@@ -441,7 +442,7 @@ Scan mode tracks Home Assistant state *changes* -- when the card is first starte
|
||||
|
||||
| `trigger` | `live` | :white_check_mark | When `live` the trigger will select the triggered camera in `live` view, when `none` will take no action. |
|
||||
| `untrigger` | `default` | :white_check_mark | When `default` the untrigger will return to the default view and camera, when `none` will take no action. |
|
||||
| `interaction_mode` | `inactive` | :white_check_mark: | Whether actions should be taken when the card is being interacted with. If `all`, actions will always be taken regardless. If `inactive` actions will only be taken if the card has *not* had human interaction recently (as defined by `view.timeout_seconds`). If `active` actions will only be taken if the card *has* had human interaction recently. This does not stop triggering itself (i.e. border will still pulse if `show_trigger_status` is true) but rather just prevents the actions being performed.|
|
||||
| `interaction_mode` | `inactive` | :white_check_mark: | Whether actions should be taken when the card is being interacted with. If `all`, actions will always be taken regardless. If `inactive` actions will only be taken if the card has *not* had human interaction recently (as defined by `view.interaction_seconds`). If `active` actions will only be taken if the card *has* had human interaction recently. This does not stop triggering itself (i.e. border will still pulse if `show_trigger_status` is true) but rather just prevents the actions being performed.|
|
||||
|
||||
### Menu Options
|
||||
|
||||
@@ -1283,12 +1284,14 @@ All variables listed are under a `conditions:` section.
|
||||
| Condition | Description |
|
||||
| ------------- | --------------------------------------------- |
|
||||
| `view` | A list of [views](#views) in which this condition is satified (e.g. `clips`) |
|
||||
| `camera` | A list of camera ids in which this condition is satisfied. See [camera IDs](#camera-ids).|
|
||||
| `camera` | A list of camera IDs in which this condition is satisfied. See [camera IDs](#camera-ids).|
|
||||
| `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode.|
|
||||
| `expand` | If `true` the condition is satisfied if the card is in expanded mode (in a dialog/popup). If `false` the condition is satisfied if the card is **NOT** in expanded mode (in a dialog/popup).|
|
||||
| `state` | A list of state conditions to compare with Home Assistant state. See below. |
|
||||
| `media_loaded` | If `true` the condition is satisfied if there is media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. Note that if `true` this condition will never be satisfied for views that do not themselves load media directly (e.g. gallery).|
|
||||
| `media_query` | Any valid [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) string. Media queries must start and end with parentheses. This may be used to alter card configuration based on device/media properties (e.g. viewport width, orientation). Please note that `width` and `height` refer to the entire viewport not just the card. See the [media query example](#media-query-example).|
|
||||
| `interacted` | If `true` the condition is satisfied if the card has had human interaction within `view.interaction_seconds` elapsed seconds. If `false` the condition is satisfied if the card has **NOT** had human interaction in that time. |
|
||||
| `triggered` | A list of camera IDs which, if triggered in [scan mode](#scan-mode), satisfy the condition.|
|
||||
|
||||
See the [example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used.
|
||||
|
||||
@@ -1456,7 +1459,7 @@ This card supports several different views:
|
||||
|`clip`|Shows a viewer for the most recent clip for this camera. Can also be accessed by holding down the `clips` menu icon.|
|
||||
|`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.|
|
||||
|`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.interaction_seconds`).|
|
||||
|`timeline`|Shows an event timeline.|
|
||||
|
||||
### Navigating From A Snapshot To A Clip
|
||||
@@ -1908,7 +1911,7 @@ Reference: [View Options](#view-options).
|
||||
view:
|
||||
default: live
|
||||
camera_select: current
|
||||
timeout_seconds: 300
|
||||
interaction_seconds: 300
|
||||
update_seconds: 0
|
||||
update_force: false
|
||||
update_cycle_camera: false
|
||||
@@ -2860,7 +2863,7 @@ overrides:
|
||||
view:
|
||||
default: live
|
||||
camera_select: current
|
||||
timeout_seconds: 300
|
||||
interaction_seconds: 300
|
||||
update_seconds: 0
|
||||
update_force: false
|
||||
update_cycle_camera: false
|
||||
@@ -3663,6 +3666,37 @@ view:
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Expand: Showing an alarm menu button when a camera is triggered</summary>
|
||||
|
||||
This example adds a menu button to optionally activate a siren when the camera is triggered.
|
||||
|
||||
```yaml
|
||||
type: custom:frigate-card
|
||||
cameras:
|
||||
- camera_entity: camera.office
|
||||
elements:
|
||||
- type: custom:frigate-card-conditional
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-icon
|
||||
icon: mdi:alarm-bell
|
||||
title: Activate alarm
|
||||
style:
|
||||
color: red
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: homeassistant.toggle
|
||||
data:
|
||||
entity_id: siren.siren
|
||||
conditions:
|
||||
triggered:
|
||||
- camera.office
|
||||
view:
|
||||
scan:
|
||||
enabled: true
|
||||
```
|
||||
</details>
|
||||
|
||||
<a name="media-layout-examples"></a>
|
||||
|
||||
### Changing the Media Layout
|
||||
@@ -4051,6 +4085,44 @@ views:
|
||||
|
||||
</details>
|
||||
|
||||
### Interaction conditions
|
||||
|
||||
The card can automatically execute actions when the card is interacted with (mouse or touch).
|
||||
|
||||
<details>
|
||||
<summary>Expand: Automatically show a high-bandwidth stream on interaction</summary>
|
||||
|
||||
This example will automatically use a HD live substream when the mouse cursor interacts with the card.
|
||||
|
||||
```yaml
|
||||
type: custom:frigate-card
|
||||
cameras:
|
||||
- live_provider: go2rtc
|
||||
camera_entity: camera.office
|
||||
triggers:
|
||||
entities:
|
||||
- switch.office_detect
|
||||
dependencies:
|
||||
cameras:
|
||||
- camera.office_hd
|
||||
- camera_entity: camera.office_hd
|
||||
live_provider: go2rtc
|
||||
hide: true
|
||||
automations:
|
||||
- actions:
|
||||
- action: custom:frigate-card-action
|
||||
frigate_card_action: live_substream_on
|
||||
actions_not:
|
||||
- action: custom:frigate-card-action
|
||||
frigate_card_action: live_substream_off
|
||||
conditions:
|
||||
interaction: true
|
||||
view:
|
||||
scan:
|
||||
enabled: true
|
||||
```
|
||||
</details>
|
||||
|
||||
<a name="media-layout-examples"></a>
|
||||
|
||||
## Card Refreshes
|
||||
@@ -4066,7 +4138,7 @@ Note that no (other) automated updates are permitted when [scan mode](#scan-mode
|
||||
|
||||
In the below "Trigger Entities" refers to the combination of `view.update_entities` and the `triggers.entities` for the currently selected camera (which in turn will also include the occupancy and motion sensor entities for Frigate cameras if `triggers.occupancy` and `triggers.motion` options are enabled, see [Trigger Configuration](#camera-triggers-configuration)).
|
||||
|
||||
| `view . update_seconds` | `view . timeout_seconds` | `view . update_force` | Trigger Entities | Behavior |
|
||||
| `view . update_seconds` | `view . interaction_seconds` | `view . update_force` | Trigger Entities | Behavior |
|
||||
| :-: | :-: | :-: | :-: | - |
|
||||
| `0` | `0` | *(Any value)* | Unset | Card will not automatically refresh. |
|
||||
| `0` | `0` | *(Any value)* | *(Any entity)* | Card will reload default view & camera when entity state changes. |
|
||||
@@ -4108,7 +4180,7 @@ view:
|
||||
```yaml
|
||||
view:
|
||||
default: clip
|
||||
timeout_seconds: 30
|
||||
interaction_seconds: 30
|
||||
```
|
||||
|
||||
<a name="query-string-actions"></a>
|
||||
|
||||
@@ -54,6 +54,13 @@ export class CardElementManager {
|
||||
}
|
||||
|
||||
public elementConnected(): void {
|
||||
// Set initial condition state. Must be done after the element is connected to
|
||||
// allow callbacks to interact with the card.
|
||||
this._api.getInteractionManager().initialize();
|
||||
this._api.getFullscreenManager().initialize();
|
||||
this._api.getExpandManager().initialize();
|
||||
this._api.getMediaLoadedInfoManager().initialize();
|
||||
|
||||
// Whether or not the card is in panel mode on the dashboard.
|
||||
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
frigateConditionalSchema,
|
||||
OverrideConfigurationKey,
|
||||
RawFrigateCardConfig,
|
||||
ViewDisplayMode
|
||||
ViewDisplayMode,
|
||||
} from '../config/types';
|
||||
import { CardConditionAPI } from './types';
|
||||
|
||||
@@ -18,6 +18,8 @@ interface ConditionState {
|
||||
state?: HassEntities;
|
||||
media_loaded?: boolean;
|
||||
displayMode?: ViewDisplayMode;
|
||||
triggered?: Set<string>;
|
||||
interaction?: boolean;
|
||||
}
|
||||
|
||||
export class ConditionEvaluateRequestEvent extends Event {
|
||||
@@ -133,7 +135,7 @@ export class ConditionsManager {
|
||||
this._listeners = [
|
||||
() => this._api.getConfigManager().computeOverrideConfig(),
|
||||
() => this._api.getAutomationsManager().execute(),
|
||||
...(listener ? [listener] : [])
|
||||
...(listener ? [listener] : []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -243,6 +245,15 @@ export class ConditionsManager {
|
||||
if (condition.display_mode) {
|
||||
result &&= !!state.displayMode && condition.display_mode === state.displayMode;
|
||||
}
|
||||
if (condition.triggered?.length) {
|
||||
result &&= condition.triggered.some((triggeredCameraID) =>
|
||||
state.triggered?.has(triggeredCameraID),
|
||||
);
|
||||
}
|
||||
if (condition.interaction !== undefined) {
|
||||
result &&=
|
||||
state.interaction !== undefined && condition.interaction === state.interaction;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ export class ExpandManager {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this._setConditionState();
|
||||
}
|
||||
|
||||
public isExpanded(): boolean {
|
||||
return this._expanded;
|
||||
}
|
||||
@@ -23,9 +27,13 @@ export class ExpandManager {
|
||||
}
|
||||
|
||||
this._expanded = expanded;
|
||||
this._api.getConditionsManager()?.setState({
|
||||
expand: expanded,
|
||||
});
|
||||
this._setConditionState();
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager()?.setState({
|
||||
expand: this._expanded,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ export class FullscreenManager {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this._setConditionState();
|
||||
}
|
||||
|
||||
public connect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on('change', this._fullscreenHandler);
|
||||
@@ -32,12 +36,16 @@ export class FullscreenManager {
|
||||
screenfull.exit();
|
||||
}
|
||||
|
||||
protected _fullscreenHandler = (): void => {
|
||||
this._api.getExpandManager().setExpanded(false);
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager()?.setState({
|
||||
fullscreen: this.isInFullscreen(),
|
||||
});
|
||||
}
|
||||
|
||||
protected _fullscreenHandler = (): void => {
|
||||
this._api.getExpandManager().setExpanded(false);
|
||||
|
||||
this._setConditionState();
|
||||
|
||||
// Re-render after a change to fullscreen mode to take advantage of
|
||||
// the expanded screen real-estate (vs staying in aspect-ratio locked
|
||||
|
||||
@@ -16,32 +16,37 @@ export class InteractionManager {
|
||||
this._reportInteraction();
|
||||
}, 1 * 1000);
|
||||
|
||||
public initialize(): void {
|
||||
this._api.getConditionsManager().setState({ interaction: false });
|
||||
}
|
||||
|
||||
public hasInteraction(): boolean {
|
||||
return this._timer.isRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the user interaction ('screensaver') timer to reset the view to
|
||||
* default `view.timeout_seconds` after user interaction.
|
||||
* default `view.interaction_seconds` after user interaction.
|
||||
*/
|
||||
protected _reportInteraction(): void {
|
||||
this._timer.stop();
|
||||
|
||||
const timeoutSeconds = this._api.getConfigManager().getConfig()
|
||||
?.view.timeout_seconds;
|
||||
?.view.interaction_seconds;
|
||||
|
||||
if (timeoutSeconds) {
|
||||
this._api.getConditionsManager().setState({ interaction: true });
|
||||
|
||||
this._timer.start(timeoutSeconds, () => {
|
||||
if (this._isAutomatedUpdateAllowed()) {
|
||||
this._api.getConditionsManager().setState({ interaction: false });
|
||||
|
||||
if (!this._api.getTriggersManager().isTriggered()) {
|
||||
if (this._api.getConfigManager().getConfig()?.view.reset_after_interaction) {
|
||||
this._api.getViewManager().setViewDefault();
|
||||
}
|
||||
this._api.getStyleManager().setLightOrDarkMode();
|
||||
}
|
||||
});
|
||||
}
|
||||
this._api.getStyleManager().setLightOrDarkMode();
|
||||
}
|
||||
|
||||
protected _isAutomatedUpdateAllowed(): boolean {
|
||||
return !this._api.getTriggersManager().isTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ export class MediaLoadedInfoManager {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public set(mediaInfo: MediaLoadedInfo): void {
|
||||
if (!isValidMediaLoadedInfo(mediaInfo)) {
|
||||
return;
|
||||
|
||||
@@ -95,6 +95,7 @@ export class TriggersManager {
|
||||
cameraID === this._api.getViewManager().getView()?.camera)
|
||||
) {
|
||||
this._triggeredCameras.set(cameraID, now);
|
||||
this._setConditionState();
|
||||
this._triggerAction(cameraID);
|
||||
}
|
||||
}
|
||||
@@ -132,6 +133,14 @@ export class TriggersManager {
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager().setState({
|
||||
triggered: this._triggeredCameras.size
|
||||
? new Set(this._triggeredCameras.keys())
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
protected _untriggerAction(cameraID: string): void {
|
||||
const action = this._api.getConfigManager().getConfig()?.view.scan.actions.untrigger;
|
||||
|
||||
@@ -140,6 +149,7 @@ export class TriggersManager {
|
||||
}
|
||||
this._triggeredCameras.delete(cameraID);
|
||||
this._deleteTimer(cameraID);
|
||||
this._setConditionState();
|
||||
|
||||
// Must update master element to remove border pulsing.
|
||||
this._api.getCardElementManager().update();
|
||||
|
||||
@@ -100,6 +100,7 @@ export interface CardDownloadAPI {
|
||||
|
||||
export interface CardElementAPI {
|
||||
getActionsManager(): ActionsManager;
|
||||
getExpandManager(): ExpandManager;
|
||||
getFullscreenManager(): FullscreenManager;
|
||||
getInteractionManager(): InteractionManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
@@ -147,6 +148,7 @@ export interface CardInitializerAPI {
|
||||
}
|
||||
|
||||
export interface CardInteractionAPI {
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getStyleManager(): StyleManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
@@ -198,6 +200,7 @@ export interface CardStyleAPI {
|
||||
|
||||
export interface CardTriggersAPI {
|
||||
getCameraManager(): CameraManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getHASSManager(): HASSManager;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CONF_MEDIA_GALLERY,
|
||||
CONF_MENU_BUTTONS_CAMERA_UI,
|
||||
CONF_OVERRIDES,
|
||||
CONF_VIEW_INTERACTION_SECONDS,
|
||||
CONF_VIEW_SCAN_ACTIONS_UNTRIGGER,
|
||||
} from './const';
|
||||
import { arrayify } from './utils/basic';
|
||||
@@ -483,4 +484,5 @@ const UPGRADES = [
|
||||
transform: (val) => (val ? 'default' : null),
|
||||
},
|
||||
),
|
||||
upgradeMoveToWithOverrides('view.timeout_seconds', CONF_VIEW_INTERACTION_SECONDS),
|
||||
];
|
||||
|
||||
+8
-3
@@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
CallServiceActionConfig,
|
||||
ConfirmationRestrictionConfig,
|
||||
@@ -490,6 +489,8 @@ export const frigateCardConditionSchema = z.object({
|
||||
state: stateConditions.optional(),
|
||||
media_query: z.string().optional(),
|
||||
display_mode: viewDisplayModeSchema.optional(),
|
||||
triggered: z.string().array().optional(),
|
||||
interaction: z.boolean().optional(),
|
||||
});
|
||||
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
||||
|
||||
@@ -1114,7 +1115,8 @@ export type CamerasConfig = z.infer<typeof camerasConfigSchema>;
|
||||
const viewConfigDefault = {
|
||||
default: FRIGATE_CARD_VIEW_DEFAULT,
|
||||
camera_select: 'current' as const,
|
||||
timeout_seconds: 300,
|
||||
interaction_seconds: 300,
|
||||
reset_after_interaction: true,
|
||||
update_seconds: 0,
|
||||
update_force: false,
|
||||
update_cycle_camera: false,
|
||||
@@ -1163,7 +1165,10 @@ const viewConfigSchema = z
|
||||
camera_select: z
|
||||
.enum([...FRIGATE_CARD_VIEWS_USER_SPECIFIED, 'current'])
|
||||
.default(viewConfigDefault.camera_select),
|
||||
timeout_seconds: z.number().default(viewConfigDefault.timeout_seconds),
|
||||
interaction_seconds: z.number().default(viewConfigDefault.interaction_seconds),
|
||||
reset_after_interaction: z
|
||||
.boolean()
|
||||
.default(viewConfigDefault.reset_after_interaction),
|
||||
update_seconds: z.number().default(viewConfigDefault.update_seconds),
|
||||
update_force: z.boolean().default(viewConfigDefault.update_force),
|
||||
update_cycle_camera: z.boolean().default(viewConfigDefault.update_cycle_camera),
|
||||
|
||||
+2
-1
@@ -71,10 +71,11 @@ const CONF_VIEW = 'view' as const;
|
||||
export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const;
|
||||
export const CONF_VIEW_DARK_MODE = `${CONF_VIEW}.dark_mode` as const;
|
||||
export const CONF_VIEW_DEFAULT = `${CONF_VIEW}.default` as const;
|
||||
export const CONF_VIEW_TIMEOUT_SECONDS = `${CONF_VIEW}.timeout_seconds` as const;
|
||||
export const CONF_VIEW_INTERACTION_SECONDS = `${CONF_VIEW}.interaction_seconds` as const;
|
||||
export const CONF_VIEW_UPDATE_CYCLE_CAMERA = `${CONF_VIEW}.update_cycle_camera` as const;
|
||||
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
|
||||
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
|
||||
export const CONF_VIEW_RESET_AFTER_INTERACTION = `${CONF_VIEW}.reset_after_interaction` as const;
|
||||
export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const;
|
||||
export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const;
|
||||
export const CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS =
|
||||
|
||||
+9
-4
@@ -22,9 +22,9 @@ import {
|
||||
} from './config-mgmt.js';
|
||||
import {
|
||||
BUTTON_SIZE_MIN,
|
||||
FRIGATE_MENU_PRIORITY_MAX,
|
||||
FrigateCardConfig,
|
||||
frigateCardConfigDefaults,
|
||||
FRIGATE_MENU_PRIORITY_MAX,
|
||||
RawFrigateCardConfig,
|
||||
RawFrigateCardConfigArray,
|
||||
THUMBNAIL_WIDTH_MAX,
|
||||
@@ -157,8 +157,8 @@ import {
|
||||
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||
CONF_MEDIA_VIEWER_ZOOMABLE,
|
||||
CONF_MENU_ALIGNMENT,
|
||||
CONF_MENU_BUTTON_SIZE,
|
||||
CONF_MENU_BUTTONS,
|
||||
CONF_MENU_BUTTON_SIZE,
|
||||
CONF_MENU_POSITION,
|
||||
CONF_MENU_STYLE,
|
||||
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||
@@ -180,6 +180,8 @@ import {
|
||||
CONF_VIEW_CAMERA_SELECT,
|
||||
CONF_VIEW_DARK_MODE,
|
||||
CONF_VIEW_DEFAULT,
|
||||
CONF_VIEW_INTERACTION_SECONDS,
|
||||
CONF_VIEW_RESET_AFTER_INTERACTION,
|
||||
CONF_VIEW_SCAN,
|
||||
CONF_VIEW_SCAN_ACTIONS,
|
||||
CONF_VIEW_SCAN_ACTIONS_INTERACTION_MODE,
|
||||
@@ -189,7 +191,6 @@ import {
|
||||
CONF_VIEW_SCAN_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
|
||||
CONF_VIEW_SCAN_UNTRIGGER_SECONDS,
|
||||
CONF_VIEW_TIMEOUT_SECONDS,
|
||||
CONF_VIEW_UPDATE_CYCLE_CAMERA,
|
||||
CONF_VIEW_UPDATE_FORCE,
|
||||
CONF_VIEW_UPDATE_SECONDS,
|
||||
@@ -1948,7 +1949,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
this._cameraSelectViewModes,
|
||||
)}
|
||||
${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)}
|
||||
${this._renderNumberInput(CONF_VIEW_TIMEOUT_SECONDS)}
|
||||
${this._renderNumberInput(CONF_VIEW_INTERACTION_SECONDS)}
|
||||
${this._renderSwitch(
|
||||
CONF_VIEW_RESET_AFTER_INTERACTION,
|
||||
this._defaults.view.reset_after_interaction,
|
||||
)}
|
||||
${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)}
|
||||
${this._renderSwitch(
|
||||
CONF_VIEW_UPDATE_FORCE,
|
||||
|
||||
@@ -410,7 +410,8 @@
|
||||
"show_trigger_status": "Show pulsing border when triggered",
|
||||
"untrigger_seconds": "Seconds after inactive state change to untrigger"
|
||||
},
|
||||
"timeout_seconds": "Reset to default view X seconds after user action (0=never)",
|
||||
"interaction_seconds": "Seconds after user action to remain interacted with (0=never)",
|
||||
"reset_after_interaction": "Reset to the default view after user interaction",
|
||||
"update_cycle_camera": "Cycle through cameras when default view updates",
|
||||
"update_force": "Force card updates (ignore user interaction)",
|
||||
"update_seconds": "Refresh default view every X seconds (0=never)",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"editor_label": "",
|
||||
"method": "",
|
||||
"methods": {
|
||||
"standard": "",
|
||||
"dashboard": ""
|
||||
"dashboard": "",
|
||||
"standard": ""
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -236,27 +236,27 @@
|
||||
"editor_label": "Controlli dal vivo",
|
||||
"ptz": {
|
||||
"editor_label": "",
|
||||
"hide_home": "",
|
||||
"hide_pan_tilt": "",
|
||||
"hide_zoom": "",
|
||||
"mode": "",
|
||||
"modes": {
|
||||
"on": "",
|
||||
"auto": "",
|
||||
"off": "",
|
||||
"auto": ""
|
||||
"on": ""
|
||||
},
|
||||
"orientation": "",
|
||||
"orientations": {
|
||||
"vertical": "",
|
||||
"horizontal": ""
|
||||
"horizontal": "",
|
||||
"vertical": ""
|
||||
},
|
||||
"position": "",
|
||||
"positions": {
|
||||
"top-left": "",
|
||||
"top-right": "",
|
||||
"bottom-left": "",
|
||||
"bottom-right": ""
|
||||
},
|
||||
"hide_zoom": "",
|
||||
"hide_pan_tilt": "",
|
||||
"hide_home": ""
|
||||
"bottom-right": "",
|
||||
"top-left": "",
|
||||
"top-right": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"draggable": "Il Visualizzatore eventi può essere trascinato oppure puoi scorrere",
|
||||
@@ -380,6 +380,8 @@
|
||||
"on": "On"
|
||||
},
|
||||
"default": "Visualizzazione predefinita",
|
||||
"interaction_seconds": "",
|
||||
"reset_after_interaction": "",
|
||||
"scan": {
|
||||
"actions": {
|
||||
"editor_label": "",
|
||||
@@ -406,7 +408,6 @@
|
||||
"show_trigger_status": "Mostra bordo pulsante quando attivato",
|
||||
"untrigger_seconds": "Reimposta la vista ai valori predefiniti dopo aver annullato l'attivazione"
|
||||
},
|
||||
"timeout_seconds": "Ripristina la vista predefinita x secondi dopo l'azione dell'utente (0 = mai)",
|
||||
"update_cycle_camera": "Scorri le telecamere quando si aggiorna la visualizzazione predefinita",
|
||||
"update_force": "Aggiornamenti della scheda forza (ignora l'interazione dell'utente)",
|
||||
"update_seconds": "Aggiorna la visualizzazione predefinita ogni x secondi (0 = mai)",
|
||||
@@ -497,8 +498,8 @@
|
||||
"no_visible_cameras": "Nessuna telecamera visibile trovata, è necessario configurare almeno una telecamera non nascosta",
|
||||
"reconnecting": "Riconnessione",
|
||||
"timeline_no_cameras": "Nessuna telecamera damostrare in Frigate nella timeline",
|
||||
"troubleshooting": "Controllare la risoluzione dei problemi",
|
||||
"too_many_automations": "",
|
||||
"troubleshooting": "Controllare la risoluzione dei problemi",
|
||||
"unknown": "Errore sconosciuto",
|
||||
"upgrade_available": "È disponibile un aggiornamento di configurazione della scheda automatizzato, visitare l'editor di schede visive",
|
||||
"webrtc_card_reported_error": "La scheda WebRTC ha riportato un errore",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"editor_label": "",
|
||||
"method": "",
|
||||
"methods": {
|
||||
"standard": "",
|
||||
"dashboard": ""
|
||||
"dashboard": "",
|
||||
"standard": ""
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -236,27 +236,27 @@
|
||||
"editor_label": "Controles da visualização ao vivo",
|
||||
"ptz": {
|
||||
"editor_label": "",
|
||||
"hide_home": "",
|
||||
"hide_pan_tilt": "",
|
||||
"hide_zoom": "",
|
||||
"mode": "",
|
||||
"modes": {
|
||||
"on": "",
|
||||
"auto": "",
|
||||
"off": "",
|
||||
"auto": ""
|
||||
"on": ""
|
||||
},
|
||||
"orientation": "",
|
||||
"orientations": {
|
||||
"vertical": "",
|
||||
"horizontal": ""
|
||||
"horizontal": "",
|
||||
"vertical": ""
|
||||
},
|
||||
"position": "",
|
||||
"positions": {
|
||||
"top-left": "",
|
||||
"top-right": "",
|
||||
"bottom-left": "",
|
||||
"bottom-right": ""
|
||||
},
|
||||
"hide_zoom": "",
|
||||
"hide_pan_tilt": "",
|
||||
"hide_home": ""
|
||||
"bottom-right": "",
|
||||
"top-left": "",
|
||||
"top-right": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
|
||||
@@ -383,6 +383,8 @@
|
||||
"on": "Ligado"
|
||||
},
|
||||
"default": "Visualização padrão",
|
||||
"interaction_seconds": "",
|
||||
"reset_after_interaction": "",
|
||||
"scan": {
|
||||
"actions": {
|
||||
"editor_label": "",
|
||||
@@ -409,7 +411,6 @@
|
||||
"show_trigger_status": "Pulsar borda quando acionado",
|
||||
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar"
|
||||
},
|
||||
"timeout_seconds": "Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)",
|
||||
"update_cycle_camera": "Percorrer as câmeras quando a visualização padrão for atualizada",
|
||||
"update_force": "Forçar atualizações do cartão (ignore a interação do usuário)",
|
||||
"update_seconds": "Atualize a visualização padrão a cada X segundos (0 = nunca)",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"editor_label": "",
|
||||
"method": "",
|
||||
"methods": {
|
||||
"standard": "",
|
||||
"dashboard": ""
|
||||
"dashboard": "",
|
||||
"standard": ""
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -229,27 +229,27 @@
|
||||
"editor_label": "Controles da visualização ao vivo",
|
||||
"ptz": {
|
||||
"editor_label": "",
|
||||
"hide_home": "",
|
||||
"hide_pan_tilt": "",
|
||||
"hide_zoom": "",
|
||||
"mode": "",
|
||||
"modes": {
|
||||
"on": "",
|
||||
"auto": "",
|
||||
"off": "",
|
||||
"auto": ""
|
||||
"on": ""
|
||||
},
|
||||
"orientation": "",
|
||||
"orientations": {
|
||||
"vertical": "",
|
||||
"horizontal": ""
|
||||
"horizontal": "",
|
||||
"vertical": ""
|
||||
},
|
||||
"position": "",
|
||||
"positions": {
|
||||
"top-left": "",
|
||||
"top-right": "",
|
||||
"bottom-left": "",
|
||||
"bottom-right": ""
|
||||
},
|
||||
"hide_zoom": "",
|
||||
"hide_pan_tilt": "",
|
||||
"hide_home": ""
|
||||
"bottom-right": "",
|
||||
"top-left": "",
|
||||
"top-right": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
|
||||
@@ -373,6 +373,8 @@
|
||||
"on": "Ligado"
|
||||
},
|
||||
"default": "Visualização padrão",
|
||||
"interaction_seconds": "",
|
||||
"reset_after_interaction": "",
|
||||
"scan": {
|
||||
"actions": {
|
||||
"editor_label": "",
|
||||
@@ -399,7 +401,6 @@
|
||||
"show_trigger_status": "Exibir estado do gatilho",
|
||||
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar"
|
||||
},
|
||||
"timeout_seconds": "Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)",
|
||||
"update_cycle_camera": "Percorrer as câmeras quando a visualização padrão for atualizada",
|
||||
"update_force": "Forçar atualizações do cartão (ignore a interação do Utilizador)",
|
||||
"update_seconds": "Atualize a visualização padrão a cada X segundos (0 = nunca)",
|
||||
@@ -490,8 +491,8 @@
|
||||
"no_visible_cameras": "Sem camaras visiveis",
|
||||
"reconnecting": "A voltar a ligar",
|
||||
"timeline_no_cameras": "Nenhuma câmera do Frigate para mostrar na linha do tempo",
|
||||
"troubleshooting": "Verifique a solução de problemas",
|
||||
"too_many_automations": "",
|
||||
"troubleshooting": "Verifique a solução de problemas",
|
||||
"unknown": "Erro desconhecido",
|
||||
"upgrade_available": "Uma atualização automatizada da configuração do cartão está disponível, visite o editor visual do cartão",
|
||||
"webrtc_card_reported_error": "O cartão WebRTC relatou um erro",
|
||||
|
||||
@@ -136,6 +136,11 @@ describe('CardElementManager', () => {
|
||||
);
|
||||
expect(windowAddEventListener).toBeCalledWith('location-changed', expect.anything());
|
||||
expect(windowAddEventListener).toBeCalledWith('popstate', expect.anything());
|
||||
|
||||
expect(api.getInteractionManager().initialize).toBeCalled();
|
||||
expect(api.getFullscreenManager().initialize).toBeCalled();
|
||||
expect(api.getExpandManager().initialize).toBeCalled();
|
||||
expect(api.getMediaLoadedInfoManager().initialize).toBeCalled();
|
||||
});
|
||||
|
||||
it('should disconnect', () => {
|
||||
|
||||
@@ -360,4 +360,26 @@ describe('ConditionsManager', () => {
|
||||
manager.setState({ displayMode: 'single' });
|
||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with triggers', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const condition: FrigateCardCondition = { triggered: ['camera_1', 'camera_2'] };
|
||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||
manager.setState({ triggered: new Set(['camera_1']) });
|
||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||
manager.setState({ triggered: new Set(['camera_2', 'camera_1', 'camera_3']) });
|
||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||
manager.setState({ triggered: new Set(['camera_3']) });
|
||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with interaction', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const condition: FrigateCardCondition = { interaction: true };
|
||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||
manager.setState({ interaction: true })
|
||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||
manager.setState({ interaction: false })
|
||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,14 @@ describe('ExpandManager', () => {
|
||||
expect(manager.isExpanded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new ExpandManager(api);
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: false });
|
||||
});
|
||||
|
||||
it('should set expanded', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(true);
|
||||
|
||||
@@ -29,6 +29,17 @@ describe('FullscreenManager', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new FullscreenManager(api);
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
setScreenfulFullscreen(false);
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ fullscreen: false });
|
||||
});
|
||||
|
||||
it('should correctly determine whether in fullscreen', () => {
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
|
||||
|
||||
@@ -15,12 +15,20 @@ describe('InteractionManager', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new InteractionManager(api);
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ interaction: false });
|
||||
});
|
||||
|
||||
it('should take action when interaction is reported', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
timeout_seconds: 10,
|
||||
interaction_seconds: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -45,7 +53,7 @@ describe('InteractionManager', () => {
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
timeout_seconds: 10,
|
||||
interaction_seconds: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -64,12 +72,12 @@ describe('InteractionManager', () => {
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not take action when not configured', () => {
|
||||
it('should not take action without an interaction timeout', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
timeout_seconds: 0,
|
||||
interaction_seconds: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -81,4 +89,60 @@ describe('InteractionManager', () => {
|
||||
// true but the default view will not have been set.
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not take action without reset_after_interaction', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
reset_after_interaction: false,
|
||||
interaction_seconds: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const manager = new InteractionManager(api);
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(start);
|
||||
|
||||
manager.reportInteraction();
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
// First call is blocked by triggers (above), so interaction will report
|
||||
// true but the default view will not have been set.
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should set condition state', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
interaction_seconds: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const manager = new InteractionManager(api);
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(start);
|
||||
|
||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
||||
|
||||
manager.reportInteraction();
|
||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
interaction: true,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
interaction: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,14 @@ import { MediaLoadedInfoManager } from '../../src/card-controller/media-info-man
|
||||
import { createCardAPI, createMediaLoadedInfo } from '../test-utils.js';
|
||||
|
||||
describe('MediaLoadedInfoManager', () => {
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ media_loaded: false });
|
||||
});
|
||||
|
||||
it('should set', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
@@ -123,6 +123,10 @@ describe('TriggersManager', () => {
|
||||
manager.updateTriggerHAState(createHASS(hassInactiveState));
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
|
||||
triggered: new Set(['camera_1']),
|
||||
});
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
viewName: 'live' as const,
|
||||
cameraID: 'camera_1' as const,
|
||||
@@ -144,6 +148,9 @@ describe('TriggersManager', () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
|
||||
triggered: undefined,
|
||||
});
|
||||
|
||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||
});
|
||||
|
||||
@@ -1213,4 +1213,22 @@ describe('should handle version specific upgrades', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should move view.timeout_seconds', () => {
|
||||
const config = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
view: {
|
||||
timeout_seconds: 200,
|
||||
},
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config).toEqual({
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
view: {
|
||||
interaction_seconds: 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -288,7 +288,8 @@ describe('config defaults', () => {
|
||||
},
|
||||
filter_selected_camera: false,
|
||||
},
|
||||
timeout_seconds: 300,
|
||||
interaction_seconds: 300,
|
||||
reset_after_interaction: true,
|
||||
update_cycle_camera: false,
|
||||
update_force: false,
|
||||
update_seconds: 0,
|
||||
|
||||
+4
-4
@@ -10,10 +10,10 @@ export default defineConfig({
|
||||
// Thresholds will automatically be updated as coverage improves to avoid
|
||||
// back-sliding.
|
||||
thresholdAutoUpdate: true,
|
||||
statements: 71.82,
|
||||
branches: 60.67,
|
||||
functions: 72.86,
|
||||
lines: 71.71,
|
||||
statements: 71.95,
|
||||
branches: 60.8,
|
||||
functions: 73.04,
|
||||
lines: 71.84,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user