chore: Enforce a few house rules via eslint (#2539)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent 5572ec728e
commit 921d45e577
100 changed files with 479 additions and 295 deletions
@@ -9,13 +9,13 @@ export class EffectAction extends AdvancedCameraCardAction<EffectActionConfig> {
const action = this._getAction();
switch (action.effect_action) {
case 'start':
api.getEffectsManager().startEffect(action.effect);
void api.getEffectsManager().startEffect(action.effect);
break;
case 'stop':
api.getEffectsManager().stopEffect(action.effect);
break;
case 'toggle':
api.getEffectsManager().toggleEffect(action.effect);
void api.getEffectsManager().toggleEffect(action.effect);
break;
}
}
@@ -11,7 +11,7 @@ export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActio
// If `enabled` is explicit, use it. If only `type` is being changed, leave
// `enabled` untouched (undefined = no change). Otherwise (neither set),
// toggle the current enabled value — this is the menu-button show/hide use
// toggle the current enabled value -- this is the menu-button show/hide use
// case.
const enabled =
action.enabled ??
@@ -28,9 +28,8 @@ export class PTZMultiAction extends AdvancedCameraCardAction<PTZMultiActionConfi
return;
}
(type === 'ptz'
? this._toPTZAction(targetID)
: this._toPTZDigitalAction(targetID)
void (
type === 'ptz' ? this._toPTZAction(targetID) : this._toPTZDigitalAction(targetID)
).execute(api);
}
+1 -1
View File
@@ -106,6 +106,6 @@ export class AutomationsManager {
--this._nestedAutomationExecutions;
}
};
runActions(automation.actions);
void runActions(automation.actions);
}
}
+1 -1
View File
@@ -285,7 +285,7 @@ export class CallManager {
restoreView &&
(previousView.view !== 'live' || previousView.camera !== call.cameraID)
) {
viewManager.setViewByParametersWithExistingQuery({
void viewManager.setViewByParametersWithExistingQuery({
baseView: previousView,
force: true,
});
+1 -1
View File
@@ -63,7 +63,7 @@ export abstract class GeneratedTone implements Tone {
// a stopped tone can never re-arm its loop.
protected _scheduleNext(intervalSeconds: number): void {
/* istanbul ignore next: defensive guard against a subclass calling
_scheduleNext after stop() — JS single-threading makes this unreachable
_scheduleNext after stop() -- JS single-threading makes this unreachable
from the existing subclasses -- @preserve */
if (!this._context) {
return;
+4 -4
View File
@@ -79,8 +79,8 @@ export class CardElementManager {
// These initializers are called when the config is updated, but on initial
// creation of the card hass is not yet available when the config is first
// loaded.
this._api.getDefaultManager().initialize();
this._api.getMediaPlayerManager().initialize();
void this._api.getDefaultManager().initialize();
void this._api.getMediaPlayerManager().initialize();
this._api
.getHASSManager()
@@ -184,7 +184,7 @@ export class CardElementManager {
this._api.getFullscreenManager().disconnect();
this._api.getPIPManager().uninitialize();
this._api.getKeyboardStateManager().uninitialize();
this._api.getActionsManager().uninitialize();
void this._api.getActionsManager().uninitialize();
this._api.getInteractionManager().uninitialize();
this._api.getDefaultManager().uninitialize();
this._api.getHASSManager().getStateWatcher()?.unsubscribe(this.update);
@@ -200,7 +200,7 @@ export class CardElementManager {
this._api
.getInitializationManager()
.uninitialize(InitializationAspect.INITIAL_TRIGGER);
this._api.getCameraManager().destroy();
void this._api.getCameraManager().destroy();
this._element.removeEventListener(
'mousemove',
+3 -3
View File
@@ -154,7 +154,7 @@ export class ConfigManager {
// automations, which destroys associated ConditionsManagers. If a condition
// transition (e.g. microphone connect) triggers both a user automation and
// an unrelated override, an unconditional reload would delete the
// automation mid-transition — the freshly created replacement has no prior
// automation mid-transition -- the freshly created replacement has no prior
// state, treats the current condition as its baseline, and never fires the
// action.
const runIfChanged = <T>(
@@ -189,7 +189,7 @@ export class ConfigManager {
(config) => [config.cameras, config.cameras_global],
() => {
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
this._api.getCameraManager().destroy();
void this._api.getCameraManager().destroy();
},
true,
);
@@ -203,7 +203,7 @@ export class ConfigManager {
true,
);
/* async */ this._initializeBackgroundAndUpdate(previousConfig);
void this._initializeBackgroundAndUpdate(previousConfig);
}
/**
@@ -92,7 +92,7 @@ export class OverridesManager {
const parseResult = advancedCameraCardConfigSchema.safeParse(output);
if (!parseResult.success) {
// Surface one co-located failure object per Zod issue — path, the value
// Surface one co-located failure object per Zod issue -- path, the value
// the user actually wrote, and the most informative "expected" field for
// this issue code. Avoids dumping the full merged config (which is
// mostly schema defaults the user never wrote) and keeps the reader from
@@ -166,7 +166,7 @@ export class EffectsManager implements EffectsManagerInterface {
private _startPendingEffects(): void {
for (const [name, options] of this._pendingEffects.entries()) {
this._pendingEffects.delete(name);
this._startEffect(name, options);
void this._startEffect(name, options);
}
}
}
@@ -32,9 +32,12 @@ export class ScreenfullFullScreenProvider
}
if (fullscreen) {
screenfull.request(this._api.getCardElementManager().getElement());
// A denied request (or an exit when not in fullscreen) leaves the UI
// consistent: the 'change' handler only fires on a real transition.
// Nothing to act on.
screenfull.request(this._api.getCardElementManager().getElement()).catch(() => {});
} else {
screenfull.exit();
screenfull.exit().catch(() => {});
}
}
}
@@ -85,7 +85,10 @@ export class WebkitFullScreenProvider
// clicking the 'X' which then fires this event). That's probably the rare
// case though.
this._playTimer.start(WEBKIT_PLAY_SECONDS, () => {
this._getVideoElement()?.play();
// Best-effort resume after a fullscreen exit.
this._getVideoElement()
?.play()
.catch(() => {});
});
};
}
+1 -1
View File
@@ -61,7 +61,7 @@ export class HASSManager implements HASSManagerReadonlyInterface {
);
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
this._api.getCameraManager().destroy();
void this._api.getCameraManager().destroy();
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
this._api
.getInitializationManager()
@@ -80,7 +80,7 @@ export class InitializationManager {
if (!this._shouldInitializeMandatory()) {
return;
}
/* async */ this.initializeMandatory();
void this.initializeMandatory();
}
private _shouldInitializeMandatory(): boolean {
+3 -5
View File
@@ -29,7 +29,7 @@ export class IssueManager {
private _suspended = false;
// Reentrancy guard: evaluate() calls setState() on the condition state
// manager, which fires listeners synchronously — including the one
// manager, which fires listeners synchronously -- including the one
// registered in this constructor. Without this guard, detectDynamic()
// and presence computation would run twice per evaluation.
private _evaluating = false;
@@ -165,16 +165,14 @@ export class IssueManager {
// normal re-evaluation (on any condition-state change).
//
// `initialized: true` in the change payload means mandatory initialization
// just finished — see InitializationManager._initializeMandatory. That's
// just finished -- see InitializationManager._initializeMandatory. That's
// also the earliest point at which the full HASS object is guaranteed
// ready for websocket calls (e.g. LegacyResourceIssue's lovelace/resources
// fetch). Because `initialized` is latched (its comment notes it never
// changes again), this block fires exactly once per IssueManager life.
private _onStateChange(change: ConditionStateChange): void {
if (change.change.initialized === true && change.new.hass) {
/* async */ this._stateManager
.detectStatic(change.new.hass)
.then(() => this.evaluate());
void this._stateManager.detectStatic(change.new.hass).then(() => this.evaluate());
}
this.evaluate();
}
@@ -19,7 +19,7 @@ export class ConnectionIssue implements Issue {
private _state: ConnectionState = 'ready';
public detectDynamic(state: ConditionState): void {
// Before HASS is ever provided, leave state untouched — undefined hass is
// Before HASS is ever provided, leave state untouched -- undefined hass is
// not a disconnection, just "not yet initialized".
if (state.hass === undefined) {
return;
@@ -44,7 +44,7 @@ export class InitializationIssue extends AbstractErrorIssue {
// resources (WebSocket subscriptions, listeners) before the CAMERAS
// init aspect replaces the instance via createCameraManager().
this._api.getInitializationManager().uninitializeMandatory();
this._api.getCameraManager().destroy();
void this._api.getCameraManager().destroy();
return false;
}
@@ -37,7 +37,7 @@ export class MediaLoadIssue implements Issue {
}
// =========================================================================
// Explicit trigger — called when a component fires an issue:trigger event.
// Explicit trigger -- called when a component fires an issue:trigger event.
// =========================================================================
public trigger(context: IssueTriggerContext['media_load']): void {
@@ -45,7 +45,7 @@ export class MediaLoadIssue implements Issue {
}
// =========================================================================
// Detection — called by the manager on every state change.
// Detection -- called by the manager on every state change.
// =========================================================================
public detectDynamic(state: ConditionState): void {
@@ -62,7 +62,7 @@ export class MediaLoadIssue implements Issue {
}
// =========================================================================
// State queries — called by the manager to read current state.
// State queries -- called by the manager to read current state.
// =========================================================================
public hasIssue(): boolean {
@@ -113,7 +113,7 @@ export class MediaLoadIssue implements Issue {
}
// =========================================================================
// Retry — called by the manager to schedule a media reload.
// Retry -- called by the manager to schedule a media reload.
// =========================================================================
public needsRetry(): boolean {
@@ -144,7 +144,7 @@ export class MediaLoadIssue implements Issue {
// re-attempts loading underneath. If the retry succeeds,
// _handleMediaLoaded will clear everything when media:loaded fires. If
// it fails silently (e.g. bogus stream name), the error stays visible
// immediately — no new 10s grace period.
// immediately -- no new 10s grace period.
this._api.getViewManager().setViewWithMergedContext({ mediaEpoch });
return false;
}
@@ -212,7 +212,7 @@ export class MediaLoadIssue implements Issue {
this._timerTargetID = targetID;
this._timer.start(MEDIA_LOADING_TIMEOUT_SECONDS, () => {
// Record the error on timeout so retry() knows which epoch to bump.
// targetID is guaranteed non-null here — the null case bails at the
// targetID is guaranteed non-null here -- the null case bails at the
// top of _handleMediaNotLoaded.
this._erroredTargetIDs.add(targetID);
this._activate();
@@ -31,7 +31,7 @@ export class MediaQueryIssue extends AbstractErrorIssue {
return false;
}
this._error = null;
this._api.getViewManager().setViewByParametersWithNewQuery();
void this._api.getViewManager().setViewByParametersWithNewQuery();
// Exclusive retry. No other issue should attempt to retry until the next
// evaluation cycle, when we'll know if this was successful.
+10 -4
View File
@@ -3,7 +3,7 @@ import { summarizeNotification } from '../../components-lib/notification/summari
import { ConditionState } from '../../condition-trigger/conditions/types';
import { Notification } from '../../config/schema/actions/types';
import { HomeAssistant } from '../../ha/types';
import { isTruthy } from '../../utils/basic';
import { errorToConsole, isTruthy } from '../../utils/basic';
import {
Issue,
IssueDescription,
@@ -27,12 +27,18 @@ export class IssueStateManager implements IssueReadOnlyState {
}
// =========================================================================
// Detection — static (one-shot on init) and dynamic (on every state change).
// 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);
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 as Error);
}
this._logIfNew(issue);
}
}
@@ -57,7 +63,7 @@ export class IssueStateManager implements IssueReadOnlyState {
}
// =========================================================================
// Queries — read active issue state.
// Queries -- read active issue state.
// =========================================================================
public getFullCardIssue(): IssueDescription | null {
+11 -11
View File
@@ -28,11 +28,11 @@ export interface KeyedIssueDescription {
}
// Map of currently active issues keyed by IssueKey, with each entry's value
// being the issue's current rendered description. Stored as a Map (not just
// a Set of keys) so that sub-state changes within an issue — e.g.
// ConnectionIssue swapping between 'lost' and 'starting' — are reflected as
// real value-level diffs to the condition state, triggering re-renders and
// any user-defined conditions that depend on issue state.
// being the issue's current rendered description. Stored as a Map (not just a
// Set of keys) so that sub-state changes within an issue -- e.g.
// ConnectionIssue swapping between 'lost' and 'starting' -- are reflected as
// real value-level diffs to the condition state, triggering re-renders and any
// user-defined conditions that depend on issue state.
export type IssuePresence = Map<IssueKey, IssueDescription>;
export interface IssueReadOnlyState {
hasFullCardIssue(): boolean;
@@ -80,7 +80,7 @@ export interface Issue {
// loop (exclusive), false to allow subsequent issues to also retry.
retry?(): boolean;
// Optional user-initiated fix. Not called by the issue infrastructure —
// Optional user-initiated fix. Not called by the issue infrastructure --
// callers (e.g. notification control actions) invoke this directly.
fix?(hass: HomeAssistant): Promise<boolean>;
@@ -92,11 +92,11 @@ export interface Issue {
// Called when the card is detached. Issues with age-based timers (e.g.
// loading-timeout timers) must stop them here so that time spent offscreen
// doesn't count against the user. Must preserve already-active issue state
// — a full-card issue visible at detach should still be visible on
// reattach. No `resume` hook: IssueManager.resume() triggers a normal
// evaluate(), so any timer that should restart is re-armed via
// detectDynamic against the current condition state.
// doesn't count against the user. Must preserve already-active issue state --
// a full-card issue visible at detach should still be visible on reattach. No
// `resume` hook: IssueManager.resume() triggers a normal evaluate(), so any
// timer that should restart is re-armed via detectDynamic against the current
// condition state.
suspend?(): void;
// Release external resources (e.g. a listener registered on another manager)
+2 -2
View File
@@ -10,8 +10,8 @@ import type { LockPolicy } from './types';
// - Stream-stopping / re-init actions: pause, reload, and casting (which
// rehosts the stream to a media player).
//
// `call_start` is intentionally absent — it's the entry into the lock.
// `call_end` is also absent — it dispatches via `setViewByParameters({ force:
// `call_start` is intentionally absent -- it's the entry into the lock.
// `call_end` is also absent -- it dispatches via `setViewByParameters({ force:
// true })` to bypass the lock, so listing it here would be redundant.
const CALL_DISRUPTIVE_ACTIONS: ReadonlySet<string> = new Set([
// View / camera / substream changes.
+1 -1
View File
@@ -27,7 +27,7 @@ export class MediaLoadedInfoManager {
// never cleared by `clear` / `_clearTarget`, only by `initialize`.
private _lastKnown: Map<string, MediaLoadedInfo> = new Map();
// The currently "active" target — the one whose info drives condition state
// The currently "active" target -- the one whose info drives condition state
// and card-level side effects. Driven by ViewManager on every view change.
private _selected: string | null = null;
+2 -2
View File
@@ -17,8 +17,8 @@ interface QueryStringViewIntent {
// The substream change to apply alongside the view. Tri-state:
// - `undefined`: no substream URL action present, no modifier issued.
// - `string`: `substream_on=X` — engage stream X.
// - `null`: `substream_off` — explicitly clear the override.
// - `string`: `substream_on=X` -- engage stream X.
// - `null`: `substream_off` -- explicitly clear the override.
stream?: string | null;
};
other?: AdvancedCameraCardCustomActionConfig[];
+2 -2
View File
@@ -126,7 +126,7 @@ export class ViewManager implements ViewManagerInterface {
...options,
});
// A non-throwing factory call clears any prior view_incompatible /
// media_query state — ensures a previously-dismissed mid-session popup
// media_query state -- ensures a previously-dismissed mid-session popup
// does not linger invisibly and re-pop on the next evaluation cycle,
// and that a stale media_query failure from an abandoned gallery /
// viewer doesn't follow the user into an unrelated view.
@@ -361,7 +361,7 @@ export class ViewManager implements ViewManagerInterface {
if (!this._api.getQueryStringManager().hasViewRelatedActionsToRun()) {
// This is not awaited to allow the initialization to complete before the
// query is answered.
this.setViewDefaultWithNewQuery({ failSafe: true });
void this.setViewDefaultWithNewQuery({ failSafe: true });
}
};