207 lines
6.0 KiB
TypeScript
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}`);
|
|
}
|
|
}
|
|
}
|