feat: Triggers should fire on startup if entities are in an active state (#2095)
- Closes: #2087
This commit is contained in:
@@ -115,9 +115,8 @@ to wait prior to the acting (see `untrigger_seconds`). By default, triggering is
|
|||||||
only allowed when there is no ongoing human interaction with the card. This
|
only allowed when there is no ongoing human interaction with the card. This
|
||||||
behavior can be controlled by the `interaction_mode` parameter.
|
behavior can be controlled by the `interaction_mode` parameter.
|
||||||
|
|
||||||
Triggers based on Home Assistant entities require state _changes_ -- when the
|
If the card starts when a trigger entity is already in a triggered state, the
|
||||||
card is first started, it takes an active change in state to trigger (i.e. an
|
action will be taken on card startup.
|
||||||
already occupied room will not trigger, but a newly occupied room will).
|
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export enum InitializationAspect {
|
|||||||
CAMERAS = 'cameras',
|
CAMERAS = 'cameras',
|
||||||
MICROPHONE_CONNECT = 'microphone-connect',
|
MICROPHONE_CONNECT = 'microphone-connect',
|
||||||
VIEW = 'view',
|
VIEW = 'view',
|
||||||
|
|
||||||
|
// The initial triggering must happen after both the config is set (and
|
||||||
|
// cameras initialized), and hass is set.
|
||||||
|
INITIAL_TRIGGER = 'initial-trigger',
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -60,6 +64,7 @@ export class InitializationManager {
|
|||||||
? [InitializationAspect.MICROPHONE_CONNECT]
|
? [InitializationAspect.MICROPHONE_CONNECT]
|
||||||
: []),
|
: []),
|
||||||
InitializationAspect.VIEW,
|
InitializationAspect.VIEW,
|
||||||
|
InitializationAspect.INITIAL_TRIGGER,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +136,21 @@ export class InitializationManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await this._initializer.initializeIfNecessary(
|
||||||
|
InitializationAspect.INITIAL_TRIGGER,
|
||||||
|
async (): Promise<boolean> => {
|
||||||
|
await this._api.getTriggersManager().handleInitialCameraTriggers();
|
||||||
|
|
||||||
|
// Force a card update to continue the initialization.
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._everInitialized = true;
|
this._everInitialized = true;
|
||||||
|
|
||||||
// When the card is initialized, both the initialization state (will never
|
// When the card is initialized, both the initialization state (will never
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { orderBy, throttle } from 'lodash-es';
|
import { orderBy, throttle } from 'lodash-es';
|
||||||
import { CameraEvent } from '../camera-manager/types';
|
import { CameraEvent } from '../camera-manager/types';
|
||||||
|
import { isTriggeredState } from '../ha/is-triggered-state';
|
||||||
import { Timer } from '../utils/timer';
|
import { Timer } from '../utils/timer';
|
||||||
import { CardTriggersAPI } from './types';
|
import { CardTriggersAPI } from './types';
|
||||||
|
|
||||||
@@ -34,6 +35,31 @@ export class TriggersManager {
|
|||||||
return sorted.length ? sorted[0][0] : null;
|
return sorted.length ? sorted[0][0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleInitialCameraTriggers = async (): Promise<boolean> => {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
let triggered = false;
|
||||||
|
|
||||||
|
for (const [cameraID, camera] of this._api
|
||||||
|
.getCameraManager()
|
||||||
|
.getStore()
|
||||||
|
.getCameras()) {
|
||||||
|
if (
|
||||||
|
camera
|
||||||
|
.getConfig()
|
||||||
|
.triggers.entities.some((entityID) =>
|
||||||
|
isTriggeredState(hass?.states[entityID]?.state),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
triggered = true;
|
||||||
|
await this.handleCameraEvent({
|
||||||
|
cameraID,
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return triggered;
|
||||||
|
};
|
||||||
|
|
||||||
public async handleCameraEvent(ev: CameraEvent): Promise<void> {
|
public async handleCameraEvent(ev: CameraEvent): Promise<void> {
|
||||||
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
||||||
const selectedCameraID = this._api.getViewManager().getView()?.camera;
|
const selectedCameraID = this._api.getViewManager().getView()?.camera;
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export interface CardInitializerAPI {
|
|||||||
getMessageManager(): MessageManager;
|
getMessageManager(): MessageManager;
|
||||||
getQueryStringManager(): QueryStringManager;
|
getQueryStringManager(): QueryStringManager;
|
||||||
getResolvedMediaCache(): ResolvedMediaCache;
|
getResolvedMediaCache(): ResolvedMediaCache;
|
||||||
|
getTriggersManager(): TriggersManager;
|
||||||
getViewManager(): ViewManager;
|
getViewManager(): ViewManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +273,7 @@ export interface CardTriggersAPI {
|
|||||||
getConditionStateManager(): ConditionStateManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
getInteractionManager(): InteractionManager;
|
getInteractionManager(): InteractionManager;
|
||||||
getViewManager(): ViewManager;
|
getViewManager(): ViewManager;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
export const STATES_OFF = ['closed', 'locked', 'off'];
|
export const STATES_OFF = ['off', 'closed', 'locked'];
|
||||||
export const STATES_ON = ['open', 'unlocked', 'on'];
|
export const STATES_ON = ['on', 'open', 'unlocked'];
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ describe('InitializationManager', () => {
|
|||||||
expect(manager.isInitialized(InitializationAspect.CAMERAS)).toBeTruthy();
|
expect(manager.isInitialized(InitializationAspect.CAMERAS)).toBeTruthy();
|
||||||
expect(manager.isInitialized(InitializationAspect.MICROPHONE_CONNECT)).toBeFalsy();
|
expect(manager.isInitialized(InitializationAspect.MICROPHONE_CONNECT)).toBeFalsy();
|
||||||
expect(manager.isInitialized(InitializationAspect.VIEW)).toBeTruthy();
|
expect(manager.isInitialized(InitializationAspect.VIEW)).toBeTruthy();
|
||||||
|
expect(manager.isInitialized(InitializationAspect.INITIAL_TRIGGER)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully with microphone if configured', async () => {
|
it('successfully with microphone if configured', async () => {
|
||||||
@@ -174,6 +175,8 @@ describe('InitializationManager', () => {
|
|||||||
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
||||||
|
|
||||||
await manager.initializeMandatory();
|
await manager.initializeMandatory();
|
||||||
|
|
||||||
|
expect(manager.wasEverInitialized()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with cameras in progress', async () => {
|
it('with cameras in progress', async () => {
|
||||||
@@ -188,6 +191,27 @@ describe('InitializationManager', () => {
|
|||||||
.mockResolvedValueOnce(false);
|
.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
await manager.initializeMandatory();
|
await manager.initializeMandatory();
|
||||||
|
|
||||||
|
expect(manager.wasEverInitialized()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with triggers in progress', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
|
|
||||||
|
const initializer = mock<Initializer>();
|
||||||
|
const manager = new InitializationManager(api, initializer);
|
||||||
|
initializer.initializeMultipleIfNecessary
|
||||||
|
.mockResolvedValueOnce(true)
|
||||||
|
.mockResolvedValueOnce(true);
|
||||||
|
initializer.initializeIfNecessary
|
||||||
|
.mockResolvedValueOnce(true)
|
||||||
|
.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
await manager.initializeMandatory();
|
||||||
|
|
||||||
|
expect(manager.wasEverInitialized()).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
createCameraManager,
|
createCameraManager,
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
createConfig,
|
createConfig,
|
||||||
|
createHASS,
|
||||||
|
createStateEntity,
|
||||||
createStore,
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
flushPromises,
|
flushPromises,
|
||||||
@@ -623,4 +625,92 @@ describe('TriggersManager', () => {
|
|||||||
expect(manager.isTriggered()).toBeTruthy();
|
expect(manager.isTriggered()).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should handle initial camera triggers', () => {
|
||||||
|
it('should not trigger if no cameras have trigger entities', async () => {
|
||||||
|
const api = createTriggerAPI();
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
config: createCameraConfig({
|
||||||
|
triggers: {
|
||||||
|
entities: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
const result = await manager.handleInitialCameraTriggers();
|
||||||
|
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
expect(manager.isTriggered()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not trigger if no cameras have trigger state', async () => {
|
||||||
|
const api = createTriggerAPI();
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
config: createCameraConfig({
|
||||||
|
triggers: {
|
||||||
|
entities: ['binary_sensor.motion', 'binary_sensor.occupancy'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hass = createHASS({
|
||||||
|
'binary_sensor.motion': createStateEntity({
|
||||||
|
state: 'off',
|
||||||
|
}),
|
||||||
|
'binary_sensor.occupancy': createStateEntity({
|
||||||
|
state: 'off',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
const result = await manager.handleInitialCameraTriggers();
|
||||||
|
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
expect(manager.isTriggered()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger if cameras are triggered', async () => {
|
||||||
|
const api = createTriggerAPI();
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
config: createCameraConfig({
|
||||||
|
triggers: {
|
||||||
|
entities: ['binary_sensor.motion', 'binary_sensor.occupancy'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hass = createHASS({
|
||||||
|
'binary_sensor.motion': createStateEntity({
|
||||||
|
state: 'off',
|
||||||
|
}),
|
||||||
|
'binary_sensor.occupancy': createStateEntity({
|
||||||
|
state: 'on',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
const result = await manager.handleInitialCameraTriggers();
|
||||||
|
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(manager.isTriggered()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user