@@ -272,6 +272,28 @@ almost certainly not useful / reliable as a condition.
|
||||
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#state).
|
||||
|
||||
## `template`
|
||||
|
||||
Matches based on a template.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: template
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------- | ---------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `template`. |
|
||||
| `value_template` | The Home Assistant template to check, e.g. `{{ states('switch.office') == 'on' }}` |
|
||||
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/docs/scripts/conditions/#template-condition).
|
||||
|
||||
?> The Advanced Camera Card uses
|
||||
[ha-nunjucks](https://github.com/Nerwyn/ha-nunjucks) to process templates.
|
||||
Consult its documentation for the wide variety of different template values
|
||||
supported.
|
||||
|
||||
## `triggered`
|
||||
|
||||
Matches based on whether the selected camera has been triggered.
|
||||
|
||||
@@ -47,8 +47,7 @@ export class HASSManager {
|
||||
this._hass = hass;
|
||||
|
||||
this._api.getConditionStateManager().setState({
|
||||
state: this._hass.states,
|
||||
user: this._hass.user,
|
||||
hass: this._hass,
|
||||
});
|
||||
|
||||
// Theme may depend on HASS.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TemplateRenderer } from '../card-controller/templates';
|
||||
import { getConfigValue } from '../config/management';
|
||||
import { AdvancedCameraCardCondition } from '../config/schema/conditions/types';
|
||||
import { isCompanionApp } from '../utils/companion';
|
||||
@@ -23,6 +24,7 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
protected _listeners: ConditionsListener[] = [];
|
||||
protected _mediaQueries: MediaQueryList[] = [];
|
||||
protected _evaluation: ConditionsEvaluationResult = { result: false };
|
||||
protected _templateRenderer: TemplateRenderer = new TemplateRenderer();
|
||||
|
||||
constructor(
|
||||
conditions: AdvancedCameraCardCondition[],
|
||||
@@ -127,8 +129,8 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
switch (condition.condition) {
|
||||
case undefined:
|
||||
case 'state': {
|
||||
const fromState = oldState?.state?.[condition.entity]?.state;
|
||||
const toState = newState?.state?.[condition.entity]?.state;
|
||||
const fromState = oldState?.hass?.states?.[condition.entity]?.state;
|
||||
const toState = newState?.hass?.states?.[condition.entity]?.state;
|
||||
|
||||
return {
|
||||
result:
|
||||
@@ -207,17 +209,18 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
case 'numeric_state':
|
||||
return {
|
||||
result:
|
||||
!!newState?.state &&
|
||||
condition.entity in newState.state &&
|
||||
newState.state[condition.entity].state !== undefined &&
|
||||
!!newState?.hass?.states &&
|
||||
condition.entity in newState.hass?.states &&
|
||||
newState.hass.states[condition.entity].state !== undefined &&
|
||||
(condition.above === undefined ||
|
||||
Number(newState.state[condition.entity].state) > condition.above) &&
|
||||
Number(newState.hass.states[condition.entity].state) > condition.above) &&
|
||||
(condition.below === undefined ||
|
||||
Number(newState.state[condition.entity].state) < condition.below),
|
||||
Number(newState.hass.states[condition.entity].state) < condition.below),
|
||||
};
|
||||
case 'user':
|
||||
return {
|
||||
result: !!newState?.user && condition.users.includes(newState.user.id),
|
||||
result:
|
||||
!!newState?.hass?.user && condition.users.includes(newState.hass.user.id),
|
||||
};
|
||||
case 'media_loaded':
|
||||
return {
|
||||
@@ -341,6 +344,16 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
).result,
|
||||
};
|
||||
}
|
||||
case 'template':
|
||||
return {
|
||||
result:
|
||||
!!newState?.hass &&
|
||||
this._templateRenderer.renderRecursively(
|
||||
newState.hass,
|
||||
condition.value_template,
|
||||
{ conditionState: newState },
|
||||
) === true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { HassEntities } from 'home-assistant-js-websocket';
|
||||
import { KeysState, MicrophoneState } from '../card-controller/types';
|
||||
import { ViewDisplayMode } from '../config/schema/common/display';
|
||||
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
||||
import { CurrentUser } from '../ha/types';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { MediaLoadedInfo } from '../types';
|
||||
|
||||
export interface ConditionState {
|
||||
@@ -16,9 +15,8 @@ export interface ConditionState {
|
||||
keys?: KeysState;
|
||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
microphone?: MicrophoneState;
|
||||
state?: HassEntities;
|
||||
hass?: HomeAssistant;
|
||||
triggered?: Set<string>;
|
||||
user?: CurrentUser;
|
||||
userAgent?: string;
|
||||
view?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// https://www.home-assistant.io/docs/scripts/conditions/#template-condition
|
||||
export const templateConditionSchema = z.object({
|
||||
condition: z.literal('template'),
|
||||
value_template: z.string(),
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import { viewConditionSchema } from './custom/view';
|
||||
import { numericStateConditionSchema } from './stock/numeric';
|
||||
import { screenConditionSchema } from './stock/screen';
|
||||
import { stateConditionSchema } from './stock/state';
|
||||
import { templateConditionSchema } from './stock/template';
|
||||
import { usersConditionSchema } from './stock/users';
|
||||
|
||||
// https://www.home-assistant.io/docs/scripts/conditions/#or-condition
|
||||
@@ -65,6 +66,7 @@ export const advancedCameraCardConditionSchema = z.union([
|
||||
orConditionSchema,
|
||||
andConditionSchema,
|
||||
notConditionSchema,
|
||||
templateConditionSchema,
|
||||
|
||||
// Custom conditions:
|
||||
cameraConditionSchema,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
createHASS,
|
||||
createStateEntity,
|
||||
createStore,
|
||||
createUser,
|
||||
createView,
|
||||
} from '../../test-utils';
|
||||
|
||||
@@ -50,17 +49,13 @@ describe('HASSManager', () => {
|
||||
it('should set condition manager state', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new HASSManager(api);
|
||||
|
||||
const states = { 'switch.foo': createStateEntity() };
|
||||
const user = createUser({ id: 'user_1' });
|
||||
const hass = createHASS(states, user);
|
||||
const hass = createHASS();
|
||||
|
||||
manager.setHASS(hass);
|
||||
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
state: states,
|
||||
user: user,
|
||||
hass: hass,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { HassEntities } from 'home-assistant-js-websocket';
|
||||
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 { HomeAssistant } from '../../src/ha/types';
|
||||
import {
|
||||
createConfig,
|
||||
createHASS,
|
||||
createMediaLoadedInfo,
|
||||
createStateEntity,
|
||||
createUser,
|
||||
@@ -193,7 +196,9 @@ describe('ConditionsManager', () => {
|
||||
manager.addListener(listener);
|
||||
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'on' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'on' }),
|
||||
}),
|
||||
});
|
||||
expect(listener).toBeCalledWith({
|
||||
result: true,
|
||||
@@ -207,7 +212,9 @@ describe('ConditionsManager', () => {
|
||||
expect(listener).toBeCalledTimes(1);
|
||||
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||
}),
|
||||
});
|
||||
expect(listener).toBeCalledWith({
|
||||
result: true,
|
||||
@@ -238,11 +245,13 @@ describe('ConditionsManager', () => {
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity() },
|
||||
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
@@ -262,15 +271,19 @@ describe('ConditionsManager', () => {
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity() },
|
||||
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'active' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'active' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
@@ -292,11 +305,13 @@ describe('ConditionsManager', () => {
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity() },
|
||||
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
});
|
||||
@@ -315,15 +330,21 @@ describe('ConditionsManager', () => {
|
||||
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' }) },
|
||||
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'active' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
});
|
||||
@@ -341,10 +362,14 @@ describe('ConditionsManager', () => {
|
||||
);
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
|
||||
stateManager.setState({
|
||||
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
@@ -363,10 +388,10 @@ describe('ConditionsManager', () => {
|
||||
manager.addListener(listener);
|
||||
|
||||
stateManager.setState({
|
||||
state: {
|
||||
hass: createHASS({
|
||||
'switch.one': createStateEntity({ state: 'on' }),
|
||||
'switch.two': createStateEntity({ state: 'off' }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(listener).toHaveBeenLastCalledWith({
|
||||
result: true,
|
||||
@@ -380,10 +405,10 @@ describe('ConditionsManager', () => {
|
||||
});
|
||||
|
||||
stateManager.setState({
|
||||
state: {
|
||||
hass: createHASS({
|
||||
'switch.one': createStateEntity({ state: 'off' }),
|
||||
'switch.two': createStateEntity({ state: 'on' }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(listener).toHaveBeenLastCalledWith({
|
||||
@@ -411,10 +436,11 @@ describe('ConditionsManager', () => {
|
||||
const listener = vi.fn();
|
||||
manager.addListener(listener);
|
||||
|
||||
stateManager.setState({
|
||||
state: {
|
||||
const hass = createHASS({
|
||||
'switch.one': createStateEntity({ state: 'on' }),
|
||||
},
|
||||
});
|
||||
stateManager.setState({
|
||||
hass,
|
||||
});
|
||||
expect(listener).toBeCalledTimes(1);
|
||||
expect(manager.getEvaluation()).toEqual({
|
||||
@@ -428,11 +454,7 @@ describe('ConditionsManager', () => {
|
||||
},
|
||||
});
|
||||
|
||||
stateManager.setState({
|
||||
state: {
|
||||
'switch.one': createStateEntity({ state: 'on' }),
|
||||
},
|
||||
});
|
||||
stateManager.setState({ hass });
|
||||
expect(listener).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -453,11 +475,11 @@ describe('ConditionsManager', () => {
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
|
||||
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '11' }) }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: '9' }) },
|
||||
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: '9' }) }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
@@ -477,16 +499,74 @@ describe('ConditionsManager', () => {
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
|
||||
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '11' }) }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
state: { 'sensor.foo': createStateEntity({ state: '9' }) },
|
||||
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '9' }) }),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with template condition', () => {
|
||||
const createHASSForTemplateCondition = (states: HassEntities): HomeAssistant => {
|
||||
const hass = createHASS(states);
|
||||
vi.mocked(hass.connection.sendMessagePromise).mockResolvedValue([]);
|
||||
return hass;
|
||||
};
|
||||
|
||||
it('should evaluate true when template evalutes to true', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
const manager = new ConditionsManager(
|
||||
[
|
||||
{
|
||||
condition: 'template' as const,
|
||||
value_template: '{{ is_state("sensor.foo", "on") }}',
|
||||
},
|
||||
],
|
||||
stateManager,
|
||||
);
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
|
||||
stateManager.setState({
|
||||
hass: createHASSForTemplateCondition({
|
||||
'sensor.foo': createStateEntity({ state: 'on' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
|
||||
stateManager.setState({
|
||||
hass: createHASSForTemplateCondition({
|
||||
'sensor.foo': createStateEntity({ state: 'off' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate false when template evalutes to non-boolean', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
const manager = new ConditionsManager(
|
||||
[
|
||||
{
|
||||
condition: 'template' as const,
|
||||
// This does not result in a boolean.
|
||||
value_template: '{{ hass.states["light.office"].state }}',
|
||||
},
|
||||
],
|
||||
stateManager,
|
||||
);
|
||||
|
||||
stateManager.setState({
|
||||
hass: createHASSForTemplateCondition({
|
||||
'light.office': createStateEntity({ state: 'on' }),
|
||||
}),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call listeners for HA state changes without relevant condition', () => {
|
||||
const stateManager = new ConditionStateManager();
|
||||
const manager = new ConditionsManager(
|
||||
@@ -503,7 +583,7 @@ describe('ConditionsManager', () => {
|
||||
manager.addListener(listener);
|
||||
|
||||
stateManager.setState({
|
||||
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
|
||||
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '11' }) }),
|
||||
});
|
||||
|
||||
expect(listener).not.toBeCalled();
|
||||
@@ -524,11 +604,11 @@ describe('ConditionsManager', () => {
|
||||
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
stateManager.setState({
|
||||
user: createUser({ id: 'user_1' }),
|
||||
hass: createHASS({}, createUser({ id: 'user_1' })),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeTruthy();
|
||||
stateManager.setState({
|
||||
user: createUser({ id: 'user_WRONG' }),
|
||||
hass: createHASS({}, createUser({ id: 'user_WRONG' })),
|
||||
});
|
||||
expect(manager.getEvaluation().result).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||
import { createStateEntity } from '../test-utils';
|
||||
import { createHASS, createStateEntity } from '../test-utils';
|
||||
|
||||
describe('ConditionStateManager', () => {
|
||||
afterEach(() => {
|
||||
@@ -46,9 +46,9 @@ describe('ConditionStateManager', () => {
|
||||
expect(listener).toBeCalledTimes(1);
|
||||
|
||||
manager.setState({
|
||||
state: {
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(listener).toBeCalledTimes(2);
|
||||
|
||||
@@ -56,24 +56,24 @@ describe('ConditionStateManager', () => {
|
||||
expect(listener).toBeCalledTimes(2);
|
||||
|
||||
manager.setState({
|
||||
state: {
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(listener).toBeCalledTimes(2);
|
||||
|
||||
manager.setState({ fullscreen: false });
|
||||
expect(listener).toBeCalledTimes(3);
|
||||
|
||||
manager.setState({ fullscreen: false });
|
||||
expect(listener).toBeCalledTimes(3);
|
||||
expect(listener).toBeCalledTimes(4);
|
||||
|
||||
manager.setState({ fullscreen: false });
|
||||
expect(listener).toBeCalledTimes(4);
|
||||
|
||||
manager.setState({
|
||||
state: {
|
||||
hass: createHASS({
|
||||
'binary_sensor.foo': createStateEntity({ state: 'off' }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(listener).toBeCalledTimes(4);
|
||||
expect(listener).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ export const createHASS = (states?: HassEntities, user?: CurrentUser): HomeAssis
|
||||
hass.user = user;
|
||||
}
|
||||
hass.connection.subscribeMessage = vi.fn();
|
||||
hass.connection.sendMessagePromise = vi.fn();
|
||||
return hass;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user