Rename several view variables for clarity.

This commit is contained in:
Dermot Duffy
2024-06-11 19:48:39 -07:00
parent f193f9925f
commit afb9af5eb4
40 changed files with 769 additions and 285 deletions
+32 -8
View File
@@ -13,18 +13,40 @@ view:
| `camera_select` | `current` | The [view](view.md?id=supported-views) 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. |
| `dark_mode` | `off` | 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%`. |
| `default` | `live` | The view to show in the card by default. The default camera is the first one listed. See [Supported Views](view.md?id=supported-views) below. |
| `default_reset` | | The circumstances and behavior that cause the card to reset to the default view. See below. |
| `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions.md?id=interaction) or with `reset_after_interaction` to reset the view after the interaction is complete. `0` means no interactions are reported / acted upon. |
| `keyboard_shortcuts` | See [usage](../usage/keyboard-shortcuts.md) for defaults. | Configure keyboard shortcuts. See below. |
| `render_entities` | | **YAML only**: A list of entity ids that should cause the card to re-render 'in-place'. The view/camera is not changed. `update_*` flags do not pertain/relate to the behavior of this flag. This should **very** rarely be needed, but could be useful if the card is both setting and changing HA state of the same object as could be the case for some complex `card_mod` scenarios ([example](https://github.com/dermotduffy/frigate-hass-card/issues/343)). |
| `reset_after_interaction` | `true` | If `true` the card will reset to the default configured view (i.e. 'screensaver' functionality) after `interaction_seconds` has elapsed after user interaction. |
| `triggers` | | How to react when a camera is [triggered](cameras/README.md?id=triggers). |
| `update_cycle_camera` | `false` | When set to `true` the selected camera is cycled on each default view change. |
| `default_cycle_camera` | `false` | When set to `true` the selected camera is cycled on each default view change. |
| `update_entities` | | **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`](cameras/README.md?id=triggers) for the selected camera instead. |
| `update_force` | `false` | Whether automated card updates should ignore user interaction. |
| `update_seconds` | `0` | A number of seconds after which to automatically update/refresh the default view. If the default view occurs sooner (e.g. manually) the timer will start over. `0` disables this functionality. |
## `default_reset`
Configure the circumstances and behavior that cause the card to reset to the default view. All configuration is under:
```yaml
view:
default_reset: [...]
```
| Option | Default | Description |
| ------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `after_interaction` | `true` | If `true` the card will reset to the default configured view (i.e. 'screensaver' functionality) after `interaction_seconds` has elapsed after user interaction. |
| `entities` | | A list of entities that should cause the view to reset to the default (if the entity only pertains to a particular camera use [`triggers`](cameras/README.md?id=triggers) for the selected camera instead). |
| `interaction_mode` | `inactive` | Whether the default reset should happen when the card is being interacted with. If `all`, the reset will always happen regardless. If `inactive` the reset will only be taken if the card has _not_ had human interaction recently (as defined by `view.interaction_seconds`). If `active` the reset will only be happen if the card _has_ had human interaction recently. This controls resets triggered by `entities` and `every_seconds`, but not `after_interaction` which by definition requires no interaction. |
| `every_seconds` | `0` | A number of seconds after which to automatically reset to the default view. `0` disables this functionality. |
## `keyboard_shortcuts`
All configuration is under:
```yaml
view:
keyboard_shortcuts: [...]
```
Configure the key-bindings for the builtin keyboard shortcuts. See [usage](../usage/keyboard-shortcuts.md) information for defaults on keyboard shortcuts.
| Option | Default | Description |
@@ -106,11 +128,13 @@ view:
default: live
camera_select: current
interaction_seconds: 300
update_seconds: 0
update_force: false
update_cycle_camera: false
update_entities:
- binary_sensor.my_motion_sensor
default_cycle_camera: false
default_reset:
after_interaction: false
entities:
- binary_sensor.my_motion_sensor
every_seconds: 0
interaction_mode: inactive
render_entities:
- switch.render_card
dark_mode: 'off'
+1
View File
@@ -50,6 +50,7 @@ const plugins = [
}),
typescript({
sourceMap: dev,
inlineSources: dev,
}),
json({ exclude: 'package.json' }),
replace({
+1 -2
View File
@@ -3,6 +3,7 @@ import { CameraConfig } from '../config/types';
import { localize } from '../localize/localize';
import { allPromises } from '../utils/basic';
import {
DestroyCallback,
isTriggeredState,
parseStateChangeTrigger,
subscribeToTrigger,
@@ -13,8 +14,6 @@ import { CameraManagerEngine } from './engine';
import { CameraNoIDError } from './error';
import { CameraEventCallback } from './types';
type DestroyCallback = () => Promise<void>;
export class Camera {
protected _config: CameraConfig;
protected _engine: CameraManagerEngine;
@@ -1,42 +0,0 @@
import { Timer } from '../utils/timer';
import { CardAutoRefreshAPI } from './types';
export class AutoUpdateManager {
protected _timer = new Timer();
protected _api: CardAutoRefreshAPI;
constructor(api: CardAutoRefreshAPI) {
this._api = api;
}
/**
* Set the update timer to trigger an update refresh every
* `view.update_seconds`.
*/
public startDefaultViewTimer(): void {
this._timer.stop();
const updateSeconds = this._api.getConfigManager().getConfig()?.view.update_seconds;
if (updateSeconds) {
this._timer.start(updateSeconds, () => {
if (this._isAutomatedUpdateAllowed()) {
this._api.getViewManager().setViewDefault();
} else {
// Not allowed to update this time around, but try again at the next
// interval.
this.startDefaultViewTimer();
}
});
}
}
protected _isAutomatedUpdateAllowed(): boolean {
const triggers = this._api.getTriggersManager();
const config = this._api.getConfigManager().getConfig();
const interactionManager = this._api.getInteractionManager();
return (
!triggers.isTriggered() &&
(config?.view.update_force || !interactionManager.hasInteraction())
);
}
}
@@ -67,6 +67,7 @@ export class CardElementManager {
this._api.getMediaLoadedInfoManager().initialize();
this._api.getMicrophoneManager().initialize();
this._api.getKeyboardStateManager().initialize();
this._api.getDefaultManager().initialize();
// Whether or not the card is in panel mode on the dashboard.
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
@@ -123,6 +124,7 @@ export class CardElementManager {
this._api.getFullscreenManager().disconnect();
this._api.getKeyboardStateManager().uninitialize();
this._api.getActionsManager().uninitialize();
this._api.getDefaultManager().uninitialize();
// Uninitialize cameras to cause them to reinitialize on
// reconnection, to ensure the state subscription/unsubscription works
@@ -142,6 +142,18 @@ export class ConfigManager {
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
}
if (
previousConfig &&
!isEqual(
previousConfig?.view.default_reset,
this._overriddenConfig?.view.default_reset,
)
) {
this._api
.getInitializationManager()
.uninitialize(InitializationAspect.DEFAULT_RESET);
}
this._api.getCardElementManager().update();
}
}
+9 -8
View File
@@ -6,7 +6,7 @@ import { EntityRegistryManager } from '../utils/ha/entity-registry';
import { EntityCache } from '../utils/ha/entity-registry/cache';
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
import { ActionsManager } from './actions/actions-manager';
import { AutoUpdateManager } from './auto-update-manager';
import { DefaultManager } from './default-manager';
import { AutomationsManager } from './automations-manager';
import { CameraURLManager } from './camera-url-manager';
import {
@@ -32,12 +32,12 @@ import { StyleManager } from './style-manager';
import { TriggersManager } from './triggers-manager';
import {
CardActionsManagerAPI,
CardAutoRefreshAPI,
CardAutomationsAPI,
CardCameraAPI,
CardCameraURLAPI,
CardConditionAPI,
CardConfigAPI,
CardDefaultManagerAPI,
CardDownloadAPI,
CardElementAPI,
CardExpandAPI,
@@ -62,11 +62,11 @@ export class CardController
implements
CardActionsManagerAPI,
CardAutomationsAPI,
CardAutoRefreshAPI,
CardCameraAPI,
CardCameraURLAPI,
CardConditionAPI,
CardConfigAPI,
CardDefaultManagerAPI,
CardDownloadAPI,
CardElementAPI,
CardExpandAPI,
@@ -92,12 +92,12 @@ export class CardController
protected _actionsManager = new ActionsManager(this);
protected _automationsManager = new AutomationsManager(this);
protected _autoUpdateManager = new AutoUpdateManager(this);
protected _cameraManager = new CameraManager(this);
protected _cameraURLManager = new CameraURLManager(this);
protected _cardElementManager: CardElementManager;
protected _conditionsManager: ConditionsManager;
protected _configManager = new ConfigManager(this);
protected _defaultManager = new DefaultManager(this);
protected _downloadManager = new DownloadManager(this);
protected _expandManager = new ExpandManager(this);
protected _fullscreenManager = new FullscreenManager(this);
@@ -143,10 +143,6 @@ export class CardController
return this._automationsManager;
}
public getAutoUpdateManager(): AutoUpdateManager {
return this._autoUpdateManager;
}
public getCameraManager(): CameraManager {
return this._cameraManager;
}
@@ -171,6 +167,11 @@ export class CardController
public getConfigManager(): ConfigManager {
return this._configManager;
}
public getDefaultManager(): DefaultManager {
return this._defaultManager;
}
public getDownloadManager(): DownloadManager {
return this._downloadManager;
}
+92
View File
@@ -0,0 +1,92 @@
import PQueue from 'p-queue';
import { DestroyCallback, subscribeToTrigger } from '../utils/ha';
import { isActionAllowedBasedOnInteractionState } from '../utils/interaction-mode';
import { Timer } from '../utils/timer';
import { CardDefaultManagerAPI } from './types';
/**
* Manages automated resetting to the default view.
*/
export class DefaultManager {
protected _timer = new Timer();
protected _api: CardDefaultManagerAPI;
protected _unsubscribeCallback: DestroyCallback | null = null;
protected _initializationLimit = new PQueue({ concurrency: 1 });
constructor(api: CardDefaultManagerAPI) {
this._api = api;
}
/**
* Initialize the default manager. Requires both hass and configuration to be
* effective (so cannot be called from just the configuration manager, as hass
* will not be available yet)
*/
public async initialize(): Promise<boolean> {
const result = await this._initializationLimit.add(() => this._reconfigure());
this._startTimer();
return !!result;
}
public uninitialize(): void {
this._timer.stop();
this._unsubscribeCallback?.();
this._unsubscribeCallback = null;
}
protected async _reconfigure(): Promise<boolean> {
const hass = this._api.getHASSManager().getHASS();
const config = this._api.getConfigManager().getConfig()?.view.default_reset;
if (!hass || !config) {
return false;
}
if (this._unsubscribeCallback) {
await this._unsubscribeCallback();
}
this._unsubscribeCallback = await subscribeToTrigger(
hass,
() => this._setToDefaultIfAllowed(),
{
entityID: config.entities,
platform: 'state',
stateOnly: true,
},
);
// If the timer is running, restart it with the newly configured timer.
if (this._timer.isRunning()) {
this._timer.stop();
this._startTimer();
}
return true;
}
protected _setToDefaultIfAllowed(): void {
if (this._isAutomatedUpdateAllowed()) {
this._api.getViewManager().setViewDefault();
}
}
protected _isAutomatedUpdateAllowed(): boolean {
const interactionMode = this._api.getConfigManager().getConfig()?.view
.default_reset.interaction_mode;
return (
!!interactionMode &&
isActionAllowedBasedOnInteractionState(
interactionMode,
this._api.getInteractionManager().hasInteraction(),
)
);
}
protected _startTimer(): void {
const timerSeconds = this._api.getConfigManager().getConfig()?.view
.default_reset.every_seconds;
if (timerSeconds) {
this._timer.startRepeated(timerSeconds, () => this._setToDefaultIfAllowed());
}
}
}
-24
View File
@@ -37,23 +37,6 @@ export class HASSManager {
this._hass = hass;
if (
// Home Assistant pumps a lot of updates through. Re-rendering the card is
// necessary at times (e.g. to update the 'clip' view as new clips
// arrive), but also is a jarring experience for the user (e.g. if they
// are browsing the mini-gallery). Do not allow re-rendering from a Home
// Assistant update if there's been recent interaction (e.g. clicks on the
// card) or if there is media active playing.
this._isAutomatedViewUpdateAllowed() &&
isHassDifferent(
this._hass,
oldHass,
this._api.getConfigManager().getConfig()?.view.update_entities ?? [],
)
) {
// If entities being monitored have changed then reset the view to the
// default.
this._api.getViewManager().setViewDefault();
} else if (
isHassDifferent(this._hass, oldHass, [
...(this._api.getConfigManager().getConfig()?.view.render_entities ?? []),
@@ -75,11 +58,4 @@ export class HASSManager {
// Dark mode may depend on HASS.
this._api.getStyleManager().setLightOrDarkMode();
}
protected _isAutomatedViewUpdateAllowed(): boolean {
return (
this._api.getConfigManager().getConfig()?.view.update_force ||
!this._api.getInteractionManager().hasInteraction()
);
}
}
@@ -9,6 +9,7 @@ export enum InitializationAspect {
MEDIA_PLAYERS = 'media-players',
CAMERAS = 'cameras',
MICROPHONE_CONNECT = 'microphone-connect',
DEFAULT_RESET = 'default-reset',
}
export class InitializationManager {
@@ -125,6 +126,7 @@ export class InitializationManager {
if (
this._initializer.isInitializedMultiple([
InitializationAspect.DEFAULT_RESET,
...(config.menu.buttons.media_player.enabled
? [InitializationAspect.MEDIA_PLAYERS]
: []),
@@ -135,6 +137,8 @@ export class InitializationManager {
if (
!(await this._initializer.initializeMultipleIfNecessary({
[InitializationAspect.DEFAULT_RESET]: async () =>
await this._api.getDefaultManager().initialize(),
...(config.menu.buttons.media_player.enabled && {
[InitializationAspect.MEDIA_PLAYERS]: async () =>
await this._api.getMediaPlayerManager().initialize(),
+4 -1
View File
@@ -41,7 +41,10 @@ export class InteractionManager {
this._api.getConditionsManager().setState({ interaction: false });
if (!this._api.getTriggersManager().isTriggered()) {
if (this._api.getConfigManager().getConfig()?.view.reset_after_interaction) {
if (
this._api.getConfigManager().getConfig()?.view.default_reset
.after_interaction
) {
this._api.getViewManager().setViewDefault();
}
this._api.getStyleManager().setLightOrDarkMode();
@@ -1,5 +1,5 @@
import { CardKeyboardStateAPI, KeysState } from './types';
import isEqual from 'lodash/isEqual';
import isEqual from 'lodash-es/isEqual';
export class KeyboardStateManager {
protected _api: CardKeyboardStateAPI;
+13 -9
View File
@@ -3,7 +3,7 @@ import type { ConditionsManager } from './conditions-manager';
import type { EntityRegistryManager } from '../utils/ha/entity-registry';
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
import type { ActionsManager } from './actions/actions-manager';
import type { AutoUpdateManager } from './auto-update-manager';
import type { DefaultManager } from './default-manager';
import type { AutomationsManager } from './automations-manager';
import type { CameraURLManager } from './camera-url-manager';
import type { CardElementManager } from './card-element-manager';
@@ -60,13 +60,6 @@ export interface CardAutomationsAPI {
getMessageManager(): MessageManager;
}
export interface CardAutoRefreshAPI {
getConfigManager(): ConfigManager;
getInteractionManager(): InteractionManager;
getTriggersManager(): TriggersManager;
getViewManager(): ViewManager;
}
export interface CardCameraAPI {
getActionsManager(): ActionsManager;
getConfigManager(): ConfigManager;
@@ -92,6 +85,7 @@ export interface CardConfigAPI {
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
getConfigManager(): ConfigManager;
getDefaultManager(): DefaultManager;
getInitializationManager(): InitializationManager;
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
getMessageManager(): MessageManager;
@@ -104,6 +98,14 @@ export interface CardConfigLoaderAPI {
getAutomationsManager(): AutomationsManager;
}
export interface CardDefaultManagerAPI {
getConfigManager(): ConfigManager;
getHASSManager(): HASSManager;
getInteractionManager(): InteractionManager;
getTriggersManager(): TriggersManager;
getViewManager(): ViewManager;
}
export interface CardDownloadAPI {
getCameraManager(): CameraManager;
getHASSManager(): HASSManager;
@@ -115,6 +117,7 @@ export interface CardDownloadAPI {
export interface CardElementAPI {
getActionsManager(): ActionsManager;
getCameraManager(): CameraManager;
getDefaultManager(): DefaultManager;
getExpandManager(): ExpandManager;
getFullscreenManager(): FullscreenManager;
getInitializationManager(): InitializationManager;
@@ -143,6 +146,7 @@ export interface CardHASSAPI {
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
getConfigManager(): ConfigManager;
getDefaultManager(): DefaultManager;
getInteractionManager(): InteractionManager;
getMediaPlayerManager(): MediaPlayerManager;
getMessageManager(): MessageManager;
@@ -155,6 +159,7 @@ export interface CardInitializerAPI {
getCameraManager(): CameraManager;
getCardElementManager(): CardElementManager;
getConfigManager(): ConfigManager;
getDefaultManager(): DefaultManager;
getEntityRegistryManager(): EntityRegistryManager;
getHASSManager(): HASSManager;
getMediaPlayerManager(): MediaPlayerManager;
@@ -233,7 +238,6 @@ export interface CardTriggersAPI {
}
export interface CardViewAPI {
getAutoUpdateManager(): AutoUpdateManager;
getCameraManager(): CameraManager;
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
+1 -5
View File
@@ -55,7 +55,7 @@ export class ViewManager {
let forceCameraID: string | null = params?.cameraID ?? null;
const viewName = config.view.default;
if (!forceCameraID && this._view?.camera && config.view.update_cycle_camera) {
if (!forceCameraID && this._view?.camera && config.view.default_cycle_camera) {
const cameraIDs = [
...getCameraIDsForViewName(this._api.getCameraManager(), viewName),
];
@@ -69,10 +69,6 @@ export class ViewManager {
viewName: viewName,
...(forceCameraID && { cameraID: forceCameraID }),
});
// Restart the refresh timer, so the default view is refreshed at a fixed
// interval from now (if so configured).
this._api.getAutoUpdateManager().startDefaultViewTimer();
}
}
+17
View File
@@ -25,7 +25,11 @@ import {
CONF_OVERRIDES,
CONF_PROFILES,
CONF_TIMELINE_EVENTS_MEDIA_TYPE,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
CONF_VIEW_INTERACTION_SECONDS,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_ENTITIES,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_TRIGGERS,
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
@@ -805,4 +809,17 @@ const UPGRADES = [
keepOriginal: true,
}),
upgradeWithOverrides('live.controls.ptz', ptzControlSettingsTransform),
upgradeMoveToWithOverrides('view.update_cycle_camera', CONF_VIEW_DEFAULT_CYCLE_CAMERA),
upgradeMoveToWithOverrides(
'view.update_force',
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
{
transform: (val) => (val === true ? 'all' : null),
},
),
upgradeMoveToWithOverrides(
'view.update_seconds',
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
),
upgradeMoveToWithOverrides('view.update_entities', CONF_VIEW_DEFAULT_RESET_ENTITIES),
];
+1 -1
View File
@@ -137,5 +137,5 @@ export const LOW_PERFORMANCE_PROFILE = {
[CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS]: 10,
// No trigger actions.
[CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER]: 'none'
[CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER]: 'none',
};
+26 -15
View File
@@ -1396,16 +1396,18 @@ const viewConfigDefault = {
default: FRIGATE_CARD_VIEW_DEFAULT,
camera_select: 'current' as const,
interaction_seconds: 300,
reset_after_interaction: true,
update_seconds: 0,
update_force: false,
update_cycle_camera: false,
default_reset: {
every_seconds: 0,
after_interaction: false,
entities: [],
interaction_mode: 'inactive' as const,
},
default_cycle_camera: false,
dark_mode: 'off' as const,
triggers: {
show_trigger_status: false,
filter_selected_camera: true,
actions: {
interaction_mode: 'inactive' as const,
trigger: 'update' as const,
untrigger: 'none' as const,
},
@@ -1414,12 +1416,13 @@ const viewConfigDefault = {
keyboard_shortcuts: keyboardShortcutsDefault,
};
const interactionModeSchema = z.enum(['all', 'inactive', 'active']).default('inactive');
export type InteractionMode = z.infer<typeof interactionModeSchema>;
export const triggersSchema = z.object({
actions: z
.object({
interaction_mode: z
.enum(['all', 'inactive', 'active'])
.default(viewConfigDefault.triggers.actions.interaction_mode),
interaction_mode: interactionModeSchema,
trigger: z
.enum(['default', 'live', 'media', 'none', 'update'])
.default(viewConfigDefault.triggers.actions.trigger),
@@ -1447,13 +1450,21 @@ const viewConfigSchema = z
.enum([...FRIGATE_CARD_VIEWS_USER_SPECIFIED, 'current'])
.default(viewConfigDefault.camera_select),
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),
update_entities: z.string().array().optional(),
default_cycle_camera: z.boolean().default(viewConfigDefault.default_cycle_camera),
default_reset: z
.object({
after_interaction: z
.boolean()
.default(viewConfigDefault.default_reset.after_interaction),
every_seconds: z.number().default(viewConfigDefault.default_reset.every_seconds),
entities: z.string().array().default(viewConfigDefault.default_reset.entities),
interaction_mode: interactionModeSchema.default(
viewConfigDefault.default_reset.interaction_mode,
),
})
.default(viewConfigDefault.default_reset),
render_entities: z.string().array().optional(),
dark_mode: z.enum(['on', 'off', 'auto']).optional(),
triggers: triggersSchema.default(viewConfigDefault.triggers),
+11 -5
View File
@@ -123,11 +123,17 @@ export const CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT =
`${CONF_VIEW_KEYBOARD_SHORTCUTS}.ptz_zoom_out` as const;
export const CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_HOME =
`${CONF_VIEW_KEYBOARD_SHORTCUTS}.ptz_home` 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_DEFAULT_CYCLE_CAMERA =
`${CONF_VIEW}.default_cycle_camera` as const;
export const CONF_VIEW_DEFAULT_RESET = `${CONF_VIEW}.default_reset` as const;
export const CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE =
`${CONF_VIEW_DEFAULT_RESET}.interaction_mode` as const;
export const CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS =
`${CONF_VIEW_DEFAULT_RESET}.every_seconds` as const;
export const CONF_VIEW_DEFAULT_RESET_ENTITIES =
`${CONF_VIEW_DEFAULT_RESET}.entities` as const;
export const CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION =
`${CONF_VIEW_DEFAULT_RESET}.after_interaction` as const;
export const CONF_VIEW_TRIGGERS = `${CONF_VIEW}.triggers` as const;
export const CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS =
`${CONF_VIEW_TRIGGERS}.show_trigger_status` as const;
+56 -16
View File
@@ -201,7 +201,7 @@ import {
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_UP,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_IN,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT,
CONF_VIEW_RESET_AFTER_INTERACTION,
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
CONF_VIEW_TRIGGERS,
CONF_VIEW_TRIGGERS_ACTIONS,
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
@@ -210,10 +210,12 @@ import {
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS,
CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
MEDIA_CHUNK_SIZE_MAX,
CONF_VIEW_DEFAULT_RESET,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_DEFAULT_RESET_ENTITIES,
} from './const.js';
import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss';
@@ -263,6 +265,7 @@ const MENU_PERFORMANCE_FEATURES = 'performance.features';
const MENU_PERFORMANCE_STYLE = 'performance.style';
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
const MENU_VIEW_KEYBOARD_SHORTCUTS = 'view.keyboard_shortcuts';
const MENU_VIEW_DEFAULT_RESET = 'view.default_reset';
const MENU_VIEW_TRIGGERS = 'view.triggers';
const MENU_VIEW_TRIGGERS_ACTIONS = 'view.triggers.actions';
@@ -813,6 +816,22 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
},
];
protected _defaultResetInteractionModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'all',
label: localize('config.view.default_reset.interaction_modes.all'),
},
{
value: 'inactive',
label: localize('config.view.default_reset.interaction_modes.inactive'),
},
{
value: 'active',
label: localize('config.view.default_reset.interaction_modes.active'),
},
];
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the full configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -1068,6 +1087,36 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
);
}
protected _renderViewDefaultResetMenu(): TemplateResult {
return this._putInSubmenu(
MENU_VIEW_DEFAULT_RESET,
true,
`config.${CONF_VIEW_DEFAULT_RESET}.editor_label`,
{ name: 'mdi:restart' },
html`
${this._renderSwitch(
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
this._defaults.view.default_reset.after_interaction,
)}
${this._renderNumberInput(CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS)}
${this._renderOptionSelector(
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
this._defaultResetInteractionModes,
{
label: localize('config.view.default_reset.interaction_mode'),
},
)},
${this._renderOptionSelector(
CONF_VIEW_DEFAULT_RESET_ENTITIES,
this.hass ? getEntitiesFromHASS(this.hass) : [],
{
multiple: true,
},
)}
`,
);
}
protected _renderViewTriggersMenu(): TemplateResult {
return this._putInSubmenu(
MENU_VIEW_TRIGGERS,
@@ -2330,19 +2379,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)}
${this._renderNumberInput(CONF_VIEW_INTERACTION_SECONDS)}
${this._renderSwitch(
CONF_VIEW_RESET_AFTER_INTERACTION,
this._defaults.view.reset_after_interaction,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
this._defaults.view.default_cycle_camera,
)}
${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)}
${this._renderSwitch(
CONF_VIEW_UPDATE_FORCE,
this._defaults.view.update_force,
)}
${this._renderSwitch(
CONF_VIEW_UPDATE_CYCLE_CAMERA,
this._defaults.view.update_cycle_camera,
)}
${this._renderViewTriggersMenu()}
${this._renderViewDefaultResetMenu()} ${this._renderViewTriggersMenu()}
${this._renderViewKeyboardShortcutMenu()}
</div>
`
+21 -5
View File
@@ -436,12 +436,31 @@
"on": "Activat"
},
"default": "Vista per defecte",
"default_cycle_camera": "Passeu per les càmeres quan s'actualitzi la vista predeterminada",
"default_reset": {
"after_interaction": "Restableix la vista predeterminada després de la interacció de l'usuari",
"editor_label": "",
"entities": "",
"every_seconds": "Actualitza la vista predeterminada cada X segons (0=mai)",
"interaction_mode": "",
"interaction_modes": {
"active": "",
"all": "",
"inactive": ""
}
},
"interaction_seconds": "Segons després de l'acció de l'usuari per continuar interactuant (0=mai)",
"keyboard_shortcuts": {
"editor_label": "",
"enabled": ""
"enabled": "",
"ptz_down": "",
"ptz_home": "",
"ptz_left": "",
"ptz_right": "",
"ptz_up": "",
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"reset_after_interaction": "Restableix la vista predeterminada després de la interacció de l'usuari",
"triggers": {
"actions": {
"editor_label": "Activar accions",
@@ -469,9 +488,6 @@
"show_trigger_status": "Mostra la vora intermitent quan s'activa",
"untrigger_seconds": "Segons després del canvi d'estat inactiu a desactivat"
},
"update_cycle_camera": "Passeu per les càmeres quan s'actualitzi la vista predeterminada",
"update_force": "Força les actualitzacions de la targeta (ignora la interacció de l'usuari)",
"update_seconds": "Actualitza la vista predeterminada cada X segons (0=mai)",
"views": {
"clip": "Clip més recent",
"clips": "Galeria de clips",
+14 -5
View File
@@ -362,7 +362,6 @@
"camera_ui": "Camera user interface",
"cameras": "Cameras",
"clips": "Clips",
"ptz_home": "PTZ Home",
"display_mode": "Display mode",
"download": "Download",
"enabled": "Button enabled",
@@ -378,6 +377,7 @@
"play": "Play / Pause",
"priority": "Priority",
"ptz_controls": "Show PTZ controls",
"ptz_home": "PTZ Home",
"recordings": "Recordings",
"screenshot": "Screenshot",
"snapshots": "Snapshots",
@@ -436,6 +436,19 @@
"on": "On"
},
"default": "Default view",
"default_cycle_camera": "Cycle through cameras when default view updates",
"default_reset": {
"after_interaction": "Reset to the default view after user interaction ends",
"editor_label": "Default view reset behavior",
"entities": "Reset to the default view on entity state change",
"every_seconds": "Reset to default view every X seconds (0=never)",
"interaction_mode": "How default reset behaves when the card has human interaction",
"interaction_modes": {
"active": "Only allow reset when card has active human interaction",
"all": "Reset regardless of human interaction",
"inactive": "Only reset when card has no human interaction"
}
},
"interaction_seconds": "Seconds after user action to remain interacted with (0=never)",
"keyboard_shortcuts": {
"editor_label": "Keyboard shortcuts",
@@ -448,7 +461,6 @@
"ptz_zoom_in": "PTZ Zoom In",
"ptz_zoom_out": "PTZ Zoom Out"
},
"reset_after_interaction": "Reset to the default view after user interaction",
"triggers": {
"actions": {
"editor_label": "Trigger actions",
@@ -476,9 +488,6 @@
"show_trigger_status": "Show pulsing border when triggered",
"untrigger_seconds": "Seconds after inactive state change to untrigger"
},
"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)",
"views": {
"clip": "Most recent clip",
"clips": "Clips gallery",
+21 -5
View File
@@ -436,12 +436,31 @@
"on": "Activé"
},
"default": "Vue par défaut",
"default_cycle_camera": "Parcourez les caméras lorsque la vue par défaut est mise à jour",
"default_reset": {
"after_interaction": "",
"editor_label": "",
"entities": "",
"every_seconds": "Actualiser la vue par défaut toutes les X secondes (0=jamais)",
"interaction_mode": "",
"interaction_modes": {
"active": "",
"all": "",
"inactive": ""
}
},
"interaction_seconds": "",
"keyboard_shortcuts": {
"editor_label": "",
"enabled": ""
"enabled": "",
"ptz_down": "",
"ptz_home": "",
"ptz_left": "",
"ptz_right": "",
"ptz_up": "",
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"reset_after_interaction": "",
"triggers": {
"actions": {
"editor_label": "",
@@ -469,9 +488,6 @@
"show_trigger_status": "Afficher la bordure clignotante lors du déclenchement",
"untrigger_seconds": "Quelques secondes après le changement d'état inactif pour débloquer"
},
"update_cycle_camera": "Parcourez les caméras lorsque la vue par défaut est mise à jour",
"update_force": "Forcer les mises à jour de la carte (ignorer l'interaction de l'utilisateur)",
"update_seconds": "Actualiser la vue par défaut toutes les X secondes (0=jamais)",
"views": {
"clip": "Clip le plus récent",
"clips": "Galerie de clips",
+21 -5
View File
@@ -436,12 +436,31 @@
"on": "On"
},
"default": "Visualizzazione predefinita",
"default_cycle_camera": "Scorri le telecamere quando si aggiorna la visualizzazione predefinita",
"default_reset": {
"after_interaction": "",
"editor_label": "",
"entities": "",
"every_seconds": "Aggiorna la visualizzazione predefinita ogni x secondi (0 = mai)",
"interaction_mode": "",
"interaction_modes": {
"active": "",
"all": "",
"inactive": ""
}
},
"interaction_seconds": "",
"keyboard_shortcuts": {
"editor_label": "",
"enabled": ""
"enabled": "",
"ptz_down": "",
"ptz_home": "",
"ptz_left": "",
"ptz_right": "",
"ptz_up": "",
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"reset_after_interaction": "",
"triggers": {
"actions": {
"editor_label": "",
@@ -469,9 +488,6 @@
"show_trigger_status": "Mostra bordo pulsante quando attivato",
"untrigger_seconds": "Reimposta la vista ai valori predefiniti dopo aver annullato l'attivazione"
},
"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)",
"views": {
"clip": "Clip più recente",
"clips": "Galleria delle clip",
+21 -5
View File
@@ -436,12 +436,31 @@
"on": "Ligado"
},
"default": "Visualização padrão",
"default_cycle_camera": "Percorrer as câmeras quando a visualização padrão for atualizada",
"default_reset": {
"after_interaction": "",
"editor_label": "",
"entities": "",
"every_seconds": "Atualize a visualização padrão a cada X segundos (0 = nunca)",
"interaction_mode": "",
"interaction_modes": {
"active": "",
"all": "",
"inactive": ""
}
},
"interaction_seconds": "",
"keyboard_shortcuts": {
"editor_label": "",
"enabled": ""
"enabled": "",
"ptz_down": "",
"ptz_home": "",
"ptz_left": "",
"ptz_right": "",
"ptz_up": "",
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"reset_after_interaction": "",
"triggers": {
"actions": {
"editor_label": "",
@@ -469,9 +488,6 @@
"show_trigger_status": "Pulsar borda quando acionado",
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar"
},
"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)",
"views": {
"clip": "Clipe mais recente",
"clips": "Galeria de clipes",
+21 -5
View File
@@ -436,12 +436,31 @@
"on": "Ligado"
},
"default": "Visualização padrão",
"default_cycle_camera": "Percorrer as câmeras quando a visualização padrão for atualizada",
"default_reset": {
"after_interaction": "",
"editor_label": "",
"entities": "",
"every_seconds": "Atualize a visualização padrão a cada X segundos (0 = nunca)",
"interaction_mode": "",
"interaction_modes": {
"active": "",
"all": "",
"inactive": ""
}
},
"interaction_seconds": "",
"keyboard_shortcuts": {
"editor_label": "",
"enabled": ""
"enabled": "",
"ptz_down": "",
"ptz_home": "",
"ptz_left": "",
"ptz_right": "",
"ptz_up": "",
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"reset_after_interaction": "",
"triggers": {
"actions": {
"editor_label": "",
@@ -469,9 +488,6 @@
"show_trigger_status": "Exibir estado do gatilho",
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar"
},
"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)",
"views": {
"clip": "Clipe mais recente",
"clips": "Galeria de clipes",
+2
View File
@@ -24,6 +24,8 @@ import {
SubscriptionUnsubscribe,
} from './types.js';
export type DestroyCallback = () => Promise<void>;
/**
* Make a HomeAssistant websocket request. May throw.
* @param hass The HomeAssistant object to send the request with.
+15
View File
@@ -0,0 +1,15 @@
import { InteractionMode } from '../config/types';
export const isActionAllowedBasedOnInteractionState = (
interactionMode: InteractionMode,
interactionState: boolean,
): boolean => {
switch (interactionMode) {
case 'all':
return true;
case 'active':
return interactionState;
case 'inactive':
return !interactionState;
}
};
@@ -1,73 +0,0 @@
import { add } from 'date-fns';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AutoUpdateManager } from '../../src/card-controller/auto-update-manager';
import { createCardAPI, createConfig } from '../test-utils';
// @vitest-environment jsdom
describe('AutoUpdateManager', () => {
const start = new Date('2023-09-23T19:12:00');
afterEach(() => {
vi.useRealTimers();
});
it('should set default view when allowed', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
update_seconds: 10,
},
}),
);
// Card is triggered.
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(true);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
vi.useFakeTimers();
vi.setSystemTime(start);
const manager = new AutoUpdateManager(api);
manager.startDefaultViewTimer();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
vi.setSystemTime(add(start, { seconds: 10 }));
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false);
vi.setSystemTime(add(start, { seconds: 20 }));
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).toBeCalled();
});
it('should not set default view when not configured', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
update_seconds: 0,
},
}),
);
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
vi.useFakeTimers();
vi.setSystemTime(start);
const manager = new AutoUpdateManager(api);
manager.startDefaultViewTimer();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
vi.setSystemTime(add(start, { seconds: 10 }));
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
});
@@ -301,5 +301,38 @@ describe('ConfigManager', () => {
InitializationAspect.MICROPHONE_CONNECT,
);
});
it('view.default_reset', () => {
const api = createCardAPI();
const manager = new ConfigManager(api);
const config_1 = {
type: 'custom:frigate-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
default_reset: {
every_seconds: 1,
},
},
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
manager.setConfig(config_1);
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
const config_2 = {
...config_1,
view: {
default_reset: {
every_seconds: 2,
},
},
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
manager.computeOverrideConfig();
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
InitializationAspect.DEFAULT_RESET,
);
});
});
});
+5 -5
View File
@@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CameraManager } from '../../src/camera-manager/manager';
import { ActionsManager } from '../../src/card-controller/actions/actions-manager';
import { AutoUpdateManager } from '../../src/card-controller/auto-update-manager';
import { AutomationsManager } from '../../src/card-controller/automations-manager';
import { CameraURLManager } from '../../src/card-controller/camera-url-manager';
import {
@@ -11,6 +10,7 @@ import {
import { ConditionsManager } from '../../src/card-controller/conditions-manager';
import { ConfigManager } from '../../src/card-controller/config/config-manager';
import { CardController } from '../../src/card-controller/controller';
import { DefaultManager } from '../../src/card-controller/default-manager';
import { DownloadManager } from '../../src/card-controller/download-manager';
import { ExpandManager } from '../../src/card-controller/expand-manager';
import { FullscreenManager } from '../../src/card-controller/fullscreen-manager';
@@ -32,12 +32,12 @@ import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
vi.mock('../../src/camera-manager/manager');
vi.mock('../../src/card-controller/actions/actions-manager');
vi.mock('../../src/card-controller/auto-update-manager');
vi.mock('../../src/card-controller/automations-manager');
vi.mock('../../src/card-controller/camera-url-manager');
vi.mock('../../src/card-controller/card-element-manager');
vi.mock('../../src/card-controller/conditions-manager');
vi.mock('../../src/card-controller/config/config-manager');
vi.mock('../../src/card-controller/default-manager');
vi.mock('../../src/card-controller/download-manager');
vi.mock('../../src/card-controller/expand-manager');
vi.mock('../../src/card-controller/fullscreen-manager');
@@ -107,9 +107,9 @@ describe('CardController', () => {
);
});
it('getAutoUpdateManager', () => {
expect(createController().getAutoUpdateManager()).toBe(
vi.mocked(AutoUpdateManager).mock.instances[0],
it('getDefaultManager', () => {
expect(createController().getDefaultManager()).toBe(
vi.mocked(DefaultManager).mock.instances[0],
);
});
@@ -0,0 +1,163 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DefaultManager } from '../../src/card-controller/default-manager';
import {
callHASubscribeMessageHandler,
createCardAPI,
createConfig,
createHASS,
} from '../test-utils';
// @vitest-environment jsdom
describe('DefaultManager', () => {
afterEach(() => {
vi.useRealTimers();
});
describe('time based', () => {
it('should set default view when allowed', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default_reset: {
every_seconds: 10,
},
},
}),
);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true);
vi.useFakeTimers();
const manager = new DefaultManager(api);
await manager.initialize();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).toBeCalledTimes(1);
manager.uninitialize();
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).toBeCalledTimes(1);
});
it('should not set default view when not configured', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default_reset: {
every_seconds: 0,
},
},
}),
);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
vi.useFakeTimers();
const manager = new DefaultManager(api);
manager.initialize();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
it('should restart timer when reconfigured', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default_reset: {
every_seconds: 10,
},
},
}),
);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
const hass = createHASS();
const unsubcribeCallback = vi.fn();
vi.mocked(hass.connection.subscribeMessage).mockResolvedValue(unsubcribeCallback);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.useFakeTimers();
const manager = new DefaultManager(api);
await manager.initialize();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewDefault).toBeCalled();
});
});
describe('state based', () => {
it('should set default view when state changed', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default_reset: {
every_seconds: 10,
},
},
}),
);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
const hass = createHASS();
const unsubcribeCallback = vi.fn();
vi.mocked(hass.connection.subscribeMessage).mockResolvedValue(unsubcribeCallback);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const manager = new DefaultManager(api);
await manager.initialize();
callHASubscribeMessageHandler(hass, {
variables: {
trigger: {
from_state: {
entity_id: 'binary_sensor.foo',
state: 'off',
},
to_state: {
entity_id: 'binary_sensor.foo',
state: 'on',
},
},
},
});
await manager.initialize();
expect(unsubcribeCallback).toBeCalledTimes(1);
expect(api.getViewManager().setViewDefault).toBeCalledTimes(1);
});
it('should not monitor state without config', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const manager = new DefaultManager(api);
await manager.initialize();
const mock = vi.mocked(hass.connection.subscribeMessage).mock;
expect(mock.calls.length).toBe(0);
});
});
});
+7 -25
View File
@@ -146,7 +146,7 @@ describe('HASSManager', () => {
});
});
describe('should set default view when', () => {
describe('should not set default view when', () => {
it('selected camera is unknown', () => {
const api = createAPIWithoutMediaPlayers();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
@@ -178,15 +178,18 @@ describe('HASSManager', () => {
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
it('view.update_entities changes', () => {
it('when there is card interaction', () => {
const api = createAPIWithoutMediaPlayers();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
update_entities: ['sensor.force_default_view'],
default_reset: {
entities: ['sensor.force_default_view'],
},
},
}),
);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true);
const manager = new HASSManager(api);
const hass = createHASS({
@@ -195,7 +198,7 @@ describe('HASSManager', () => {
manager.setHASS(hass);
expect(api.getViewManager().setViewDefault).toBeCalled();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
});
@@ -236,25 +239,4 @@ describe('HASSManager', () => {
expect(api.getCardElementManager().update).toBeCalled();
});
});
it('set view default is not called when there is card interaction', () => {
const api = createAPIWithoutMediaPlayers();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
update_entities: ['sensor.force_default_view'],
},
}),
);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true);
const manager = new HASSManager(api);
const hass = createHASS({
'sensor.force_default_view': createStateEntity(),
});
manager.setHASS(hass);
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
});
@@ -208,9 +208,13 @@ describe('InitializationManager', () => {
expect(await manager.initializeBackgroundIfNecessary()).toBeFalsy();
});
it('successfully with minimal initializers', async () => {
it('successfully when already initialized', async () => {
const api = createCardAPI();
const manager = new InitializationManager(api);
const initializer = mock<Initializer>();
initializer.isInitializedMultiple.mockReturnValue(true);
const manager = new InitializationManager(api, initializer);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
@@ -226,6 +230,8 @@ describe('InitializationManager', () => {
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
expect(api.getMediaPlayerManager().initialize).not.toBeCalled();
expect(api.getDefaultManager().initialize).not.toBeCalled();
expect(api.getCardElementManager().update).not.toBeCalled();
});
it('successfully with all inititalizers', async () => {
@@ -246,10 +252,11 @@ describe('InitializationManager', () => {
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
expect(api.getMediaPlayerManager().initialize).toBeCalled();
expect(api.getDefaultManager().initialize).toBeCalled();
expect(api.getCardElementManager().update).toBeCalled();
});
it('with media player in progress', async () => {
it('with initializers in progress', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
@@ -23,12 +23,15 @@ describe('InteractionManager', () => {
expect(api.getConditionsManager().setState).toBeCalledWith({ interaction: false });
});
it('should take action when interaction is reported', () => {
it('should take action after interaction ends', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
interaction_seconds: 10,
default_reset: {
after_interaction: true,
},
},
}),
);
@@ -90,12 +93,14 @@ describe('InteractionManager', () => {
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
it('should not take action without reset_after_interaction', () => {
it('should not take action without default_reset.after_interaction', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
reset_after_interaction: false,
default_reset: {
after_interaction: false,
},
interaction_seconds: 10,
},
}),
+1 -3
View File
@@ -129,7 +129,6 @@ describe('ViewManager.setViewDefault', () => {
expect(manager.getView()?.view).toBe('live');
expect(manager.getView()?.camera).toBe('camera');
expect(api.getAutoUpdateManager().startDefaultViewTimer).toBeCalled();
});
it('should not set default view without config', () => {
@@ -140,7 +139,6 @@ describe('ViewManager.setViewDefault', () => {
manager.setViewDefault();
expect(manager.getView()).toBeNull();
expect(api.getAutoUpdateManager().startDefaultViewTimer).not.toBeCalled();
});
it('should cycle camera when configured', () => {
@@ -161,7 +159,7 @@ describe('ViewManager.setViewDefault', () => {
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
update_cycle_camera: true,
default_cycle_camera: true,
},
}),
);
+94
View File
@@ -25,6 +25,7 @@ import {
frigateCardConfigSchema,
} from '../../src/config/types';
import { getParseErrorPaths } from '../../src/utils/zod';
import { update } from 'lodash-es';
describe('general functions', () => {
it('should set value', () => {
@@ -134,6 +135,7 @@ describe('upgrade functions', () => {
});
});
it('with non-number', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(createRangedTransform((_val) => 'foo')(1)).toBe('foo');
});
});
@@ -220,6 +222,7 @@ describe('upgrade functions', () => {
c: 10,
};
expect(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
moveConfigValue(config, 'c', 'd', { transform: (_val) => null }),
).toBeTruthy();
expect(config).toEqual({});
@@ -231,6 +234,7 @@ describe('upgrade functions', () => {
};
expect(
moveConfigValue(config, 'c', 'd', {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
transform: (_val) => null,
keepOriginal: true,
}),
@@ -244,6 +248,7 @@ describe('upgrade functions', () => {
c: 10,
};
expect(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
moveConfigValue(config, 'c', 'd', { transform: (_val) => undefined }),
).toBeFalsy();
expect(config).toEqual({ c: 10 });
@@ -294,11 +299,13 @@ describe('upgrade functions', () => {
describe('should upgrade array', () => {
it('in case of non-array', () => {
const config = { c: 10 };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(upgradeArrayOfObjects('c', (_val) => false)(config)).toBeFalsy();
});
it('in case of non-object items', () => {
const config = { c: [10, 11] };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(upgradeArrayOfObjects('c', (_val) => false)(config)).toBeFalsy();
});
@@ -328,6 +335,7 @@ describe('upgrade functions', () => {
describe('should recursively upgrade', () => {
it('ignoring simple objects', () => {
const config = { c: 10, d: 10 };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(upgradeObjectRecursively((_val) => false)(config)).toBeFalsy();
expect(config).toEqual({ c: 10, d: 10 });
});
@@ -3099,5 +3107,91 @@ describe('should handle version specific upgrades', () => {
postUpgradeChecks(config);
});
});
it('view.update_cycle_camera -> view.default_cycle_camera', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{}],
view: {
update_cycle_camera: true,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_cycle_camera: true,
});
postUpgradeChecks(config);
});
describe('view.update_force -> view.default_reset.interaction_mode', () => {
it('should convert to all when true', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{}],
view: {
update_force: true,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_reset: {
interaction_mode: 'all',
},
});
postUpgradeChecks(config);
});
it('should remove when false', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{}],
view: {
update_force: false,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({});
postUpgradeChecks(config);
});
});
it('view.update_seconds', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{}],
view: {
update_seconds: 42,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_reset: {
every_seconds: 42,
},
});
postUpgradeChecks(config);
});
it('view.update_entities', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{}],
view: {
update_entities: ['binary_sensor.foo', 'camera.bar'],
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_reset: {
entities: ['binary_sensor.foo', 'camera.bar'],
},
});
postUpgradeChecks(config);
});
});
});
+7 -4
View File
@@ -331,10 +331,13 @@ describe('config defaults', () => {
filter_selected_camera: true,
},
interaction_seconds: 300,
reset_after_interaction: true,
update_cycle_camera: false,
update_force: false,
update_seconds: 0,
default_cycle_camera: false,
default_reset: {
after_interaction: false,
every_seconds: 0,
entities: [],
interaction_mode: 'inactive',
},
},
});
});
+2 -2
View File
@@ -15,7 +15,7 @@ import {
CameraManagerMediaCapabilities,
} from '../src/camera-manager/types';
import { ActionsManager } from '../src/card-controller/actions/actions-manager';
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
import { DefaultManager } from '../src/card-controller/default-manager';
import { AutomationsManager } from '../src/card-controller/automations-manager';
import { CameraURLManager } from '../src/card-controller/camera-url-manager';
import { CardElementManager } from '../src/card-controller/card-element-manager';
@@ -434,7 +434,7 @@ export const createCardAPI = (): CardController => {
api.getActionsManager.mockReturnValue(mock<ActionsManager>());
api.getAutomationsManager.mockReturnValue(mock<AutomationsManager>());
api.getAutoUpdateManager.mockReturnValue(mock<AutoUpdateManager>());
api.getDefaultManager.mockReturnValue(mock<DefaultManager>());
api.getCameraManager.mockReturnValue(mock<CameraManager>());
api.getCameraURLManager.mockReturnValue(mock<CameraURLManager>());
api.getCardElementManager.mockReturnValue(mock<CardElementManager>());
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { isActionAllowedBasedOnInteractionState } from '../../src/utils/interaction-mode';
describe('isActionAllowedBasedOnInteractionState', () => {
it('should handle interactionMode: all', () => {
expect(isActionAllowedBasedOnInteractionState('all', true)).toBeTruthy();
expect(isActionAllowedBasedOnInteractionState('all', false)).toBeTruthy();
});
it('should handle interactionMode: active', () => {
expect(isActionAllowedBasedOnInteractionState('active', true)).toBeTruthy();
expect(isActionAllowedBasedOnInteractionState('active', false)).toBeFalsy();
});
it('should handle interactionMode: inactive', () => {
expect(isActionAllowedBasedOnInteractionState('inactive', true)).toBeFalsy();
expect(isActionAllowedBasedOnInteractionState('inactive', false)).toBeTruthy();
});
});
+1
View File
@@ -34,6 +34,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [
'utils/endpoint.ts',
'utils/ha/entity-registry/types.ts',
'utils/ha/types.ts',
'utils/interaction-mode.ts',
'utils/media-info.ts',
'utils/media-layout.ts',
'utils/media-to-view.ts',