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
|
||||
behavior can be controlled by the `interaction_mode` parameter.
|
||||
|
||||
Triggers based on Home Assistant entities require state _changes_ -- when the
|
||||
card is first started, it takes an active change in state to trigger (i.e. an
|
||||
already occupied room will not trigger, but a newly occupied room will).
|
||||
If the card starts when a trigger entity is already in a triggered state, the
|
||||
action will be taken on card startup.
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -10,6 +10,10 @@ export enum InitializationAspect {
|
||||
CAMERAS = 'cameras',
|
||||
MICROPHONE_CONNECT = 'microphone-connect',
|
||||
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.VIEW,
|
||||
InitializationAspect.INITIAL_TRIGGER,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -131,6 +136,21 @@ export class InitializationManager {
|
||||
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;
|
||||
|
||||
// When the card is initialized, both the initialization state (will never
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { orderBy, throttle } from 'lodash-es';
|
||||
import { CameraEvent } from '../camera-manager/types';
|
||||
import { isTriggeredState } from '../ha/is-triggered-state';
|
||||
import { Timer } from '../utils/timer';
|
||||
import { CardTriggersAPI } from './types';
|
||||
|
||||
@@ -34,6 +35,31 @@ export class TriggersManager {
|
||||
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> {
|
||||
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
||||
const selectedCameraID = this._api.getViewManager().getView()?.camera;
|
||||
|
||||
@@ -199,6 +199,7 @@ export interface CardInitializerAPI {
|
||||
getMessageManager(): MessageManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
getResolvedMediaCache(): ResolvedMediaCache;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
@@ -272,6 +273,7 @@ export interface CardTriggersAPI {
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getInteractionManager(): InteractionManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
export const STATES_OFF = ['closed', 'locked', 'off'];
|
||||
export const STATES_ON = ['open', 'unlocked', 'on'];
|
||||
export const STATES_OFF = ['off', 'closed', 'locked'];
|
||||
export const STATES_ON = ['on', 'open', 'unlocked'];
|
||||
|
||||
@@ -123,6 +123,7 @@ describe('InitializationManager', () => {
|
||||
expect(manager.isInitialized(InitializationAspect.CAMERAS)).toBeTruthy();
|
||||
expect(manager.isInitialized(InitializationAspect.MICROPHONE_CONNECT)).toBeFalsy();
|
||||
expect(manager.isInitialized(InitializationAspect.VIEW)).toBeTruthy();
|
||||
expect(manager.isInitialized(InitializationAspect.INITIAL_TRIGGER)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('successfully with microphone if configured', async () => {
|
||||
@@ -174,6 +175,8 @@ describe('InitializationManager', () => {
|
||||
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
||||
|
||||
await manager.initializeMandatory();
|
||||
|
||||
expect(manager.wasEverInitialized()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('with cameras in progress', async () => {
|
||||
@@ -188,6 +191,27 @@ describe('InitializationManager', () => {
|
||||
.mockResolvedValueOnce(false);
|
||||
|
||||
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,
|
||||
createCardAPI,
|
||||
createConfig,
|
||||
createHASS,
|
||||
createStateEntity,
|
||||
createStore,
|
||||
createView,
|
||||
flushPromises,
|
||||
@@ -623,4 +625,92 @@ describe('TriggersManager', () => {
|
||||
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