@@ -50,6 +50,9 @@ Trigger template values must be proceeded by `advanced_camera_card.trigger` (or
|
||||
conditions](../conditions.md?id=state), only data from the last listed state
|
||||
condition is available.
|
||||
|
||||
!> If you use an [`or`](../conditions.md?id=or) condition, only the trigger data
|
||||
for the first matching trigger will be included.
|
||||
|
||||
Please [request](https://github.com/dermotduffy/advanced-camera-card/issues) if
|
||||
you need data from additional conditions.
|
||||
|
||||
|
||||
@@ -14,6 +14,21 @@ certain configurations (in `overrides`) or to display "picture elements" (in
|
||||
- [condition_2]
|
||||
```
|
||||
|
||||
## `and`
|
||||
|
||||
Evaluates to `true` if _all_ embedded conditions evaluate to `true`.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: and
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `and`. |
|
||||
| `conditions` | A list of other conditions _all_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `camera`
|
||||
|
||||
Matches based on the selected camera. Does not match other cameras (whether
|
||||
@@ -174,6 +189,21 @@ conditions:
|
||||
When multiple parameters are specified they must all match for the condition to
|
||||
match.
|
||||
|
||||
## `not`
|
||||
|
||||
Evaluates to `true` if _all_ embedded conditions evaluate to `false`.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: not
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `not`. |
|
||||
| `conditions` | A list of other conditions _all_ of which must evaluate `false` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `numeric_state`
|
||||
|
||||
Matches based on numeric Home Assistant state.
|
||||
@@ -186,6 +216,21 @@ conditions:
|
||||
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#numeric-state).
|
||||
|
||||
## `or`
|
||||
|
||||
Evaluates to `true` if _any_ embedded condition evaluates to `true`.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: or
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `or`. |
|
||||
| `conditions` | A list of conditions _any_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `screen`
|
||||
|
||||
Matches based on [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries).
|
||||
|
||||
@@ -22,7 +22,6 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
|
||||
protected _listeners: ConditionsListener[] = [];
|
||||
protected _mediaQueries: MediaQueryList[] = [];
|
||||
protected _hasHAStateConditions = false;
|
||||
protected _evaluation: ConditionsEvaluationResult = { result: false };
|
||||
|
||||
constructor(
|
||||
@@ -30,13 +29,6 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
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);
|
||||
@@ -82,17 +74,6 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
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 });
|
||||
};
|
||||
|
||||
@@ -324,6 +305,42 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
}
|
||||
case 'initialized':
|
||||
return { result: !!newState?.initialized };
|
||||
case 'or':
|
||||
for (const subCondition of condition.conditions) {
|
||||
const evaluation = this._evaluateCondition(subCondition, newState, oldState);
|
||||
if (evaluation.result) {
|
||||
return evaluation;
|
||||
}
|
||||
}
|
||||
return { result: false };
|
||||
case 'and': {
|
||||
let triggerData: ConditionsTriggerData = {};
|
||||
for (const subCondition of condition.conditions) {
|
||||
const evaluation = this._evaluateCondition(subCondition, newState, oldState);
|
||||
if (!evaluation.result) {
|
||||
return { result: false };
|
||||
}
|
||||
triggerData = {
|
||||
...triggerData,
|
||||
...evaluation.triggerData,
|
||||
};
|
||||
}
|
||||
return { result: true, triggerData };
|
||||
}
|
||||
case 'not': {
|
||||
// "Not" is just an inversed `and`. There is no trigger data for "not
|
||||
// triggering".
|
||||
return {
|
||||
result: !this._evaluateCondition(
|
||||
{
|
||||
...condition,
|
||||
condition: 'and',
|
||||
},
|
||||
newState,
|
||||
oldState,
|
||||
).result,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,54 @@ import { screenConditionSchema } from './stock/screen';
|
||||
import { stateConditionSchema } from './stock/state';
|
||||
import { usersConditionSchema } from './stock/users';
|
||||
|
||||
export const advancedCameraCardConditionSchema = z.discriminatedUnion('condition', [
|
||||
// https://www.home-assistant.io/docs/scripts/conditions/#or-condition
|
||||
type OrCondition = {
|
||||
condition: 'or';
|
||||
conditions: AdvancedCameraCardCondition[];
|
||||
};
|
||||
const orConditionSchema: z.ZodSchema<OrCondition> = z.object({
|
||||
condition: z.literal('or'),
|
||||
conditions: z
|
||||
.lazy(() => advancedCameraCardConditionSchema)
|
||||
.array()
|
||||
.min(1),
|
||||
});
|
||||
|
||||
// https://www.home-assistant.io/docs/scripts/conditions/#and-condition
|
||||
type AndCondition = {
|
||||
condition: 'and';
|
||||
conditions: AdvancedCameraCardCondition[];
|
||||
};
|
||||
const andConditionSchema: z.ZodSchema<AndCondition> = z.object({
|
||||
condition: z.literal('and'),
|
||||
conditions: z
|
||||
.lazy(() => advancedCameraCardConditionSchema)
|
||||
.array()
|
||||
.min(1),
|
||||
});
|
||||
|
||||
// https://www.home-assistant.io/docs/scripts/conditions/#not-condition
|
||||
type NotCondition = {
|
||||
condition: 'not';
|
||||
conditions: AdvancedCameraCardCondition[];
|
||||
};
|
||||
const notConditionSchema: z.ZodSchema<NotCondition> = z.object({
|
||||
condition: z.literal('not'),
|
||||
conditions: z
|
||||
.lazy(() => advancedCameraCardConditionSchema)
|
||||
.array()
|
||||
.min(1),
|
||||
});
|
||||
|
||||
export const advancedCameraCardConditionSchema = z.union([
|
||||
// Stock conditions:
|
||||
numericStateConditionSchema,
|
||||
screenConditionSchema,
|
||||
stateConditionSchema,
|
||||
usersConditionSchema,
|
||||
orConditionSchema,
|
||||
andConditionSchema,
|
||||
notConditionSchema,
|
||||
|
||||
// Custom conditions:
|
||||
cameraConditionSchema,
|
||||
|
||||
@@ -1072,6 +1072,188 @@ describe('ConditionsManager', () => {
|
||||
stateManager.setState({ initialized: false });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('with simple OR condition', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
const manager = new ConditionsManager(
|
||||
[
|
||||
{
|
||||
condition: 'or' as const,
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
{
|
||||
condition: 'expand' as const,
|
||||
expand: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
stateManager,
|
||||
);
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({ fullscreen: true });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({ expand: true });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({ fullscreen: false });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({ expand: false });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('with triggered OR condition', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
|
||||
// This is not a terribly realistic example, but chosen so that trigger
|
||||
// data for both camera and view should be returned.
|
||||
const manager = new ConditionsManager(
|
||||
[
|
||||
{
|
||||
condition: 'or' as const,
|
||||
conditions: [
|
||||
{ condition: 'camera' as const },
|
||||
{ condition: 'view' as const },
|
||||
],
|
||||
},
|
||||
],
|
||||
stateManager,
|
||||
);
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
|
||||
stateManager.setState({ camera: 'camera-1' });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
expect(manager.getEvaluation().triggerData).toEqual({
|
||||
camera: { to: 'camera-1' },
|
||||
});
|
||||
|
||||
stateManager.setState({ view: 'view-1' });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
expect(manager.getEvaluation().triggerData).toEqual({
|
||||
view: { to: 'view-1' },
|
||||
});
|
||||
|
||||
stateManager.setState({ camera: 'camera-2', view: 'view-2' });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
expect(manager.getEvaluation().triggerData).toEqual({
|
||||
camera: { to: 'camera-2', from: 'camera-1' },
|
||||
|
||||
// View data will not be here as the view condition is not evaluated,
|
||||
// since the camera one will evaluate to true first.
|
||||
});
|
||||
});
|
||||
|
||||
it('with simple AND condition', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
const manager = new ConditionsManager(
|
||||
[
|
||||
{
|
||||
condition: 'and' as const,
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
{
|
||||
condition: 'expand' as const,
|
||||
expand: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
stateManager,
|
||||
);
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({ fullscreen: true });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({ expand: true });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({ fullscreen: false });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({ expand: false });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('with triggered AND condition', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
|
||||
// This is not a terribly realistic example, but chosen so that trigger
|
||||
// data for both camera and view should be returned.
|
||||
const manager = new ConditionsManager(
|
||||
[
|
||||
{
|
||||
condition: 'and' as const,
|
||||
conditions: [
|
||||
{ condition: 'camera' as const },
|
||||
{ condition: 'view' as const },
|
||||
],
|
||||
},
|
||||
],
|
||||
stateManager,
|
||||
);
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
|
||||
stateManager.setState({ camera: 'camera-1' });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
|
||||
stateManager.setState({ view: 'view-1' });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
|
||||
stateManager.setState({ camera: 'camera-2', view: 'view-2' });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
expect(manager.getEvaluation().triggerData).toEqual({
|
||||
camera: { from: 'camera-1', to: 'camera-2' },
|
||||
view: { from: 'view-1', to: 'view-2' },
|
||||
});
|
||||
|
||||
stateManager.setState({ view: 'view-3' });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('with not condition', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
const manager = new ConditionsManager(
|
||||
[
|
||||
{
|
||||
condition: 'not' as const,
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
{
|
||||
condition: 'expand' as const,
|
||||
expand: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
stateManager,
|
||||
);
|
||||
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
|
||||
stateManager.setState({ fullscreen: true });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
|
||||
stateManager.setState({ expand: true });
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
|
||||
stateManager.setState({ fullscreen: false });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
|
||||
stateManager.setState({ expand: false });
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
|
||||
// `not` conditions never have trigger data (as nothing is triggering).
|
||||
expect(manager.getEvaluation().triggerData).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle listeners correctly', () => {
|
||||
|
||||
@@ -696,6 +696,7 @@ describe('config defaults', () => {
|
||||
|
||||
it('should include all conditions', () => {
|
||||
const conditions = [
|
||||
{ condition: 'and', conditions: [{ condition: 'initialized' }] },
|
||||
{ condition: 'camera', cameras: ['camera.office'] },
|
||||
{ condition: 'config', paths: ['menu.style'] },
|
||||
{ condition: 'display_mode', display_mode: 'single' },
|
||||
@@ -714,12 +715,14 @@ describe('config defaults', () => {
|
||||
},
|
||||
{ condition: 'media_loaded', media_loaded: true },
|
||||
{ condition: 'microphone', connected: true, muted: true },
|
||||
{ condition: 'not', conditions: [{ condition: 'initialized' }] },
|
||||
{
|
||||
condition: 'numeric_state',
|
||||
entity: 'sensor.office_temperature',
|
||||
above: 10,
|
||||
below: 20,
|
||||
},
|
||||
{ condition: 'or', conditions: [{ condition: 'initialized' }] },
|
||||
{ condition: 'screen', media_query: '(orientation: landscape)' },
|
||||
{
|
||||
condition: 'state',
|
||||
|
||||
Reference in New Issue
Block a user