feat: Add support for config and initialized conditions (#1894)

These conditions are probably not hugely useful for most card users, but
allows the inbuilt automation systems to be used for card internal
operations.

[skip ci]
This commit is contained in:
Dermot Duffy authored and GitHub committed 2025-02-16 14:17:04 -08:00
1 parent 7fb710d45d
commit da79664a67
10 files changed
+313 -35

No files matched your search

@@ -184,6 +184,18 @@ export class ConfigManager {
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
await this._api.getMediaPlayerManager().initializeIfNecessary(previousConfig);
// The config is only set in the state if the card is already fully
// initialized. If not, the config will be set post initialization in the
// InitializationManager.
if (
this._overriddenConfig &&
this._api.getInitializationManager().isInitializedMandatory()
) {
this._api.getConditionStateManager().setState({
config: this._overriddenConfig,
});
}
this._api.getCardElementManager().update();
}
}
@@ -128,6 +128,18 @@ export class InitializationManager {
}
this._everInitialized = true;
// When the card is initialized, both the initialization state (will never
// change again), and the config are set in the condition state. The
// config is set here, rather than in the ConfigManager, in order to
// ensure actions (that trigger on config change) are not run before hass
// is available and the card is initialzied (the first config is set in
// the card *before* hass is set in the card).
this._api.getConditionStateManager().setState({
config: config,
initialized: this._everInitialized,
});
this._api.getCardElementManager().update();
}
+1
View File
@@ -176,6 +176,7 @@ export interface CardInitializerAPI {
getMicrophoneManager(): MicrophoneManager;
getCardElementManager(): CardElementManager;
getConditionStateManager(): ConditionStateManager;
getConfigManager(): ConfigManager;
getDefaultManager(): DefaultManager;
getEntityRegistryManager(): EntityRegistryManager;
+30 -2
View File
@@ -1,4 +1,5 @@
import { isEqual } from 'lodash-es';
import { getConfigValue } from '../config/management';
import { AdvancedCameraCardCondition } from '../config/types';
import { isCompanionApp } from '../utils/companion';
import {
@@ -146,8 +147,12 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
return {
result:
!!newState?.state &&
((!condition.state && !condition.state_not) ||
(condition.entity in newState.state &&
((!condition.state &&
!condition.state_not &&
newState.state[condition.entity]?.state !==
oldState?.state?.[condition.entity]?.state) ||
((!!condition.state || !!condition.state_not) &&
condition.entity in newState.state &&
(!condition.state ||
(Array.isArray(condition.state)
? condition.state.includes(newState.state[condition.entity].state)
@@ -281,6 +286,29 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
(condition.user_agent_re === undefined ||
new RegExp(condition.user_agent_re).test(newState.userAgent)),
};
case 'config': {
return {
result:
!!newState?.config &&
newState.config !== oldState?.config &&
(!condition.paths?.length ||
condition.paths.some(
(key) =>
getConfigValue(newState.config!, key) !==
(oldState?.config ? getConfigValue(oldState?.config, key) : undefined),
)),
data: {
config: {
...((oldState?.config || newState?.config) && {
...(oldState?.config && { from: oldState?.config }),
...(newState?.config && { to: newState?.config }),
}),
},
},
};
}
case 'initialized':
return { result: !!newState?.initialized };
}
}
}
+9 -1
View File
@@ -1,14 +1,16 @@
import { CurrentUser } from '@dermotduffy/custom-card-helpers';
import { HassEntities } from 'home-assistant-js-websocket';
import { ViewDisplayMode } from '../config/types';
import { AdvancedCameraCardConfig, ViewDisplayMode } from '../config/types';
import { MediaLoadedInfo } from '../types';
import { KeysState, MicrophoneState } from '../card-controller/types';
export interface ConditionState {
camera?: string;
config?: AdvancedCameraCardConfig;
displayMode?: ViewDisplayMode;
expand?: boolean;
fullscreen?: boolean;
initialized?: boolean;
interaction?: boolean;
keys?: KeysState;
mediaLoadedInfo?: MediaLoadedInfo | null;
@@ -43,10 +45,16 @@ interface ConditionsEvaluationDataState extends ConditionsEvaluationDataFromTo {
entity: string;
}
interface ConditionsEvaluationDataConfig {
from?: AdvancedCameraCardConfig;
to?: AdvancedCameraCardConfig;
}
export interface ConditionsEvaluationData {
camera?: ConditionsEvaluationDataFromTo;
view?: ConditionsEvaluationDataFromTo;
state?: ConditionsEvaluationDataState;
config?: ConditionsEvaluationDataConfig;
}
export interface ConditionsEvaluationResult {
result: boolean;
+39 -30
View File
@@ -683,43 +683,33 @@ export type StatusBarItem = z.infer<typeof statusBarItemSchema>;
// Custom Element Configuration: Conditions
// *************************************************************************
const viewConditionSchema = z.object({
condition: z.literal('view'),
views: z.string().array().optional(),
});
const fullscreenConditionSchema = z.object({
condition: z.literal('fullscreen'),
fullscreen: z.boolean(),
});
const expandConditionSchema = z.object({
condition: z.literal('expand'),
expand: z.boolean(),
});
const cameraConditionSchema = z.object({
condition: z.literal('camera'),
cameras: z.string().array().optional(),
});
const mediaLoadedConditionSchema = z.object({
condition: z.literal('media_loaded'),
media_loaded: z.boolean(),
const configConditionSchema = z.object({
condition: z.literal('config'),
paths: z.string().array().optional(),
});
const displayModeConditionSchema = z.object({
condition: z.literal('display_mode'),
display_mode: viewDisplayModeSchema,
});
const triggeredConditionSchema = z.object({
condition: z.literal('triggered'),
triggered: z.string().array(),
const expandConditionSchema = z.object({
condition: z.literal('expand'),
expand: z.boolean(),
});
const fullscreenConditionSchema = z.object({
condition: z.literal('fullscreen'),
fullscreen: z.boolean(),
});
const initializedConditionSchema = z.object({
condition: z.literal('initialized'),
});
const interactionConditionSchema = z.object({
condition: z.literal('interaction'),
interaction: z.boolean(),
});
const microphoneConditionSchema = z.object({
condition: z.literal('microphone'),
connected: z.boolean().optional(),
muted: z.boolean().optional(),
});
const keyConditionSchema = z.object({
condition: z.literal('key'),
key: z.string(),
@@ -729,6 +719,19 @@ const keyConditionSchema = z.object({
alt: z.boolean().optional(),
meta: z.boolean().optional(),
});
const mediaLoadedConditionSchema = z.object({
condition: z.literal('media_loaded'),
media_loaded: z.boolean(),
});
const microphoneConditionSchema = z.object({
condition: z.literal('microphone'),
connected: z.boolean().optional(),
muted: z.boolean().optional(),
});
const triggeredConditionSchema = z.object({
condition: z.literal('triggered'),
triggered: z.string().array(),
});
const userAgentConditionSchema = z.object({
condition: z.literal('user_agent'),
user_agent: z.string().optional(),
@@ -748,26 +751,32 @@ const userAgentConditionSchema = z.object({
.optional(),
companion: z.boolean().optional(),
});
const viewConditionSchema = z.object({
condition: z.literal('view'),
views: z.string().array().optional(),
});
export const advancedCameraCardConditionSchema = z.discriminatedUnion('condition', [
// Stock conditions:
stateConditionSchema,
numericStateConditionSchema,
screenConditionSchema,
stateConditionSchema,
usersConditionSchema,
// Custom conditions:
viewConditionSchema,
fullscreenConditionSchema,
expandConditionSchema,
cameraConditionSchema,
mediaLoadedConditionSchema,
configConditionSchema,
displayModeConditionSchema,
triggeredConditionSchema,
expandConditionSchema,
fullscreenConditionSchema,
initializedConditionSchema,
interactionConditionSchema,
microphoneConditionSchema,
keyConditionSchema,
mediaLoadedConditionSchema,
microphoneConditionSchema,
triggeredConditionSchema,
userAgentConditionSchema,
viewConditionSchema,
]);
export type AdvancedCameraCardCondition = z.infer<
typeof advancedCameraCardConditionSchema