feat: Add event-based automation triggers (#2537)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent b701366762
commit a31816c168
109 changed files with 4607 additions and 1623 deletions
+9 -4
View File
@@ -4,6 +4,7 @@ import { createIssueManager } from '../../../src/card-controller/issues/factory'
import { IssueManager } from '../../../src/card-controller/issues/issue-manager';
import { ConditionStateManager } from '../../../src/condition-trigger/conditions/state-manager';
import { createCardAPI } from '../../test-utils';
import { createSubscriptionHealth } from '../test-utils';
describe('createIssueManager', () => {
beforeEach(() => {
@@ -15,12 +16,15 @@ describe('createIssueManager', () => {
});
it('should return a IssueManager instance', () => {
const manager = createIssueManager(createCardAPI());
const manager = createIssueManager(createCardAPI(), createSubscriptionHealth());
expect(manager).toBeInstanceOf(IssueManager);
});
it('should register all expected issues', () => {
const manager = createIssueManager(createCardAPI()).getStateManager();
const manager = createIssueManager(
createCardAPI(),
createSubscriptionHealth(),
).getStateManager();
expect(manager.getIssueDescriptions()).toHaveLength(0);
@@ -29,6 +33,7 @@ describe('createIssueManager', () => {
'config_upgrade',
'config_upgrade_failure',
'connection',
'event_subscription',
'initialization',
'legacy_resource',
'media_query',
@@ -51,7 +56,7 @@ describe('createIssueManager', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = createIssueManager(api);
const manager = createIssueManager(api, createSubscriptionHealth());
manager.trigger('media_query', { error: new Error('x') });
manager.trigger('initialization', { error: new Error('x') });
@@ -76,7 +81,7 @@ describe('createIssueManager', () => {
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = createIssueManager(api);
const manager = createIssueManager(api, createSubscriptionHealth());
// Setting view starts the media_load timer (via the condition state
// listener → evaluate → detectDynamic).
@@ -0,0 +1,88 @@
import { describe, expect, it, vi } from 'vitest';
import { EventSubscriptionIssue } from '../../../../src/card-controller/issues/issues/event-subscription';
import { Issue } from '../../../../src/card-controller/issues/types';
import { SubscriptionFailure } from '../../../../src/ha/connection/subscription-health-monitor';
import { localize } from '../../../../src/localize/localize';
import { createSubscriptionHealth } from '../../test-utils';
const createSubscriptionFailure = (key: string): SubscriptionFailure<string> => ({
key,
error: new Error(key),
failureCount: 1,
});
describe('EventSubscriptionIssue', () => {
it('should register the change callback as a health listener', () => {
const health = createSubscriptionHealth();
const changeCallback = vi.fn();
new EventSubscriptionIssue(health, changeCallback);
expect(health.addListener).toBeCalledWith(changeCallback);
});
it('should have no issue when there are no failures', () => {
const health = createSubscriptionHealth();
const issue = new EventSubscriptionIssue(health, vi.fn());
expect(issue.hasIssue()).toBe(false);
expect(issue.getIssue()).toBeNull();
expect(issue.getNotification()).toBeNull();
});
it('should describe the failing event types sorted, at medium severity', () => {
const health = createSubscriptionHealth();
health.getFailures.mockReturnValue([
createSubscriptionFailure('zebra_event'),
createSubscriptionFailure('alpha_event'),
]);
const issue = new EventSubscriptionIssue(health, vi.fn());
expect(issue.hasIssue()).toBe(true);
const description = issue.getIssue();
expect(description?.severity).toBe('medium');
expect(description?.notification.heading?.text).toBe(
localize('issues.event_subscription.heading'),
);
expect(description?.notification.metadata?.map((detail) => detail.text)).toEqual([
'alpha_event',
'zebra_event',
]);
});
it('should offer a retry control on the notification', () => {
const health = createSubscriptionHealth();
health.getFailures.mockReturnValue([createSubscriptionFailure('zha_event')]);
const issue = new EventSubscriptionIssue(health, vi.fn());
expect(issue.getNotification()?.controls?.[0].icon).toBe('mdi:refresh');
});
it('should re-drive the failing subscriptions on retry', () => {
const health = createSubscriptionHealth();
const issue = new EventSubscriptionIssue(health, vi.fn());
expect(issue.retry()).toBe(true);
expect(health.retry).toBeCalledTimes(1);
});
it('should not opt into IssueManager-scheduled retries', () => {
// No `needsRetry()` means the subscription manager stays the sole auto-retry
// loop; the IssueManager never schedules this issue.
const issue: Issue = new EventSubscriptionIssue(createSubscriptionHealth(), vi.fn());
expect(issue.needsRetry).toBeUndefined();
});
it('should remove its health listener on destroy', () => {
const health = createSubscriptionHealth();
const unsubscribe = vi.fn();
health.addListener.mockReturnValue(unsubscribe);
const issue = new EventSubscriptionIssue(health, vi.fn());
issue.destroy();
expect(unsubscribe).toBeCalledTimes(1);
});
});
@@ -539,7 +539,7 @@ describe('IssueStateManager', () => {
});
describe('destroy', () => {
it('should destroy all issues and clear', () => {
it('should clear, reset and destroy all issues', () => {
const manager = createManager();
manager.destroy();
@@ -549,6 +549,14 @@ describe('IssueStateManager', () => {
expect(mockConfigUpgrade.reset).toBeCalled();
expect(mockLegacyResource.reset).toBeCalled();
expect(mockMediaLoad.reset).toBeCalled();
assert(mockConfigUpgrade.destroy);
assert(mockLegacyResource.destroy);
assert(mockMediaLoad.destroy);
expect(mockConfigUpgrade.destroy).toBeCalled();
expect(mockLegacyResource.destroy).toBeCalled();
expect(mockMediaLoad.destroy).toBeCalled();
expect(manager.getIssuePresence().size).toBe(0);
});
});