feat: Add hardened error handling and retries (#2451)
- Closes #1830 - Closes #2099
This commit is contained in:
committed by
dermotduffy
parent
4bc787e2b7
commit
47bcce93d3
@@ -0,0 +1,172 @@
|
||||
import type { IssueTriggerContext } from 'issue';
|
||||
import { summarizeNotification } from '../../components-lib/notification/summarize';
|
||||
import { ConditionState } from '../../conditions/types';
|
||||
import { Notification } from '../../config/schema/actions/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { isTruthy } from '../../utils/basic';
|
||||
import {
|
||||
Issue,
|
||||
IssueDescription,
|
||||
IssueKey,
|
||||
IssuePresence,
|
||||
IssueReadOnlyState,
|
||||
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) and dynamic (on every state change).
|
||||
// =========================================================================
|
||||
|
||||
public async detectStatic(hass: HomeAssistant): Promise<void> {
|
||||
for (const issue of this._issues.values()) {
|
||||
await issue.detectStatic?.(hass);
|
||||
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 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 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 && !issue.needsRetry?.()) {
|
||||
continue;
|
||||
}
|
||||
if (issue.retry?.()) {
|
||||
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 {
|
||||
this.reset();
|
||||
this._issues.clear();
|
||||
this._loggedKeys.clear();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Private helpers.
|
||||
// =========================================================================
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user