refactor: Significantly refactor how conditions work internally (#1886)
- Much improved API cleanliness and testability to allow further extensibility in future - Simplified code in `live` view This technically contains a small change in how overrides work in the `live` view. Since that change is _closer_ to the documentation, and since this is likely to be rarely used, this is not considered a breaking change. Previously, overrides for a given live camera would always render _as if_ that camera was selected, vs was actually selected. Now, overrides will only apply in the live view when the camera is _actually_ selected. If this is an issue for you in practice, lets discuss.
This commit is contained in:
@@ -16,7 +16,8 @@ certain configurations (in `overrides`) or to display "picture elements" (in
|
|||||||
|
|
||||||
## `camera`
|
## `camera`
|
||||||
|
|
||||||
Matches based on the selected camera.
|
Matches based on the selected camera. Does not match other cameras (whether
|
||||||
|
visible or not).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
conditions:
|
conditions:
|
||||||
@@ -24,10 +25,10 @@ conditions:
|
|||||||
# [...]
|
# [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
| ----------- | ------------------------------------------------------------------------------------------------------------ |
|
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `condition` | Must be `camera`. |
|
| `condition` | Must be `camera`. |
|
||||||
| `cameras` | A list of camera IDs in which this condition is satisfied. See the camera [id](cameras/README.md) parameter. |
|
| `cameras` | An optional list of camera IDs in which this condition is satisfied. If not specified, any camera change will satisy the condition. See the camera [id](cameras/README.md) parameter. |
|
||||||
|
|
||||||
## `expand`
|
## `expand`
|
||||||
|
|
||||||
@@ -167,6 +168,18 @@ conditions:
|
|||||||
# [...]
|
# [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| ----------- | ------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `condition` | Must be `state`. |
|
||||||
|
| `entity` | The entity to check the state of. |
|
||||||
|
| `state` | A single entity state, or list of entity states, against which the entity state is compared. |
|
||||||
|
| `state_not` | A single entity state, or list of entity states, against which the entity state is inversely compared. |
|
||||||
|
|
||||||
|
!> If multiple state conditions are used together with neither `state` nor
|
||||||
|
`state_not` specified, this effectively means the state for multiple entities
|
||||||
|
needs to _change_ simultaneously. This is unlikely to happen in reality, and
|
||||||
|
almost certainly not useful / reliable as a condition.
|
||||||
|
|
||||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#state).
|
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#state).
|
||||||
|
|
||||||
## `triggered`
|
## `triggered`
|
||||||
@@ -226,10 +239,10 @@ conditions:
|
|||||||
# [...]
|
# [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
| ----------- | ------------------------------------------------------------------------------------------------- |
|
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `condition` | Must be `view`. |
|
| `condition` | Must be `view`. |
|
||||||
| `views` | A list of [views](view.md?id=supported-views) in which this condition is satified (e.g. `clips`). |
|
| `views` | An optional list of [views](view.md?id=supported-views) in which this condition is satified (e.g. `clips`). If not specified, any view change will satisy the condition. |
|
||||||
|
|
||||||
?> Internally, views associated with the media viewer (e.g. `clip`, `snapshot`,
|
?> Internally, views associated with the media viewer (e.g. `clip`, `snapshot`,
|
||||||
`recording`) are translated to a special view called `media` after the relevant
|
`recording`) are translated to a special view called `media` after the relevant
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
4px 4px;
|
4px 4px;
|
||||||
}
|
}
|
||||||
#__sidebar img {
|
#__sidebar img {
|
||||||
max-width: 100px;
|
max-width: 48px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { ConditionsManager } from '../conditions/conditions-manager.js';
|
||||||
|
import { ConditionsEvaluationResult } from '../conditions/types.js';
|
||||||
import { Automation, AutomationActions } from '../config/types.js';
|
import { Automation, AutomationActions } from '../config/types.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import { CardAutomationsAPI, TaggedAutomations } from './types.js';
|
import { CardAutomationsAPI, TaggedAutomation } from './types.js';
|
||||||
|
|
||||||
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
||||||
|
|
||||||
export class AutomationsManager {
|
export class AutomationsManager {
|
||||||
protected _api: CardAutomationsAPI;
|
protected _api: CardAutomationsAPI;
|
||||||
|
|
||||||
protected _automations: TaggedAutomations = [];
|
protected _automations = new Map<TaggedAutomation, ConditionsManager>();
|
||||||
protected _priorEvaluations: Map<Automation, boolean> = new Map();
|
|
||||||
|
|
||||||
// A counter to avoid infinite loops, increases every time actions are run,
|
// A counter to avoid infinite loops, increases every time actions are run,
|
||||||
// decreases every time actions are complete.
|
// decreases every time actions are complete.
|
||||||
@@ -19,14 +20,28 @@ export class AutomationsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deleteAutomations(tag?: unknown) {
|
public deleteAutomations(tag?: unknown) {
|
||||||
this._automations = this._automations.filter((automation) => automation.tag !== tag);
|
for (const [automation, conditionManager] of this._automations) {
|
||||||
|
if (automation.tag === tag) {
|
||||||
|
this._automations.delete(automation);
|
||||||
|
conditionManager.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addAutomations(automations: TaggedAutomations): void {
|
public addAutomations(automations: TaggedAutomation[]): void {
|
||||||
this._automations.push(...automations);
|
for (const automation of automations) {
|
||||||
|
const conditionManager = new ConditionsManager(
|
||||||
|
automation.conditions,
|
||||||
|
this._api.getConditionStateManager(),
|
||||||
|
);
|
||||||
|
conditionManager.addListener((result: ConditionsEvaluationResult) =>
|
||||||
|
this._execute(automation, result),
|
||||||
|
);
|
||||||
|
this._automations.set(automation, conditionManager);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public execute(): void {
|
protected _execute(automation: Automation, result: ConditionsEvaluationResult): void {
|
||||||
if (
|
if (
|
||||||
!this._api.getHASSManager().hasHASS() ||
|
!this._api.getHASSManager().hasHASS() ||
|
||||||
// Never execute automations if the card hasn't finished initializing, as
|
// Never execute automations if the card hasn't finished initializing, as
|
||||||
@@ -40,20 +55,10 @@ export class AutomationsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionsToRun: AutomationActions = [];
|
const shouldExecute = result.result;
|
||||||
for (const automation of this._automations) {
|
const actions = shouldExecute ? automation.actions : automation.actions_not;
|
||||||
const shouldExecute = this._api
|
|
||||||
.getConditionsManager()
|
|
||||||
.evaluateConditions(automation.conditions);
|
|
||||||
const actions = shouldExecute ? automation.actions : automation.actions_not;
|
|
||||||
const priorEvaluation = this._priorEvaluations.get(automation);
|
|
||||||
this._priorEvaluations.set(automation, shouldExecute);
|
|
||||||
if (shouldExecute !== priorEvaluation && actions) {
|
|
||||||
actionsToRun.push(...actions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!actionsToRun.length) {
|
if (!actions?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +75,6 @@ export class AutomationsManager {
|
|||||||
await this._api.getActionsManager().executeActions(actions);
|
await this._api.getActionsManager().executeActions(actions);
|
||||||
--this._nestedAutomationExecutions;
|
--this._nestedAutomationExecutions;
|
||||||
};
|
};
|
||||||
runActions(actionsToRun);
|
runActions(actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class CardElementManager {
|
|||||||
this._api.getQueryStringManager().requestExecution,
|
this._api.getQueryStringManager().requestExecution,
|
||||||
);
|
);
|
||||||
|
|
||||||
this._api.getConditionsManager()?.setState({
|
this._api.getConditionStateManager()?.setState({
|
||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,385 +0,0 @@
|
|||||||
import { CurrentUser } from '@dermotduffy/custom-card-helpers';
|
|
||||||
import { HassEntities } from 'home-assistant-js-websocket';
|
|
||||||
import isEqual from 'lodash-es/isEqual';
|
|
||||||
import merge from 'lodash-es/merge';
|
|
||||||
import { ZodSchema } from 'zod';
|
|
||||||
import {
|
|
||||||
copyConfig,
|
|
||||||
deleteConfigValue,
|
|
||||||
getConfigValue,
|
|
||||||
setConfigValue,
|
|
||||||
} from '../config/management';
|
|
||||||
import {
|
|
||||||
AdvancedCameraCardCondition,
|
|
||||||
advancedCameraCardConditionalSchema,
|
|
||||||
Overrides,
|
|
||||||
RawAdvancedCameraCardConfig,
|
|
||||||
ViewDisplayMode,
|
|
||||||
} from '../config/types';
|
|
||||||
import { localize } from '../localize/localize';
|
|
||||||
import { AdvancedCameraCardError, MediaLoadedInfo } from '../types';
|
|
||||||
import { desparsifyArrays } from '../utils/basic';
|
|
||||||
import { isCompanionApp } from '../utils/companion';
|
|
||||||
import { CardConditionAPI, KeysState, MicrophoneState } from './types';
|
|
||||||
|
|
||||||
export interface ConditionState {
|
|
||||||
camera?: string;
|
|
||||||
displayMode?: ViewDisplayMode;
|
|
||||||
expand?: boolean;
|
|
||||||
fullscreen?: boolean;
|
|
||||||
interaction?: boolean;
|
|
||||||
keys?: KeysState;
|
|
||||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
|
||||||
microphone?: MicrophoneState;
|
|
||||||
state?: HassEntities;
|
|
||||||
triggered?: Set<string>;
|
|
||||||
user?: CurrentUser;
|
|
||||||
userAgent?: string;
|
|
||||||
view?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OverrideConfigurationError extends AdvancedCameraCardError {}
|
|
||||||
|
|
||||||
export class ConditionsEvaluateRequestEvent extends Event {
|
|
||||||
public conditions: AdvancedCameraCardCondition[];
|
|
||||||
public evaluation?: boolean;
|
|
||||||
|
|
||||||
constructor(conditions: AdvancedCameraCardCondition[], eventInitDict?: EventInit) {
|
|
||||||
super('advanced-camera-card:conditions:evaluate', eventInitDict);
|
|
||||||
this.conditions = conditions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate whether an AdvancedCameraCardCondition is met using an event to
|
|
||||||
* evaluate.
|
|
||||||
* @returns A boolean indicating whether the condition is met.
|
|
||||||
*/
|
|
||||||
export function evaluateConditionViaEvent(
|
|
||||||
element: HTMLElement,
|
|
||||||
conditions?: AdvancedCameraCardCondition[],
|
|
||||||
): boolean {
|
|
||||||
if (!conditions) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const evaluateEvent = new ConditionsEvaluateRequestEvent(conditions, {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Special note on what's going on here:
|
|
||||||
*
|
|
||||||
* Some parts of the card (e.g. <advanced-camera-card-elements>) may have arbitrary
|
|
||||||
* complexity and layers (that this card doesn't control) between that master
|
|
||||||
* element and the element that needs to evaluate the condition. In these
|
|
||||||
* cases there's no clean way to pass state from the rest of card down through
|
|
||||||
* these layers. Instead, an event is dispatched as a "request for evaluation"
|
|
||||||
* (ConditionEvaluateRequestEvent) upwards which is caught by the outer card
|
|
||||||
* and the evaluation result is added to the event object. Because event
|
|
||||||
* propagation is handled synchronously, the result will be added to the event
|
|
||||||
* before the flow proceeds.
|
|
||||||
*/
|
|
||||||
element.dispatchEvent(evaluateEvent);
|
|
||||||
return evaluateEvent.evaluation ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOverriddenConfig<RT extends RawAdvancedCameraCardConfig>(
|
|
||||||
manager: Readonly<ConditionsManager>,
|
|
||||||
config: Readonly<RT>,
|
|
||||||
options?: {
|
|
||||||
configOverrides?: Readonly<Overrides>;
|
|
||||||
stateOverrides?: Partial<ConditionState>;
|
|
||||||
schema?: ZodSchema;
|
|
||||||
},
|
|
||||||
): RT {
|
|
||||||
let output = copyConfig(config);
|
|
||||||
let overridden = false;
|
|
||||||
if (options?.configOverrides) {
|
|
||||||
for (const override of options.configOverrides) {
|
|
||||||
if (manager.evaluateConditions(override.conditions, options?.stateOverrides)) {
|
|
||||||
override.delete?.forEach((deletionKey) => {
|
|
||||||
deleteConfigValue(output, deletionKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(override.set ?? {}).forEach((setKey) => {
|
|
||||||
setConfigValue(output, setKey, override.set?.[setKey]);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(override.merge ?? {}).forEach((mergeKey) => {
|
|
||||||
setConfigValue(
|
|
||||||
output,
|
|
||||||
mergeKey,
|
|
||||||
merge({}, getConfigValue(output, mergeKey), override.merge?.[mergeKey]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
overridden = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overridden) {
|
|
||||||
// Attempt to return the same configuration object if it has not been
|
|
||||||
// overridden (to reduce re-renders for a configuration that has not changed).
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.configOverrides?.some((override) => override.delete?.length)) {
|
|
||||||
// If anything was deleted during this override, empty undefined slots may
|
|
||||||
// be left in arrays where values were unset. Desparsify them.
|
|
||||||
output = desparsifyArrays(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.schema) {
|
|
||||||
const parseResult = options.schema.safeParse(output);
|
|
||||||
if (!parseResult.success) {
|
|
||||||
throw new OverrideConfigurationError(
|
|
||||||
localize('error.invalid_configuration_override'),
|
|
||||||
[parseResult.error.errors, output],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return parseResult.data;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A tiny wrapper interface to allow the same manager to be passed around
|
|
||||||
// immutably within objects that will not be equal (===). Every state change
|
|
||||||
// generates a new epoch. This is used for Lit rendering to ensure changes to
|
|
||||||
// condition state are recognized as changes even though the manager is the
|
|
||||||
// same.
|
|
||||||
export interface ConditionsManagerEpoch {
|
|
||||||
manager: Readonly<ConditionsManager>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConditionsManagerListener = (
|
|
||||||
newState: ConditionState,
|
|
||||||
oldState?: ConditionState,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export class ConditionsManager {
|
|
||||||
protected _api: CardConditionAPI;
|
|
||||||
|
|
||||||
protected _state: ConditionState = {};
|
|
||||||
protected _epoch: ConditionsManagerEpoch = this._createEpoch();
|
|
||||||
protected _listeners: ConditionsManagerListener[];
|
|
||||||
|
|
||||||
// Whether or not to include HA state in ConditionState. Doing so increases
|
|
||||||
// CPU usage as HA state is pumped out very fast, so this is only enabled if
|
|
||||||
// the configuration needs to consume it.
|
|
||||||
protected _hasHAStateConditions = false;
|
|
||||||
protected _mediaQueries: MediaQueryList[] = [];
|
|
||||||
protected _mediaQueryTrigger = () => this._triggerChange(this._state);
|
|
||||||
|
|
||||||
constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) {
|
|
||||||
this._api = api;
|
|
||||||
this._listeners = [
|
|
||||||
() => this._api.getConfigManager().computeOverrideConfig(),
|
|
||||||
() => this._api.getAutomationsManager().execute(),
|
|
||||||
...(listener ? [listener] : []),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public addListener(listener: ConditionsManagerListener): void {
|
|
||||||
this._listeners.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeListener(listener: ConditionsManagerListener): void {
|
|
||||||
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeConditions(): void {
|
|
||||||
this._mediaQueries.forEach((mql) =>
|
|
||||||
mql.removeEventListener('change', this._mediaQueryTrigger),
|
|
||||||
);
|
|
||||||
this._mediaQueries = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConditionsFromConfig(): void {
|
|
||||||
this.removeConditions();
|
|
||||||
|
|
||||||
const getAllConditions = (): AdvancedCameraCardCondition[] => {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
const conditions: AdvancedCameraCardCondition[] = [];
|
|
||||||
config?.overrides?.forEach((override) => conditions.push(...override.conditions));
|
|
||||||
config?.automations?.forEach((automation) =>
|
|
||||||
conditions.push(...automation.conditions),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Element conditions can be arbitrarily nested underneath conditionals and
|
|
||||||
// custom elements that this card may not known. Here we recursively parse
|
|
||||||
// down the elements tree, parsing as we go to find valid conditions.
|
|
||||||
const getElementsConditions = (data: unknown): void => {
|
|
||||||
const parseResult = advancedCameraCardConditionalSchema.safeParse(data);
|
|
||||||
if (parseResult.success) {
|
|
||||||
conditions.push(...parseResult.data.conditions);
|
|
||||||
parseResult.data.elements?.forEach(getElementsConditions);
|
|
||||||
} else if (data && typeof data === 'object') {
|
|
||||||
Object.keys(data).forEach((key) => getElementsConditions(data[key]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
config?.elements?.forEach(getElementsConditions);
|
|
||||||
return conditions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const conditions = getAllConditions();
|
|
||||||
this._hasHAStateConditions = conditions.some(
|
|
||||||
(conditionObj) =>
|
|
||||||
!conditionObj.condition ||
|
|
||||||
['state', 'numeric_state', 'user'].includes(conditionObj.condition),
|
|
||||||
);
|
|
||||||
conditions.forEach((conditionObj) => {
|
|
||||||
if (conditionObj.condition === 'screen') {
|
|
||||||
const mql = window.matchMedia(conditionObj.media_query);
|
|
||||||
mql.addEventListener('change', this._mediaQueryTrigger);
|
|
||||||
this._mediaQueries.push(mql);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setState(state: Partial<ConditionState>): void {
|
|
||||||
// Performance: Compare the new state with the existing state and only
|
|
||||||
// trigger a change if the new state is different. Only the new keys are
|
|
||||||
// compared, since some of the values (e.g. 'state') will be large.
|
|
||||||
if (Object.keys(state).every((key) => isEqual(state[key], this._state[key]))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const oldState = this._state;
|
|
||||||
this._state = {
|
|
||||||
...oldState,
|
|
||||||
...state,
|
|
||||||
};
|
|
||||||
this._triggerChange(oldState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getState(): ConditionState {
|
|
||||||
return this._state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasHAStateConditions(): boolean {
|
|
||||||
return this._hasHAStateConditions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEpoch(): ConditionsManagerEpoch {
|
|
||||||
return this._epoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
public evaluateConditions(
|
|
||||||
conditions: Readonly<AdvancedCameraCardCondition>[],
|
|
||||||
stateOverrides?: Partial<ConditionState>,
|
|
||||||
): boolean {
|
|
||||||
return conditions.every((conditionObj) =>
|
|
||||||
this._evaluateCondition(conditionObj, stateOverrides),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _evaluateCondition(
|
|
||||||
conditionObj: Readonly<AdvancedCameraCardCondition>,
|
|
||||||
stateOverrides?: Partial<ConditionState>,
|
|
||||||
): boolean {
|
|
||||||
const state = {
|
|
||||||
...this._state,
|
|
||||||
...stateOverrides,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (conditionObj.condition) {
|
|
||||||
case undefined:
|
|
||||||
case 'state':
|
|
||||||
return (
|
|
||||||
!!state.state &&
|
|
||||||
((!conditionObj.state && !conditionObj.state_not) ||
|
|
||||||
(conditionObj.entity in state.state &&
|
|
||||||
(!conditionObj.state ||
|
|
||||||
(Array.isArray(conditionObj.state)
|
|
||||||
? conditionObj.state.includes(state.state[conditionObj.entity].state)
|
|
||||||
: conditionObj.state === state.state[conditionObj.entity].state)) &&
|
|
||||||
(!conditionObj.state_not ||
|
|
||||||
(Array.isArray(conditionObj.state_not)
|
|
||||||
? !conditionObj.state_not.includes(
|
|
||||||
state.state[conditionObj.entity].state,
|
|
||||||
)
|
|
||||||
: conditionObj.state_not !== state.state[conditionObj.entity].state))))
|
|
||||||
);
|
|
||||||
case 'view':
|
|
||||||
return !!state?.view && conditionObj.views.includes(state.view);
|
|
||||||
case 'fullscreen':
|
|
||||||
return (
|
|
||||||
state.fullscreen !== undefined && conditionObj.fullscreen === state.fullscreen
|
|
||||||
);
|
|
||||||
case 'expand':
|
|
||||||
return state.expand !== undefined && conditionObj.expand === state.expand;
|
|
||||||
case 'camera':
|
|
||||||
return !!state.camera && conditionObj.cameras.includes(state.camera);
|
|
||||||
case 'numeric_state':
|
|
||||||
return (
|
|
||||||
!!state.state &&
|
|
||||||
conditionObj.entity in state.state &&
|
|
||||||
state.state[conditionObj.entity].state !== undefined &&
|
|
||||||
(conditionObj.above === undefined ||
|
|
||||||
Number(state.state[conditionObj.entity].state) > conditionObj.above) &&
|
|
||||||
(conditionObj.below === undefined ||
|
|
||||||
Number(state.state[conditionObj.entity].state) < conditionObj.below)
|
|
||||||
);
|
|
||||||
case 'user':
|
|
||||||
return !!state.user && conditionObj.users.includes(state.user.id);
|
|
||||||
case 'media_loaded':
|
|
||||||
return (
|
|
||||||
state.mediaLoadedInfo !== undefined &&
|
|
||||||
conditionObj.media_loaded === !!state.mediaLoadedInfo
|
|
||||||
);
|
|
||||||
case 'screen':
|
|
||||||
return window.matchMedia(conditionObj.media_query).matches;
|
|
||||||
case 'display_mode':
|
|
||||||
return !!state.displayMode && conditionObj.display_mode === state.displayMode;
|
|
||||||
case 'triggered':
|
|
||||||
return conditionObj.triggered.some((triggeredCameraID) =>
|
|
||||||
state.triggered?.has(triggeredCameraID),
|
|
||||||
);
|
|
||||||
case 'interaction':
|
|
||||||
return (
|
|
||||||
state.interaction !== undefined &&
|
|
||||||
conditionObj.interaction === state.interaction
|
|
||||||
);
|
|
||||||
case 'microphone':
|
|
||||||
return (
|
|
||||||
(conditionObj.connected === undefined ||
|
|
||||||
state.microphone?.connected === conditionObj.connected) &&
|
|
||||||
(conditionObj.muted === undefined ||
|
|
||||||
state.microphone?.muted === conditionObj.muted)
|
|
||||||
);
|
|
||||||
case 'key':
|
|
||||||
return (
|
|
||||||
!!state.keys &&
|
|
||||||
conditionObj.key in state.keys &&
|
|
||||||
(conditionObj.state ?? 'down') === state.keys[conditionObj.key].state &&
|
|
||||||
(conditionObj.ctrl === undefined ||
|
|
||||||
conditionObj.ctrl === !!state.keys[conditionObj.key].ctrl) &&
|
|
||||||
(conditionObj.alt === undefined ||
|
|
||||||
conditionObj.alt === !!state.keys[conditionObj.key].alt) &&
|
|
||||||
(conditionObj.meta === undefined ||
|
|
||||||
conditionObj.meta === !!state.keys[conditionObj.key].meta) &&
|
|
||||||
(conditionObj.shift === undefined ||
|
|
||||||
conditionObj.shift === !!state.keys[conditionObj.key].shift)
|
|
||||||
);
|
|
||||||
case 'user_agent':
|
|
||||||
return (
|
|
||||||
!!state.userAgent &&
|
|
||||||
(!conditionObj.user_agent || conditionObj.user_agent === state.userAgent) &&
|
|
||||||
(conditionObj.companion === undefined ||
|
|
||||||
conditionObj.companion === isCompanionApp(state.userAgent)) &&
|
|
||||||
(conditionObj.user_agent_re === undefined ||
|
|
||||||
new RegExp(conditionObj.user_agent_re).test(state.userAgent))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _createEpoch(): ConditionsManagerEpoch {
|
|
||||||
return { manager: this };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _triggerChange(oldState?: ConditionState): void {
|
|
||||||
this._epoch = this._createEpoch();
|
|
||||||
this._listeners.forEach((listener) => listener(this._state, oldState));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { ConditionsManager } from '../../conditions/conditions-manager.js';
|
||||||
import { isConfigUpgradeable } from '../../config/management.js';
|
import { isConfigUpgradeable } from '../../config/management.js';
|
||||||
import { setProfiles } from '../../config/profiles/index.js';
|
import { setProfiles } from '../../config/profiles/index.js';
|
||||||
import {
|
import {
|
||||||
@@ -9,9 +10,9 @@ import {
|
|||||||
} from '../../config/types.js';
|
} from '../../config/types.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import { getParseErrorPaths } from '../../utils/zod.js';
|
import { getParseErrorPaths } from '../../utils/zod.js';
|
||||||
import { getOverriddenConfig } from '../conditions-manager.js';
|
|
||||||
import { InitializationAspect } from '../initialization-manager.js';
|
import { InitializationAspect } from '../initialization-manager.js';
|
||||||
import { CardConfigAPI } from '../types.js';
|
import { CardConfigAPI } from '../types.js';
|
||||||
|
import { getOverriddenConfig } from './get-overridden-config.js';
|
||||||
import { setAutomationsFromConfig } from './load-automations.js';
|
import { setAutomationsFromConfig } from './load-automations.js';
|
||||||
import { setKeyboardShortcutsFromConfig } from './load-keyboard-shortcuts.js';
|
import { setKeyboardShortcutsFromConfig } from './load-keyboard-shortcuts.js';
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export class ConfigManager {
|
|||||||
protected _overriddenConfig: AdvancedCameraCardConfig | null = null;
|
protected _overriddenConfig: AdvancedCameraCardConfig | null = null;
|
||||||
protected _rawConfig: RawAdvancedCameraCardConfig | null = null;
|
protected _rawConfig: RawAdvancedCameraCardConfig | null = null;
|
||||||
protected _cardWideConfig: CardWideConfig | null = null;
|
protected _cardWideConfig: CardWideConfig | null = null;
|
||||||
|
protected _overridesConditionsManager: ConditionsManager | null = null;
|
||||||
|
|
||||||
constructor(api: CardConfigAPI) {
|
constructor(api: CardConfigAPI) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
@@ -85,8 +87,16 @@ export class ConfigManager {
|
|||||||
debug: config.debug,
|
debug: config.debug,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._api.getConditionsManager().setConditionsFromConfig();
|
this._overridesConditionsManager?.destroy();
|
||||||
this._api.getConditionsManager().setState({
|
this._overridesConditionsManager = this._config.overrides?.length
|
||||||
|
? new ConditionsManager(
|
||||||
|
this._config.overrides.map((override) => override.conditions).flat(),
|
||||||
|
this._api.getConditionStateManager(),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
this._overridesConditionsManager?.addListener(() => this._processOverrideConfig());
|
||||||
|
|
||||||
|
this._api.getConditionStateManager().setState({
|
||||||
view: undefined,
|
view: undefined,
|
||||||
displayMode: undefined,
|
displayMode: undefined,
|
||||||
camera: undefined,
|
camera: undefined,
|
||||||
@@ -102,31 +112,22 @@ export class ConfigManager {
|
|||||||
setKeyboardShortcutsFromConfig(this._api, this);
|
setKeyboardShortcutsFromConfig(this._api, this);
|
||||||
setAutomationsFromConfig(this._api);
|
setAutomationsFromConfig(this._api);
|
||||||
|
|
||||||
this.computeOverrideConfig();
|
this._processOverrideConfig();
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public computeOverrideConfig(): void {
|
protected _processOverrideConfig(): void {
|
||||||
const conditionsManager = this._api.getConditionsManager();
|
/* istanbul ignore if: No (current) way to reach this code -- @preserve */
|
||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let overriddenConfig: AdvancedCameraCardConfig | null = null;
|
const overriddenConfig = this._getOverriddenConfig();
|
||||||
try {
|
|
||||||
overriddenConfig = getOverriddenConfig(conditionsManager, this._config, {
|
|
||||||
configOverrides: this._config.overrides,
|
|
||||||
schema: advancedCameraCardConfigSchema,
|
|
||||||
});
|
|
||||||
} catch (ev) {
|
|
||||||
this._api.getMessageManager().setErrorIfHigherPriority(ev);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save on Lit re-rendering costs by only updating the configuration if it
|
// Save on Lit re-rendering costs by only updating the configuration if it
|
||||||
// actually changes.
|
// actually changes.
|
||||||
if (isEqual(overriddenConfig, this._overriddenConfig)) {
|
if (!overriddenConfig || isEqual(overriddenConfig, this._overriddenConfig)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,14 +155,30 @@ export class ConfigManager {
|
|||||||
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* async */ this._initializeBackground(previousConfig);
|
/* async */ this._initializeBackgroundAndUpdate(previousConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getOverriddenConfig(): AdvancedCameraCardConfig | null {
|
||||||
|
if (!this._overridesConditionsManager || !this._config) {
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getOverriddenConfig(this._overridesConditionsManager, this._config, {
|
||||||
|
configOverrides: this._config.overrides,
|
||||||
|
schema: advancedCameraCardConfigSchema,
|
||||||
|
});
|
||||||
|
} catch (ev) {
|
||||||
|
this._api.getMessageManager().setErrorIfHigherPriority(ev);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize config dependent items in the background. For items that the
|
* Initialize config dependent items in the background. For items that the
|
||||||
* card hard requires, use InitializationManager instead.
|
* card hard requires, use InitializationManager instead.
|
||||||
*/
|
*/
|
||||||
protected async _initializeBackground(
|
protected async _initializeBackgroundAndUpdate(
|
||||||
previousConfig: AdvancedCameraCardConfig | null,
|
previousConfig: AdvancedCameraCardConfig | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
|
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { merge } from 'lodash-es';
|
||||||
|
import { ZodType as ZodSchema } from 'zod';
|
||||||
|
import { ConditionsManagerReadonlyInterface } from '../../conditions/types';
|
||||||
|
import {
|
||||||
|
copyConfig,
|
||||||
|
deleteConfigValue,
|
||||||
|
getConfigValue,
|
||||||
|
setConfigValue,
|
||||||
|
} from '../../config/management';
|
||||||
|
import { Overrides, RawAdvancedCameraCardConfig } from '../../config/types';
|
||||||
|
import { localize } from '../../localize/localize';
|
||||||
|
import { AdvancedCameraCardError } from '../../types';
|
||||||
|
import { desparsifyArrays } from '../../utils/basic';
|
||||||
|
|
||||||
|
class OverrideConfigurationError extends AdvancedCameraCardError {}
|
||||||
|
|
||||||
|
export function getOverriddenConfig<RT extends RawAdvancedCameraCardConfig>(
|
||||||
|
manager: ConditionsManagerReadonlyInterface,
|
||||||
|
config: Readonly<RT>,
|
||||||
|
options?: {
|
||||||
|
configOverrides?: Readonly<Overrides>;
|
||||||
|
schema?: ZodSchema;
|
||||||
|
},
|
||||||
|
): RT {
|
||||||
|
if (!options?.configOverrides) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = copyConfig(config);
|
||||||
|
let overridden = false;
|
||||||
|
for (const override of options.configOverrides) {
|
||||||
|
if (manager.getEvaluation()?.result) {
|
||||||
|
override.delete?.forEach((deletionKey) => {
|
||||||
|
deleteConfigValue(output, deletionKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(override.set ?? {}).forEach((setKey) => {
|
||||||
|
setConfigValue(output, setKey, override.set?.[setKey]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(override.merge ?? {}).forEach((mergeKey) => {
|
||||||
|
setConfigValue(
|
||||||
|
output,
|
||||||
|
mergeKey,
|
||||||
|
merge({}, getConfigValue(output, mergeKey), override.merge?.[mergeKey]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
overridden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overridden) {
|
||||||
|
// Return the same configuration object if it has not been overridden (to
|
||||||
|
// reduce re-renders for a configuration that has not changed).
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.configOverrides?.some((override) => override.delete?.length)) {
|
||||||
|
// If anything was deleted during this override, empty undefined slots may
|
||||||
|
// be left in arrays where values were unset. Desparsify them.
|
||||||
|
output = desparsifyArrays(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.schema) {
|
||||||
|
const parseResult = options.schema.safeParse(output);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
throw new OverrideConfigurationError(
|
||||||
|
localize('error.invalid_configuration_override'),
|
||||||
|
[parseResult.error.errors, output],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parseResult.data;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
PTZKeyboardShortcutName,
|
PTZKeyboardShortcutName,
|
||||||
} from '../../config/keyboard-shortcuts';
|
} from '../../config/keyboard-shortcuts';
|
||||||
import { PTZAction } from '../../config/ptz';
|
import { PTZAction } from '../../config/ptz';
|
||||||
import { CardConfigLoaderAPI, TaggedAutomations } from '../types';
|
|
||||||
import { createPTZMultiAction } from '../../utils/action';
|
import { createPTZMultiAction } from '../../utils/action';
|
||||||
|
import { CardConfigLoaderAPI, TaggedAutomation } from '../types';
|
||||||
|
|
||||||
export const setKeyboardShortcutsFromConfig = (
|
export const setKeyboardShortcutsFromConfig = (
|
||||||
api: CardConfigLoaderAPI,
|
api: CardConfigLoaderAPI,
|
||||||
@@ -47,12 +47,12 @@ const ptzKeyboardShortcutToPTZAction = (
|
|||||||
const convertKeyboardShortcutsToAutomations = (
|
const convertKeyboardShortcutsToAutomations = (
|
||||||
tag: unknown,
|
tag: unknown,
|
||||||
shortcuts: KeyboardShortcuts,
|
shortcuts: KeyboardShortcuts,
|
||||||
): TaggedAutomations => {
|
): TaggedAutomation[] => {
|
||||||
if (!shortcuts.enabled) {
|
if (!shortcuts.enabled) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const automations: TaggedAutomations = [];
|
const automations: TaggedAutomation[] = [];
|
||||||
|
|
||||||
for (const name of [
|
for (const name of [
|
||||||
'ptz_down',
|
'ptz_down',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LovelaceCardEditor } from '@dermotduffy/custom-card-helpers';
|
import { LovelaceCardEditor } from '@dermotduffy/custom-card-helpers';
|
||||||
import { ReactiveController } from 'lit';
|
import { ReactiveController } from 'lit';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { ConditionStateManager } from '../conditions/state-manager';
|
||||||
import { AdvancedCameraCardConfig } from '../config/types';
|
import { AdvancedCameraCardConfig } from '../config/types';
|
||||||
import {
|
import {
|
||||||
createDeviceRegistryCache,
|
createDeviceRegistryCache,
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
MenuToggleCallback,
|
MenuToggleCallback,
|
||||||
ScrollCallback,
|
ScrollCallback,
|
||||||
} from './card-element-manager';
|
} from './card-element-manager';
|
||||||
import { ConditionsManager, ConditionsManagerListener } from './conditions-manager';
|
|
||||||
import { ConfigManager } from './config/config-manager';
|
import { ConfigManager } from './config/config-manager';
|
||||||
import { DefaultManager } from './default-manager';
|
import { DefaultManager } from './default-manager';
|
||||||
import { DownloadManager } from './download-manager';
|
import { DownloadManager } from './download-manager';
|
||||||
@@ -92,6 +92,8 @@ export class CardController
|
|||||||
CardViewAPI,
|
CardViewAPI,
|
||||||
ReactiveController
|
ReactiveController
|
||||||
{
|
{
|
||||||
|
protected _conditionStateManager = new ConditionStateManager();
|
||||||
|
|
||||||
// These properties may be used in the construction of 'managers' (and should
|
// These properties may be used in the construction of 'managers' (and should
|
||||||
// be created first).
|
// be created first).
|
||||||
protected _deviceRegistryManager = new DeviceRegistryManager(
|
protected _deviceRegistryManager = new DeviceRegistryManager(
|
||||||
@@ -107,7 +109,6 @@ export class CardController
|
|||||||
protected _cameraManager = new CameraManager(this);
|
protected _cameraManager = new CameraManager(this);
|
||||||
protected _cameraURLManager = new CameraURLManager(this);
|
protected _cameraURLManager = new CameraURLManager(this);
|
||||||
protected _cardElementManager: CardElementManager;
|
protected _cardElementManager: CardElementManager;
|
||||||
protected _conditionsManager: ConditionsManager;
|
|
||||||
protected _configManager = new ConfigManager(this);
|
protected _configManager = new ConfigManager(this);
|
||||||
protected _defaultManager = new DefaultManager(this);
|
protected _defaultManager = new DefaultManager(this);
|
||||||
protected _downloadManager = new DownloadManager(this);
|
protected _downloadManager = new DownloadManager(this);
|
||||||
@@ -131,11 +132,9 @@ export class CardController
|
|||||||
host: CardHTMLElement,
|
host: CardHTMLElement,
|
||||||
scrollCallback: ScrollCallback,
|
scrollCallback: ScrollCallback,
|
||||||
menuToggleCallback: MenuToggleCallback,
|
menuToggleCallback: MenuToggleCallback,
|
||||||
conditionListener: ConditionsManagerListener,
|
|
||||||
) {
|
) {
|
||||||
host.addController(this);
|
host.addController(this);
|
||||||
|
|
||||||
this._conditionsManager = new ConditionsManager(this, conditionListener);
|
|
||||||
this._cardElementManager = new CardElementManager(
|
this._cardElementManager = new CardElementManager(
|
||||||
this,
|
this,
|
||||||
host,
|
host,
|
||||||
@@ -171,8 +170,8 @@ export class CardController
|
|||||||
return this._cardElementManager;
|
return this._cardElementManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getConditionsManager(): ConditionsManager {
|
public getConditionStateManager(): ConditionStateManager {
|
||||||
return this._conditionsManager;
|
return this._conditionStateManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class ExpandManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _setConditionState(): void {
|
protected _setConditionState(): void {
|
||||||
this._api.getConditionsManager()?.setState({
|
this._api.getConditionStateManager()?.setState({
|
||||||
expand: this._expanded,
|
expand: this._expanded,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class FullscreenManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected _setConditionState(): void {
|
protected _setConditionState(): void {
|
||||||
this._api.getConditionsManager()?.setState({
|
this._api.getConditionStateManager()?.setState({
|
||||||
fullscreen: this.isInFullscreen(),
|
fullscreen: this.isInFullscreen(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { ConditionStateChange } from '../../../conditions/types';
|
||||||
import { WebkitHTMLVideoElement } from '../../../types';
|
import { WebkitHTMLVideoElement } from '../../../types';
|
||||||
import { Timer } from '../../../utils/timer';
|
import { Timer } from '../../../utils/timer';
|
||||||
import { ConditionState } from '../../conditions-manager';
|
|
||||||
import { FullscreenProviderBase } from '../provider';
|
import { FullscreenProviderBase } from '../provider';
|
||||||
import { FullscreenProvider } from '../types';
|
import { FullscreenProvider } from '../types';
|
||||||
|
|
||||||
@@ -18,26 +18,23 @@ export class WebkitFullScreenProvider
|
|||||||
protected _playTimer = new Timer();
|
protected _playTimer = new Timer();
|
||||||
|
|
||||||
public connect(): void {
|
public connect(): void {
|
||||||
this._api.getConditionsManager().addListener(this._conditionChangeHandler);
|
this._api.getConditionStateManager().addListener(this._stateChangeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect(): void {
|
public disconnect(): void {
|
||||||
this._api.getConditionsManager().removeListener(this._conditionChangeHandler);
|
this._api.getConditionStateManager().removeListener(this._stateChangeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _conditionChangeHandler = (
|
protected _stateChangeHandler = (change: ConditionStateChange): void => {
|
||||||
newState: ConditionState,
|
|
||||||
oldState?: ConditionState,
|
|
||||||
): void => {
|
|
||||||
if (
|
if (
|
||||||
newState.mediaLoadedInfo?.player?.getFullscreenElement() !==
|
change.old.mediaLoadedInfo?.player?.getFullscreenElement() !==
|
||||||
oldState?.mediaLoadedInfo?.player?.getFullscreenElement()
|
change.new.mediaLoadedInfo?.player?.getFullscreenElement()
|
||||||
) {
|
) {
|
||||||
const oldElement = oldState?.mediaLoadedInfo?.player?.getFullscreenElement();
|
const oldElement = change.old.mediaLoadedInfo?.player?.getFullscreenElement();
|
||||||
oldElement?.removeEventListener('webkitbeginfullscreen', this._handler);
|
oldElement?.removeEventListener('webkitbeginfullscreen', this._handler);
|
||||||
oldElement?.removeEventListener('webkitendfullscreen', this._endHandler);
|
oldElement?.removeEventListener('webkitendfullscreen', this._endHandler);
|
||||||
|
|
||||||
const newElement = newState.mediaLoadedInfo?.player?.getFullscreenElement();
|
const newElement = change.new.mediaLoadedInfo?.player?.getFullscreenElement();
|
||||||
newElement?.addEventListener('webkitbeginfullscreen', this._handler);
|
newElement?.addEventListener('webkitbeginfullscreen', this._handler);
|
||||||
newElement?.addEventListener('webkitendfullscreen', this._endHandler);
|
newElement?.addEventListener('webkitendfullscreen', this._endHandler);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,10 @@ export class HASSManager {
|
|||||||
const oldHass = this._hass;
|
const oldHass = this._hass;
|
||||||
this._hass = hass;
|
this._hass = hass;
|
||||||
|
|
||||||
if (this._api.getConditionsManager().hasHAStateConditions()) {
|
this._api.getConditionStateManager().setState({
|
||||||
this._api.getConditionsManager().setState({
|
state: this._hass.states,
|
||||||
state: this._hass.states,
|
user: this._hass.user,
|
||||||
user: this._hass.user,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme may depend on HASS.
|
// Theme may depend on HASS.
|
||||||
this._api.getStyleManager().applyTheme();
|
this._api.getStyleManager().applyTheme();
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class InteractionManager {
|
|||||||
val,
|
val,
|
||||||
'interaction',
|
'interaction',
|
||||||
);
|
);
|
||||||
this._api.getConditionsManager().setState({ interaction: val });
|
this._api.getConditionStateManager().setState({ interaction: val });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _reportInteraction(): void {
|
protected _reportInteraction(): void {
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ export class KeyboardStateManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected _processStateChange(): void {
|
protected _processStateChange(): void {
|
||||||
this._api.getConditionsManager().setState({ keys: this._state });
|
this._api.getConditionStateManager().setState({ keys: this._state });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class MediaLoadedInfoManager {
|
|||||||
this._current = mediaLoadedInfo;
|
this._current = mediaLoadedInfo;
|
||||||
this._lastKnown = mediaLoadedInfo;
|
this._lastKnown = mediaLoadedInfo;
|
||||||
|
|
||||||
this._api.getConditionsManager().setState({ mediaLoadedInfo: mediaLoadedInfo });
|
this._api.getConditionStateManager().setState({ mediaLoadedInfo: mediaLoadedInfo });
|
||||||
|
|
||||||
// Fresh media information may change how the card is rendered.
|
// Fresh media information may change how the card is rendered.
|
||||||
this._api.getStyleManager().setExpandedMode();
|
this._api.getStyleManager().setExpandedMode();
|
||||||
@@ -47,7 +47,7 @@ export class MediaLoadedInfoManager {
|
|||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this._current = null;
|
this._current = null;
|
||||||
this._api.getConditionsManager().setState({ mediaLoadedInfo: null });
|
this._api.getConditionStateManager().setState({ mediaLoadedInfo: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
public has(): boolean {
|
public has(): boolean {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class MicrophoneManager {
|
|||||||
muted: this.isMuted(),
|
muted: this.isMuted(),
|
||||||
forbidden: this.isForbidden(),
|
forbidden: this.isForbidden(),
|
||||||
};
|
};
|
||||||
this._api.getConditionsManager().setState({
|
this._api.getConditionStateManager().setState({
|
||||||
microphone: this._state,
|
microphone: this._state,
|
||||||
});
|
});
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import isEqual from 'lodash-es/isEqual';
|
|
||||||
import orderBy from 'lodash-es/orderBy';
|
import orderBy from 'lodash-es/orderBy';
|
||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import { CameraEvent } from '../camera-manager/types';
|
import { CameraEvent } from '../camera-manager/types';
|
||||||
@@ -132,13 +131,9 @@ export class TriggersManager {
|
|||||||
const triggeredCameraIDs = new Set(this._triggeredCameras.keys());
|
const triggeredCameraIDs = new Set(this._triggeredCameras.keys());
|
||||||
const triggeredState = triggeredCameraIDs.size ? triggeredCameraIDs : undefined;
|
const triggeredState = triggeredCameraIDs.size ? triggeredCameraIDs : undefined;
|
||||||
|
|
||||||
if (
|
this._api.getConditionStateManager().setState({
|
||||||
!isEqual(triggeredState, this._api.getConditionsManager().getState().triggered)
|
triggered: triggeredState,
|
||||||
) {
|
});
|
||||||
this._api.getConditionsManager().setState({
|
|
||||||
triggered: triggeredState,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _untriggerAction(cameraID: string): Promise<void> {
|
protected async _untriggerAction(cameraID: string): Promise<void> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CameraManager } from '../camera-manager/manager';
|
import type { CameraManager } from '../camera-manager/manager';
|
||||||
|
import type { ConditionStateManager } from '../conditions/state-manager';
|
||||||
import type { Automation } from '../config/types';
|
import type { Automation } from '../config/types';
|
||||||
import type { EntityRegistryManager } from '../utils/ha/registry/entity';
|
import type { EntityRegistryManager } from '../utils/ha/registry/entity';
|
||||||
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||||
@@ -6,7 +7,6 @@ import type { ActionsManager } from './actions/actions-manager';
|
|||||||
import type { AutomationsManager } from './automations-manager';
|
import type { AutomationsManager } from './automations-manager';
|
||||||
import type { CameraURLManager } from './camera-url-manager';
|
import type { CameraURLManager } from './camera-url-manager';
|
||||||
import type { CardElementManager } from './card-element-manager';
|
import type { CardElementManager } from './card-element-manager';
|
||||||
import type { ConditionsManager } from './conditions-manager';
|
|
||||||
import type { ConfigManager } from './config/config-manager';
|
import type { ConfigManager } from './config/config-manager';
|
||||||
import type { DefaultManager } from './default-manager';
|
import type { DefaultManager } from './default-manager';
|
||||||
import type { DownloadManager } from './download-manager';
|
import type { DownloadManager } from './download-manager';
|
||||||
@@ -38,7 +38,7 @@ export interface CardActionsAPI {
|
|||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
getCameraURLManager(): CameraURLManager;
|
getCameraURLManager(): CameraURLManager;
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getDownloadManager(): DownloadManager;
|
getDownloadManager(): DownloadManager;
|
||||||
getExpandManager(): ExpandManager;
|
getExpandManager(): ExpandManager;
|
||||||
@@ -57,7 +57,7 @@ export type CardActionsManagerAPI = CardActionsAPI;
|
|||||||
export interface CardAutomationsAPI {
|
export interface CardAutomationsAPI {
|
||||||
getActionsManager(): ActionsManager;
|
getActionsManager(): ActionsManager;
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getHASSManager(): HASSManager;
|
getHASSManager(): HASSManager;
|
||||||
getInitializationManager(): InitializationManager;
|
getInitializationManager(): InitializationManager;
|
||||||
getMessageManager(): MessageManager;
|
getMessageManager(): MessageManager;
|
||||||
@@ -87,7 +87,7 @@ export interface CardConfigAPI {
|
|||||||
getAutomationsManager(): AutomationsManager;
|
getAutomationsManager(): AutomationsManager;
|
||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getDefaultManager(): DefaultManager;
|
getDefaultManager(): DefaultManager;
|
||||||
getInitializationManager(): InitializationManager;
|
getInitializationManager(): InitializationManager;
|
||||||
@@ -125,7 +125,7 @@ export interface CardDownloadAPI {
|
|||||||
export interface CardElementAPI {
|
export interface CardElementAPI {
|
||||||
getActionsManager(): ActionsManager;
|
getActionsManager(): ActionsManager;
|
||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getDefaultManager(): DefaultManager;
|
getDefaultManager(): DefaultManager;
|
||||||
getExpandManager(): ExpandManager;
|
getExpandManager(): ExpandManager;
|
||||||
@@ -142,13 +142,13 @@ export interface CardElementAPI {
|
|||||||
|
|
||||||
export interface CardExpandAPI {
|
export interface CardExpandAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getFullscreenManager(): FullscreenManager;
|
getFullscreenManager(): FullscreenManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardFullscreenAPI {
|
export interface CardFullscreenAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getExpandManager(): ExpandManager;
|
getExpandManager(): ExpandManager;
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
getMediaPlayerManager(): MediaPlayerManager;
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
@@ -157,7 +157,7 @@ export interface CardFullscreenAPI {
|
|||||||
export interface CardHASSAPI {
|
export interface CardHASSAPI {
|
||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getDefaultManager(): DefaultManager;
|
getDefaultManager(): DefaultManager;
|
||||||
getInteractionManager(): InteractionManager;
|
getInteractionManager(): InteractionManager;
|
||||||
@@ -189,7 +189,7 @@ export interface CardInitializerAPI {
|
|||||||
|
|
||||||
export interface CardInteractionAPI {
|
export interface CardInteractionAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getStyleManager(): StyleManager;
|
getStyleManager(): StyleManager;
|
||||||
getTriggersManager(): TriggersManager;
|
getTriggersManager(): TriggersManager;
|
||||||
@@ -198,13 +198,13 @@ export interface CardInteractionAPI {
|
|||||||
|
|
||||||
export interface CardKeyboardStateAPI {
|
export interface CardKeyboardStateAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardMediaLoadedAPI {
|
export interface CardMediaLoadedAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getFullscreenManager(): FullscreenManager;
|
getFullscreenManager(): FullscreenManager;
|
||||||
getStyleManager(): StyleManager;
|
getStyleManager(): StyleManager;
|
||||||
@@ -221,13 +221,13 @@ export interface CardMediaPlayerAPI {
|
|||||||
|
|
||||||
export interface CardMessageAPI {
|
export interface CardMessageAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardMicrophoneAPI {
|
export interface CardMicrophoneAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ export interface CardStyleAPI {
|
|||||||
|
|
||||||
export interface CardTriggersAPI {
|
export interface CardTriggersAPI {
|
||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getInteractionManager(): InteractionManager;
|
getInteractionManager(): InteractionManager;
|
||||||
@@ -264,7 +264,7 @@ export interface CardTriggersAPI {
|
|||||||
export interface CardViewAPI {
|
export interface CardViewAPI {
|
||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getHASSManager(): HASSManager;
|
getHASSManager(): HASSManager;
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
@@ -295,7 +295,6 @@ export interface MicrophoneState {
|
|||||||
forbidden: boolean;
|
forbidden: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaggedAutomation extends Automation {
|
export interface TaggedAutomation extends Automation {
|
||||||
tag?: unknown;
|
tag?: unknown;
|
||||||
}
|
}
|
||||||
export type TaggedAutomations = TaggedAutomation[];
|
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export class ViewManager implements ViewManagerInterface {
|
|||||||
this._api.getMessageManager().reset();
|
this._api.getMessageManager().reset();
|
||||||
this._api.getStyleManager().setExpandedMode();
|
this._api.getStyleManager().setExpandedMode();
|
||||||
|
|
||||||
this._api.getConditionsManager()?.setState({
|
this._api.getConditionStateManager()?.setState({
|
||||||
view: view?.view,
|
view: view?.view,
|
||||||
camera: view?.camera,
|
camera: view?.camera,
|
||||||
displayMode: view?.displayMode ?? undefined,
|
displayMode: view?.displayMode ?? undefined,
|
||||||
|
|||||||
+5
-32
@@ -7,7 +7,6 @@ import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import 'web-dialog';
|
import 'web-dialog';
|
||||||
import { actionHandler } from './action-handler-directive.js';
|
import { actionHandler } from './action-handler-directive.js';
|
||||||
import { ConditionsEvaluateRequestEvent } from './card-controller/conditions-manager.js';
|
|
||||||
import { CardController } from './card-controller/controller';
|
import { CardController } from './card-controller/controller';
|
||||||
import { MenuButtonController } from './components-lib/menu-button-controller';
|
import { MenuButtonController } from './components-lib/menu-button-controller';
|
||||||
import './components/elements.js';
|
import './components/elements.js';
|
||||||
@@ -23,6 +22,7 @@ import './components/status-bar';
|
|||||||
import './components/thumbnail-carousel.js';
|
import './components/thumbnail-carousel.js';
|
||||||
import './components/views.js';
|
import './components/views.js';
|
||||||
import { AdvancedCameraCardViews } from './components/views.js';
|
import { AdvancedCameraCardViews } from './components/views.js';
|
||||||
|
import { ConditionStateManagerGetEvent } from './conditions/state-manager-via-event.js';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardConfig,
|
AdvancedCameraCardConfig,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -95,7 +95,6 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
// diagnostics starting at the top).
|
// diagnostics starting at the top).
|
||||||
() => this._refMain.value?.scroll({ top: 0 }),
|
() => this._refMain.value?.scroll({ top: 0 }),
|
||||||
() => this._refMenu.value?.toggleMenu(),
|
() => this._refMenu.value?.toggleMenu(),
|
||||||
this._requestUpdateForComponentsThatUseConditions.bind(this),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
protected _menuButtonController = new MenuButtonController();
|
protected _menuButtonController = new MenuButtonController();
|
||||||
@@ -143,21 +142,6 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
return CardController.getStubConfig(entities);
|
return CardController.getStubConfig(entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _requestUpdateForComponentsThatUseConditions(): void {
|
|
||||||
// Update the components that need to know about condition changes. Trigger
|
|
||||||
// updates directly on them to them to avoid the performance hit of a entire
|
|
||||||
// card re-render (esp. when using card-mod).
|
|
||||||
// https://github.com/dermotduffy/advanced-camera-card/issues/678
|
|
||||||
if (this._refViews.value) {
|
|
||||||
this._refViews.value.conditionsManagerEpoch =
|
|
||||||
this._controller.getConditionsManager().getEpoch() ?? undefined;
|
|
||||||
}
|
|
||||||
if (this._refElements.value) {
|
|
||||||
this._refElements.value.conditionsManagerEpoch =
|
|
||||||
this._controller.getConditionsManager().getEpoch() ?? undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConfig(config: RawAdvancedCameraCardConfig): void {
|
public setConfig(config: RawAdvancedCameraCardConfig): void {
|
||||||
this._controller.getConfigManager().setConfig(config);
|
this._controller.getConfigManager().setConfig(config);
|
||||||
}
|
}
|
||||||
@@ -390,16 +374,10 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
.viewManagerEpoch=${this._controller.getViewManager().getEpoch()}
|
.viewManagerEpoch=${this._controller.getViewManager().getEpoch()}
|
||||||
.cameraManager=${cameraManager}
|
.cameraManager=${cameraManager}
|
||||||
.resolvedMediaCache=${this._controller.getResolvedMediaCache()}
|
.resolvedMediaCache=${this._controller.getResolvedMediaCache()}
|
||||||
.nonOverriddenConfig=${this._controller
|
.config=${this._controller.getConfigManager().getConfig()}
|
||||||
.getConfigManager()
|
|
||||||
.getNonOverriddenConfig()}
|
|
||||||
.overriddenConfig=${this._controller.getConfigManager().getConfig()}
|
|
||||||
.cardWideConfig=${this._controller.getConfigManager().getCardWideConfig()}
|
.cardWideConfig=${this._controller.getConfigManager().getCardWideConfig()}
|
||||||
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
|
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
|
||||||
.configManager=${this._controller.getConfigManager()}
|
.configManager=${this._controller.getConfigManager()}
|
||||||
.conditionsManagerEpoch=${this._controller
|
|
||||||
.getConditionsManager()
|
|
||||||
?.getEpoch()}
|
|
||||||
.hide=${!!this._controller.getMessageManager().hasMessage()}
|
.hide=${!!this._controller.getMessageManager().hasMessage()}
|
||||||
.microphoneState=${this._controller.getMicrophoneManager().getState()}
|
.microphoneState=${this._controller.getMicrophoneManager().getState()}
|
||||||
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
||||||
@@ -421,9 +399,6 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
${ref(this._refElements)}
|
${ref(this._refElements)}
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.elements=${this._config?.elements}
|
.elements=${this._config?.elements}
|
||||||
.conditionsManagerEpoch=${this._controller
|
|
||||||
.getConditionsManager()
|
|
||||||
?.getEpoch()}
|
|
||||||
@advanced-camera-card:menu:add=${(ev: CustomEvent<MenuItem>) => {
|
@advanced-camera-card:menu:add=${(ev: CustomEvent<MenuItem>) => {
|
||||||
this._menuButtonController.addDynamicMenuButton(ev.detail);
|
this._menuButtonController.addDynamicMenuButton(ev.detail);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -446,12 +421,10 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
.getStatusBarItemManager()
|
.getStatusBarItemManager()
|
||||||
.removeDynamicStatusBarItem(ev.detail);
|
.removeDynamicStatusBarItem(ev.detail);
|
||||||
}}
|
}}
|
||||||
@advanced-camera-card:conditions:evaluate=${(
|
@advanced-camera-card:condition-state-manager:get=${(
|
||||||
ev: ConditionsEvaluateRequestEvent,
|
ev: ConditionStateManagerGetEvent,
|
||||||
) => {
|
) => {
|
||||||
ev.evaluation = this._controller
|
ev.conditionStateManager = this._controller.getConditionStateManager();
|
||||||
.getConditionsManager()
|
|
||||||
?.evaluateConditions(ev.conditions);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-elements>`
|
</advanced-camera-card-elements>`
|
||||||
|
|||||||
+24
-16
@@ -8,10 +8,8 @@ import {
|
|||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import {
|
import { ConditionsManager } from '../conditions/conditions-manager.js';
|
||||||
ConditionsManagerEpoch,
|
import { getConditionStateManagerViaEvent } from '../conditions/state-manager-via-event.js';
|
||||||
evaluateConditionViaEvent,
|
|
||||||
} from '../card-controller/conditions-manager.js';
|
|
||||||
import { dispatchAdvancedCameraCardErrorEvent } from '../components-lib/message/dispatch.js';
|
import { dispatchAdvancedCameraCardErrorEvent } from '../components-lib/message/dispatch.js';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardConditional,
|
AdvancedCameraCardConditional,
|
||||||
@@ -76,13 +74,6 @@ export class AdvancedCameraCardElementsCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public elements?: PictureElements;
|
public elements?: PictureElements;
|
||||||
|
|
||||||
/**
|
|
||||||
* Need to ensure card re-renders when conditions change, hence having it as a
|
|
||||||
* property even though it is not currently directly used by this class.
|
|
||||||
*/
|
|
||||||
@property({ attribute: false })
|
|
||||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
|
||||||
|
|
||||||
protected _root: HuiConditionalElement | null = null;
|
protected _root: HuiConditionalElement | null = null;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -162,9 +153,6 @@ export class AdvancedCameraCardElements extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public elements: PictureElements;
|
public elements: PictureElements;
|
||||||
|
|
||||||
@@ -248,7 +236,6 @@ export class AdvancedCameraCardElements extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`<advanced-camera-card-elements-core
|
return html`<advanced-camera-card-elements-core
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
|
||||||
.elements=${this.elements}
|
.elements=${this.elements}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-elements-core>`;
|
</advanced-camera-card-elements-core>`;
|
||||||
@@ -267,6 +254,7 @@ export class AdvancedCameraCardElements extends LitElement {
|
|||||||
@customElement('advanced-camera-card-conditional')
|
@customElement('advanced-camera-card-conditional')
|
||||||
export class AdvancedCameraCardElementsConditional extends LitElement {
|
export class AdvancedCameraCardElementsConditional extends LitElement {
|
||||||
protected _config?: AdvancedCameraCardConditional;
|
protected _config?: AdvancedCameraCardConditional;
|
||||||
|
protected _conditionManager: ConditionsManager | null = null;
|
||||||
|
|
||||||
// A note on hass as an update mechanism:
|
// A note on hass as an update mechanism:
|
||||||
//
|
//
|
||||||
@@ -283,6 +271,7 @@ export class AdvancedCameraCardElementsConditional extends LitElement {
|
|||||||
*/
|
*/
|
||||||
public setConfig(config: AdvancedCameraCardConditional): void {
|
public setConfig(config: AdvancedCameraCardConditional): void {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
|
this._createConditionManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -303,13 +292,32 @@ export class AdvancedCameraCardElementsConditional extends LitElement {
|
|||||||
// this is a transparent 'conditional' element (just like the stock HA
|
// this is a transparent 'conditional' element (just like the stock HA
|
||||||
// 'conditional' element), it should not have positioning.
|
// 'conditional' element), it should not have positioning.
|
||||||
this.className = '';
|
this.className = '';
|
||||||
|
|
||||||
|
this._createConditionManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this._conditionManager?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _createConditionManager(): void {
|
||||||
|
const conditionStateManager = getConditionStateManagerViaEvent(this);
|
||||||
|
if (!this._config || !conditionStateManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._conditionManager?.destroy();
|
||||||
|
this._conditionManager = new ConditionsManager(
|
||||||
|
this._config.conditions,
|
||||||
|
conditionStateManager,
|
||||||
|
);
|
||||||
|
this._conditionManager.addListener(() => this.requestUpdate());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the card.
|
* Render the card.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (evaluateConditionViaEvent(this, this._config?.conditions)) {
|
if (this._conditionManager?.getEvaluation()?.result) {
|
||||||
return html` <advanced-camera-card-elements-core
|
return html` <advanced-camera-card-elements-core
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.elements=${this._config?.elements}
|
.elements=${this._config?.elements}
|
||||||
|
|||||||
@@ -11,14 +11,9 @@ import { guard } from 'lit/directives/guard.js';
|
|||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { CameraManagerCameraMetadata } from '../../camera-manager/types.js';
|
import { CameraManagerCameraMetadata } from '../../camera-manager/types.js';
|
||||||
import {
|
|
||||||
ConditionsManagerEpoch,
|
|
||||||
getOverriddenConfig,
|
|
||||||
} from '../../card-controller/conditions-manager.js';
|
|
||||||
import { MicrophoneState } from '../../card-controller/types.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
||||||
import { dispatchAdvancedCameraCardErrorEvent } from '../../components-lib/message/dispatch.js';
|
|
||||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||||
import {
|
import {
|
||||||
@@ -26,14 +21,11 @@ import {
|
|||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
configDefaults,
|
configDefaults,
|
||||||
LiveConfig,
|
LiveConfig,
|
||||||
liveConfigAbsoluteRootSchema,
|
|
||||||
Overrides,
|
|
||||||
TransitionEffect,
|
TransitionEffect,
|
||||||
} from '../../config/types.js';
|
} from '../../config/types.js';
|
||||||
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
||||||
import { ExtendedHomeAssistant } from '../../types.js';
|
import { ExtendedHomeAssistant } from '../../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||||
import { contentsChanged } from '../../utils/basic.js';
|
|
||||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||||
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
||||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||||
@@ -70,16 +62,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
public viewManagerEpoch?: ViewManagerEpoch;
|
public viewManagerEpoch?: ViewManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public nonOverriddenLiveConfig?: LiveConfig;
|
public liveConfig?: LiveConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public overriddenLiveConfig?: LiveConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
|
||||||
public overrides?: Overrides;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
@@ -116,10 +99,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getTransitionEffect(): TransitionEffect {
|
protected _getTransitionEffect(): TransitionEffect {
|
||||||
return (
|
return this.liveConfig?.transition_effect ?? configDefaults.live.transition_effect;
|
||||||
this.overriddenLiveConfig?.transition_effect ??
|
|
||||||
configDefaults.live.transition_effect
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getSelectedCameraIndex(): number {
|
protected _getSelectedCameraIndex(): number {
|
||||||
@@ -138,29 +118,25 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (
|
if (changedProps.has('microphoneState') || changedProps.has('liveConfig')) {
|
||||||
changedProps.has('microphoneState') ||
|
|
||||||
changedProps.has('overriddenLiveConfig')
|
|
||||||
) {
|
|
||||||
this._mediaActionsController.setOptions({
|
this._mediaActionsController.setOptions({
|
||||||
playerSelector: ADVANCED_CAMERA_CARD_LIVE_PROVIDER,
|
playerSelector: ADVANCED_CAMERA_CARD_LIVE_PROVIDER,
|
||||||
...(this.overriddenLiveConfig?.auto_play && {
|
...(this.liveConfig?.auto_play && {
|
||||||
autoPlayConditions: this.overriddenLiveConfig.auto_play,
|
autoPlayConditions: this.liveConfig.auto_play,
|
||||||
}),
|
}),
|
||||||
...(this.overriddenLiveConfig?.auto_pause && {
|
...(this.liveConfig?.auto_pause && {
|
||||||
autoPauseConditions: this.overriddenLiveConfig.auto_pause,
|
autoPauseConditions: this.liveConfig.auto_pause,
|
||||||
}),
|
}),
|
||||||
...(this.overriddenLiveConfig?.auto_mute && {
|
...(this.liveConfig?.auto_mute && {
|
||||||
autoMuteConditions: this.overriddenLiveConfig.auto_mute,
|
autoMuteConditions: this.liveConfig.auto_mute,
|
||||||
}),
|
}),
|
||||||
...(this.overriddenLiveConfig?.auto_unmute && {
|
...(this.liveConfig?.auto_unmute && {
|
||||||
autoUnmuteConditions: this.overriddenLiveConfig.auto_unmute,
|
autoUnmuteConditions: this.liveConfig.auto_unmute,
|
||||||
}),
|
}),
|
||||||
...((this.overriddenLiveConfig?.auto_unmute ||
|
...((this.liveConfig?.auto_unmute || this.liveConfig?.auto_mute) && {
|
||||||
this.overriddenLiveConfig?.auto_mute) && {
|
|
||||||
microphoneState: this.microphoneState,
|
microphoneState: this.microphoneState,
|
||||||
microphoneMuteSeconds:
|
microphoneMuteSeconds:
|
||||||
this.overriddenLiveConfig.microphone.mute_after_microphone_mute_seconds,
|
this.liveConfig.microphone.mute_after_microphone_mute_seconds,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -169,11 +145,11 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
protected _getPlugins(): EmblaCarouselPlugins {
|
protected _getPlugins(): EmblaCarouselPlugins {
|
||||||
return [
|
return [
|
||||||
AutoLazyLoad({
|
AutoLazyLoad({
|
||||||
...(this.overriddenLiveConfig?.lazy_load && {
|
...(this.liveConfig?.lazy_load && {
|
||||||
lazyLoadCallback: (index, slide) =>
|
lazyLoadCallback: (index, slide) =>
|
||||||
this._lazyloadOrUnloadSlide('load', index, slide),
|
this._lazyloadOrUnloadSlide('load', index, slide),
|
||||||
}),
|
}),
|
||||||
lazyUnloadConditions: this.overriddenLiveConfig?.lazy_unload,
|
lazyUnloadConditions: this.liveConfig?.lazy_unload,
|
||||||
lazyUnloadCallback: (index, slide) =>
|
lazyUnloadCallback: (index, slide) =>
|
||||||
this._lazyloadOrUnloadSlide('unload', index, slide),
|
this._lazyloadOrUnloadSlide('unload', index, slide),
|
||||||
}),
|
}),
|
||||||
@@ -191,7 +167,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected _getLazyLoadCount(): number | null {
|
protected _getLazyLoadCount(): number | null {
|
||||||
// Defaults to fully-lazy loading.
|
// Defaults to fully-lazy loading.
|
||||||
return this.overriddenLiveConfig?.lazy_load === false ? null : 0;
|
return this.liveConfig?.lazy_load === false ? null : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getSlides(): [TemplateResult[], Record<string, number>] {
|
protected _getSlides(): [TemplateResult[], Record<string, number>] {
|
||||||
@@ -265,43 +241,17 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
cameraID: string,
|
cameraID: string,
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
if (
|
if (!this.liveConfig || !this.hass || !this.cameraManager) {
|
||||||
!this.overriddenLiveConfig ||
|
|
||||||
!this.nonOverriddenLiveConfig ||
|
|
||||||
!this.hass ||
|
|
||||||
!this.cameraManager ||
|
|
||||||
!this.conditionsManagerEpoch
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let liveConfig: LiveConfig | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// The condition controller object contains the currently live camera, which
|
|
||||||
// (in the carousel for example) is not necessarily the live camera *this*
|
|
||||||
// <advanced-camera-card-live-provider> is rendering right now, so we provide a
|
|
||||||
// stateOverride to evaluate the condition in that context.
|
|
||||||
liveConfig = getOverriddenConfig(
|
|
||||||
this.conditionsManagerEpoch.manager,
|
|
||||||
{ live: this.nonOverriddenLiveConfig },
|
|
||||||
{
|
|
||||||
configOverrides: this.overrides,
|
|
||||||
stateOverrides: { camera: cameraID },
|
|
||||||
schema: liveConfigAbsoluteRootSchema,
|
|
||||||
},
|
|
||||||
).live;
|
|
||||||
} catch (ev) {
|
|
||||||
return dispatchAdvancedCameraCardErrorEvent(this, ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
|
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="embla__slide">
|
<div class="embla__slide">
|
||||||
<advanced-camera-card-live-provider
|
<advanced-camera-card-live-provider
|
||||||
?load=${!liveConfig.lazy_load}
|
?load=${!this.liveConfig.lazy_load}
|
||||||
.microphoneState=${view?.camera === cameraID
|
.microphoneState=${view?.camera === cameraID
|
||||||
? this.microphoneState
|
? this.microphoneState
|
||||||
: undefined}
|
: undefined}
|
||||||
@@ -311,7 +261,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
||||||
)}
|
)}
|
||||||
.label=${cameraMetadata?.title ?? ''}
|
.label=${cameraMetadata?.title ?? ''}
|
||||||
.liveConfig=${liveConfig}
|
.liveConfig=${this.liveConfig}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
|
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
|
||||||
@@ -385,7 +335,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
slot=${side}
|
slot=${side}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.side=${side}
|
.side=${side}
|
||||||
.controlConfig=${this.overriddenLiveConfig?.controls.next_previous}
|
.controlConfig=${this.liveConfig?.controls.next_previous}
|
||||||
.label=${neighbor?.metadata?.title ?? ''}
|
.label=${neighbor?.metadata?.title ?? ''}
|
||||||
.icon=${neighbor?.metadata?.icon}
|
.icon=${neighbor?.metadata?.icon}
|
||||||
?disabled=${!neighbor}
|
?disabled=${!neighbor}
|
||||||
@@ -399,7 +349,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
if (!this.overriddenLiveConfig || !this.hass || !view || !this.cameraManager) {
|
if (!this.liveConfig || !this.hass || !view || !this.cameraManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,9 +377,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
<advanced-camera-card-carousel
|
<advanced-camera-card-carousel
|
||||||
${ref(this._refCarousel)}
|
${ref(this._refCarousel)}
|
||||||
.loop=${hasMultipleCameras}
|
.loop=${hasMultipleCameras}
|
||||||
.dragEnabled=${hasMultipleCameras && this.overriddenLiveConfig?.draggable}
|
.dragEnabled=${hasMultipleCameras && this.liveConfig?.draggable}
|
||||||
.plugins=${guard(
|
.plugins=${guard(
|
||||||
[this.cameraManager, this.overriddenLiveConfig],
|
[this.cameraManager, this.liveConfig],
|
||||||
this._getPlugins.bind(this),
|
this._getPlugins.bind(this),
|
||||||
)}
|
)}
|
||||||
.selected=${this._getSelectedCameraIndex()}
|
.selected=${this._getSelectedCameraIndex()}
|
||||||
@@ -449,7 +399,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
${this._renderNextPrevious('right', neighbors)}
|
${this._renderNextPrevious('right', neighbors)}
|
||||||
</advanced-camera-card-carousel>
|
</advanced-camera-card-carousel>
|
||||||
<advanced-camera-card-ptz
|
<advanced-camera-card-ptz
|
||||||
.config=${this.overriddenLiveConfig.controls.ptz}
|
.config=${this.liveConfig.controls.ptz}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cameraID=${getStreamCameraID(view, this.viewFilterCameraID)}
|
.cameraID=${getStreamCameraID(view, this.viewFilterCameraID)}
|
||||||
.forceVisibility=${forcePTZVisibility}
|
.forceVisibility=${forcePTZVisibility}
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import {
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
|
||||||
import { MicrophoneState } from '../../card-controller/types.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
||||||
import { CardWideConfig, LiveConfig, Overrides } from '../../config/types.js';
|
import { CardWideConfig, LiveConfig } from '../../config/types.js';
|
||||||
import liveGridStyle from '../../scss/live-grid.scss';
|
import liveGridStyle from '../../scss/live-grid.scss';
|
||||||
import { ExtendedHomeAssistant } from '../../types.js';
|
import { ExtendedHomeAssistant } from '../../types.js';
|
||||||
import { contentsChanged } from '../../utils/basic.js';
|
|
||||||
import './carousel.js';
|
import './carousel.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-grid')
|
@customElement('advanced-camera-card-live-grid')
|
||||||
@@ -28,16 +26,7 @@ export class AdvancedCameraCardLiveGrid extends LitElement {
|
|||||||
public viewManagerEpoch?: ViewManagerEpoch;
|
public viewManagerEpoch?: ViewManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public nonOverriddenLiveConfig?: LiveConfig;
|
public liveConfig?: LiveConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public overriddenLiveConfig?: LiveConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
|
||||||
public overrides?: Overrides;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
@@ -61,10 +50,7 @@ export class AdvancedCameraCardLiveGrid extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
.viewFilterCameraID=${cameraID}
|
.viewFilterCameraID=${cameraID}
|
||||||
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
.liveConfig=${this.liveConfig}
|
||||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
|
||||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
|
||||||
.overrides=${this.overrides}
|
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.microphoneState=${this.microphoneState}
|
.microphoneState=${this.microphoneState}
|
||||||
@@ -101,9 +87,6 @@ export class AdvancedCameraCardLiveGrid extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.conditionsManagerEpoch || !this.nonOverriddenLiveConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||||
if (!cameraIDs?.size || !this._needsGrid()) {
|
if (!cameraIDs?.size || !this._needsGrid()) {
|
||||||
return this._renderCarousel();
|
return this._renderCarousel();
|
||||||
@@ -112,7 +95,7 @@ export class AdvancedCameraCardLiveGrid extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<advanced-camera-card-media-grid
|
<advanced-camera-card-media-grid
|
||||||
.selected=${this.viewManagerEpoch?.manager.getView()?.camera}
|
.selected=${this.viewManagerEpoch?.manager.getView()?.camera}
|
||||||
.displayConfig=${this.overriddenLiveConfig?.display}
|
.displayConfig=${this.liveConfig?.display}
|
||||||
@advanced-camera-card:media-grid:selected=${(
|
@advanced-camera-card:media-grid:selected=${(
|
||||||
ev: CustomEvent<MediaGridSelected>,
|
ev: CustomEvent<MediaGridSelected>,
|
||||||
) => this._gridSelectCamera(ev.detail.selected)}
|
) => this._gridSelectCamera(ev.detail.selected)}
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
|
||||||
import { MicrophoneState } from '../../card-controller/types.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { LiveController } from '../../components-lib/live/live-controller.js';
|
import { LiveController } from '../../components-lib/live/live-controller.js';
|
||||||
import { CardWideConfig, LiveConfig, Overrides } from '../../config/types.js';
|
import { CardWideConfig, LiveConfig } from '../../config/types.js';
|
||||||
import basicBlockStyle from '../../scss/basic-block.scss';
|
import basicBlockStyle from '../../scss/basic-block.scss';
|
||||||
import { ExtendedHomeAssistant } from '../../types.js';
|
import { ExtendedHomeAssistant } from '../../types.js';
|
||||||
import { contentsChanged } from '../../utils/basic.js';
|
|
||||||
import './grid.js';
|
import './grid.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live')
|
@customElement('advanced-camera-card-live')
|
||||||
export class AdvancedCameraCardLive extends LitElement {
|
export class AdvancedCameraCardLive extends LitElement {
|
||||||
@property({ attribute: false })
|
|
||||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
@@ -23,13 +18,7 @@ export class AdvancedCameraCardLive extends LitElement {
|
|||||||
public viewManagerEpoch?: ViewManagerEpoch;
|
public viewManagerEpoch?: ViewManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public nonOverriddenLiveConfig?: LiveConfig;
|
public liveConfig?: LiveConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public overriddenLiveConfig?: LiveConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
|
||||||
public overrides?: Overrides;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
@@ -46,7 +35,7 @@ export class AdvancedCameraCardLive extends LitElement {
|
|||||||
protected _controller = new LiveController(this);
|
protected _controller = new LiveController(this);
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.nonOverriddenLiveConfig || !this.cameraManager) {
|
if (!this.hass || !this.cameraManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,11 +49,8 @@ export class AdvancedCameraCardLive extends LitElement {
|
|||||||
<advanced-camera-card-live-grid
|
<advanced-camera-card-live-grid
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
.liveConfig=${this.liveConfig}
|
||||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
|
||||||
.inBackground=${this._controller.isInBackground()}
|
.inBackground=${this._controller.isInBackground()}
|
||||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
|
||||||
.overrides=${this.overrides}
|
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.microphoneState=${this.microphoneState}
|
.microphoneState=${this.microphoneState}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
|||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||||
import {
|
import {
|
||||||
ExtendedHomeAssistant,
|
|
||||||
AdvancedCameraCardMediaPlayer,
|
AdvancedCameraCardMediaPlayer,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
FullscreenElement,
|
FullscreenElement,
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { aspectRatioToString } from '../../utils/basic.js';
|
import { aspectRatioToString } from '../../utils/basic.js';
|
||||||
@@ -210,6 +210,7 @@ export class AdvancedCameraCardLiveProvider
|
|||||||
dispatchMediaUnloadedEvent(this);
|
dispatchMediaUnloadedEvent(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has('liveConfig')) {
|
if (changedProps.has('liveConfig')) {
|
||||||
if (this.liveConfig?.show_image_during_load) {
|
if (this.liveConfig?.show_image_during_load) {
|
||||||
this._importPromises.push(import('./providers/image.js'));
|
this._importPromises.push(import('./providers/image.js'));
|
||||||
|
|||||||
+15
-53
@@ -9,7 +9,6 @@ import {
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch } from '../card-controller/conditions-manager.js';
|
|
||||||
import { MicrophoneState } from '../card-controller/types.js';
|
import { MicrophoneState } from '../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||||
import {
|
import {
|
||||||
@@ -39,10 +38,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public nonOverriddenConfig?: AdvancedCameraCardConfig;
|
public config?: AdvancedCameraCardConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public overriddenConfig?: AdvancedCameraCardConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
@@ -53,9 +49,6 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public resolvedMediaCache?: ResolvedMediaCache;
|
public resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hide?: boolean;
|
public hide?: boolean;
|
||||||
|
|
||||||
@@ -94,31 +87,12 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
protected shouldUpdate(_: PropertyValues): boolean {
|
|
||||||
// Future: Updates to `hass` and `conditionState` here will be frequent.
|
|
||||||
// Throttling here may be necessary if users report performance degradation
|
|
||||||
// > v5.0.0-beta1 .
|
|
||||||
//
|
|
||||||
// These updates are necessary in these cases:
|
|
||||||
// - conditionState: Required to let `advanced-camera-card-live` calculate its own
|
|
||||||
// overrides.
|
|
||||||
// - hass: Required for anything that needs to sign URLs. Of note is
|
|
||||||
// anything that renders an image (e.g. a thumbnail -- almost everything,
|
|
||||||
// or the main `advanced-camera-card-image` view).
|
|
||||||
//
|
|
||||||
// It should instead be possible to pass conditionState to live only (every
|
|
||||||
// update required), and pass hass only once / 5 minutes (see
|
|
||||||
// HASS_REJECTION_CUTOFF_MS).
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _shouldLivePreload(): boolean {
|
protected _shouldLivePreload(): boolean {
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
return (
|
return (
|
||||||
// Special case: Never preload for diagnostics -- we want that to be as
|
// Special case: Never preload for diagnostics -- we want that to be as
|
||||||
// minimal as possible.
|
// minimal as possible.
|
||||||
!!this.overriddenConfig?.live.preload && !view?.is('diagnostics')
|
!!this.config?.live.preload && !view?.is('diagnostics')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,12 +101,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
// overall views pane to render in ~almost all cases (e.g. for a camera
|
// overall views pane to render in ~almost all cases (e.g. for a camera
|
||||||
// initialization error to display, `view` and `cameraConfig` may both be
|
// initialization error to display, `view` and `cameraConfig` may both be
|
||||||
// undefined, but we still want to render).
|
// undefined, but we still want to render).
|
||||||
if (
|
if (!this.hass || !this.config || !this.cardWideConfig) {
|
||||||
!this.hass ||
|
|
||||||
!this.overriddenConfig ||
|
|
||||||
!this.nonOverriddenConfig ||
|
|
||||||
!this.cardWideConfig
|
|
||||||
) {
|
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,17 +117,17 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const thumbnailConfig = view?.is('live')
|
const thumbnailConfig = view?.is('live')
|
||||||
? this.overriddenConfig.live.controls.thumbnails
|
? this.config.live.controls.thumbnails
|
||||||
: view?.isViewerView()
|
: view?.isViewerView()
|
||||||
? this.overriddenConfig.media_viewer.controls.thumbnails
|
? this.config.media_viewer.controls.thumbnails
|
||||||
: view?.is('timeline')
|
: view?.is('timeline')
|
||||||
? this.overriddenConfig.timeline.controls.thumbnails
|
? this.config.timeline.controls.thumbnails
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const miniTimelineConfig = view?.is('live')
|
const miniTimelineConfig = view?.is('live')
|
||||||
? this.overriddenConfig.live.controls.timeline
|
? this.config.live.controls.timeline
|
||||||
: view?.isViewerView()
|
: view?.isViewerView()
|
||||||
? this.overriddenConfig.media_viewer.controls.timeline
|
? this.config.media_viewer.controls.timeline
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const cameraConfig = view
|
const cameraConfig = view
|
||||||
@@ -176,7 +145,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
>
|
>
|
||||||
${!this.hide && view?.is('image') && cameraConfig
|
${!this.hide && view?.is('image') && cameraConfig
|
||||||
? html` <advanced-camera-card-image
|
? html` <advanced-camera-card-image
|
||||||
.imageConfig=${this.overriddenConfig.image}
|
.imageConfig=${this.config.image}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
@@ -188,7 +157,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
? html` <advanced-camera-card-gallery
|
? html` <advanced-camera-card-gallery
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
.galleryConfig=${this.overriddenConfig.media_gallery}
|
.galleryConfig=${this.config.media_gallery}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
@@ -199,7 +168,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
<advanced-camera-card-viewer
|
<advanced-camera-card-viewer
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
.viewerConfig=${this.overriddenConfig.media_viewer}
|
.viewerConfig=${this.config.media_viewer}
|
||||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
@@ -211,7 +180,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
? html` <advanced-camera-card-timeline
|
? html` <advanced-camera-card-timeline
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
.timelineConfig=${this.overriddenConfig.timeline}
|
.timelineConfig=${this.config.timeline}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
@@ -226,21 +195,14 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
</advanced-camera-card-diagnostics>`
|
</advanced-camera-card-diagnostics>`
|
||||||
: ``}
|
: ``}
|
||||||
${
|
${
|
||||||
// Note: Subtle difference in condition below vs the other views in order
|
// Note: Subtle difference in condition below vs the other views in
|
||||||
// to always render the live view for live.preload mode.
|
// order to always render the live view for live.preload mode.
|
||||||
|
|
||||||
// Note: <advanced-camera-card-live> uses nonOverriddenConfig rather than the
|
|
||||||
// overriden config as it does it's own overriding as part of the camera
|
|
||||||
// carousel.
|
|
||||||
this._shouldLivePreload() || (!this.hide && view?.is('live'))
|
this._shouldLivePreload() || (!this.hide && view?.is('live'))
|
||||||
? html`
|
? html`
|
||||||
<advanced-camera-card-live
|
<advanced-camera-card-live
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
.nonOverriddenLiveConfig=${this.nonOverriddenConfig.live}
|
.liveConfig=${this.config.live}
|
||||||
.overriddenLiveConfig=${this.overriddenConfig.live}
|
|
||||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
|
||||||
.overrides=${this.overriddenConfig.overrides}
|
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.microphoneState=${this.microphoneState}
|
.microphoneState=${this.microphoneState}
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { AdvancedCameraCardCondition } from '../config/types';
|
||||||
|
import { isCompanionApp } from '../utils/companion';
|
||||||
|
import {
|
||||||
|
ConditionsEvaluationData,
|
||||||
|
ConditionsEvaluationResult,
|
||||||
|
ConditionsListener,
|
||||||
|
ConditionsManagerReadonlyInterface,
|
||||||
|
ConditionState,
|
||||||
|
ConditionStateChange,
|
||||||
|
ConditionStateManagerReadonlyInterface,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to evaluate an array of conditions, and notify listeners when the
|
||||||
|
* evaluation changes (a change is either the result changing, or the data
|
||||||
|
* associated with a result).
|
||||||
|
*/
|
||||||
|
export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||||
|
protected _conditions: AdvancedCameraCardCondition[];
|
||||||
|
protected _stateManager: ConditionStateManagerReadonlyInterface | null;
|
||||||
|
|
||||||
|
protected _listeners: ConditionsListener[] = [];
|
||||||
|
protected _mediaQueries: MediaQueryList[] = [];
|
||||||
|
protected _hasHAStateConditions = false;
|
||||||
|
protected _evaluation: ConditionsEvaluationResult = { result: false };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
conditions: AdvancedCameraCardCondition[],
|
||||||
|
stateManager?: ConditionStateManagerReadonlyInterface | null,
|
||||||
|
) {
|
||||||
|
this._conditions = conditions;
|
||||||
|
|
||||||
|
this._hasHAStateConditions = conditions.some(
|
||||||
|
(condition) =>
|
||||||
|
!condition.condition ||
|
||||||
|
['state', 'numeric_state', 'user'].includes(condition.condition),
|
||||||
|
);
|
||||||
|
|
||||||
|
conditions.forEach((condition) => {
|
||||||
|
if (condition.condition === 'screen') {
|
||||||
|
const mql = window.matchMedia(condition.media_query);
|
||||||
|
mql.addEventListener('change', this._mediaQueryHandler);
|
||||||
|
this._mediaQueries.push(mql);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._stateManager = stateManager ?? null;
|
||||||
|
|
||||||
|
// Do an initial condition evaluation, but without calling listeners.
|
||||||
|
this._evaluate({ callListeners: false });
|
||||||
|
|
||||||
|
this._stateManager?.addListener(this._stateManagerHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this._stateManager?.removeListener(this._stateManagerHandler);
|
||||||
|
|
||||||
|
this._listeners.forEach((l) => this.removeListener(l));
|
||||||
|
|
||||||
|
this._mediaQueries.forEach((mql) =>
|
||||||
|
mql.removeEventListener('change', this._mediaQueryHandler),
|
||||||
|
);
|
||||||
|
this._mediaQueries = [];
|
||||||
|
this._conditions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(listener: ConditionsListener): void {
|
||||||
|
if (!this._listeners.includes(listener)) {
|
||||||
|
this._listeners.push(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(listener: ConditionsListener): void {
|
||||||
|
this._listeners = this._listeners.filter((l) => l !== listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEvaluation(): ConditionsEvaluationResult {
|
||||||
|
return this._evaluation;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _mediaQueryHandler = () => this._evaluate();
|
||||||
|
|
||||||
|
protected _stateManagerHandler = (stateChange: ConditionStateChange): void => {
|
||||||
|
// As a performance optmization, if only Home Assistant state has changed
|
||||||
|
// (very frequent), and there aren't any related conditions, don't bother
|
||||||
|
// calling for the evealuation / listeners.
|
||||||
|
if (
|
||||||
|
Object.keys(stateChange.change).length === 1 &&
|
||||||
|
'state' in stateChange.change &&
|
||||||
|
!this._hasHAStateConditions
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._evaluate({ stateChange });
|
||||||
|
};
|
||||||
|
|
||||||
|
protected _evaluate(options?: {
|
||||||
|
stateChange?: ConditionStateChange;
|
||||||
|
callListeners?: boolean;
|
||||||
|
}): void {
|
||||||
|
const state = options?.stateChange?.new ?? this._stateManager?.getState();
|
||||||
|
|
||||||
|
let result = true;
|
||||||
|
let data: ConditionsEvaluationData = {};
|
||||||
|
|
||||||
|
for (const condition of this._conditions) {
|
||||||
|
const evaluation = this._evaluateCondition(
|
||||||
|
condition,
|
||||||
|
state,
|
||||||
|
options?.stateChange?.old,
|
||||||
|
);
|
||||||
|
if (!evaluation.result) {
|
||||||
|
result = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
...data,
|
||||||
|
...evaluation.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluation: ConditionsEvaluationResult = result
|
||||||
|
? { result, data }
|
||||||
|
: { result };
|
||||||
|
|
||||||
|
if (!isEqual(evaluation, this._evaluation)) {
|
||||||
|
this._evaluation = evaluation;
|
||||||
|
if (options?.callListeners ?? true) {
|
||||||
|
this._listeners.forEach(
|
||||||
|
(listener) => this._evaluation && listener(this._evaluation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _evaluateCondition(
|
||||||
|
condition: AdvancedCameraCardCondition,
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
switch (condition.condition) {
|
||||||
|
case undefined:
|
||||||
|
case 'state':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.state &&
|
||||||
|
((!condition.state && !condition.state_not) ||
|
||||||
|
(condition.entity in newState.state &&
|
||||||
|
(!condition.state ||
|
||||||
|
(Array.isArray(condition.state)
|
||||||
|
? condition.state.includes(newState.state[condition.entity].state)
|
||||||
|
: condition.state === newState.state[condition.entity].state)) &&
|
||||||
|
(!condition.state_not ||
|
||||||
|
(Array.isArray(condition.state_not)
|
||||||
|
? !condition.state_not.includes(
|
||||||
|
newState.state[condition.entity].state,
|
||||||
|
)
|
||||||
|
: condition.state_not !== newState.state[condition.entity].state)))),
|
||||||
|
data: {
|
||||||
|
state: {
|
||||||
|
entity: condition.entity,
|
||||||
|
...(oldState?.state?.[condition.entity]?.state && {
|
||||||
|
from: oldState?.state?.[condition.entity]?.state,
|
||||||
|
}),
|
||||||
|
...(newState?.state?.[condition.entity]?.state && {
|
||||||
|
to: newState?.state?.[condition.entity]?.state,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'view':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
(!!newState?.view && condition.views?.includes(newState.view)) ||
|
||||||
|
(newState?.view !== oldState?.view && !condition.views?.length),
|
||||||
|
data: {
|
||||||
|
...((oldState?.view || newState?.view) && {
|
||||||
|
view: {
|
||||||
|
...(oldState?.view && { from: oldState.view }),
|
||||||
|
...(newState?.view && { to: newState.view }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'fullscreen':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
newState?.fullscreen !== undefined &&
|
||||||
|
condition.fullscreen === newState.fullscreen,
|
||||||
|
};
|
||||||
|
case 'expand':
|
||||||
|
return {
|
||||||
|
result: newState?.expand !== undefined && condition.expand === newState.expand,
|
||||||
|
};
|
||||||
|
case 'camera':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
(!!newState?.camera && !!condition.cameras?.includes(newState.camera)) ||
|
||||||
|
(newState?.camera !== oldState?.camera && !condition.cameras?.length),
|
||||||
|
data: {
|
||||||
|
...((oldState?.camera || newState?.camera) && {
|
||||||
|
camera: {
|
||||||
|
...(oldState?.camera && { from: oldState?.camera }),
|
||||||
|
...(newState?.camera && { to: newState?.camera }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'numeric_state':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.state &&
|
||||||
|
condition.entity in newState.state &&
|
||||||
|
newState.state[condition.entity].state !== undefined &&
|
||||||
|
(condition.above === undefined ||
|
||||||
|
Number(newState.state[condition.entity].state) > condition.above) &&
|
||||||
|
(condition.below === undefined ||
|
||||||
|
Number(newState.state[condition.entity].state) < condition.below),
|
||||||
|
};
|
||||||
|
case 'user':
|
||||||
|
return {
|
||||||
|
result: !!newState?.user && condition.users.includes(newState.user.id),
|
||||||
|
};
|
||||||
|
case 'media_loaded':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
newState?.mediaLoadedInfo !== undefined &&
|
||||||
|
condition.media_loaded === !!newState.mediaLoadedInfo,
|
||||||
|
};
|
||||||
|
case 'screen':
|
||||||
|
return { result: window.matchMedia(condition.media_query).matches };
|
||||||
|
case 'display_mode':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.displayMode && condition.display_mode === newState.displayMode,
|
||||||
|
};
|
||||||
|
case 'triggered':
|
||||||
|
return {
|
||||||
|
result: condition.triggered.some((triggeredCameraID) =>
|
||||||
|
newState?.triggered?.has(triggeredCameraID),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case 'interaction':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
newState?.interaction !== undefined &&
|
||||||
|
condition.interaction === newState.interaction,
|
||||||
|
};
|
||||||
|
case 'microphone':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
(condition.connected === undefined ||
|
||||||
|
newState?.microphone?.connected === condition.connected) &&
|
||||||
|
(condition.muted === undefined ||
|
||||||
|
newState?.microphone?.muted === condition.muted),
|
||||||
|
};
|
||||||
|
case 'key':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.keys &&
|
||||||
|
condition.key in newState.keys &&
|
||||||
|
(condition.state ?? 'down') === newState.keys[condition.key].state &&
|
||||||
|
(condition.ctrl === undefined ||
|
||||||
|
condition.ctrl === !!newState.keys[condition.key].ctrl) &&
|
||||||
|
(condition.alt === undefined ||
|
||||||
|
condition.alt === !!newState.keys[condition.key].alt) &&
|
||||||
|
(condition.meta === undefined ||
|
||||||
|
condition.meta === !!newState.keys[condition.key].meta) &&
|
||||||
|
(condition.shift === undefined ||
|
||||||
|
condition.shift === !!newState.keys[condition.key].shift),
|
||||||
|
};
|
||||||
|
case 'user_agent':
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.userAgent &&
|
||||||
|
(!condition.user_agent || condition.user_agent === newState.userAgent) &&
|
||||||
|
(condition.companion === undefined ||
|
||||||
|
condition.companion === isCompanionApp(newState.userAgent)) &&
|
||||||
|
(condition.user_agent_re === undefined ||
|
||||||
|
new RegExp(condition.user_agent_re).test(newState.userAgent)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ConditionStateManager } from './state-manager';
|
||||||
|
|
||||||
|
export class ConditionStateManagerGetEvent extends Event {
|
||||||
|
public conditionStateManager?: ConditionStateManager;
|
||||||
|
|
||||||
|
constructor(eventInitDict?: EventInit) {
|
||||||
|
super('advanced-camera-card:condition-state-manager:get', eventInitDict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the main ConditionStateManager via an event.
|
||||||
|
* @returns The ConditionStateManager or null if not found.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getConditionStateManagerViaEvent(
|
||||||
|
element: HTMLElement,
|
||||||
|
): ConditionStateManager | null {
|
||||||
|
const getEvent = new ConditionStateManagerGetEvent({
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Special note on what's going on here:
|
||||||
|
*
|
||||||
|
* Some parts of the card (e.g. <advanced-camera-card-elements>) may have arbitrary
|
||||||
|
* complexity and layers (that this card doesn't control) between that master
|
||||||
|
* element and the element that needs to evaluate the condition. In these
|
||||||
|
* cases there's no clean way to pass state from the rest of card down through
|
||||||
|
* these layers. Instead, an event is dispatched as a "request for evaluation"
|
||||||
|
* (ConditionEvaluateRequestEvent) upwards which is caught by the outer card
|
||||||
|
* and the evaluation result is added to the event object. Because event
|
||||||
|
* propagation is handled synchronously, the result will be added to the event
|
||||||
|
* before the flow proceeds.
|
||||||
|
*/
|
||||||
|
element.dispatchEvent(getEvent);
|
||||||
|
return getEvent.conditionStateManager ?? null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import {
|
||||||
|
ConditionState,
|
||||||
|
ConditionStateChange,
|
||||||
|
ConditionStateListener,
|
||||||
|
ConditionStateManagerReadonlyInterface,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to manage state used in the evaluation of conditions.
|
||||||
|
*/
|
||||||
|
export class ConditionStateManager implements ConditionStateManagerReadonlyInterface {
|
||||||
|
protected _listeners: ConditionStateListener[] = [];
|
||||||
|
protected _state: ConditionState = {};
|
||||||
|
|
||||||
|
public addListener(listener: ConditionStateListener): void {
|
||||||
|
this._listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(listener: ConditionStateListener): void {
|
||||||
|
this._listeners = this._listeners.filter((l) => l !== listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getState(): ConditionState {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setState(state: ConditionState): void {
|
||||||
|
this._processStateChange(this._calculateTrueChange(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _processStateChange(changeState: ConditionState): void {
|
||||||
|
if (!Object.keys(changeState).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldState = this._state;
|
||||||
|
this._state = {
|
||||||
|
...oldState,
|
||||||
|
...changeState,
|
||||||
|
};
|
||||||
|
this._callListeners({ old: oldState, change: changeState, new: this._state });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _calculateTrueChange(change: ConditionState): ConditionState {
|
||||||
|
const changeState: ConditionState = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(change)) {
|
||||||
|
if (!isEqual(change[key], this._state[key])) {
|
||||||
|
changeState[key] = change[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _callListeners = (stateChange: ConditionStateChange): void => {
|
||||||
|
this._listeners.forEach((listener) => listener(stateChange));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { CurrentUser } from '@dermotduffy/custom-card-helpers';
|
||||||
|
import { HassEntities } from 'home-assistant-js-websocket';
|
||||||
|
import { ViewDisplayMode } from '../config/types';
|
||||||
|
import { MediaLoadedInfo } from '../types';
|
||||||
|
import { KeysState, MicrophoneState } from '../card-controller/types';
|
||||||
|
|
||||||
|
export interface ConditionState {
|
||||||
|
camera?: string;
|
||||||
|
displayMode?: ViewDisplayMode;
|
||||||
|
expand?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
|
interaction?: boolean;
|
||||||
|
keys?: KeysState;
|
||||||
|
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
|
microphone?: MicrophoneState;
|
||||||
|
state?: HassEntities;
|
||||||
|
triggered?: Set<string>;
|
||||||
|
user?: CurrentUser;
|
||||||
|
userAgent?: string;
|
||||||
|
view?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConditionStateChange {
|
||||||
|
old: ConditionState;
|
||||||
|
change: ConditionState;
|
||||||
|
new: ConditionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConditionStateListener = (change: ConditionStateChange) => void;
|
||||||
|
|
||||||
|
export interface ConditionStateManagerReadonlyInterface {
|
||||||
|
addListener(listener: ConditionStateListener): void;
|
||||||
|
removeListener(listener: ConditionStateListener): void;
|
||||||
|
getState(): ConditionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConditionsEvaluationDataFromTo {
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConditionsEvaluationDataState extends ConditionsEvaluationDataFromTo {
|
||||||
|
entity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConditionsEvaluationData {
|
||||||
|
camera?: ConditionsEvaluationDataFromTo;
|
||||||
|
view?: ConditionsEvaluationDataFromTo;
|
||||||
|
state?: ConditionsEvaluationDataState;
|
||||||
|
}
|
||||||
|
interface ConditionsEvaluationResultTrue {
|
||||||
|
result: true;
|
||||||
|
data?: ConditionsEvaluationData;
|
||||||
|
}
|
||||||
|
interface ConditionsEvaluationResultFalse {
|
||||||
|
result: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConditionsEvaluationResult =
|
||||||
|
| ConditionsEvaluationResultTrue
|
||||||
|
| ConditionsEvaluationResultFalse;
|
||||||
|
|
||||||
|
export type ConditionsListener = (result: ConditionsEvaluationResult) => void;
|
||||||
|
|
||||||
|
export interface ConditionsManagerReadonlyInterface {
|
||||||
|
addListener(listener: ConditionsListener): void;
|
||||||
|
removeListener(listener: ConditionsListener): void;
|
||||||
|
getEvaluation(): ConditionsEvaluationResult | null;
|
||||||
|
}
|
||||||
+3
-13
@@ -685,7 +685,7 @@ export type StatusBarItem = z.infer<typeof statusBarItemSchema>;
|
|||||||
|
|
||||||
const viewConditionSchema = z.object({
|
const viewConditionSchema = z.object({
|
||||||
condition: z.literal('view'),
|
condition: z.literal('view'),
|
||||||
views: z.string().array(),
|
views: z.string().array().optional(),
|
||||||
});
|
});
|
||||||
const fullscreenConditionSchema = z.object({
|
const fullscreenConditionSchema = z.object({
|
||||||
condition: z.literal('fullscreen'),
|
condition: z.literal('fullscreen'),
|
||||||
@@ -697,7 +697,7 @@ const expandConditionSchema = z.object({
|
|||||||
});
|
});
|
||||||
const cameraConditionSchema = z.object({
|
const cameraConditionSchema = z.object({
|
||||||
condition: z.literal('camera'),
|
condition: z.literal('camera'),
|
||||||
cameras: z.string().array(),
|
cameras: z.string().array().optional(),
|
||||||
});
|
});
|
||||||
const mediaLoadedConditionSchema = z.object({
|
const mediaLoadedConditionSchema = z.object({
|
||||||
condition: z.literal('media_loaded'),
|
condition: z.literal('media_loaded'),
|
||||||
@@ -773,7 +773,7 @@ export type AdvancedCameraCardCondition = z.infer<
|
|||||||
typeof advancedCameraCardConditionSchema
|
typeof advancedCameraCardConditionSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const advancedCameraCardConditionalSchema = z.object({
|
const advancedCameraCardConditionalSchema = z.object({
|
||||||
type: z.literal('custom:advanced-camera-card-conditional'),
|
type: z.literal('custom:advanced-camera-card-conditional'),
|
||||||
conditions: advancedCameraCardConditionSchema.array(),
|
conditions: advancedCameraCardConditionSchema.array(),
|
||||||
elements: z.lazy(() => pictureElementsSchema),
|
elements: z.lazy(() => pictureElementsSchema),
|
||||||
@@ -1328,16 +1328,6 @@ const liveConfigSchema = z
|
|||||||
.default(liveConfigDefault);
|
.default(liveConfigDefault);
|
||||||
export type LiveConfig = z.infer<typeof liveConfigSchema>;
|
export type LiveConfig = z.infer<typeof liveConfigSchema>;
|
||||||
|
|
||||||
// This schema is used when the live config needs to be overridden (see
|
|
||||||
// `live.ts`). Overrides will always be "relative" to the config root, so this
|
|
||||||
// schema maintains that 'depth' from the root but without the other
|
|
||||||
// requirements that advancedCameraCardConfigSchema has. Without this, overrides
|
|
||||||
// calculated in `live.ts` would fail since cameras/type are not provided (as
|
|
||||||
// these are mandatory parameters in the full config).
|
|
||||||
export const liveConfigAbsoluteRootSchema = z.object({
|
|
||||||
live: liveConfigSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
// Cast Configuration
|
// Cast Configuration
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
|
|
||||||
import { createCardAPI } from '../test-utils.js';
|
|
||||||
import { ActionType } from '../../src/config/types.js';
|
|
||||||
import { AuxillaryActionConfig } from '../../src/card-controller/actions/types.js';
|
import { AuxillaryActionConfig } from '../../src/card-controller/actions/types.js';
|
||||||
|
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
|
||||||
|
import { ConditionStateManager } from '../../src/conditions/state-manager.js';
|
||||||
|
import { ActionType } from '../../src/config/types.js';
|
||||||
|
import { createCardAPI } from '../test-utils.js';
|
||||||
|
|
||||||
describe('AutomationsManager', () => {
|
describe('AutomationsManager', () => {
|
||||||
const actions = [
|
const actions = [
|
||||||
@@ -25,39 +26,55 @@ describe('AutomationsManager', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing without hass', () => {
|
describe('should not execute actions', () => {
|
||||||
const api = createCardAPI();
|
it('should do nothing without hass', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
const automationsManager = new AutomationsManager(api);
|
||||||
automationsManager.execute();
|
automationsManager.addAutomations([automation]);
|
||||||
|
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
stateManager.setState({ fullscreen: true });
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing without being initialized', () => {
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
const api = createCardAPI();
|
});
|
||||||
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
|
|
||||||
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
it('should do nothing without being initialized', () => {
|
||||||
automationsManager.execute();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
|
||||||
|
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
const automationsManager = new AutomationsManager(api);
|
||||||
});
|
automationsManager.addAutomations([automation]);
|
||||||
|
|
||||||
it('should do nothing without automations', () => {
|
stateManager.setState({ fullscreen: true });
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
|
|
||||||
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
automationsManager.execute();
|
});
|
||||||
|
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
it('should do nothing with an error message present', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
|
||||||
|
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
vi.mocked(api.getMessageManager().hasErrorMessage).mockReturnValue(true);
|
||||||
|
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
|
const automationsManager = new AutomationsManager(api);
|
||||||
|
automationsManager.addAutomations([automation]);
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute actions', () => {
|
it('should execute actions', () => {
|
||||||
@@ -66,31 +83,25 @@ describe('AutomationsManager', () => {
|
|||||||
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
const automationsManager = new AutomationsManager(api);
|
||||||
automationsManager.addAutomations([automation]);
|
automationsManager.addAutomations([automation]);
|
||||||
|
|
||||||
automationsManager.execute();
|
stateManager.setState({ fullscreen: true });
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
|
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
||||||
|
|
||||||
// Automation will not re-fire when condition continues to evaluate the
|
// Automation will not re-fire when condition continues to evaluate the
|
||||||
// same.
|
// same.
|
||||||
automationsManager.execute();
|
stateManager.setState({ fullscreen: true });
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(false);
|
stateManager.setState({ fullscreen: false });
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,14 +111,16 @@ describe('AutomationsManager', () => {
|
|||||||
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(false);
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
const automationsManager = new AutomationsManager(api);
|
||||||
automationsManager.addAutomations([not_automation]);
|
automationsManager.addAutomations([not_automation]);
|
||||||
|
|
||||||
automationsManager.execute();
|
stateManager.setState({ fullscreen: true });
|
||||||
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: false });
|
||||||
expect(api.getActionsManager().executeActions).toBeCalled();
|
expect(api.getActionsManager().executeActions).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,12 +130,23 @@ describe('AutomationsManager', () => {
|
|||||||
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
const automationsManager = new AutomationsManager(api);
|
||||||
automationsManager.addAutomations([automation, not_automation]);
|
automationsManager.addAutomations([
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
|
||||||
|
actions: actions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen' as const, fullscreen: false }],
|
||||||
|
actions_not: actions,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
// Create a setup where one automation action causes another...
|
// Create a setup where one automation action causes another...
|
||||||
let evaluation = true;
|
let fullscreen = true;
|
||||||
|
|
||||||
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
||||||
async (
|
async (
|
||||||
@@ -131,17 +155,12 @@ describe('AutomationsManager', () => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_config?: AuxillaryActionConfig,
|
_config?: AuxillaryActionConfig,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
evaluation = !evaluation;
|
fullscreen = !fullscreen;
|
||||||
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(
|
stateManager.setState({ fullscreen: fullscreen });
|
||||||
evaluation,
|
|
||||||
);
|
|
||||||
automationsManager.execute();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(evaluation);
|
stateManager.setState({ fullscreen: fullscreen });
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
|
|
||||||
expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith(
|
expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -160,18 +179,40 @@ describe('AutomationsManager', () => {
|
|||||||
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
const automationsManager = new AutomationsManager(api);
|
||||||
automationsManager.addAutomations([automation]);
|
automationsManager.addAutomations([
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'expand' as const, expand: true }],
|
||||||
|
actions: actions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
|
||||||
|
actions: actions,
|
||||||
|
tag: 'fullscreen',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
// Delete the fullscreen automation.
|
||||||
|
automationsManager.deleteAutomations('fullscreen');
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: false });
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({ expand: true });
|
||||||
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
// Delete all automations.
|
||||||
automationsManager.deleteAutomations();
|
automationsManager.deleteAutomations();
|
||||||
|
|
||||||
automationsManager.execute();
|
stateManager.setState({ fullscreen: false });
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
stateManager.setState({ fullscreen: true });
|
||||||
|
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,10 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import { getOverriddenConfig } from '../../../src/card-controller/conditions-manager';
|
|
||||||
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
||||||
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
||||||
|
import { ConditionStateManager } from '../../../src/conditions/state-manager';
|
||||||
import { advancedCameraCardConfigSchema } from '../../../src/config/types';
|
import { advancedCameraCardConfigSchema } from '../../../src/config/types';
|
||||||
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
|
import { createCardAPI, flushPromises } from '../../test-utils';
|
||||||
|
|
||||||
vi.mock('../../../src/card-controller/conditions-manager.js');
|
|
||||||
|
|
||||||
describe('ConfigManager', () => {
|
describe('ConfigManager', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -84,8 +82,7 @@ describe('ConfigManager', () => {
|
|||||||
expect(manager.getConfig()?.menu.alignment).toBe('left');
|
expect(manager.getConfig()?.menu.alignment).toBe('left');
|
||||||
|
|
||||||
// Verify appropriate API calls are made.
|
// Verify appropriate API calls are made.
|
||||||
expect(api.getConditionsManager().setConditionsFromConfig).toBeCalled();
|
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
|
||||||
view: undefined,
|
view: undefined,
|
||||||
displayMode: undefined,
|
displayMode: undefined,
|
||||||
camera: undefined,
|
camera: undefined,
|
||||||
@@ -165,179 +162,222 @@ describe('ConfigManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore overrides without a config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(manager.getConfig()).toBeNull();
|
|
||||||
expect(api.getStyleManager().updateFromConfig).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore overrides with same config', () => {
|
it('should ignore overrides with same config', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
|
const cameras = [{ camera_entity: 'camera.office' }];
|
||||||
const config = {
|
const config = {
|
||||||
type: 'custom:advanced-camera-card',
|
type: 'custom:advanced-camera-card',
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
cameras: cameras,
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen', fullscreen: true }],
|
||||||
|
set: {
|
||||||
|
// Override with the same.
|
||||||
|
cameras: cameras,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(config);
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
manager.setConfig(config);
|
||||||
expect(api.getStyleManager().updateFromConfig).toBeCalled();
|
|
||||||
|
|
||||||
vi.mocked(api.getStyleManager().updateFromConfig).mockClear();
|
expect(api.getStyleManager().updateFromConfig).toBeCalledTimes(1);
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getStyleManager().updateFromConfig).not.toBeCalled();
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
expect(api.getStyleManager().updateFromConfig).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override', () => {
|
it('should override', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
const config_1 = {
|
const config = {
|
||||||
type: 'custom:advanced-camera-card',
|
type: 'custom:advanced-camera-card',
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen', fullscreen: true }],
|
||||||
|
set: { 'menu.style': 'none' },
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
manager.setConfig(config_1);
|
|
||||||
vi.mocked(api.getStyleManager().updateFromConfig).mockClear();
|
|
||||||
|
|
||||||
const config_2 = {
|
manager.setConfig(config);
|
||||||
type: 'custom:advanced-camera-card',
|
expect(manager.getConfig()?.menu?.style).toBe('hidden');
|
||||||
cameras: [{ camera_entity: 'camera.kitchen' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(config_2);
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getStyleManager().updateFromConfig).toBeCalled();
|
stateManager.setState({ fullscreen: true });
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
expect(manager.getConfig()?.menu?.style).toBe('none');
|
||||||
expect(manager.getConfig()).not.toEqual(manager.getNonOverriddenConfig());
|
expect(manager.getConfig()).not.toEqual(manager.getNonOverriddenConfig());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set error on invalid override', () => {
|
it('should set error on invalid override', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
manager.setConfig({
|
const config = {
|
||||||
type: 'custom:advanced-camera-card',
|
type: 'custom:advanced-camera-card',
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
});
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen', fullscreen: true }],
|
||||||
|
delete: ['cameras'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const error = new Error('Invalid override configuration');
|
manager.setConfig(config);
|
||||||
vi.mocked(getOverriddenConfig).mockImplementation(() => {
|
expect(manager.getConfig()).not.toBeNull();
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.computeOverrideConfig();
|
stateManager.setState({ fullscreen: true });
|
||||||
|
expect(manager.getConfig()).not.toBeNull();
|
||||||
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
|
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(
|
||||||
|
expect.objectContaining({ message: 'Invalid override configuration' }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should uninitialize on override', () => {
|
describe('should uninitialize on override', () => {
|
||||||
it('cameras', () => {
|
it('cameras', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
const config_1 = {
|
const config = {
|
||||||
type: 'custom:advanced-camera-card',
|
type: 'custom:advanced-camera-card',
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen', fullscreen: true }],
|
||||||
|
set: {
|
||||||
|
cameras: [{ camera_entity: 'camera.kitchen' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
manager.setConfig(config);
|
||||||
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
|
||||||
InitializationAspect.VIEW,
|
expect(api.getInitializationManager().uninitialize).not.toHaveBeenCalledWith(
|
||||||
|
InitializationAspect.CAMERAS,
|
||||||
);
|
);
|
||||||
|
|
||||||
const config_2 = {
|
stateManager.setState({ fullscreen: true });
|
||||||
type: 'custom:advanced-camera-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.kitchen' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
expect(api.getInitializationManager().uninitialize).toHaveBeenCalledWith(
|
||||||
InitializationAspect.CAMERAS,
|
InitializationAspect.CAMERAS,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cameras_global', () => {
|
it('cameras_global', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
const config_1 = {
|
const config = {
|
||||||
type: 'custom:advanced-camera-card',
|
type: 'custom:advanced-camera-card',
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen', fullscreen: true }],
|
||||||
|
set: {
|
||||||
|
cameras_global: { live_provider: 'jsmpeg' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
manager.setConfig(config);
|
||||||
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
|
||||||
InitializationAspect.VIEW,
|
expect(api.getInitializationManager().uninitialize).not.toHaveBeenCalledWith(
|
||||||
|
InitializationAspect.CAMERAS,
|
||||||
);
|
);
|
||||||
|
|
||||||
const config_2 = {
|
stateManager.setState({ fullscreen: true });
|
||||||
...config_1,
|
|
||||||
cameras_global: {
|
|
||||||
live_provider: 'jsmpeg',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
expect(api.getInitializationManager().uninitialize).toHaveBeenCalledWith(
|
||||||
InitializationAspect.CAMERAS,
|
InitializationAspect.CAMERAS,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('live.microphone.always_connected', () => {
|
it('live.microphone.always_connected', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
const config_1 = {
|
const config = {
|
||||||
type: 'custom:advanced-camera-card',
|
type: 'custom:advanced-camera-card',
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
live: {
|
overrides: [
|
||||||
microphone: {
|
{
|
||||||
always_connected: false,
|
conditions: [{ condition: 'fullscreen', fullscreen: true }],
|
||||||
|
set: {
|
||||||
|
'live.microphone.always_connected': true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
};
|
};
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
manager.setConfig(config);
|
||||||
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
|
||||||
InitializationAspect.VIEW,
|
expect(api.getInitializationManager().uninitialize).not.toHaveBeenCalledWith(
|
||||||
|
InitializationAspect.MICROPHONE_CONNECT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const config_2 = {
|
stateManager.setState({ fullscreen: true });
|
||||||
...config_1,
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
expect(api.getInitializationManager().uninitialize).toHaveBeenCalledWith(
|
||||||
InitializationAspect.MICROPHONE_CONNECT,
|
InitializationAspect.MICROPHONE_CONNECT,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize background items', async () => {
|
describe('should initialize on override', () => {
|
||||||
const api = createCardAPI();
|
it('should initialize background items', async () => {
|
||||||
const manager = new ConfigManager(api);
|
const api = createCardAPI();
|
||||||
const config = {
|
const stateManager = new ConditionStateManager();
|
||||||
type: 'custom:advanced-camera-card',
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config));
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
const manager = new ConfigManager(api);
|
||||||
|
const config = {
|
||||||
|
type: 'custom:advanced-camera-card',
|
||||||
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [{ condition: 'fullscreen', fullscreen: true }],
|
||||||
|
set: {
|
||||||
|
cameras: [{ camera_entity: 'camera.kitchen' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
await flushPromises();
|
manager.setConfig(config);
|
||||||
|
|
||||||
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledWith(null);
|
await flushPromises();
|
||||||
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledWith(null);
|
|
||||||
|
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(1);
|
||||||
|
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(2);
|
||||||
|
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { getOverriddenConfig } from '../../../src/card-controller/config/get-overridden-config';
|
||||||
|
import { ConditionsManagerReadonlyInterface } from '../../../src/conditions/types';
|
||||||
|
|
||||||
|
describe('getOverriddenConfig', () => {
|
||||||
|
const config = {
|
||||||
|
menu: {
|
||||||
|
style: 'none',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should not override without overrides', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(getOverriddenConfig(manager, config)).toBe(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override when conditions do not match', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: false });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
merge: {
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: ['menu.style'],
|
||||||
|
set: {
|
||||||
|
'menu.style': 'overlay',
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toBe(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should merge', () => {
|
||||||
|
it('with path', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
merge: {
|
||||||
|
'live.controls.thumbnails': {
|
||||||
|
mode: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
menu: {
|
||||||
|
style: 'none',
|
||||||
|
},
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
thumbnails: {
|
||||||
|
mode: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without path', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
merge: {
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with invalid merge', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
merge: 6 as unknown as Record<string, unknown>,
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
menu: {
|
||||||
|
style: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set', () => {
|
||||||
|
it('leaf node', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
set: {
|
||||||
|
'menu.style': 'hidden',
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('root node', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
set: {
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should delete', () => {
|
||||||
|
it('leaf node', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
delete: ['menu.style' as const],
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
menu: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('root node', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
delete: ['menu' as const],
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should validate schema', () => {
|
||||||
|
const testSchema = z.object({
|
||||||
|
menu: z.object({
|
||||||
|
style: z.enum(['none', 'hidden']),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passing', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
set: {
|
||||||
|
'menu.style': 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: testSchema,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
menu: {
|
||||||
|
style: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('failing', () => {
|
||||||
|
const manager = mock<ConditionsManagerReadonlyInterface>();
|
||||||
|
manager.getEvaluation.mockReturnValue({ result: true });
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getOverriddenConfig(manager, config, {
|
||||||
|
configOverrides: [
|
||||||
|
{
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
set: {
|
||||||
|
'menu.style': 'NOT_A_STYLE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: testSchema,
|
||||||
|
}),
|
||||||
|
).toThrowError(/Invalid override configuration/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
CardElementManager,
|
CardElementManager,
|
||||||
CardHTMLElement,
|
CardHTMLElement,
|
||||||
} from '../../src/card-controller/card-element-manager';
|
} from '../../src/card-controller/card-element-manager';
|
||||||
import { ConditionsManager } from '../../src/card-controller/conditions-manager';
|
|
||||||
import { ConfigManager } from '../../src/card-controller/config/config-manager';
|
import { ConfigManager } from '../../src/card-controller/config/config-manager';
|
||||||
import { CardController } from '../../src/card-controller/controller';
|
import { CardController } from '../../src/card-controller/controller';
|
||||||
import { DefaultManager } from '../../src/card-controller/default-manager';
|
import { DefaultManager } from '../../src/card-controller/default-manager';
|
||||||
@@ -27,6 +26,7 @@ import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-
|
|||||||
import { StyleManager } from '../../src/card-controller/style-manager';
|
import { StyleManager } from '../../src/card-controller/style-manager';
|
||||||
import { TriggersManager } from '../../src/card-controller/triggers-manager';
|
import { TriggersManager } from '../../src/card-controller/triggers-manager';
|
||||||
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
||||||
|
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||||
import { AdvancedCameraCardEditor } from '../../src/editor';
|
import { AdvancedCameraCardEditor } from '../../src/editor';
|
||||||
import { DeviceRegistryManager } from '../../src/utils/ha/registry/device';
|
import { DeviceRegistryManager } from '../../src/utils/ha/registry/device';
|
||||||
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity';
|
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity';
|
||||||
@@ -37,7 +37,6 @@ vi.mock('../../src/card-controller/actions/actions-manager');
|
|||||||
vi.mock('../../src/card-controller/automations-manager');
|
vi.mock('../../src/card-controller/automations-manager');
|
||||||
vi.mock('../../src/card-controller/camera-url-manager');
|
vi.mock('../../src/card-controller/camera-url-manager');
|
||||||
vi.mock('../../src/card-controller/card-element-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/config/config-manager');
|
||||||
vi.mock('../../src/card-controller/default-manager');
|
vi.mock('../../src/card-controller/default-manager');
|
||||||
vi.mock('../../src/card-controller/download-manager');
|
vi.mock('../../src/card-controller/download-manager');
|
||||||
@@ -56,6 +55,7 @@ vi.mock('../../src/card-controller/status-bar-item-manager');
|
|||||||
vi.mock('../../src/card-controller/style-manager');
|
vi.mock('../../src/card-controller/style-manager');
|
||||||
vi.mock('../../src/card-controller/triggers-manager');
|
vi.mock('../../src/card-controller/triggers-manager');
|
||||||
vi.mock('../../src/card-controller/view/view-manager');
|
vi.mock('../../src/card-controller/view/view-manager');
|
||||||
|
vi.mock('../../src/conditions/state-manager');
|
||||||
vi.mock('../../src/utils/ha/registry/device');
|
vi.mock('../../src/utils/ha/registry/device');
|
||||||
vi.mock('../../src/utils/ha/registry/entity');
|
vi.mock('../../src/utils/ha/registry/entity');
|
||||||
vi.mock('../../src/utils/ha/resolved-media');
|
vi.mock('../../src/utils/ha/resolved-media');
|
||||||
@@ -67,7 +67,7 @@ const createCardElement = (): CardHTMLElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createController = (): CardController => {
|
const createController = (): CardController => {
|
||||||
return new CardController(createCardElement(), vi.fn(), vi.fn(), vi.fn());
|
return new CardController(createCardElement(), vi.fn(), vi.fn());
|
||||||
};
|
};
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
@@ -80,16 +80,9 @@ describe('CardController', () => {
|
|||||||
const element = createCardElement();
|
const element = createCardElement();
|
||||||
const scrollCallback = vi.fn();
|
const scrollCallback = vi.fn();
|
||||||
const menuToggleCallback = vi.fn();
|
const menuToggleCallback = vi.fn();
|
||||||
const conditionListener = vi.fn();
|
|
||||||
|
|
||||||
const manager = new CardController(
|
const manager = new CardController(element, scrollCallback, menuToggleCallback);
|
||||||
element,
|
|
||||||
scrollCallback,
|
|
||||||
menuToggleCallback,
|
|
||||||
conditionListener,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(ConditionsManager).toBeCalledWith(manager, conditionListener);
|
|
||||||
expect(CardElementManager).toBeCalledWith(
|
expect(CardElementManager).toBeCalledWith(
|
||||||
manager,
|
manager,
|
||||||
element,
|
element,
|
||||||
@@ -135,9 +128,9 @@ describe('CardController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getConditionsManager', () => {
|
it('ConditionStateManager', () => {
|
||||||
expect(createController().getConditionsManager()).toBe(
|
expect(createController().getConditionStateManager()).toBe(
|
||||||
vi.mocked(ConditionsManager).mock.instances[0],
|
vi.mocked(ConditionStateManager).mock.instances[0],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('ExpandManager', () => {
|
|||||||
const manager = new ExpandManager(api);
|
const manager = new ExpandManager(api);
|
||||||
|
|
||||||
manager.initialize();
|
manager.initialize();
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: false });
|
expect(api.getConditionStateManager().setState).toBeCalledWith({ expand: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set expanded', () => {
|
it('should set expanded', () => {
|
||||||
@@ -26,7 +26,7 @@ describe('ExpandManager', () => {
|
|||||||
|
|
||||||
expect(manager.isExpanded()).toBeTruthy();
|
expect(manager.isExpanded()).toBeTruthy();
|
||||||
expect(api.getFullscreenManager().setFullscreen).toBeCalledWith(false);
|
expect(api.getFullscreenManager().setFullscreen).toBeCalledWith(false);
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true });
|
expect(api.getConditionStateManager().setState).toBeCalledWith({ expand: true });
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
expect(api.getCardElementManager().update).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ describe('FullscreenManager', () => {
|
|||||||
const manager = new FullscreenManager(api, mock<FullscreenProvider>());
|
const manager = new FullscreenManager(api, mock<FullscreenProvider>());
|
||||||
|
|
||||||
manager.initialize();
|
manager.initialize();
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({ fullscreen: false });
|
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||||
|
fullscreen: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly determine whether in fullscreen', () => {
|
it('should correctly determine whether in fullscreen', () => {
|
||||||
@@ -133,7 +135,7 @@ describe('FullscreenManager', () => {
|
|||||||
|
|
||||||
handler();
|
handler();
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||||
fullscreen: fullscreen,
|
fullscreen: fullscreen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { CardController } from '../../../../src/card-controller/controller';
|
import { CardController } from '../../../../src/card-controller/controller';
|
||||||
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
|
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
|
||||||
|
import { ConditionStateManager } from '../../../../src/conditions/state-manager';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardMediaPlayer,
|
AdvancedCameraCardMediaPlayer,
|
||||||
WebkitHTMLVideoElement,
|
WebkitHTMLVideoElement,
|
||||||
@@ -37,7 +38,7 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
|
|
||||||
expect(api.getConditionsManager().addListener).toBeCalledWith(expect.anything());
|
expect(api.getConditionStateManager().addListener).toBeCalledWith(expect.anything());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disconnect', () => {
|
it('should disconnect', () => {
|
||||||
@@ -46,7 +47,9 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
|
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
|
|
||||||
expect(api.getConditionsManager().removeListener).toBeCalledWith(expect.anything());
|
expect(api.getConditionStateManager().removeListener).toBeCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should return if in fullscreen', () => {
|
describe('should return if in fullscreen', () => {
|
||||||
@@ -152,20 +155,19 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
(event: string) => {
|
(event: string) => {
|
||||||
const handler = vi.fn();
|
const handler = vi.fn();
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const provider = new WebkitFullScreenProvider(api, handler);
|
const provider = new WebkitFullScreenProvider(api, handler);
|
||||||
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
|
|
||||||
const conditionChangeHandler = vi.mocked(
|
|
||||||
api.getConditionsManager().addListener,
|
|
||||||
).mock.calls[0][0];
|
|
||||||
|
|
||||||
const element_1 = createWebkitVideoElement();
|
const element_1 = createWebkitVideoElement();
|
||||||
const player_1 = mock<AdvancedCameraCardMediaPlayer>();
|
const player_1 = mock<AdvancedCameraCardMediaPlayer>();
|
||||||
player_1.getFullscreenElement.mockReturnValue(element_1);
|
player_1.getFullscreenElement.mockReturnValue(element_1);
|
||||||
const mediaLoadedInfo_1 = createMediaLoadedInfo({ player: player_1 });
|
const mediaLoadedInfo_1 = createMediaLoadedInfo({ player: player_1 });
|
||||||
|
|
||||||
conditionChangeHandler({ mediaLoadedInfo: mediaLoadedInfo_1 });
|
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_1 });
|
||||||
|
|
||||||
element_1.dispatchEvent(new Event(event));
|
element_1.dispatchEvent(new Event(event));
|
||||||
|
|
||||||
@@ -176,10 +178,7 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
player_2.getFullscreenElement.mockReturnValue(element_2);
|
player_2.getFullscreenElement.mockReturnValue(element_2);
|
||||||
const mediaLoadedInfo_2 = createMediaLoadedInfo({ player: player_2 });
|
const mediaLoadedInfo_2 = createMediaLoadedInfo({ player: player_2 });
|
||||||
|
|
||||||
conditionChangeHandler(
|
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_2 });
|
||||||
{ mediaLoadedInfo: mediaLoadedInfo_2 },
|
|
||||||
{ mediaLoadedInfo: mediaLoadedInfo_1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
element_2.dispatchEvent(new Event(event));
|
element_2.dispatchEvent(new Event(event));
|
||||||
|
|
||||||
@@ -190,11 +189,10 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
|
|
||||||
expect(handler).toBeCalledTimes(2);
|
expect(handler).toBeCalledTimes(2);
|
||||||
|
|
||||||
// Test the media loaded info not changing.
|
// Test the media loaded info changing, but the player not changing.
|
||||||
conditionChangeHandler(
|
stateManager.setState({
|
||||||
{ mediaLoadedInfo: mediaLoadedInfo_2 },
|
mediaLoadedInfo: { ...mediaLoadedInfo_2, width: 101 },
|
||||||
{ mediaLoadedInfo: mediaLoadedInfo_2 },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Events on the new element should still be handled.
|
// Events on the new element should still be handled.
|
||||||
element_2.dispatchEvent(new Event(event));
|
element_2.dispatchEvent(new Event(event));
|
||||||
@@ -210,13 +208,13 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
|
|
||||||
const handler = vi.fn();
|
const handler = vi.fn();
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const provider = new WebkitFullScreenProvider(api, handler);
|
const provider = new WebkitFullScreenProvider(api, handler);
|
||||||
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
|
|
||||||
const conditionChangeHandler = vi.mocked(api.getConditionsManager().addListener).mock
|
|
||||||
.calls[0][0];
|
|
||||||
|
|
||||||
const element = createWebkitVideoElement();
|
const element = createWebkitVideoElement();
|
||||||
element.play = vi.fn();
|
element.play = vi.fn();
|
||||||
|
|
||||||
@@ -226,7 +224,7 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
player.getFullscreenElement.mockReturnValue(element);
|
player.getFullscreenElement.mockReturnValue(element);
|
||||||
const mediaLoadedInfo = createMediaLoadedInfo({ player });
|
const mediaLoadedInfo = createMediaLoadedInfo({ player });
|
||||||
|
|
||||||
conditionChangeHandler({ mediaLoadedInfo });
|
stateManager.setState({ mediaLoadedInfo });
|
||||||
|
|
||||||
element.dispatchEvent(new Event('webkitendfullscreen'));
|
element.dispatchEvent(new Event('webkitendfullscreen'));
|
||||||
|
|
||||||
|
|||||||
@@ -47,35 +47,22 @@ describe('HASSManager', () => {
|
|||||||
expect(api.getStyleManager().applyTheme).toBeCalled();
|
expect(api.getStyleManager().applyTheme).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should set condition manager state', () => {
|
it('should set condition manager state', () => {
|
||||||
it('positively', () => {
|
const api = createCardAPI();
|
||||||
const api = createCardAPI();
|
const manager = new HASSManager(api);
|
||||||
const manager = new HASSManager(api);
|
|
||||||
vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(true);
|
|
||||||
|
|
||||||
const states = { 'switch.foo': createStateEntity() };
|
const states = { 'switch.foo': createStateEntity() };
|
||||||
const user = createUser({ id: 'user_1' });
|
const user = createUser({ id: 'user_1' });
|
||||||
const hass = createHASS(states, user);
|
const hass = createHASS(states, user);
|
||||||
|
|
||||||
manager.setHASS(hass);
|
manager.setHASS(hass);
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
expect(api.getConditionStateManager().setState).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
state: states,
|
state: states,
|
||||||
user: user,
|
user: user,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it('negatively', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(false);
|
|
||||||
|
|
||||||
manager.setHASS(createHASS());
|
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should handle connection state change when', () => {
|
describe('should handle connection state change when', () => {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ describe('InteractionManager', () => {
|
|||||||
const manager = new InteractionManager(api);
|
const manager = new InteractionManager(api);
|
||||||
|
|
||||||
manager.initialize();
|
manager.initialize();
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({ interaction: false });
|
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||||
|
interaction: false,
|
||||||
|
});
|
||||||
expect(element.getAttribute('interaction')).toBeNull();
|
expect(element.getAttribute('interaction')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,11 +64,11 @@ describe('InteractionManager', () => {
|
|||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.setSystemTime(start);
|
vi.setSystemTime(start);
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||||
|
|
||||||
manager.reportInteraction();
|
manager.reportInteraction();
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
interaction: true,
|
interaction: true,
|
||||||
}),
|
}),
|
||||||
@@ -77,7 +79,7 @@ describe('InteractionManager', () => {
|
|||||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
interaction: false,
|
interaction: false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('KeyboardStateManager', () => {
|
|||||||
|
|
||||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenCalledWith({
|
expect(api.getConditionStateManager().setState).toHaveBeenCalledWith({
|
||||||
keys: {
|
keys: {
|
||||||
a: { state: 'down', ctrl: false, alt: false, meta: false, shift: false },
|
a: { state: 'down', ctrl: false, alt: false, meta: false, shift: false },
|
||||||
},
|
},
|
||||||
@@ -26,7 +26,7 @@ describe('KeyboardStateManager', () => {
|
|||||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||||
|
|
||||||
// Duplicate keydown should not re-set the state.
|
// Duplicate keydown should not re-set the state.
|
||||||
expect(api.getConditionsManager().setState).toBeCalledTimes(1);
|
expect(api.getConditionStateManager().setState).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set state on keyup', () => {
|
it('should set state on keyup', () => {
|
||||||
@@ -39,13 +39,13 @@ describe('KeyboardStateManager', () => {
|
|||||||
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
|
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
|
||||||
|
|
||||||
// Key not held down in the first place should not update the state.
|
// Key not held down in the first place should not update the state.
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||||
|
|
||||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||||
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
|
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledTimes(2);
|
expect(api.getConditionStateManager().setState).toBeCalledTimes(2);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
||||||
keys: {
|
keys: {
|
||||||
a: { state: 'up', ctrl: false, alt: false, meta: false, shift: false },
|
a: { state: 'up', ctrl: false, alt: false, meta: false, shift: false },
|
||||||
},
|
},
|
||||||
@@ -60,13 +60,13 @@ describe('KeyboardStateManager', () => {
|
|||||||
manager.initialize();
|
manager.initialize();
|
||||||
|
|
||||||
element.dispatchEvent(new FocusEvent('blur'));
|
element.dispatchEvent(new FocusEvent('blur'));
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||||
|
|
||||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||||
element.dispatchEvent(new FocusEvent('blur'));
|
element.dispatchEvent(new FocusEvent('blur'));
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledTimes(2);
|
expect(api.getConditionStateManager().setState).toBeCalledTimes(2);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
||||||
keys: {},
|
keys: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -81,6 +81,6 @@ describe('KeyboardStateManager', () => {
|
|||||||
|
|
||||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe('MediaLoadedInfoManager', () => {
|
|||||||
const manager = new MediaLoadedInfoManager(api);
|
const manager = new MediaLoadedInfoManager(api);
|
||||||
|
|
||||||
manager.initialize();
|
manager.initialize();
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||||
mediaLoadedInfo: null,
|
mediaLoadedInfo: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ describe('MediaLoadedInfoManager', () => {
|
|||||||
|
|
||||||
expect(manager.has()).toBeTruthy();
|
expect(manager.has()).toBeTruthy();
|
||||||
expect(manager.get()).toBe(mediaInfo);
|
expect(manager.get()).toBe(mediaInfo);
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
expect(api.getConditionStateManager().setState).toBeCalledWith(
|
||||||
expect.objectContaining({ mediaLoadedInfo: mediaInfo }),
|
expect.objectContaining({ mediaLoadedInfo: mediaInfo }),
|
||||||
);
|
);
|
||||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
||||||
@@ -38,7 +38,7 @@ describe('MediaLoadedInfoManager', () => {
|
|||||||
|
|
||||||
expect(manager.has()).toBeFalsy();
|
expect(manager.has()).toBeFalsy();
|
||||||
expect(manager.get()).toBeNull();
|
expect(manager.get()).toBeNull();
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get last known', () => {
|
it('should get last known', () => {
|
||||||
@@ -54,7 +54,7 @@ describe('MediaLoadedInfoManager', () => {
|
|||||||
|
|
||||||
expect(manager.has()).toBeFalsy();
|
expect(manager.has()).toBeFalsy();
|
||||||
expect(manager.getLastKnown()).toBe(mediaLoadedInfo);
|
expect(manager.getLastKnown()).toBe(mediaLoadedInfo);
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
expect(api.getConditionStateManager().setState).toBeCalledWith(
|
||||||
expect.objectContaining({ mediaLoadedInfo }),
|
expect.objectContaining({ mediaLoadedInfo }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ describe('MicrophoneManager', () => {
|
|||||||
const manager = new MicrophoneManager(api);
|
const manager = new MicrophoneManager(api);
|
||||||
|
|
||||||
manager.initialize();
|
manager.initialize();
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||||
microphone: { connected: false, muted: true, forbidden: false, stream: undefined },
|
microphone: { connected: false, muted: true, forbidden: false, stream: undefined },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -313,7 +313,7 @@ describe('MicrophoneManager', () => {
|
|||||||
const stream = createMockStream();
|
const stream = createMockStream();
|
||||||
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(stream);
|
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(stream);
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||||
|
|
||||||
await manager.connect();
|
await manager.connect();
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ describe('MicrophoneManager', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(manager.getState()).toEqual(expectedState);
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: expectedState,
|
microphone: expectedState,
|
||||||
}),
|
}),
|
||||||
@@ -340,7 +340,7 @@ describe('MicrophoneManager', () => {
|
|||||||
muted: false,
|
muted: false,
|
||||||
};
|
};
|
||||||
expect(manager.getState()).toEqual(expectedState);
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: expectedState,
|
microphone: expectedState,
|
||||||
}),
|
}),
|
||||||
@@ -355,7 +355,7 @@ describe('MicrophoneManager', () => {
|
|||||||
muted: true,
|
muted: true,
|
||||||
};
|
};
|
||||||
expect(manager.getState()).toEqual(expectedState);
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: expectedState,
|
microphone: expectedState,
|
||||||
}),
|
}),
|
||||||
@@ -370,7 +370,7 @@ describe('MicrophoneManager', () => {
|
|||||||
muted: true,
|
muted: true,
|
||||||
};
|
};
|
||||||
expect(manager.getState()).toEqual(expectedState);
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: expectedState,
|
microphone: expectedState,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const createTriggerAPI = (options?: {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
vi.mocked(api.getConditionsManager().getState).mockReturnValue({});
|
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({});
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
createStore([
|
createStore([
|
||||||
@@ -319,10 +319,10 @@ describe('TriggersManager', () => {
|
|||||||
|
|
||||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
||||||
triggered: new Set(['camera_1']),
|
triggered: new Set(['camera_1']),
|
||||||
});
|
});
|
||||||
vi.mocked(api.getConditionsManager().getState).mockReturnValue({
|
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({
|
||||||
triggered: new Set(['camera_1']),
|
triggered: new Set(['camera_1']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -331,7 +331,7 @@ describe('TriggersManager', () => {
|
|||||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
||||||
triggered: undefined,
|
triggered: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('should act correctly when view is set', () => {
|
|||||||
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
||||||
expect(api.getMessageManager().reset).toBeCalled();
|
expect(api.getMessageManager().reset).toBeCalled();
|
||||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
||||||
expect(api.getConditionsManager()?.setState).toBeCalledWith({
|
expect(api.getConditionStateManager()?.setState).toBeCalledWith({
|
||||||
view: 'live',
|
view: 'live',
|
||||||
camera: 'camera',
|
camera: 'camera',
|
||||||
displayMode: 'grid',
|
displayMode: 'grid',
|
||||||
|
|||||||
@@ -0,0 +1,933 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { MicrophoneState } from '../../src/card-controller/types';
|
||||||
|
import { ConditionsManager } from '../../src/conditions/conditions-manager';
|
||||||
|
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||||
|
import { createMediaLoadedInfo, createStateEntity, createUser } from '../test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('ConditionsManager', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should evaluate conditions', () => {
|
||||||
|
describe('with a view condition', () => {
|
||||||
|
it('should match named view change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'view' as const, views: ['foo'] }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ view: 'foo' });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match any view change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'view' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'clips' });
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
view: {
|
||||||
|
to: 'clips',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'timeline' });
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
view: {
|
||||||
|
from: 'clips',
|
||||||
|
to: 'timeline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with fullscreen condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'fullscreen' as const, fullscreen: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ fullscreen: false });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with expand condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'expand' as const, expand: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ expand: true });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ expand: false });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with camera condition', () => {
|
||||||
|
it('should match named camera change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'camera' as const, cameras: ['bar'] }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ camera: 'bar' });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ camera: 'will-not-match' });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match any camera change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'camera' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ camera: 'bar' });
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
camera: {
|
||||||
|
to: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
stateManager.setState({ camera: 'foo' });
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
camera: {
|
||||||
|
from: 'bar',
|
||||||
|
to: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with stock HA conditions', () => {
|
||||||
|
describe('with state condition', () => {
|
||||||
|
it('neither positive nor negative', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'state' as const,
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'on' }) },
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
state: {
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
to: 'on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
state: {
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
from: 'on',
|
||||||
|
to: 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('positive', () => {
|
||||||
|
it('single state', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'state' as const,
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
state: 'on',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity() },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple states', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'state' as const,
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
state: ['active', 'on'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity() },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'active' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('negative', () => {
|
||||||
|
it('single state', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'state' as const,
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
state_not: 'on',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity() },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple states', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'state' as const,
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
state_not: ['active', 'on'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'active' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('implicit state condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
state: 'on',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match any state change when state and state_not omitted', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{ condition: 'state' as const, entity: 'switch.one' },
|
||||||
|
{ condition: 'state' as const, entity: 'switch.two' },
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({
|
||||||
|
state: {
|
||||||
|
'switch.one': createStateEntity({ state: 'on' }),
|
||||||
|
'switch.two': createStateEntity({ state: 'off' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
// Only the last matching state will be included in the data.
|
||||||
|
state: {
|
||||||
|
entity: 'switch.two',
|
||||||
|
to: 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
stateManager.setState({
|
||||||
|
state: {
|
||||||
|
'switch.one': createStateEntity({ state: 'off' }),
|
||||||
|
'switch.two': createStateEntity({ state: 'on' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
// Only the last matching state will be included in the data.
|
||||||
|
state: {
|
||||||
|
entity: 'switch.two',
|
||||||
|
from: 'off',
|
||||||
|
to: 'on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with numeric state condition', () => {
|
||||||
|
it('above', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'numeric_state' as const,
|
||||||
|
entity: 'sensor.foo',
|
||||||
|
above: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'binary_sensor.foo': createStateEntity({ state: '9' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('below', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'numeric_state' as const,
|
||||||
|
entity: 'sensor.foo',
|
||||||
|
below: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'sensor.foo': createStateEntity({ state: '9' }) },
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call listeners for HA state changes without relevant condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'fullscreen' as const,
|
||||||
|
fullscreen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({
|
||||||
|
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).not.toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with user condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'user' as const,
|
||||||
|
users: ['user_1', 'user_2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
user: createUser({ id: 'user_1' }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
user: createUser({ id: 'user_WRONG' }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with media loaded condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'media_loaded' as const, media_loaded: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ mediaLoadedInfo: createMediaLoadedInfo() });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ mediaLoadedInfo: null });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with screen condition', () => {
|
||||||
|
it('on evaluation', () => {
|
||||||
|
vi.spyOn(window, 'matchMedia')
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
} as unknown as MediaQueryList)
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
matches: true,
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
const manager = new ConditionsManager([
|
||||||
|
{ condition: 'screen' as const, media_query: 'whatever' },
|
||||||
|
]);
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('on trigger', () => {
|
||||||
|
const addEventListener = vi.fn();
|
||||||
|
const removeEventListener = vi.fn();
|
||||||
|
vi.spyOn(window, 'matchMedia')
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
addEventListener: addEventListener,
|
||||||
|
removeEventListener: removeEventListener,
|
||||||
|
} as unknown as MediaQueryList)
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
matches: false,
|
||||||
|
} as unknown as MediaQueryList)
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
matches: true,
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
const manager = new ConditionsManager([
|
||||||
|
{
|
||||||
|
condition: 'screen' as const,
|
||||||
|
media_query: 'media query goes here',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(addEventListener).toHaveBeenCalledWith('change', expect.anything());
|
||||||
|
|
||||||
|
const callback = vi.fn();
|
||||||
|
manager.addListener(callback);
|
||||||
|
|
||||||
|
// Call the media query callback and use it to pretend a match happened. The
|
||||||
|
// callback is the 0th mock innvocation and the 1st argument.
|
||||||
|
addEventListener.mock.calls[0][1]();
|
||||||
|
|
||||||
|
// This should result in a callback to our state listener.
|
||||||
|
expect(callback).toBeCalledWith({ result: true, data: {} });
|
||||||
|
|
||||||
|
// Destroy the manager and ensure the event listener is removed.
|
||||||
|
manager.destroy();
|
||||||
|
expect(removeEventListener).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with display mode condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'display_mode' as const, display_mode: 'grid' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ displayMode: 'grid' });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ displayMode: 'single' });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with triggered condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'triggered' as const, triggered: ['camera_1', 'camera_2'] }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ triggered: new Set(['camera_1']) });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
triggered: new Set(['camera_2', 'camera_1', 'camera_3']),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ triggered: new Set(['camera_3']) });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with interaction condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'interaction' as const, interaction: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ interaction: true });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ interaction: false });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with microphone condition', () => {
|
||||||
|
const createMicrophoneState = (
|
||||||
|
state: Partial<MicrophoneState>,
|
||||||
|
): MicrophoneState => {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
muted: false,
|
||||||
|
forbidden: false,
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
it('empty', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'microphone' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: true }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: false }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connected is true', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'microphone' as const, connected: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: true }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: false }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connected is false', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'microphone' as const, connected: false }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: true }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: false }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('muted is true', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'microphone' as const, muted: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('muted is false', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'microphone' as const, muted: false }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connected and muted', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'microphone' as const, muted: false, connected: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: false, muted: false }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
microphone: createMicrophoneState({ connected: true, muted: false }),
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with key condition', () => {
|
||||||
|
it('simple keypress', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'key' as const, key: 'a' }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: false, shift: false, alt: false, meta: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'up', ctrl: false, shift: false, alt: false, meta: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keypress with modifiers', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'key' as const,
|
||||||
|
key: 'a',
|
||||||
|
state: 'down' as const,
|
||||||
|
ctrl: true,
|
||||||
|
shift: true,
|
||||||
|
alt: true,
|
||||||
|
meta: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: false, shift: false, alt: false, meta: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: true, shift: true, alt: true, meta: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: true, shift: true, alt: true, meta: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with user agent condition', () => {
|
||||||
|
const userAgent =
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||||
|
|
||||||
|
it('should match exact user agent', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'user_agent' as const, user_agent: userAgent }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: userAgent,
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: 'Something else',
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match user agent regex', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'user_agent' as const, user_agent_re: 'Chrome/' }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: userAgent,
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: 'Something else',
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match companion app', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'user_agent' as const, companion: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: 'Home Assistant/',
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: userAgent,
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match multiple parameters', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'user_agent' as const,
|
||||||
|
companion: true,
|
||||||
|
user_agent: 'Home Assistant/',
|
||||||
|
user_agent_re: 'Home.Assistant',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: 'Home Assistant/',
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({
|
||||||
|
userAgent: 'Something else',
|
||||||
|
});
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle listeners correctly', () => {
|
||||||
|
it('should add listener', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'fullscreen' as const, fullscreen: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
expect(listener).toBeCalledWith({ result: true, data: {} });
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: false });
|
||||||
|
expect(listener).toBeCalledWith({ result: false });
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
// Re-add the same listener (will still only be called once).
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
expect(listener).toBeCalledWith({ result: true, data: {} });
|
||||||
|
expect(listener).toBeCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'fullscreen' as const, fullscreen: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
manager.removeListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
expect(listener).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener on destroy', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'fullscreen' as const, fullscreen: true }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
manager.destroy();
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
expect(listener).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with not call listeners when condition result does not change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'view' as const, views: ['foo'] }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'foo' });
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'bar' });
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'bar' });
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'foo' });
|
||||||
|
expect(listener).toBeCalledTimes(3);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'foo' });
|
||||||
|
expect(listener).toBeCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||||
|
import {
|
||||||
|
ConditionStateManagerGetEvent,
|
||||||
|
getConditionStateManagerViaEvent,
|
||||||
|
} from '../../src/conditions/state-manager-via-event';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('getConditionStateManagerViaEvent', () => {
|
||||||
|
it('should dispatch event and retrieve state manager', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const stateManager = mock<ConditionStateManager>();
|
||||||
|
|
||||||
|
const handler = vi.fn().mockImplementation((ev: ConditionStateManagerGetEvent) => {
|
||||||
|
ev.conditionStateManager = stateManager;
|
||||||
|
});
|
||||||
|
element.addEventListener(
|
||||||
|
'advanced-camera-card:condition-state-manager:get',
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getConditionStateManagerViaEvent(element)).toBe(stateManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch event and retrieve state manager', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const handler = vi.fn();
|
||||||
|
element.addEventListener(
|
||||||
|
'advanced-camera-card:condition-state-manager:get',
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getConditionStateManagerViaEvent(element)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||||
|
import { createStateEntity } from '../test-utils';
|
||||||
|
|
||||||
|
describe('ConditionStateManager', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get state', () => {
|
||||||
|
const state = { fullscreen: true };
|
||||||
|
|
||||||
|
const manager = new ConditionStateManager();
|
||||||
|
manager.setState(state);
|
||||||
|
expect(manager.getState()).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set state', () => {
|
||||||
|
it('should set and be able to get it again', () => {
|
||||||
|
const state = {
|
||||||
|
fullscreen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const manager = new ConditionStateManager();
|
||||||
|
|
||||||
|
manager.setState(state);
|
||||||
|
expect(manager.getState()).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set but only trigger when necessary', () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const manager = new ConditionStateManager();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
fullscreen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.setState(state);
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
manager.setState(state);
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
manager.setState({ ...state });
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
manager.setState({
|
||||||
|
state: {
|
||||||
|
'binary_sensor.foo': createStateEntity(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
manager.setState({ fullscreen: true });
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
manager.setState({
|
||||||
|
state: {
|
||||||
|
'binary_sensor.foo': createStateEntity(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
manager.setState({ fullscreen: false });
|
||||||
|
expect(listener).toBeCalledTimes(3);
|
||||||
|
|
||||||
|
manager.setState({ fullscreen: false });
|
||||||
|
expect(listener).toBeCalledTimes(3);
|
||||||
|
|
||||||
|
manager.setState({
|
||||||
|
state: {
|
||||||
|
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledTimes(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add listener', () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const manager = new ConditionStateManager();
|
||||||
|
|
||||||
|
manager.setState({ fullscreen: true });
|
||||||
|
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
manager.setState({ expand: true });
|
||||||
|
|
||||||
|
expect(listener).toBeCalledWith({
|
||||||
|
old: { fullscreen: true },
|
||||||
|
change: { expand: true },
|
||||||
|
new: { fullscreen: true, expand: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener', () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const manager = new ConditionStateManager();
|
||||||
|
|
||||||
|
manager.addListener(listener);
|
||||||
|
manager.removeListener(listener);
|
||||||
|
|
||||||
|
const state = { fullscreen: true };
|
||||||
|
manager.setState(state);
|
||||||
|
|
||||||
|
expect(listener).not.toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
+2
-8
@@ -19,7 +19,7 @@ import { ActionsManager } from '../src/card-controller/actions/actions-manager';
|
|||||||
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
||||||
import { CameraURLManager } from '../src/card-controller/camera-url-manager';
|
import { CameraURLManager } from '../src/card-controller/camera-url-manager';
|
||||||
import { CardElementManager } from '../src/card-controller/card-element-manager';
|
import { CardElementManager } from '../src/card-controller/card-element-manager';
|
||||||
import { ConditionsManager } from '../src/card-controller/conditions-manager';
|
import { ConditionStateManager } from '../src/conditions/state-manager';
|
||||||
import { ConfigManager } from '../src/card-controller/config/config-manager';
|
import { ConfigManager } from '../src/card-controller/config/config-manager';
|
||||||
import { CardController } from '../src/card-controller/controller';
|
import { CardController } from '../src/card-controller/controller';
|
||||||
import { DefaultManager } from '../src/card-controller/default-manager';
|
import { DefaultManager } from '../src/card-controller/default-manager';
|
||||||
@@ -77,12 +77,6 @@ export const createCameraConfig = (config?: unknown): CameraConfig => {
|
|||||||
return cameraConfigSchema.parse(config ?? {});
|
return cameraConfigSchema.parse(config ?? {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createCondition = (
|
|
||||||
condition?: Partial<AdvancedCameraCardCondition>,
|
|
||||||
): AdvancedCameraCardCondition => {
|
|
||||||
return advancedCameraCardConditionSchema.parse(condition ?? {});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createRawConfig = (
|
export const createRawConfig = (
|
||||||
config?: Partial<RawAdvancedCameraCardConfig>,
|
config?: Partial<RawAdvancedCameraCardConfig>,
|
||||||
): RawAdvancedCameraCardConfig => {
|
): RawAdvancedCameraCardConfig => {
|
||||||
@@ -487,7 +481,7 @@ export const createCardAPI = (): CardController => {
|
|||||||
api.getCameraManager.mockReturnValue(mock<CameraManager>());
|
api.getCameraManager.mockReturnValue(mock<CameraManager>());
|
||||||
api.getCameraURLManager.mockReturnValue(mock<CameraURLManager>());
|
api.getCameraURLManager.mockReturnValue(mock<CameraURLManager>());
|
||||||
api.getCardElementManager.mockReturnValue(mock<CardElementManager>());
|
api.getCardElementManager.mockReturnValue(mock<CardElementManager>());
|
||||||
api.getConditionsManager.mockReturnValue(mock<ConditionsManager>());
|
api.getConditionStateManager.mockReturnValue(mock<ConditionStateManager>());
|
||||||
api.getConfigManager.mockReturnValue(mock<ConfigManager>());
|
api.getConfigManager.mockReturnValue(mock<ConfigManager>());
|
||||||
api.getDownloadManager.mockReturnValue(mock<DownloadManager>());
|
api.getDownloadManager.mockReturnValue(mock<DownloadManager>());
|
||||||
api.getEntityRegistryManager.mockReturnValue(mock<EntityRegistryManager>());
|
api.getEntityRegistryManager.mockReturnValue(mock<EntityRegistryManager>());
|
||||||
|
|||||||
Reference in New Issue
Block a user