Add support for users and numeric_conditions.

This commit is contained in:
Dermot Duffy
2024-03-10 14:30:45 -07:00
parent 219191e869
commit ff78cc51a1
7 changed files with 522 additions and 281 deletions
+27 -3
View File
@@ -1,3 +1,4 @@
import { CurrentUser } from '@dermotduffy/custom-card-helpers';
import { HassEntities } from 'home-assistant-js-websocket'; import { HassEntities } from 'home-assistant-js-websocket';
import merge from 'lodash-es/merge'; import merge from 'lodash-es/merge';
import { copyConfig } from '../config-mgmt'; import { copyConfig } from '../config-mgmt';
@@ -22,6 +23,7 @@ interface ConditionState {
triggered?: Set<string>; triggered?: Set<string>;
interaction?: boolean; interaction?: boolean;
microphone?: MicrophoneConditionState; microphone?: MicrophoneConditionState;
user?: CurrentUser;
} }
export class ConditionEvaluateRequestEvent extends Event { export class ConditionEvaluateRequestEvent extends Event {
@@ -174,7 +176,10 @@ export class ConditionsManager {
const conditions = getAllConditions(); const conditions = getAllConditions();
this._hasHAStateConditions = conditions.some( this._hasHAStateConditions = conditions.some(
(condition) => !!condition.state?.length, (condition) =>
!!condition.state?.length ||
!!condition.numeric_state?.length ||
!!condition.users?.length,
); );
conditions.forEach((condition) => { conditions.forEach((condition) => {
if (condition.media_query) { if (condition.media_query) {
@@ -235,11 +240,30 @@ export class ConditionsManager {
((!stateTest.state && !stateTest.state_not) || ((!stateTest.state && !stateTest.state_not) ||
(stateTest.entity in state.state && (stateTest.entity in state.state &&
(!stateTest.state || (!stateTest.state ||
state.state[stateTest.entity].state === stateTest.state) && (Array.isArray(stateTest.state)
? stateTest.state.includes(state.state[stateTest.entity].state)
: stateTest.state === state.state[stateTest.entity].state)) &&
(!stateTest.state_not || (!stateTest.state_not ||
state.state[stateTest.entity].state !== stateTest.state_not))); (Array.isArray(stateTest.state_not)
? !stateTest.state_not.includes(state.state[stateTest.entity].state)
: stateTest.state_not !== state.state[stateTest.entity].state))));
} }
} }
if (condition.numeric_state?.length) {
for (const stateTest of condition.numeric_state) {
result &&=
!!state.state &&
stateTest.entity in state.state &&
state.state[stateTest.entity].state !== undefined &&
(stateTest.above === undefined ||
Number(state.state[stateTest.entity].state) > stateTest.above) &&
(stateTest.below === undefined ||
Number(state.state[stateTest.entity].state) < stateTest.below);
}
}
if (condition.users?.length) {
result &&= !!state.user && condition.users.includes(state.user.id);
}
if (condition.media_loaded !== undefined) { if (condition.media_loaded !== undefined) {
result &&= result &&=
state.media_loaded !== undefined && state.media_loaded !== undefined &&
+4 -1
View File
@@ -66,7 +66,10 @@ export class HASSManager {
} }
if (this._api.getConditionsManager().hasHAStateConditions()) { if (this._api.getConditionsManager().hasHAStateConditions()) {
this._api.getConditionsManager().setState({ state: this._hass.states }); this._api.getConditionsManager().setState({
state: this._hass.states,
user: this._hass.user,
});
} }
// Dark mode may depend on HASS. // Dark mode may depend on HASS.
+40 -11
View File
@@ -400,20 +400,47 @@ const imageSchema = elementsBaseSchema.extend({
aspect_ratio: z.string().optional(), aspect_ratio: z.string().optional(),
}); });
// This state condition is used both for the Picture elements conditional // https://www.home-assistant.io/dashboards/conditional/#state
// schema, and also in frigateCardConditionSchema. const stateCondition = z.object({
const stateConditions = z condition: z.literal('state'),
.object({
entity: z.string(), entity: z.string(),
state: z.string().optional(), state: z.string().or(z.string().array()).optional(),
state_not: z.string().optional(), state_not: z.string().or(z.string().array()).optional(),
}) });
.array();
// https://www.home-assistant.io/dashboards/conditional/#numeric-state
const numericCondition = z.object({
condition: z.literal('numeric_state'),
entity: z.string(),
above: z.number().optional(),
below: z.number().optional(),
});
const mediaQuerySchema = z.string();
// https://www.home-assistant.io/dashboards/conditional/#screen
const screenCondition = z.object({
condition: z.literal('screen'),
media_query: mediaQuerySchema,
});
// https://www.home-assistant.io/dashboards/conditional/#user
const usersCondition = z.object({
condition: z.literal('user'),
users: z.string().array().min(1),
});
const stockCondition = z.union([
stateCondition,
numericCondition,
screenCondition,
usersCondition,
]);
// https://www.home-assistant.io/lovelace/picture-elements/#image-element // https://www.home-assistant.io/lovelace/picture-elements/#image-element
export const conditionalSchema = z.object({ export const conditionalSchema = z.object({
type: z.literal('conditional'), type: z.literal('conditional'),
conditions: stateConditions, conditions: stockCondition.array(),
elements: z.lazy(() => pictureElementsSchema), elements: z.lazy(() => pictureElementsSchema),
}); });
@@ -501,12 +528,14 @@ export const frigateCardConditionSchema = z.object({
expand: z.boolean().optional(), expand: z.boolean().optional(),
camera: z.string().array().optional(), camera: z.string().array().optional(),
media_loaded: z.boolean().optional(), media_loaded: z.boolean().optional(),
state: stateConditions.optional(), state: stateCondition.array().optional(),
media_query: z.string().optional(), numeric_state: numericCondition.array().optional(),
media_query: mediaQuerySchema.optional(),
display_mode: viewDisplayModeSchema.optional(), display_mode: viewDisplayModeSchema.optional(),
triggered: z.string().array().optional(), triggered: z.string().array().optional(),
interaction: z.boolean().optional(), interaction: z.boolean().optional(),
microphone: microphoneConditionSchema.optional(), microphone: microphoneConditionSchema.optional(),
users: z.string().array().optional()
}); });
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>; export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
+234 -67
View File
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { FrigateCardCondition } from '../../src/config/types'; import { FrigateCardCondition } from '../../src/config/types';
import { import {
ConditionEvaluateRequestEvent, ConditionEvaluateRequestEvent,
@@ -12,6 +12,7 @@ import {
createCondition, createCondition,
createConfig, createConfig,
createStateEntity, createStateEntity,
createUser,
} from '../test-utils'; } from '../test-utils';
// @vitest-environment jsdom // @vitest-environment jsdom
@@ -145,49 +146,6 @@ describe('getOverridesByKey', () => {
}); });
describe('ConditionsManager', () => { describe('ConditionsManager', () => {
const config = {
type: 'custom:frigate-card',
cameras: [],
elements: [
{
type: 'custom:frigate-card-conditional',
conditions: {
fullscreen: true,
},
elements: [
{
type: 'custom:nested-unknown-object',
unknown_key: {
type: 'custom:frigate-card-conditional',
conditions: {
media_query: 'media query goes here',
},
elements: [],
},
},
],
},
],
overrides: [
{
overrides: {
menu: {
style: 'overlay',
},
},
conditions: {
fullscreen: true,
state: [
{
entity: 'binary_sensor.foo',
state: 'on',
},
],
},
},
],
};
afterEach(() => { afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
@@ -214,26 +172,85 @@ describe('ConditionsManager', () => {
expect(manager.getState()).toEqual(state); expect(manager.getState()).toEqual(state);
}); });
it('should not return hasHAStateConditions without HA state conditions', () => { describe('should handle hasHAStateConditions', () => {
const manager = new ConditionsManager(createCardAPI()); beforeEach(() => {
expect(manager.hasHAStateConditions()).toBeFalsy();
});
it('should return hasHAStateConditions with HA state conditions', () => {
vi.spyOn(window, 'matchMedia').mockReturnValueOnce({ vi.spyOn(window, 'matchMedia').mockReturnValueOnce({
matches: false, matches: false,
addEventListener: vi.fn(), addEventListener: vi.fn(),
} as unknown as MediaQueryList); } as unknown as MediaQueryList);
const api = createCardAPI(); });
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
const manager = new ConditionsManager(api);
afterEach(() => {
vi.restoreAllMocks();
});
const createSuitableConfig = (conditions: FrigateCardCondition) => {
return createConfig({
overrides: [
{
overrides: {},
conditions: conditions,
},
],
});
};
it('without HA state conditions', () => {
const manager = new ConditionsManager(createCardAPI());
expect(manager.hasHAStateConditions()).toBeFalsy();
});
it('with HA state conditions', () => {
const api = createCardAPI();
const numericConfig = createSuitableConfig({
state: [
{
condition: 'state',
entity: 'binary_sensor.foo',
state: 'on',
},
],
});
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(numericConfig);
const manager = new ConditionsManager(api);
manager.setConditionsFromConfig(); manager.setConditionsFromConfig();
expect(manager.hasHAStateConditions()).toBeTruthy(); expect(manager.hasHAStateConditions()).toBeTruthy();
}); });
it('should evaluate conditions with a view', () => { it('with HA numeric_state conditions', () => {
const api = createCardAPI();
const stateConfig = createSuitableConfig({
numeric_state: [
{
entity: 'sensor.foo',
condition: 'numeric_state' as const,
above: 10,
},
],
});
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(stateConfig);
const manager = new ConditionsManager(api);
manager.setConditionsFromConfig();
expect(manager.hasHAStateConditions()).toBeTruthy();
});
it('with HA user conditions', () => {
const api = createCardAPI();
const userConfig = createSuitableConfig({
users: ['user_1'],
});
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(userConfig);
const manager = new ConditionsManager(api);
manager.setConditionsFromConfig();
expect(manager.hasHAStateConditions()).toBeTruthy();
});
});
describe('should evaluate conditions', () => {
it('with a view', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition = { view: ['foo'] }; const condition = { view: ['foo'] };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -241,7 +258,7 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeTruthy(); expect(manager.evaluateCondition(condition)).toBeTruthy();
}); });
it('should evaluate conditions with fullscreen', () => { it('with fullscreen', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition = { fullscreen: true }; const condition = { fullscreen: true };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -251,7 +268,7 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should evaluate conditions with expand', () => { it('with expand', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition = { expand: true }; const condition = { expand: true };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -261,7 +278,7 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should evaluate conditions with camera', () => { it('camera', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition = { camera: ['bar'] }; const condition = { camera: ['bar'] };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -271,11 +288,15 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should evaluate conditions with ha state positive check', () => { describe('with stock HA conditions', () => {
describe('state check', () => {
describe('positive', () => {
it('single', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition = { const condition = {
state: [ state: [
{ {
condition: 'state' as const,
entity: 'binary_sensor.foo', entity: 'binary_sensor.foo',
state: 'on', state: 'on',
}, },
@@ -290,11 +311,38 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should evaluate conditions with ha state negative check', () => { it('multiple', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition = { const condition = {
state: [ state: [
{ {
condition: 'state' as const,
entity: 'binary_sensor.foo',
state: ['active', 'on'],
},
],
};
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
expect(manager.evaluateCondition(condition)).toBeTruthy();
manager.setState({
state: { 'binary_sensor.foo': createStateEntity({ state: 'active' }) },
});
expect(manager.evaluateCondition(condition)).toBeTruthy();
manager.setState({
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
});
expect(manager.evaluateCondition(condition)).toBeFalsy();
});
});
describe('negative', () => {
it('single', () => {
const manager = new ConditionsManager(createCardAPI());
const condition = {
state: [
{
condition: 'state' as const,
entity: 'binary_sensor.foo', entity: 'binary_sensor.foo',
state_not: 'on', state_not: 'on',
}, },
@@ -308,8 +356,97 @@ describe('ConditionsManager', () => {
}); });
expect(manager.evaluateCondition(condition)).toBeTruthy(); expect(manager.evaluateCondition(condition)).toBeTruthy();
}); });
});
it('should evaluate conditions with media_loaded', () => { it('multiple', () => {
const manager = new ConditionsManager(createCardAPI());
const condition = {
state: [
{
condition: 'state' as const,
entity: 'binary_sensor.foo',
state_not: ['active', 'on'],
},
],
};
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({
state: { 'binary_sensor.foo': createStateEntity({ state: 'active' }) },
});
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
});
expect(manager.evaluateCondition(condition)).toBeTruthy();
});
});
describe('numeric state check', () => {
it('above', () => {
const manager = new ConditionsManager(createCardAPI());
const condition = {
numeric_state: [
{
condition: 'numeric_state' as const,
entity: 'sensor.foo',
above: 10,
},
],
};
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
});
expect(manager.evaluateCondition(condition)).toBeTruthy();
manager.setState({
state: { 'binary_sensor.foo': createStateEntity({ state: '9' }) },
});
expect(manager.evaluateCondition(condition)).toBeFalsy();
});
it('below', () => {
const manager = new ConditionsManager(createCardAPI());
const condition = {
numeric_state: [
{
condition: 'numeric_state' as const,
entity: 'sensor.foo',
below: 10,
},
],
};
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({
state: { 'sensor.foo': createStateEntity({ state: '11' }) },
});
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({
state: { 'sensor.foo': createStateEntity({ state: '9' }) },
});
expect(manager.evaluateCondition(condition)).toBeTruthy();
});
});
});
it('with users', () => {
const manager = new ConditionsManager(createCardAPI());
const condition = {
users: ['user_1', 'user_2'],
};
expect(manager.evaluateCondition(condition)).toBeFalsy();
manager.setState({
user: createUser({ id: 'user_1' }),
});
expect(manager.evaluateCondition(condition)).toBeTruthy();
manager.setState({
user: createUser({ id: 'user_WRONG' }),
});
expect(manager.evaluateCondition(condition)).toBeFalsy();
});
it('with media_loaded', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition = { media_loaded: true }; const condition = { media_loaded: true };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -319,7 +456,33 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should evaluate conditions with media query', () => { describe('with media query', () => {
const mediaQueryConfig = {
type: 'custom:frigate-card',
cameras: [],
elements: [
{
type: 'custom:frigate-card-conditional',
conditions: {
fullscreen: true,
},
elements: [
{
type: 'custom:nested-unknown-object',
unknown_key: {
type: 'custom:frigate-card-conditional',
conditions: {
media_query: 'media query goes here',
},
elements: [],
},
},
],
},
],
};
it('on evaluation', () => {
vi.spyOn(window, 'matchMedia') vi.spyOn(window, 'matchMedia')
.mockReturnValueOnce(<MediaQueryList>{ matches: true }) .mockReturnValueOnce(<MediaQueryList>{ matches: true })
.mockReturnValueOnce(<MediaQueryList>{ matches: false }); .mockReturnValueOnce(<MediaQueryList>{ matches: false });
@@ -330,7 +493,7 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should trigger on changes to media query conditions', () => { it('on trigger', () => {
const addEventListener = vi.fn(); const addEventListener = vi.fn();
const removeEventListener = vi.fn(); const removeEventListener = vi.fn();
vi.spyOn(window, 'matchMedia').mockReturnValueOnce({ vi.spyOn(window, 'matchMedia').mockReturnValueOnce({
@@ -339,7 +502,9 @@ describe('ConditionsManager', () => {
removeEventListener: removeEventListener, removeEventListener: removeEventListener,
} as unknown as MediaQueryList); } as unknown as MediaQueryList);
const api = createCardAPI(); const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config)); vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig(mediaQueryConfig),
);
const callback = vi.fn(); const callback = vi.fn();
const manager = new ConditionsManager(api, callback); const manager = new ConditionsManager(api, callback);
@@ -358,8 +523,9 @@ describe('ConditionsManager', () => {
manager.removeConditions(); manager.removeConditions();
expect(removeEventListener).toBeCalled(); expect(removeEventListener).toBeCalled();
}); });
});
it('should evaluate conditions with display mode', () => { it('with display mode', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition: FrigateCardCondition = { display_mode: 'grid' }; const condition: FrigateCardCondition = { display_mode: 'grid' };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -369,7 +535,7 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should evaluate conditions with triggers', () => { it('with triggers', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition: FrigateCardCondition = { triggered: ['camera_1', 'camera_2'] }; const condition: FrigateCardCondition = { triggered: ['camera_1', 'camera_2'] };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -381,7 +547,7 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
it('should evaluate conditions with interaction', () => { it('with interaction', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition: FrigateCardCondition = { interaction: true }; const condition: FrigateCardCondition = { interaction: true };
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
@@ -391,7 +557,7 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeFalsy(); expect(manager.evaluateCondition(condition)).toBeFalsy();
}); });
describe('should evaluate conditions with microphone', () => { describe('with microphone', () => {
it('empty', () => { it('empty', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
const condition: FrigateCardCondition = { microphone: {} }; const condition: FrigateCardCondition = { microphone: {} };
@@ -462,4 +628,5 @@ describe('ConditionsManager', () => {
expect(manager.evaluateCondition(condition)).toBeTruthy(); expect(manager.evaluateCondition(condition)).toBeTruthy();
}); });
}); });
});
}); });
+4 -1
View File
@@ -9,6 +9,7 @@ import {
createHASS, createHASS,
createStateEntity, createStateEntity,
createStore, createStore,
createUser,
createView, createView,
} from '../test-utils'; } from '../test-utils';
@@ -44,13 +45,15 @@ describe('HASSManager', () => {
vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(true); vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(true);
const states = { 'switch.foo': createStateEntity() }; const states = { 'switch.foo': createStateEntity() };
const hass = createHASS(states); const user = createUser({ id: 'user_1' });
const hass = createHASS(states, user);
manager.setHASS(hass); manager.setHASS(hass);
expect(api.getConditionsManager().setState).toBeCalledWith( expect(api.getConditionsManager().setState).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
state: states, state: states,
user: user,
}), }),
); );
}); });
+2
View File
@@ -381,6 +381,7 @@ describe('should lazy evaluate', () => {
type: 'conditional', type: 'conditional',
conditions: [ conditions: [
{ {
condition: 'state',
entity: 'light.office_main_lights', entity: 'light.office_main_lights',
state: 'on', state: 'on',
state_not: 'off', state_not: 'off',
@@ -401,6 +402,7 @@ describe('should lazy evaluate', () => {
).toEqual({ ).toEqual({
conditions: [ conditions: [
{ {
condition: 'state',
entity: 'light.office_main_lights', entity: 'light.office_main_lights',
state: 'on', state: 'on',
state_not: 'off', state_not: 'off',
+15 -2
View File
@@ -1,4 +1,4 @@
import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CurrentUser, HomeAssistant } from '@dermotduffy/custom-card-helpers';
import { HassEntities, HassEntity } from 'home-assistant-js-websocket'; import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
import { expect, vi } from 'vitest'; import { expect, vi } from 'vitest';
import { mock } from 'vitest-mock-extended'; import { mock } from 'vitest-mock-extended';
@@ -80,15 +80,28 @@ export const createCamera = (
return new Camera(config, engine, { capabilities: capabilities }); return new Camera(config, engine, { capabilities: capabilities });
}; };
export const createHASS = (states?: HassEntities): ExtendedHomeAssistant => { export const createHASS = (states?: HassEntities, user?: CurrentUser): ExtendedHomeAssistant => {
const hass = mock<ExtendedHomeAssistant>(); const hass = mock<ExtendedHomeAssistant>();
if (states) { if (states) {
hass.states = states; hass.states = states;
} }
if (user) {
hass.user = user;
}
hass.connection.subscribeMessage = vi.fn(); hass.connection.subscribeMessage = vi.fn();
return hass; return hass;
}; };
export const createUser = (user?: Partial<CurrentUser>): CurrentUser => ({
id: 'user',
is_owner: false,
is_admin: false,
name: 'User',
credentials: [],
mfa_modules: [],
...user,
});
export const createRegistryEntity = (entity?: Partial<Entity>): Entity => { export const createRegistryEntity = (entity?: Partial<Entity>): Entity => {
return { return {
config_entry_id: entity?.config_entry_id ?? null, config_entry_id: entity?.config_entry_id ?? null,