fix: Clear the initialized condition state when the card is not usable (#2646)

The `initialized` state (used in conditions/triggers) was written once
and never cleared, so it meant "has this card ever been initialized"
while everything reading it took it as "is this card usable now". Home
Assistant takes a card off the page and puts it back whenever its
dashboard tab is left and returned to, so the card initialized again
while the state claimed it was initialized throughout: `trigger:
initialized` fired once per card rather than once per startup, and
automations were dropped in between.

The card lifecycle is now an explicit state machine (`SessionManager`),
the only writer of `initialized`, which separates a card that is
starting up from one initializing part of itself again while it runs. A
new `ever` parameter (conditions/triggers) selects the old latched
behaviour.

`remote_control` uses that parameter to keep its two camera priorities
correct under repeated starts. With `camera_priority: entity` the card
now re-reads the entity every time it starts, so a camera selected while
the card was away is picked up on return. With `camera_priority: card`
the card writes the entity on its first start only, unchanged, since
repeating that write would overwrite a camera the user had selected.

Closes: #2642


BREAKING CHANGE: `condition: initialized` is now `false` whenever the
card is not usable, and `trigger: initialized` fires each time the card
starts up rather than only the first time. Set `ever: true` on either to
keep the previous behaviour.
This commit is contained in:
Dermot Duffy
2026-08-02 12:52:58 -07:00
committed by GitHub
parent e590f72783
commit 17146c8ca6
44 changed files with 1967 additions and 654 deletions
+2 -2
View File
@@ -56,7 +56,7 @@ export class AutomationsManager {
// automation once initialization completes so that the trigger evaluators
// baseline their initial pre-trigger value against a card whose template
// renderer has loaded.
if (this._api.getInitializationManager().isInitializedMandatory()) {
if (this._api.getInitializationManager().areMandatoryAspectsInitialized()) {
triggers.subscribe();
}
}
@@ -83,7 +83,7 @@ export class AutomationsManager {
// Never execute automations if the card hasn't finished initializing, as
// it could cause a view change when camera loads are not finished.
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1407
!this._api.getInitializationManager().isInitializedMandatory() ||
!this._api.getInitializationManager().areMandatoryAspectsInitialized() ||
// Never execute automations if there's an error (as our automation loop
// avoidance -- which shows as an error -- would not work!).
this._api.getIssueManager().getStateManager().hasFullCardIssue()
+9 -5
View File
@@ -8,7 +8,7 @@ import { isAncestorInEventPath } from '../utils/event-ancestor';
import type { CardMediaReviewEventTarget } from '../utils/review';
import type { ViewItem } from '../view/item';
import type { ActionExecutionRequestEventTarget } from './actions/utils/execution-request';
import { InitializationAspect } from './initialization-manager';
import { InitializationAspect } from './initialization/initialization-manager';
import type { CardElementAPI } from './types';
export type ScrollCallback = () => void;
@@ -201,10 +201,14 @@ export class CardElementManager {
this._api.getCameraTriggersManager().reset();
this._api.getCallManager().uninitialize();
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
this._api
.getInitializationManager()
.uninitialize(InitializationAspect.INITIAL_TRIGGER);
// The view is deliberately left initialized, so the user returns to what
// they left. Leaving the page ends the card's initialization session.
const initializationManager = this._api.getInitializationManager();
initializationManager.invalidateAspect(InitializationAspect.CAMERAS);
initializationManager.invalidateAspect(InitializationAspect.INITIAL_TRIGGER);
initializationManager.getSessionManager().end();
void this._api.getCameraManager().destroy();
this._element.removeEventListener(
+7 -5
View File
@@ -15,7 +15,7 @@ import { computeDomain } from '../../ha/compute-domain.js';
import type { HomeAssistant } from '../../ha/types.js';
import { localize } from '../../localize/localize.js';
import { getParseError } from '../../utils/zod/parse-errors.js';
import { InitializationAspect } from '../initialization-manager.js';
import { InitializationAspect } from '../initialization/initialization-manager.js';
import { TemplateManager } from '../templates';
import type { CardConfigAPI } from '../types.js';
import { ConfigParseError } from './error.js';
@@ -152,7 +152,7 @@ export class ConfigManager {
});
this._api.getMediaLoadedInfoManager().clear();
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
this._api.getInitializationManager().invalidateAspect(InitializationAspect.VIEW);
this._api.getViewManager().reset();
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
@@ -234,7 +234,9 @@ export class ConfigManager {
runIfChanged(
(config) => [config.cameras, config.cameras_global],
() => {
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
this._api
.getInitializationManager()
.invalidateAspect(InitializationAspect.CAMERAS);
void this._api.getCameraManager().destroy();
},
true,
@@ -244,7 +246,7 @@ export class ConfigManager {
() => {
this._api
.getInitializationManager()
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
.invalidateAspect(InitializationAspect.MICROPHONE_CONNECT);
},
true,
);
@@ -267,7 +269,7 @@ export class ConfigManager {
// InitializationManager.
if (
this._overriddenConfig &&
this._api.getInitializationManager().isInitializedMandatory()
this._api.getInitializationManager().areMandatoryAspectsInitialized()
) {
this._api.getConditionStateManager().setState({
config: this._overriddenConfig,
@@ -71,6 +71,21 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
triggers: [
{
trigger: 'initialized' as const,
// A card initializes more than once -- on return to a dashboard tab,
// and after Home Assistant restarts -- and the two priorities want
// different answers to whether this should run again each time.
//
// `entity` reads the entity and changes only what the card displays.
// Repeating it is safe (it does nothing when the two already agree)
// and is the only way to pick up an entity change made while the card
// was unable to act on it.
//
// `card` writes the entity. Repeating it is not safe: a card that has
// just re-initialized is showing a freshly-defaulted view, not
// anything the user chose, so a second run would overwrite a
// selection the card never authored.
ever: cameraPriority === 'card',
},
],
actions: [
+1 -1
View File
@@ -26,7 +26,7 @@ import { ExpandManager } from './expand-manager';
import { FoldersManager } from './folders/manager';
import { FullscreenManager } from './fullscreen/fullscreen-manager';
import { HASSManager } from './hass/hass-manager';
import { InitializationManager } from './initialization-manager';
import { InitializationManager } from './initialization/initialization-manager';
import { InteractionManager } from './interaction-manager';
import { createIssueManager } from './issues/factory';
import type { IssueManager } from './issues/issue-manager';
+26 -13
View File
@@ -3,7 +3,7 @@ import type { HASSListener } from '../../ha/source';
import type { HomeAssistant } from '../../ha/types';
import type { UnsubscribeCallback } from '../../types';
import { log } from '../../utils/debug';
import { InitializationAspect } from '../initialization-manager';
import { InitializationAspect } from '../initialization/initialization-manager';
import type { CardHASSAPI } from '../types';
import { EventWatcher, type EventWatcherSubscriptionInterface } from './event-watcher';
import { StateWatcher, type StateWatcherSubscriptionInterface } from './state-watcher';
@@ -48,12 +48,27 @@ export class HASSManager implements HASSManagerReadonlyInterface {
}
public setHASS(hass?: HomeAssistant | null): void {
// No hass at all is an absence of news rather than a change, so nothing
// below it runs and `_hass` keeps whatever it last held.
if (!hass) {
return;
}
const wasReady = !!this._hass && isHassReady(this._hass);
const isReady = isHassReady(hass);
// A card cannot be started without Home Assistant, so losing it ends the
// card's initialization session. The aspects initialized during that
// session are left in place until it returns, when they are initialized
// again against whatever entities it comes back with.
if (wasReady && !isReady) {
this._api.getInitializationManager().getSessionManager().end();
}
// When HA goes from "not ready" to "ready" (WebSocket reconnected AND all
// integrations finished loading), rebuild cameras and the view from
// scratch: the available entities may have changed while it was down.
const becameReady = !!this._hass && !isHassReady(this._hass) && isHassReady(hass);
if (becameReady) {
if (!!this._hass && !wasReady && isReady) {
// Tear cameras down before the listeners below see the new hass,
// otherwise they would briefly rebuild against the old entities.
log(
@@ -61,16 +76,14 @@ export class HASSManager implements HASSManagerReadonlyInterface {
'Advanced Camera Card: HA fully ready, reinitializing...',
);
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
void this._api.getCameraManager().destroy();
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
this._api
.getInitializationManager()
.uninitialize(InitializationAspect.INITIAL_TRIGGER);
}
// The entities may differ from those the cameras and the view were
// initialized against, so both are initialized again.
const initializationManager = this._api.getInitializationManager();
initializationManager.invalidateAspect(InitializationAspect.CAMERAS);
initializationManager.invalidateAspect(InitializationAspect.VIEW);
initializationManager.invalidateAspect(InitializationAspect.INITIAL_TRIGGER);
if (!hass) {
return;
void this._api.getCameraManager().destroy();
}
const oldHass = this._hass;
@@ -1,12 +1,12 @@
import { STATE_RUNNING } from 'home-assistant-js-websocket';
import PQueue from 'p-queue';
import { isHassReady } from '../ha/is-hass-ready';
import { sideLoadHomeAssistantElements } from '../ha/side-load-ha-elements';
import { loadLanguages } from '../localize/localize';
import { errorToConsole } from '../utils/basic';
import { Initializer } from '../utils/initializer/initializer';
import type { CardInitializerAPI } from './types';
import { isHassReady } from '../../ha/is-hass-ready';
import { sideLoadHomeAssistantElements } from '../../ha/side-load-ha-elements';
import { loadLanguages } from '../../localize/localize';
import { errorToConsole } from '../../utils/basic';
import { Initializer } from '../../utils/initializer/initializer';
import type { CardInitializerAPI } from '../types';
import { SessionManager } from './session-manager';
export enum InitializationAspect {
LANGUAGES = 'languages',
@@ -40,22 +40,30 @@ export class InitializationManager {
// initialization" (above) are followed.
private _initializationQueue = new PQueue({ concurrency: 1 });
private _initializer: Initializer;
private _everInitialized = false;
constructor(api: CardInitializerAPI, initializer?: Initializer) {
// Tracks an "initialization session" (the "useful" card time between full
// readiness -> disconnection of various kinds).
private _sessionManager: SessionManager;
constructor(
api: CardInitializerAPI,
initializer?: Initializer,
sessionManager?: SessionManager,
) {
this._api = api;
this._initializer = initializer ?? new Initializer();
this._sessionManager = sessionManager ?? new SessionManager(api);
}
public wasEverInitialized(): boolean {
return this._everInitialized;
public getSessionManager(): SessionManager {
return this._sessionManager;
}
public isInitialized(aspect: InitializationAspect): boolean {
return this._initializer.isInitialized(aspect);
}
public isInitializedMandatory(): boolean {
public areMandatoryAspectsInitialized(): boolean {
const config = this._api.getConfigManager().getConfig();
if (!config) {
return false;
@@ -77,10 +85,16 @@ export class InitializationManager {
}
// The one place that decides whether to (re)start mandatory initialization,
// so callers don't re-check the conditions themselves. Called on every render
// so callers don't check the conditions themselves. Called on every render
// (from the card's shouldUpdate) and whenever hass changes (from
// HASSManager); a reconnect or a cleared issue reaches it by causing a
// render.
//
// The check here is only to keep cost down: a card that has finished
// initializing re-renders often, and without it each of those renders would
// queue an attempt that does nothing. `_initializeMandatory()` checks the
// same conditions again when it actually runs, and that is the one that
// matters for correctness.
public triggerInitialization(): void {
if (!this._shouldInitializeMandatory()) {
return;
@@ -93,7 +107,7 @@ export class InitializationManager {
this._api.getConfigManager().hasConfig() &&
this._api.getCardElementManager().isConnected() &&
isHassReady(this._api.getHASSManager().getHASS()) &&
!this.isInitializedMandatory() &&
!this.areMandatoryAspectsInitialized() &&
// Don't start while a full-card issue (e.g. the "Home Assistant is
// starting" notice) is shown: each initialization step aborts as soon as
// it sees one, so an attempt now would be wasted. The card tries again
@@ -112,21 +126,24 @@ export class InitializationManager {
private async _initializeMandatory(): Promise<void> {
const hass = this._api.getHASSManager().getHASS();
if (!hass || this.isInitializedMandatory()) {
// The authoritative check, made when the attempt actually runs rather than
// when it was queued: an attempt can sit in the queue behind another one,
// and the card may be detached, lose Home Assistant, or finish initializing
// while it waits. This is what stops a stale attempt running.
//
// The `isHassReady` call also narrows `hass` for the steps below. Its
// RUNNING requirement waits out a Home Assistant that is still loading
// integrations, against which integration-specific WS calls fail with
// "Unknown command".
if (!isHassReady(hass) || !this._shouldInitializeMandatory()) {
return;
}
// Wait until HA has finished loading integrations before attempting init.
// Otherwise integration-specific WS calls (e.g. Frigate event
// subscriptions) fail with "Unknown command" against a half-loaded HA. The
// HASSManager will trigger another init attempt as soon as
// hass.config.state transitions to RUNNING.
if (hass.config?.state !== STATE_RUNNING) {
return;
}
const token = this._sessionManager.startInitialization();
if (
!(await this._tryInitialize(() =>
!(await this._runStep(token, () =>
this._initializer.initializeMultipleIfNecessary({
// Caution: Ensure nothing in this set of initializers requires
// config or languages since they will not yet have been initialized.
@@ -142,13 +159,17 @@ export class InitializationManager {
return;
}
const config = this._api.getConfigManager().getConfig();
if (!config) {
// The configuration may have vanished during the await above. The CAMERAS
// initializer returns void and quietly does nothing without a
// configuration, which would mark the aspect initialized against nothing
// -- so stop before it runs.
if (!this._api.getConfigManager().hasConfig()) {
this._sessionManager.reportInitializationDeclined(token);
return;
}
if (
!(await this._tryInitialize(() =>
!(await this._runStep(token, () =>
this._initializer.initializeMultipleIfNecessary({
[InitializationAspect.CAMERAS]: async () => {
// Recreate the camera manager to guarantee an immediate re-render.
@@ -185,7 +206,7 @@ export class InitializationManager {
}
if (
!(await this._tryInitialize(() =>
!(await this._runStep(token, () =>
this._initializer.initializeIfNecessary(
InitializationAspect.VIEW,
this._api.getViewManager().initialize,
@@ -196,7 +217,7 @@ export class InitializationManager {
}
if (
!(await this._tryInitialize(() =>
!(await this._runStep(token, () =>
this._initializer.initializeIfNecessary(
InitializationAspect.INITIAL_TRIGGER,
async () => {
@@ -211,57 +232,74 @@ export class InitializationManager {
return;
}
this._everInitialized = true;
// The config is read here, at the last moment, so the card is never
// reported as started against a configuration that a change during the
// awaits above has already replaced. It is written to condition state here,
// rather than by the ConfigManager, to ensure actions (that trigger on
// config change) are not run before hass is available and the card is
// initialized (the first config is set in the card *before* hass is set in
// the card).
const config = this._api.getConfigManager().getConfig();
if (
!config ||
!this._sessionManager.isCurrentInitialization(token) ||
!this.areMandatoryAspectsInitialized()
) {
this._sessionManager.reportInitializationDeclined(token);
return;
}
// Subscribe any automations now: the template renderer (a mandatory
// automation trigger evaluators can baseline pre-trigger (which potentially
// involves rendering templates). This must run before the `setState` below
// so that triggers watching `config`/`initialized` are attached in time to
// involves rendering templates). This must run before the report below so
// that triggers watching `config`/`initialized` are attached in time to
// fire on *that* very change.
this._api.getAutomationsManager().subscribe();
// 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._sessionManager.reportInitializationSucceeded(token, config);
this._api.getCardElementManager().update();
}
private async _tryInitialize(fn: () => Promise<boolean>): Promise<boolean> {
// Run one step of initialization, telling the session manager the outcome and
// returning whether the remaining steps should run.
//
// A step "declines" when it raises no error of its own but yet the
// initialization process should not continue.
private async _runStep(token: number, fn: () => Promise<boolean>): Promise<boolean> {
let initialized = false;
try {
initialized = await fn();
} catch (e: unknown) {
if (e instanceof Error) {
errorToConsole(e);
if (this._sessionManager.isCurrentInitialization(token)) {
if (e instanceof Error) {
errorToConsole(e);
}
this._api.getIssueManager().trigger('initialization', { error: e });
this._sessionManager.reportInitializationFailed(token);
}
this._setInitializationIssue(e);
return false;
}
if (this._api.getIssueManager().getStateManager().hasFullCardIssue()) {
if (
!initialized ||
this._api.getIssueManager().getStateManager().hasFullCardIssue()
) {
this._sessionManager.reportInitializationDeclined(token);
return false;
}
return initialized;
return true;
}
private _setInitializationIssue(error: unknown): void {
this._api.getIssueManager().trigger('initialization', { error });
}
public uninitialize(aspect: InitializationAspect): void {
public invalidateAspect(aspect: InitializationAspect): void {
this._initializer.uninitialize(aspect);
}
public uninitializeMandatory(): void {
// Mark every mandatory aspect uninitialized, so a fresh initialization starts
// from nothing. Which aspects those are is this class's own question -- see
// `areMandatoryAspectsInitialized()`.
public invalidateMandatoryAspects(): void {
for (const aspect of [
InitializationAspect.CAMERAS,
InitializationAspect.MICROPHONE_CONNECT,
@@ -0,0 +1,172 @@
import type { AdvancedCameraCardConfig } from '../../config/schema/types';
import { Generation } from '../../utils/concurrency/generation';
import type { CardSessionAPI } from '../types';
// ============================================================================
// The card runs in "initialization sessions". A session begins when the card is
// attached to the page with a ready Home Assistant, and ends when the card is
// detached, when Home Assistant stops being ready, or when initialization
// fails. Initializing an individual aspect again (e.g. because the
// configuration changed) happens *within* a session and does not end it, akin
// to a running application staying started while it reconnects one subsystem.
//
// A session therefore involves one or more *initialization runs*: the first
// starts the card, and each later one initializes whichever aspects were
// invalidated while the session carried on. A run ends in one of three ways:
//
// - it *succeeds*, and the card is started;
// - it *fails*, meaning a step threw an error, and the "Issue" manager takes
// over to display and handle the issue.
// - it *declines*, meaning it stopped early without an error of its own -- a
// step could not complete yet, or something else had already gone wrong.
//
// Whichever way a run ends is its outcome, reported back here with the number
// the run was given when it started.
//
// This class is the session lifecycle as a state machine, and is the only
// writer of the `initialized` and `everInitialized` condition state -- what
// users write conditions and triggers against. Whether every mandatory aspect
// is initialized right now is a separate question owned by the
// InitializationManager, and the two deliberately disagree while an aspect is
// being initialized again mid-session.
//
// There is no "failed" state: a card blocked after a failed run is IDLE with a
// full-card issue showing, and the issue system is the authority on that.
// ============================================================================
export enum SessionState {
// No initialized session: the card is detached, Home Assistant is not ready,
// or a run declined or failed and nothing has started another yet.
IDLE = 'idle',
// A session has started and its first run is under way; nothing written to
// the condition state yet.
INITIALIZING = 'initializing',
// The card has started: `initialized` is true. Later runs that initialize an
// aspect again may happen without leaving this state.
RUNNING = 'running',
}
export class SessionManager {
private _api: CardSessionAPI;
private _state = SessionState.IDLE;
private _everInitialized = false;
// Numbers the "initialization run" currently in progress (not the session,
// which may contain several runs). The number changes when a session ends and
// when a run reports its outcome, so an outcome from a run that something has
// since replaced is ignored, and so is a second outcome from the same run.
private _generation = new Generation();
constructor(api: CardSessionAPI) {
this._api = api;
}
public getState(): SessionState {
return this._state;
}
public wasEverInitialized(): boolean {
return this._everInitialized;
}
// Start an "initialization run" and return the number identifying it, which
// must be handed back with its outcome. From IDLE this is the session's first
// run; from RUNNING it initializes an aspect again mid-session, and the state
// (with the published `initialized`) stays as it is.
public startInitialization(): number {
if (this._state === SessionState.IDLE) {
this._state = SessionState.INITIALIZING;
}
return this._generation.next();
}
public isCurrentInitialization(token: number): boolean {
return this._generation.isCurrent(token);
}
// The run completed, so the card is now started. The config is written in the
// same change as the session state, so a trigger watching either sees one
// consistent state. When an aspect was initialized again mid-session
// `initialized` and
// `everInitialized` already hold these values, so the change carries only the
// config -- which is what stops an `initialized` trigger firing again in the
// middle of a session.
public reportInitializationSucceeded(
token: number,
config: AdvancedCameraCardConfig,
): void {
if (!this._acceptOutcome(token)) {
return;
}
// Set before the condition state is written, so anything that reads this
// class while handling that change sees the new state.
this._state = SessionState.RUNNING;
this._everInitialized = true;
this._api.getConditionStateManager().setState({
config,
initialized: true,
everInitialized: true,
});
}
// The run stopped early without producing an error of its own: either a step
// could not complete yet (e.g. the view declining while the cameras are being
// initialized), or a full-card issue raised elsewhere makes continuing
// pointless.
//
// Nothing is written -- a card that had not started has nothing to take back,
// and one that had is still started.
public reportInitializationDeclined(token: number): void {
if (!this._acceptOutcome(token)) {
return;
}
if (this._state === SessionState.INITIALIZING) {
this._state = SessionState.IDLE;
}
}
// The run threw an error which ends the session: a card missing a mandatory
// aspect is not started. The initialization issue was already raised (by the
// Issue Manager).
public reportInitializationFailed(token: number): void {
if (!this._acceptOutcome(token)) {
return;
}
this._toIdle();
}
// The session is over -- the card left the page or Home Assistant went away.
// Aspects are left as they are: a caller that knows which of them the next
// session must initialize again invalidates those itself.
public end(): void {
this._generation.invalidate();
this._toIdle();
}
private _toIdle(): void {
const wasRunning = this._state === SessionState.RUNNING;
this._state = SessionState.IDLE;
// `initialized: false` is written only on a card that had previously
// started.
if (wasRunning) {
this._api.getConditionStateManager().setState({ initialized: false });
}
}
// Whether an outcome should be acted on: it must come from the run currently
// in progress, and only the first outcome from that run counts. Invalidating
// the generation number here is what makes a second run of the same
// initialization do nothing.
private _acceptOutcome(token: number): boolean {
if (!this._generation.isCurrent(token)) {
return false;
}
this._generation.invalidate();
return true;
}
}
+7 -7
View File
@@ -23,7 +23,7 @@ export const RETRY_EXPONENTIAL_MAX_SECONDS = 600;
// Wraps the passive IssueStateManager with reaction logic. A single
// condition-state listener drives everything: it runs one-shot static detection
// when mandatory-init completes (`initialized` transitions to true), then
// when mandatory-init first completes (`everInitialized` becomes true), then
// evaluates dynamic issues on every subsequent state change, schedules retries,
// and updates the card. Full-card issues are rendered by card.ts via
// getStateManager().getFullCardIssue(). Non-full-card issue notifications are
@@ -183,14 +183,14 @@ export class IssueManager {
// Drives both one-shot static detection (on mandatory-init completion) and
// normal re-evaluation (on any condition-state change).
//
// `initialized: true` in the change payload means mandatory initialization
// just finished -- see InitializationManager._initializeMandatory. That's
// also the earliest point at which the full HASS object is guaranteed
// `everInitialized` is set when mandatory initialization first completes --
// see InitializationManager._initializeMandatory -- and is never cleared, so
// this block runs exactly once however many times the card initializes. That
// is also the earliest point at which the full HASS object is guaranteed
// ready for websocket calls (e.g. LegacyResourceIssue's lovelace/resources
// fetch). Because `initialized` is latched (its comment notes it never
// changes again), this block fires exactly once per IssueManager life.
// fetch).
private _onStateChange(change: ConditionStateChange): void {
if (change.change.initialized === true && change.new.hass) {
if (change.change.everInitialized === true && change.new.hass) {
void this._stateManager.detectStatic(change.new.hass).then(() => this.evaluate());
}
this.evaluate();
@@ -47,7 +47,7 @@ export class InitializationIssue extends AbstractErrorIssue {
}
public detectDynamic(): void {
if (!this._api.getInitializationManager().isInitializedMandatory()) {
if (!this._api.getInitializationManager().areMandatoryAspectsInitialized()) {
return;
}
// The success settle: mandatory init completed, so there is no error to show
@@ -88,7 +88,8 @@ export class InitializationIssue extends AbstractErrorIssue {
// render cycle. destroy() releases the existing CameraManager's held
// resources (WebSocket subscriptions, listeners) before the CAMERAS
// init aspect replaces the instance via createCameraManager().
this._api.getInitializationManager().uninitializeMandatory();
this._api.getInitializationManager().invalidateMandatoryAspects();
this._api.getInitializationManager().getSessionManager().end();
void this._api.getCameraManager().destroy();
return false;
}
+5 -1
View File
@@ -17,7 +17,7 @@ import type { ExpandManager } from './expand-manager';
import type { FoldersManager } from './folders/manager';
import type { FullscreenManager } from './fullscreen/fullscreen-manager';
import type { HASSManager } from './hass/hass-manager';
import type { InitializationManager } from './initialization-manager';
import type { InitializationManager } from './initialization/initialization-manager';
import type { InteractionManager } from './interaction-manager';
import type { IssueManager } from './issues/issue-manager';
import type { KeyboardStateManager } from './keyboard-state-manager';
@@ -307,6 +307,10 @@ export interface CardQueryStringAPI {
getViewManager(): ViewManager;
}
export interface CardSessionAPI {
getConditionStateManager(): ConditionStateManager;
}
export interface CardStatusBarAPI {
getCardElementManager(): CardElementManager;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { log } from '../../utils/debug';
import { getStreamCameraID } from '../../view/substream';
import { getViewTargetID } from '../../view/target-id';
import type { View } from '../../view/view';
import { InitializationAspect } from '../initialization-manager';
import { InitializationAspect } from '../initialization/initialization-manager';
import type { CardViewAPI } from '../types';
import { ViewFactory } from './factory';
import { applyViewModifiers } from './modifiers';
+3 -2
View File
@@ -360,7 +360,7 @@ export class AdvancedCameraCard extends LitElement {
}
protected updated(): void {
if (this._controller.getInitializationManager().isInitializedMandatory()) {
if (this._controller.getInitializationManager().areMandatoryAspectsInitialized()) {
void this._controller.getQueryStringManager().executeIfNecessary();
}
}
@@ -461,6 +461,7 @@ export class AdvancedCameraCard extends LitElement {
? html`<advanced-camera-card-loading
.loaded=${this._controller
.getInitializationManager()
.getSessionManager()
.wasEverInitialized()}
.effectsManager=${this._config?.performance?.features
.card_loading_effects !== false
@@ -503,7 +504,7 @@ export class AdvancedCameraCard extends LitElement {
</div>
${this._renderMenuStatusContainer('bottom')}
${this._config?.elements &&
this._controller.getInitializationManager().isInitializedMandatory()
this._controller.getInitializationManager().areMandatoryAspectsInitialized()
? // Elements need to render after the main views so it can render 'on
// top'. They are held until the card is initialized: the template
// renderer loads lazily as a mandatory init aspect (when the
@@ -1,8 +1,19 @@
import type { InitializedBase } from '../../../config/schema/condition-trigger/common/initialized';
import type { ConditionsEvaluationResult, ConditionState } from '../types';
import type { ConditionEvaluator } from './types';
export class InitializedConditionEvaluator implements ConditionEvaluator {
private _condition: InitializedBase;
constructor(condition: InitializedBase) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return { result: !!newState?.initialized };
return {
result: !!(this._condition.ever
? newState?.everInitialized
: newState?.initialized),
};
}
}
+3 -1
View File
@@ -62,7 +62,7 @@ export const createConditionEvaluator = (
case 'user_agent':
return new UserAgentConditionEvaluator(condition);
case 'initialized':
return new InitializedConditionEvaluator();
return new InitializedConditionEvaluator(condition);
case 'template':
return new TemplateConditionEvaluator(condition, context);
case 'or':
@@ -123,6 +123,8 @@ export const createConditionEvaluatorForTrigger = (
return new ExpandConditionEvaluator(trigger);
case 'fullscreen':
return new FullscreenConditionEvaluator(trigger);
case 'initialized':
return new InitializedConditionEvaluator(trigger);
case 'interaction':
return new InteractionConditionEvaluator(trigger);
case 'key':
+6 -1
View File
@@ -26,7 +26,6 @@ export interface ConditionState {
displayMode?: ViewDisplayMode;
expand?: boolean;
fullscreen?: boolean;
initialized?: boolean;
interaction?: boolean;
keys?: KeysState;
mediaLoadedInfo?: MediaLoadedInfo | null;
@@ -34,6 +33,12 @@ export interface ConditionState {
panel?: boolean;
hass?: HomeAssistant;
// Initialization:
// - Currently initialized.
initialized?: boolean;
// - Ever initialized.
everInitialized?: boolean;
// Generic media target identifier. See @view/target-id for details.
targetID?: string;
triggered?: Set<string>;
@@ -6,6 +6,6 @@ export class InitializedTrigger extends ConditionStateTriggerBase<
TriggerOfType<'initialized'>
> {
protected _getValue(state: ConditionState): unknown {
return state.initialized;
return this._trigger.ever ? state.everInitialized : state.initialized;
}
}
@@ -1,3 +1,13 @@
import { z } from 'zod';
export const initializedBaseSchema = z.object({});
export const initializedBaseSchema = z.object({
// Matches the card having ever been initialized, rather than it being
// initialized right now.
//
// Defaulted rather than optional so the trigger always carries a value. A
// valueless trigger fires on any change of what it watches, which here would
// include the card becoming uninitialized -- and an action fired then would
// run against a card that has just been torn down.
ever: z.boolean().default(false),
});
export type InitializedBase = z.infer<typeof initializedBaseSchema>;