From 5bc1d3375c056007e917c86439c2008e5b06120e Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 10 May 2022 14:48:06 -0700 Subject: [PATCH] Add consistent initialization logic to improve Chromecast support. --- src/card.ts | 17 +++++++++++++++ src/common.ts | 59 +++++++++++++++++++++++++++++++++++++++++++-------- src/editor.ts | 55 ++++++++++++++--------------------------------- src/types.ts | 9 ++++++++ 4 files changed, 92 insertions(+), 48 deletions(-) diff --git a/src/card.ts b/src/card.ts index 8b99b9d5..18909c92 100644 --- a/src/card.ts +++ b/src/card.ts @@ -57,6 +57,7 @@ import { homeAssistantWSRequest, isValidMediaShowInfo, shouldUpdateBasedOnHass, + sideLoadHomeAssistantElements, } from './common.js'; import { localize } from './localize/localize.js'; import { renderMessage, renderProgressIndicator } from './components/message.js'; @@ -186,6 +187,9 @@ export class FrigateCard extends LitElement { // per second for performance reasons. protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000); + // Whether the card has been successfully initialized. + protected _initialized = false; + /** * Set the Home Assistant object. */ @@ -830,6 +834,19 @@ export class FrigateCard extends LitElement { this._changeView({ view: e.detail }); } + /** + * Called before each update. + */ + protected willUpdate(): void { + if (!this._initialized) { + sideLoadHomeAssistantElements().then((success) => { + if (success) { + this._initialized = true; + } + }) + } + } + /** * Determine whether the element should be updated. * @param changedProps The changed properties if any. diff --git a/src/common.ts b/src/common.ts index 42a50576..4a1d2a92 100644 --- a/src/common.ts +++ b/src/common.ts @@ -21,6 +21,7 @@ import { ActionsConfig, ActionType, CameraConfig, + CardHelpers, ExtendedHomeAssistant, FrigateCardAction, FrigateCardCustomAction, @@ -33,7 +34,7 @@ import { signedPathSchema, StateParameters, } from './types.js'; -import { stateIcon } from './icons/state-icon.js' +import { stateIcon } from './icons/state-icon.js'; const MEDIA_INFO_HEIGHT_CUTOFF = 50; const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF; @@ -290,10 +291,10 @@ export function convertActionToFrigateCardCustomAction( export function createFrigateCardCustomAction( action: FrigateCardAction, args?: { - camera?: string, - media_player?: string, - media_player_action?: 'play' | 'stop', - } + camera?: string; + media_player?: string; + media_player_action?: 'play' | 'stop'; + }, ): FrigateCardCustomAction | null { if (action === 'camera_select') { if (!args?.camera) { @@ -473,10 +474,7 @@ export function getEntityTitle( * @param hass The Home Assistant object. * @returns The icon or undefined. */ -export function getEntityIcon( - hass?: HomeAssistant, - entity?: string, -): string { +export function getEntityIcon(hass?: HomeAssistant, entity?: string): string { return stateIcon(entity ? hass?.states[entity] : null); } @@ -682,3 +680,46 @@ export function getEventDurationString(event: FrigateEvent): string { duration += `${seconds}s`; return duration; } + +/** + * Side loads the HA elements this card needs. This trickery is unfortunate + * necessary, see: + * - https://github.com/thomasloven/hass-config/wiki/PreLoading-Lovelace-Elements + * @returns `true` if the load is successful, `false` otherwise. + */ +export const sideLoadHomeAssistantElements = async (): Promise => { + const neededElements = [ + 'ha-selector', + 'ha-menu-button', + 'ha-camera-stream', + 'ha-hls-player', + 'ha-web-rtc-player', + 'ha-icon', + 'ha-circular-progress', + 'ha-icon-button', + 'ha-card', + 'ha-svg-icon', + 'ha-button-menu', + ]; + + if (neededElements.every((element) => customElements.get(element))) { + return true; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const helpers: CardHelpers = await (window as any).loadCardHelpers(); + + // The picture-glance editor loads everything this card needs. In particular: + // + // See: https://github.com/thomasloven/hass-config/wiki/PreLoading-Lovelace-Elements + const pictureGlance = await helpers.createCardElement({ + type: 'picture-glance', + entities: [], + camera_image: 'dummy-to-load-editor-components', + }); + if (pictureGlance.constructor.getConfigElement) { + await pictureGlance.constructor.getConfigElement(); + return true; + } + return false; +}; diff --git a/src/editor.ts b/src/editor.ts index 739439f0..b003f60b 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; @@ -90,7 +89,12 @@ import { CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, } from './const.js'; -import { arrayMove, getCameraID, getCameraTitle } from './common.js'; +import { + arrayMove, + getCameraID, + getCameraTitle, + sideLoadHomeAssistantElements, +} from './common.js'; import { copyConfig, deleteConfigValue, @@ -183,7 +187,6 @@ const options: EditorOptions = { export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { @property({ attribute: false }) public hass?: HomeAssistant; @state() protected _config?: RawFrigateCardConfig; - @state() protected _helpers?: any; protected _initialized = false; protected _configUpgradeable = false; @@ -386,15 +389,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor // such, RawFrigateCardConfig is used as the type. this._config = config; this._configUpgradeable = isConfigUpgradeable(config); - this.loadCardHelpers(); } - protected shouldUpdate(): boolean { + /** + * Called before each update. + */ + protected willUpdate(): void { if (!this._initialized) { - this._initialize(); + sideLoadHomeAssistantElements().then((success) => { + if (success) { + this._initialized = true; + } + }); } - - return true; } protected _getEntities(domain: string): string[] { @@ -929,7 +936,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor } protected render(): TemplateResult | void { - if (!this.hass || !this._helpers || !this._config) { + if (!this.hass || !this._config) { return html``; } @@ -1219,36 +1226,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor `; } - /** - * Verify editor is initialized. - */ - protected _initialize(): void { - if (this.hass === undefined) return; - if (this._config === undefined) return; - if (this._helpers === undefined) return; - - (async (): Promise => { - // The picture-glance editor loads the ha-selectors. - // See: https://github.com/thomasloven/hass-config/wiki/PreLoading-Lovelace-Elements - const pictureGlance = await this._helpers.createCardElement({ - type: 'picture-glance', - entities: [], - camera_image: 'dummy-to-load-editor-components', - }); - if (pictureGlance.constructor.getConfigElement) { - await pictureGlance.constructor.getConfigElement(); - this._initialized = true; - } - })(); - } - - /** - * Load card helpers. - */ - protected async loadCardHelpers(): Promise { - this._helpers = await (window as any).loadCardHelpers(); - } - /** * Close the editor menu with the given domain. * @param targetDomain The menu domain to close. diff --git a/src/types.ts b/src/types.ts index 0187ce73..67873814 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ import { CustomActionConfig, HomeAssistant, LovelaceCard, + LovelaceCardConfig, LovelaceCardEditor, MoreInfoActionConfig, NavigateActionConfig, @@ -1152,6 +1153,14 @@ export interface FrigateCardMediaPlayer { unmute(): void; } +export interface CardHelpers { + createCardElement(config: LovelaceCardConfig): Promise<{ + constructor: { + getConfigElement(): HTMLElement + }; + }>; +} + /** * Home Assistant API types. */