Files
advanced-camera-card/src/card-controller/issues/state-manager.ts
T

207 lines
6.0 KiB
TypeScript

import type { IssueResolveContext, IssueTriggerContext } from 'issue';
import { summarizeNotification } from '../../components-lib/notification/summarize';
import type { ConditionState } from '../../condition-trigger/conditions/types';
import type { Notification } from '../../config/schema/actions/types';
import type { HomeAssistant } from '../../ha/types';
import { errorToConsole, isTruthy } from '../../utils/basic';
import type {
Issue,
IssueDescription,
IssueKey,
IssuePresence,
IssueReadOnlyState,
IssueResolveContextKey,
IssueTriggerContextKey,
KeyedIssueDescription,
} from './types';
export class IssueStateManager implements IssueReadOnlyState {
private _issues = new Map<IssueKey, Issue>();
private _loggedKeys = new Set<IssueKey>();
// =========================================================================
// Setup.
// =========================================================================
public addIssue(issue: Issue): void {
this._issues.set(issue.key, issue);
}
// =========================================================================
// Detection -- static (one-shot on init), dynamic (on every state change) and
// explicit (triggered or resolved by a component).
// =========================================================================
public async detectStatic(hass: HomeAssistant): Promise<void> {
for (const issue of this._issues.values()) {
try {
await issue.detectStatic?.(hass);
} catch (e) {
// Isolate one issue's detection failure so it cannot abort detection
// for the rest; log so the cause is visible.
errorToConsole(e);
}
this._logIfNew(issue);
}
}
public trigger<K extends IssueTriggerContextKey>(
key: K,
context: IssueTriggerContext[K],
): void {
const issue = this._issues.get(key);
if (!issue) {
return;
}
issue.trigger?.(context);
this._logIfNew(issue);
}
public resolve<K extends IssueResolveContextKey>(
key: K,
context: IssueResolveContext[K],
): void {
const issue = this._issues.get(key);
if (!issue) {
return;
}
issue.resolve?.(context);
this._logIfNew(issue);
}
public detectDynamic(context: ConditionState): void {
for (const issue of this._issues.values()) {
issue.detectDynamic?.(context);
this._logIfNew(issue);
}
}
// =========================================================================
// Queries -- read active issue state.
// =========================================================================
public getFullCardIssue(): IssueDescription | null {
for (const issue of this._issues.values()) {
if (issue.hasIssue() && issue.isFullCardIssue?.()) {
return issue.getIssue();
}
}
return null;
}
public hasFullCardIssue(): boolean {
return !!this.getFullCardIssue();
}
public getIssueDescriptions(): KeyedIssueDescription[] {
const descriptions: KeyedIssueDescription[] = [];
for (const issue of this._issues.values()) {
const description = issue.getIssue();
if (description) {
descriptions.push({ key: issue.key, issue: description });
}
}
return descriptions;
}
public getIssuePresence(): IssuePresence {
const presence: IssuePresence = new Map();
for (const issue of this._issues.values()) {
const description = issue.getIssue();
if (description) {
presence.set(issue.key, description);
}
}
return presence;
}
public getNotification(key: IssueKey): Notification | null {
return this._issues.get(key)?.getNotification?.() ?? null;
}
// =========================================================================
// Retry.
// =========================================================================
public needsRetry(): boolean {
return [...this._issues.values()].some((issue) => issue.needsRetry?.());
}
public canRetryNow(): boolean {
return [...this._issues.values()].some((issue) => this._canRetryNow(issue));
}
public retry(key?: IssueKey, force?: boolean): void {
const issues = key
? [this._issues.get(key)].filter(isTruthy)
: [...this._issues.values()];
for (const issue of issues) {
if (!force && !this._canRetryNow(issue)) {
continue;
}
if (issue.retry?.(force)) {
return;
}
}
}
// =========================================================================
// Lifecycle.
// =========================================================================
public reset(key?: IssueKey): void {
const issues = key
? [this._issues.get(key)].filter(isTruthy)
: [...this._issues.values()];
for (const issue of issues) {
issue.reset?.();
}
}
public suspend(): void {
for (const issue of this._issues.values()) {
issue.suspend?.();
}
}
public destroy(): void {
for (const issue of this._issues.values()) {
issue.destroy?.();
}
this.reset();
this._issues.clear();
this._loggedKeys.clear();
}
// =========================================================================
// Private helpers.
// =========================================================================
// An issue that does not implement canRetryNow falls back to needsRetry, so
// issues with no in-flight concept can be retried whenever they need one.
private _canRetryNow(issue: Issue): boolean {
return issue.canRetryNow?.() ?? issue.needsRetry?.() ?? false;
}
private _logIfNew(issue: Issue): void {
const description = issue.getIssue();
if (!description) {
// Issue cleared (explicit reset, or self-clear via detectDynamic).
// Release the dedupe so the next activation is logged as a new episode.
this._loggedKeys.delete(issue.key);
return;
}
if (this._loggedKeys.has(issue.key)) {
return;
}
this._loggedKeys.add(issue.key);
const summary = summarizeNotification(description.notification);
if (summary) {
console.warn(`Advanced Camera Card [issue=${issue.key}]: ${summary}`);
}
}
}