Use custom websocket to avoid needing admin privileges.

This commit is contained in:
Dermot Duffy
2024-08-24 20:00:00 -07:00
parent 5a1d08f3ea
commit 429dd90972
47 changed files with 1219 additions and 874 deletions
+14 -2
View File
@@ -46,9 +46,9 @@ export class CardElementManager {
this._menuToggleCallback();
}
public update(): void {
public update = (): void => {
this._element.requestUpdate();
}
};
public hasUpdated(): boolean {
return this._element.hasUpdated;
@@ -69,6 +69,17 @@ export class CardElementManager {
this._api.getKeyboardStateManager().initialize();
this._api.getDefaultManager().initialize();
this._api
.getHASSManager()
.getStateWatcher()
?.subscribe(this.update, [
...(this._api.getConfigManager().getConfig()?.view.render_entities ?? []),
// Refresh the card if media player state changes:
// https://github.com/dermotduffy/frigate-hass-card/issues/881
...(this._api.getMediaPlayerManager().getMediaPlayers() ?? []),
]);
// Whether or not the card is in panel mode on the dashboard.
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
setOrRemoveAttribute(this._element, true, 'tabindex', '0');
@@ -129,6 +140,7 @@ export class CardElementManager {
this._api.getKeyboardStateManager().uninitialize();
this._api.getActionsManager().uninitialize();
this._api.getDefaultManager().uninitialize();
this._api.getHASSManager().getStateWatcher()?.unsubscribe(this.update);
// Uninitialize cameras to cause them to reinitialize on
// reconnection, to ensure the state subscription/unsubscription works
+1 -1
View File
@@ -20,7 +20,7 @@ import { ConfigManager } from './config/config-manager';
import { DownloadManager } from './download-manager';
import { ExpandManager } from './expand-manager';
import { FullscreenManager } from './fullscreen-manager';
import { HASSManager } from './hass-manager';
import { HASSManager } from './hass/hass-manager';
import { InitializationManager } from './initialization-manager';
import { InteractionManager } from './interaction-manager';
import { MediaLoadedInfoManager } from './media-info-manager';
+11 -18
View File
@@ -1,9 +1,8 @@
import PQueue from 'p-queue';
import { DestroyCallback, subscribeToTrigger } from '../utils/ha';
import { createGeneralAction } from '../utils/action';
import { isActionAllowedBasedOnInteractionState } from '../utils/interaction-mode';
import { Timer } from '../utils/timer';
import { CardDefaultManagerAPI } from './types';
import { createGeneralAction } from '../utils/action';
/**
* Manages automated resetting to the default view.
@@ -11,7 +10,6 @@ import { createGeneralAction } from '../utils/action';
export class DefaultManager {
protected _timer = new Timer();
protected _api: CardDefaultManagerAPI;
protected _unsubscribeCallback: DestroyCallback | null = null;
protected _initializationLimit = new PQueue({ concurrency: 1 });
constructor(api: CardDefaultManagerAPI) {
@@ -47,8 +45,7 @@ export class DefaultManager {
public uninitialize(): void {
this._timer.stop();
this._unsubscribeCallback?.();
this._unsubscribeCallback = null;
this._api.getHASSManager().getStateWatcher().unsubscribe(this._stateChangeHandler);
this._api.getAutomationsManager().deleteAutomations(this);
}
@@ -59,19 +56,11 @@ export class DefaultManager {
return false;
}
if (this._unsubscribeCallback) {
await this._unsubscribeCallback();
}
this._unsubscribeCallback = await subscribeToTrigger(
hass,
() => this._setToDefaultIfAllowed(),
{
entityID: config.entities,
platform: 'state',
stateOnly: true,
},
);
this._api.getHASSManager().getStateWatcher().unsubscribe(this._stateChangeHandler);
this._api
.getHASSManager()
.getStateWatcher()
.subscribe(this._stateChangeHandler, config.entities);
// If the timer is running, restart it with the newly configured timer.
if (this._timer.isRunning()) {
@@ -82,6 +71,10 @@ export class DefaultManager {
return true;
}
protected _stateChangeHandler = (): void => {
this._setToDefaultIfAllowed();
};
protected _setToDefaultIfAllowed(): void {
if (this._isAutomatedUpdateAllowed()) {
this._api.getViewManager().setViewDefault();
@@ -1,11 +1,13 @@
import { localize } from '../localize/localize';
import { ExtendedHomeAssistant } from '../types';
import { hasHAConnectionStateChanged, isHassDifferent } from '../utils/ha';
import { CardHASSAPI } from './types';
import { localize } from '../../localize/localize';
import { ExtendedHomeAssistant } from '../../types';
import { hasHAConnectionStateChanged } from '../../utils/ha';
import { CardHASSAPI } from '../types';
import { StateWatcher, StateWatcherSubscriptionInterface } from './state-watcher';
export class HASSManager {
protected _hass: ExtendedHomeAssistant | null = null;
protected _api: CardHASSAPI;
protected _stateWatcher: StateWatcher = new StateWatcher();
constructor(api: CardHASSAPI) {
this._api = api;
@@ -15,6 +17,10 @@ export class HASSManager {
return this._hass;
}
public getStateWatcher(): StateWatcherSubscriptionInterface {
return this._stateWatcher;
}
public setHASS(hass?: ExtendedHomeAssistant | null): void {
if (hasHAConnectionStateChanged(this._hass, hass)) {
if (!hass?.connected) {
@@ -36,18 +42,6 @@ export class HASSManager {
const oldHass = this._hass;
this._hass = hass;
if (
isHassDifferent(this._hass, oldHass, [
...(this._api.getConfigManager().getConfig()?.view.render_entities ?? []),
// Refresh the card if media player state changes:
// https://github.com/dermotduffy/frigate-hass-card/issues/881
...this._api.getMediaPlayerManager().getMediaPlayers(),
])
) {
this._api.getCardElementManager().update();
}
if (this._api.getConditionsManager().hasHAStateConditions()) {
this._api.getConditionsManager().setState({
state: this._hass.states,
@@ -57,5 +51,7 @@ export class HASSManager {
// Dark mode may depend on HASS.
this._api.getStyleManager().setLightOrDarkMode();
this._stateWatcher.setHASS(oldHass, hass);
}
}
+51
View File
@@ -0,0 +1,51 @@
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import { getHassDifferences, HassStateDifference } from '../../utils/ha';
type StateWatcherCallback = (difference: HassStateDifference) => void;
export interface StateWatcherSubscriptionInterface {
subscribe(callback: StateWatcherCallback, entityIDs: string[]): void;
unsubscribe(callback: StateWatcherCallback): void;
}
export class StateWatcher implements StateWatcherSubscriptionInterface {
protected _watcherCallbacks = new Map<StateWatcherCallback, string[]>();
public setHASS(oldHass: HomeAssistant | null, hass: HomeAssistant): void {
if (!oldHass) {
return;
}
for (const [callback, entityIDs] of this._watcherCallbacks.entries()) {
const differences = getHassDifferences(hass, oldHass, entityIDs, {
stateOnly: true,
firstOnly: true,
});
if (differences.length) {
callback(differences[0]);
}
}
}
/**
* Calls callback when the state of any of the entities changes. The callback is
* called with the state difference of the first entity that changed.
* @param callback The callback.
* @param entityIDs An array of entity IDs to watch.
*/
public subscribe(callback: StateWatcherCallback, entityIDs: string[]): boolean {
if (!entityIDs.length) {
return false;
}
if (this._watcherCallbacks.has(callback)) {
this._watcherCallbacks.get(callback)?.push(...entityIDs);
} else {
this._watcherCallbacks.set(callback, entityIDs);
}
return true;
}
public unsubscribe(callback: StateWatcherCallback): void {
this._watcherCallbacks.delete(callback);
}
}
+5 -4
View File
@@ -42,16 +42,17 @@ export class MessageManager {
}
}
public setErrorIfHigherPriority(error: unknown): void {
public setErrorIfHigherPriority(error: unknown, prefix?: string): void {
// This object should accept unknown objects to be able to seamlessly
// process arguments to catch() which can only be unknown/any.
if (!(error instanceof Error)) {
// process arguments to catch() which can only be unknown/any. HA may throw
// non Error() based errors.
if (!error || typeof error !== 'object' || !('message' in error)) {
return;
}
errorToConsole(error);
this.setMessageIfHigherPriority({
message: error.message,
message: prefix ? `${prefix}: ${error.message}` : error.message,
type: 'error',
...(error instanceof FrigateCardError && { context: error.context }),
});
+2 -2
View File
@@ -11,7 +11,7 @@ export class StyleManager {
this._api = api;
}
public setLightOrDarkMode(): void {
public setLightOrDarkMode = (): void => {
const config = this._api.getConfigManager().getConfig();
const isDarkMode =
config?.view.dark_mode === 'on' ||
@@ -24,7 +24,7 @@ export class StyleManager {
isDarkMode,
'dark',
);
}
};
public setExpandedMode(): void {
const card = this._api.getCardElementManager().getElement();
+4 -1
View File
@@ -12,7 +12,7 @@ import type { DefaultManager } from './default-manager';
import type { DownloadManager } from './download-manager';
import type { ExpandManager } from './expand-manager';
import type { FullscreenManager } from './fullscreen-manager';
import type { HASSManager } from './hass-manager';
import type { HASSManager } from './hass/hass-manager';
import type { InitializationManager } from './initialization-manager';
import type { InteractionManager } from './interaction-manager';
import type { KeyboardStateManager } from './keyboard-state-manager';
@@ -121,13 +121,16 @@ export interface CardDownloadAPI {
export interface CardElementAPI {
getActionsManager(): ActionsManager;
getCameraManager(): CameraManager;
getConfigManager(): ConfigManager;
getDefaultManager(): DefaultManager;
getExpandManager(): ExpandManager;
getFullscreenManager(): FullscreenManager;
getInitializationManager(): InitializationManager;
getInteractionManager(): InteractionManager;
getHASSManager(): HASSManager;
getKeyboardStateManager(): KeyboardStateManager;
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
getMediaPlayerManager(): MediaPlayerManager;
getMicrophoneManager(): MicrophoneManager;
getQueryStringManager(): QueryStringManager;
}