mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import type { PresentationActivityRuntime } from './device-presentation';
|
|
import { combineVisualSamples, edgeActivity, type EntityVisualSample } from './device-visual';
|
|
|
|
export const ACTIVITY_WINDOW_MS = 3300;
|
|
export const ACTIVITY_REPAINT_DELAY_MS = ACTIVITY_WINDOW_MS + 60;
|
|
|
|
/** Shared finite-edge history used by both plan renderers. Keeping the edge
|
|
* ordering here prevents the full and read-only cards from assigning different
|
|
* meaning to the same HA state sequence. */
|
|
export interface FiniteActivityRuntime extends PresentationActivityRuntime {
|
|
last: Record<string, string>;
|
|
timer: number;
|
|
}
|
|
|
|
export function createFiniteActivityRuntime(
|
|
sources: string,
|
|
samples: readonly EntityVisualSample[],
|
|
): FiniteActivityRuntime {
|
|
return {
|
|
sources,
|
|
last: Object.fromEntries(samples.map((sample) => [sample.eid, sample.state])),
|
|
flashTs: 0,
|
|
flashKind: null,
|
|
timer: 0,
|
|
gen: 0,
|
|
expiresAt: 0,
|
|
alarmActive: combineVisualSamples(samples).status === 'alarm',
|
|
};
|
|
}
|
|
|
|
export function resetFiniteActivityRuntime(
|
|
runtime: FiniteActivityRuntime,
|
|
sources: string,
|
|
samples: readonly EntityVisualSample[],
|
|
clearTimer: (timer: number) => void,
|
|
): void {
|
|
clearTimer(runtime.timer);
|
|
const fresh = createFiniteActivityRuntime(sources, samples);
|
|
Object.assign(runtime, fresh);
|
|
}
|
|
|
|
export function stampFiniteActivity(
|
|
runtime: FiniteActivityRuntime,
|
|
kind: 'event' | 'transition',
|
|
now: number,
|
|
clearTimer: (timer: number) => void,
|
|
scheduleRepaint: (delayMs: number) => number,
|
|
): void {
|
|
if (runtime.flashTs && now - runtime.flashTs < ACTIVITY_WINDOW_MS
|
|
&& runtime.flashKind === 'event' && kind === 'transition') return;
|
|
runtime.flashTs = now;
|
|
runtime.expiresAt = now + ACTIVITY_WINDOW_MS;
|
|
runtime.flashKind = kind;
|
|
runtime.gen++;
|
|
clearTimer(runtime.timer);
|
|
runtime.timer = scheduleRepaint(ACTIVITY_REPAINT_DELAY_MS);
|
|
}
|
|
|
|
/** Advance one already-baselined runtime. Alarm and an integration-provided
|
|
* travelling state own the timeline; recovery only establishes a baseline. */
|
|
export function advanceFiniteActivity(
|
|
runtime: FiniteActivityRuntime,
|
|
samples: readonly EntityVisualSample[],
|
|
clearTimer: (timer: number) => void,
|
|
): 'event' | 'transition' | null {
|
|
if (runtime.flashKind === 'transition'
|
|
&& samples.some((sample) => sample.activity === 'transition')) {
|
|
clearTimer(runtime.timer);
|
|
runtime.flashTs = 0;
|
|
runtime.flashKind = null;
|
|
runtime.expiresAt = 0;
|
|
}
|
|
|
|
const alarm = combineVisualSamples(samples).status === 'alarm';
|
|
if (alarm) {
|
|
if (!runtime.alarmActive) {
|
|
clearTimer(runtime.timer);
|
|
runtime.flashTs = 0;
|
|
runtime.flashKind = null;
|
|
runtime.expiresAt = 0;
|
|
}
|
|
for (const sample of samples) runtime.last[sample.eid] = sample.state;
|
|
runtime.alarmActive = true;
|
|
return null;
|
|
}
|
|
if (runtime.alarmActive) {
|
|
for (const sample of samples) runtime.last[sample.eid] = sample.state;
|
|
runtime.alarmActive = false;
|
|
return null;
|
|
}
|
|
|
|
let edge: 'event' | 'transition' | null = null;
|
|
for (const sample of samples) {
|
|
const found = edgeActivity(runtime.last[sample.eid], sample);
|
|
if (found === 'event' || (!edge && found)) edge = found;
|
|
runtime.last[sample.eid] = sample.state;
|
|
}
|
|
return edge;
|
|
}
|