The `initialized` state (used in conditions/triggers) was written once and never cleared, so it meant "has this card ever been initialized" while everything reading it took it as "is this card usable now". Home Assistant takes a card off the page and puts it back whenever its dashboard tab is left and returned to, so the card initialized again while the state claimed it was initialized throughout: `trigger: initialized` fired once per card rather than once per startup, and automations were dropped in between. The card lifecycle is now an explicit state machine (`SessionManager`), the only writer of `initialized`, which separates a card that is starting up from one initializing part of itself again while it runs. A new `ever` parameter (conditions/triggers) selects the old latched behaviour. `remote_control` uses that parameter to keep its two camera priorities correct under repeated starts. With `camera_priority: entity` the card now re-reads the entity every time it starts, so a camera selected while the card was away is picked up on return. With `camera_priority: card` the card writes the entity on its first start only, unchanged, since repeating that write would overwrite a camera the user had selected. Closes: #2642 BREAKING CHANGE: `condition: initialized` is now `false` whenever the card is not usable, and `trigger: initialized` fires each time the card starts up rather than only the first time. Set `ever: true` on either to keep the previous behaviour.
596 lines
23 KiB
TypeScript
596 lines
23 KiB
TypeScript
import {
|
|
html,
|
|
LitElement,
|
|
unsafeCSS,
|
|
type CSSResultGroup,
|
|
type TemplateResult,
|
|
} from 'lit';
|
|
import { customElement } from 'lit/decorators.js';
|
|
import { classMap } from 'lit/directives/class-map.js';
|
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
|
import { styleMap } from 'lit/directives/style-map.js';
|
|
|
|
import 'web-dialog';
|
|
|
|
import { actionHandler } from './action-handler-directive.js';
|
|
import { ConfigManager } from './card-controller/config/config-manager';
|
|
import { CardController } from './card-controller/controller';
|
|
import type {
|
|
IssueKey,
|
|
IssueResolveEventData,
|
|
IssueTriggerEventData,
|
|
} from './card-controller/issues/types.js';
|
|
import { resolveAutoHideState, type AutoHideState } from './components-lib/auto-hide.js';
|
|
import type { MicrophoneError } from './components-lib/live/utils/dispatch-microphone-error.js';
|
|
import { MenuButtonController } from './components-lib/menu-button-controller';
|
|
|
|
import './components/effects/effects';
|
|
import './components/elements.js';
|
|
|
|
import type { AdvancedCameraCardElements } from './components/elements.js';
|
|
|
|
import './components/loading.js';
|
|
import './components/menu.js';
|
|
|
|
import type { AdvancedCameraCardMenu } from './components/menu.js';
|
|
|
|
import './components/notification/block.js';
|
|
|
|
import { renderNotificationBlock } from './components/notification/block.js';
|
|
|
|
import './components/notification/popup.js';
|
|
import './components/overlay.js';
|
|
|
|
import type { AdvancedCameraCardOverlay } from './components/overlay.js';
|
|
|
|
import './components/status-bar';
|
|
import './components/thumbnail-carousel.js';
|
|
import './components/views.js';
|
|
|
|
import type { TemplateRendererGetEvent } from './card-controller/templates/renderer-via-event.js';
|
|
import type { AdvancedCameraCardViews } from './components/views.js';
|
|
import type { ConditionStateManagerGetEvent } from './condition-trigger/conditions/state-manager-via-event.js';
|
|
import type { StatusBarItem } from './config/schema/actions/types.js';
|
|
import type { MenuItem } from './config/schema/elements/custom/menu/types.js';
|
|
import type { AdvancedCameraCardConfig } from './config/schema/types.js';
|
|
import type {
|
|
PartialAdvancedCameraCardConfig,
|
|
RawAdvancedCameraCardConfig,
|
|
} from './config/types.js';
|
|
import { REPO_URL } from './const.js';
|
|
import { registerCustomIconset } from './ha/custom-icons.js';
|
|
import type { HomeAssistant, LovelaceCardEditor } from './ha/types.js';
|
|
import { localize } from './localize/localize.js';
|
|
import cardStyle from './scss/card.scss';
|
|
import type { MediaLoadedInfoEventDetail } from './types.js';
|
|
import { hasAction } from './utils/action.js';
|
|
import { getReleaseVersion } from './utils/diagnostics';
|
|
|
|
// ***************************************************************************
|
|
// General Card-Wide Notes
|
|
// ***************************************************************************
|
|
|
|
/** Media callbacks:
|
|
*
|
|
* Media elements (e.g. <video>, <img> or <canvas>) need to callback when:
|
|
* - Metadata is loaded / dimensions are known (for aspect-ratio)
|
|
* - Media is playing / paused (to avoid reloading)
|
|
*
|
|
* A number of different approaches used to attach event handlers to
|
|
* get these callbacks (which need to be attached directly to the media
|
|
* elements, which may be 'buried' down the DOM):
|
|
* - Extend the `ha-hls-player` and `ha-camera-stream` to specify the required
|
|
* hooks (as querySelecting the media elements after rendering was a fight
|
|
* with the Lit rendering engine and was very fragile) .
|
|
* - For non-Lit elements (e.g. WebRTC) query selecting after rendering.
|
|
* - Library provided hooks (e.g. JSMPEG)
|
|
* - Directly specifying hooks (e.g. for snapshot viewing with simple <img> tags)
|
|
*/
|
|
|
|
// ***************************************************************************
|
|
// Static Initializers
|
|
// ***************************************************************************
|
|
|
|
console.info(
|
|
`%c 📷 Advanced Camera Card %c ${getReleaseVersion()} `,
|
|
'padding: 3px; color: black; background: pink;',
|
|
'padding: 3px; color: black; background: white;',
|
|
);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(window as any).customCards = (window as any).customCards || [];
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(window as any).customCards.push({
|
|
type: 'advanced-camera-card',
|
|
name: localize('common.advanced_camera_card'),
|
|
description: localize('common.advanced_camera_card_description'),
|
|
preview: true,
|
|
documentationURL: REPO_URL,
|
|
getEntitySuggestion: ConfigManager.getEntitySuggestion,
|
|
});
|
|
|
|
registerCustomIconset();
|
|
|
|
// Expose currently-connected card instances on `window.advancedCameraCards` for
|
|
// console-based debugging and user support. `??=` so a double-loaded card
|
|
// shares one array (same pattern as `customCards` above).
|
|
const advancedCameraCards: AdvancedCameraCard[] = (window.advancedCameraCards ??= []);
|
|
|
|
// ***************************************************************************
|
|
// Main AdvancedCameraCard WebComponent
|
|
//
|
|
// Any non-rendering / non-lit related functionality should be added to
|
|
// CardController instead of this file.
|
|
// ***************************************************************************
|
|
|
|
@customElement('advanced-camera-card')
|
|
export class AdvancedCameraCard extends LitElement {
|
|
protected _controller = new CardController(
|
|
this,
|
|
// Callback to scroll the main pane back to the top (example usecase: scrolling
|
|
// half way down the gallery, then viewing diagnostics should result in
|
|
// diagnostics starting at the top).
|
|
() => this._refMain.value?.scroll({ top: 0 }),
|
|
() => this._refMenu.value?.toggleMenu(),
|
|
);
|
|
|
|
protected _menuButtonController = new MenuButtonController();
|
|
|
|
protected _refElements: Ref<AdvancedCameraCardElements> = createRef();
|
|
protected _refMain: Ref<HTMLElement> = createRef();
|
|
protected _refMenu: Ref<AdvancedCameraCardMenu> = createRef();
|
|
protected _refOverlay: Ref<AdvancedCameraCardOverlay> = createRef();
|
|
protected _refViews: Ref<AdvancedCameraCardViews> = createRef();
|
|
|
|
// Convenience methods for very frequently accessed attributes.
|
|
get _config(): AdvancedCameraCardConfig | null {
|
|
return this._controller.getConfigManager().getConfig();
|
|
}
|
|
|
|
get _hass(): HomeAssistant | null {
|
|
return this._controller.getHASSManager().getHASS();
|
|
}
|
|
|
|
set hass(hass: HomeAssistant) {
|
|
this._controller.getHASSManager().setHASS(hass);
|
|
|
|
// Manually set hass in the menu, elements and image. This is to allow these
|
|
// to update, without necessarily re-rendering the entire card (re-rendering
|
|
// is expensive).
|
|
if (this._refMenu.value) {
|
|
this._refMenu.value.hass = hass;
|
|
}
|
|
if (this._refElements.value) {
|
|
this._refElements.value.hass = hass;
|
|
}
|
|
if (this._refViews.value) {
|
|
this._refViews.value.hass = hass;
|
|
}
|
|
}
|
|
|
|
set isPanel(isPanel: boolean) {
|
|
this._controller.getConditionStateManager().setState({
|
|
panel: isPanel,
|
|
});
|
|
}
|
|
get isPanel(): boolean {
|
|
return !!this._controller.getConditionStateManager().getState().panel;
|
|
}
|
|
|
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
|
return await CardController.getConfigElement();
|
|
}
|
|
|
|
public static getStubConfig(
|
|
_: HomeAssistant,
|
|
entities: string[],
|
|
): PartialAdvancedCameraCardConfig {
|
|
return ConfigManager.getStubConfig(entities);
|
|
}
|
|
|
|
public setConfig(config: RawAdvancedCameraCardConfig): void {
|
|
this._controller.getConfigManager().setConfig(config);
|
|
}
|
|
|
|
public connectedCallback(): void {
|
|
super.connectedCallback();
|
|
advancedCameraCards.push(this);
|
|
}
|
|
|
|
public disconnectedCallback(): void {
|
|
const i = advancedCameraCards.indexOf(this);
|
|
if (i >= 0) {
|
|
advancedCameraCards.splice(i, 1);
|
|
}
|
|
super.disconnectedCallback();
|
|
}
|
|
|
|
protected shouldUpdate(): boolean {
|
|
// Do not allow a disconnected element to update, as it may cause cameras to
|
|
// reinitialize/subscribe for an element that is no longer part of the
|
|
// document.
|
|
if (!this.isConnected) {
|
|
return false;
|
|
}
|
|
|
|
this._controller.getInitializationManager().triggerInitialization();
|
|
return true;
|
|
}
|
|
|
|
protected _renderMenuStatusContainer(
|
|
position: 'top' | 'bottom' | 'overlay',
|
|
): TemplateResult | void {
|
|
if (!this._config) {
|
|
return;
|
|
}
|
|
|
|
const menuStyle = this._config.menu.style;
|
|
const menuPosition = this._config.menu.position;
|
|
const statusBarStyle = this._config.status_bar.style;
|
|
const statusBarPosition = this._config.status_bar.position;
|
|
|
|
if (
|
|
// If there's nothing to render...
|
|
(menuStyle === 'none' && statusBarStyle === 'none') ||
|
|
// ... or the position I'm rendering does not contain the menu/status bar
|
|
(position === 'overlay' &&
|
|
menuStyle === 'outside' &&
|
|
statusBarStyle === 'outside') ||
|
|
(position !== 'overlay' &&
|
|
(menuStyle !== 'outside' || menuPosition !== position) &&
|
|
(statusBarStyle !== 'outside' || statusBarPosition !== position))
|
|
) {
|
|
// ... then there's nothing to do.
|
|
return;
|
|
}
|
|
|
|
const getContents = (kind: 'overlay' | 'outerlay'): TemplateResult => {
|
|
const shouldRenderMenu =
|
|
menuStyle !== 'none' &&
|
|
((menuStyle === 'outside' && kind === 'outerlay' && menuPosition === position) ||
|
|
(menuStyle !== 'outside' && kind === 'overlay'));
|
|
|
|
const shouldRenderStatusBar =
|
|
statusBarStyle !== 'none' &&
|
|
((statusBarStyle === 'outside' &&
|
|
kind === 'outerlay' &&
|
|
statusBarPosition === position) ||
|
|
(statusBarStyle !== 'outside' && kind === 'overlay'));
|
|
|
|
// Complex logic to try to always put the menu in the right-looking place.
|
|
const renderMenuFirst =
|
|
menuPosition === 'left' ||
|
|
menuPosition === 'right' ||
|
|
(menuPosition === 'bottom' &&
|
|
menuStyle === 'hidden' &&
|
|
statusBarStyle !== 'popup') ||
|
|
(menuPosition === 'top' && statusBarStyle === 'popup');
|
|
|
|
return html`
|
|
${shouldRenderMenu && renderMenuFirst ? this._renderMenu(menuPosition) : ''}
|
|
${shouldRenderStatusBar ? this._renderStatusBar(statusBarPosition) : ''}
|
|
${shouldRenderMenu && !renderMenuFirst ? this._renderMenu(menuPosition) : ''}
|
|
`;
|
|
};
|
|
|
|
return html`
|
|
${position === 'overlay'
|
|
? html`<advanced-camera-card-overlay>
|
|
${getContents('overlay')}
|
|
</advanced-camera-card-overlay>`
|
|
: html`<div class="outerlay" data-position="${position}">
|
|
${getContents('outerlay')}
|
|
</div>`}
|
|
`;
|
|
}
|
|
|
|
protected _getAutoHideState(): AutoHideState {
|
|
return resolveAutoHideState(this._controller.getCallManager().isActive());
|
|
}
|
|
|
|
protected _renderMenu(slot?: string): TemplateResult | void {
|
|
const view = this._controller.getViewManager().getView();
|
|
if (!this._hass || !this._config) {
|
|
return;
|
|
}
|
|
return html`
|
|
<advanced-camera-card-menu
|
|
${ref(this._refMenu)}
|
|
slot=${ifDefined(slot)}
|
|
.hass=${this._hass}
|
|
.lockManagerEpoch=${this._controller.getLockManager().getEpoch()}
|
|
.menuConfig=${this._config.menu}
|
|
.buttons=${this._menuButtonController.calculateButtons(
|
|
this._hass,
|
|
this._config,
|
|
this._controller.getCameraManager(),
|
|
this._controller.getFoldersManager(),
|
|
{
|
|
callManager: this._controller.getCallManager(),
|
|
currentMediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
|
fullscreenManager: this._controller.getFullscreenManager(),
|
|
inExpandedMode: this._controller.getExpandManager().isExpanded(),
|
|
mediaPlayerController: this._controller.getMediaPlayerManager(),
|
|
microphoneManager: this._controller.getMicrophoneManager(),
|
|
pipManager: this._controller.getPIPManager(),
|
|
showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(),
|
|
view: view,
|
|
viewManager: this._controller.getViewManager(),
|
|
},
|
|
)}
|
|
.entityRegistryManager=${this._controller.getEntityRegistryManager()}
|
|
.autoHideState=${this._getAutoHideState()}
|
|
></advanced-camera-card-menu>
|
|
`;
|
|
}
|
|
|
|
protected _renderStatusBar(slot?: string): TemplateResult | void {
|
|
if (!this._config) {
|
|
return;
|
|
}
|
|
|
|
// Suppress the status bar entirely while a full-card issue (config
|
|
// error, lost connection, failed initialization) is rendering. Those
|
|
// take over the card and the status bar would be either redundant
|
|
// chrome (showing the engine icon etc. over an error) or physically
|
|
// obscuring the notification (in popup/overlay styles).
|
|
if (this._controller.getIssueManager().getStateManager().hasFullCardIssue()) {
|
|
return;
|
|
}
|
|
|
|
return html`
|
|
<advanced-camera-card-status-bar
|
|
slot=${ifDefined(slot)}
|
|
.items=${this._controller.getStatusBarItemManager().calculateItems({
|
|
statusConfig: this._config.status_bar,
|
|
cameraManager: this._controller.getCameraManager(),
|
|
view: this._controller.getViewManager().getView(),
|
|
mediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
|
issues: this._controller
|
|
.getIssueManager()
|
|
.getStateManager()
|
|
.getIssueDescriptions(),
|
|
})}
|
|
.config=${this._config.status_bar}
|
|
.autoHideState=${this._getAutoHideState()}
|
|
></advanced-camera-card-status-bar>
|
|
`;
|
|
}
|
|
|
|
protected updated(): void {
|
|
if (this._controller.getInitializationManager().areMandatoryAspectsInitialized()) {
|
|
void this._controller.getQueryStringManager().executeIfNecessary();
|
|
}
|
|
}
|
|
|
|
protected _renderInDialogIfNecessary(contents: TemplateResult): TemplateResult | void {
|
|
if (this._controller.getExpandManager().isExpanded()) {
|
|
return html` <web-dialog
|
|
open
|
|
center
|
|
@close=${() => {
|
|
this._controller.getExpandManager().setExpanded(false);
|
|
}}
|
|
>
|
|
${contents}
|
|
</web-dialog>`;
|
|
} else {
|
|
return contents;
|
|
}
|
|
}
|
|
|
|
protected render(): TemplateResult | void {
|
|
if (!this._hass) {
|
|
return;
|
|
}
|
|
|
|
const outerlayUsed =
|
|
this._config?.menu.style === 'outside' ||
|
|
this._config?.status_bar.style === 'outside';
|
|
|
|
const mainClasses = {
|
|
main: true,
|
|
'curve-top':
|
|
!outerlayUsed ||
|
|
(this._config?.menu.position !== 'top' &&
|
|
this._config?.status_bar.position !== 'top'),
|
|
'curve-bottom':
|
|
!outerlayUsed ||
|
|
(this._config?.menu.position !== 'bottom' &&
|
|
this._config?.status_bar.position !== 'bottom'),
|
|
};
|
|
|
|
const actions = this._controller.getActionsManager().getMergedActions();
|
|
const cameraManager = this._controller.getCameraManager();
|
|
const fullCardIssue = this._controller
|
|
.getIssueManager()
|
|
.getStateManager()
|
|
.getFullCardIssue();
|
|
|
|
const showLoading =
|
|
this._config?.performance?.features.card_loading_indicator !== false &&
|
|
!fullCardIssue;
|
|
|
|
// Caution: Keep the main div and the menu next to one another in order to
|
|
// ensure the hover menu styling continues to work.
|
|
return this._renderInDialogIfNecessary(
|
|
html` <advanced-camera-card-effects
|
|
.effectsManager=${this._controller.getEffectsManager()}
|
|
></advanced-camera-card-effects>
|
|
<ha-card
|
|
id="ha-card"
|
|
.actionHandler=${actionHandler({
|
|
hasHold: hasAction(actions.hold_action),
|
|
hasDoubleClick: hasAction(actions.double_tap_action),
|
|
})}
|
|
style="${styleMap(this._controller.getStyleManager().getAspectRatioStyle())}"
|
|
@advanced-camera-card:issue:notify=${(ev: CustomEvent<IssueKey>) =>
|
|
this._controller.getIssueManager().showNotification(ev.detail)}
|
|
@advanced-camera-card:issue:trigger=${({
|
|
detail: { key, ...context },
|
|
}: CustomEvent<IssueTriggerEventData>) =>
|
|
this._controller.getIssueManager().trigger(key, context)}
|
|
@advanced-camera-card:issue:resolve=${({
|
|
detail: { key, ...context },
|
|
}: CustomEvent<IssueResolveEventData>) =>
|
|
this._controller.getIssueManager().resolve(key, context)}
|
|
@advanced-camera-card:microphone:error=${({
|
|
detail,
|
|
}: CustomEvent<MicrophoneError>) =>
|
|
this._controller
|
|
.getCallManager()
|
|
.reportCallMicrophoneError(detail.targetID, detail.description)}
|
|
@advanced-camera-card:media:loaded=${(
|
|
ev: CustomEvent<MediaLoadedInfoEventDetail>,
|
|
) => this._controller.getMediaLoadedInfoManager().handleLoadEvent(ev)}
|
|
@advanced-camera-card:media:volumechange=${
|
|
() => this.requestUpdate() /* Refresh mute menu button */
|
|
}
|
|
@advanced-camera-card:media:play=${
|
|
() => this.requestUpdate() /* Refresh play/pause menu button */
|
|
}
|
|
@advanced-camera-card:media:pause=${
|
|
() => this.requestUpdate() /* Refresh play/pause menu button */
|
|
}
|
|
@advanced-camera-card:notification:dismiss=${() =>
|
|
this._controller.getNotificationManager().reset()}
|
|
>
|
|
${showLoading
|
|
? html`<advanced-camera-card-loading
|
|
.loaded=${this._controller
|
|
.getInitializationManager()
|
|
.getSessionManager()
|
|
.wasEverInitialized()}
|
|
.effectsManager=${this._config?.performance?.features
|
|
.card_loading_effects !== false
|
|
? this._controller.getEffectsManager()
|
|
: undefined}
|
|
></advanced-camera-card-loading>`
|
|
: ''}
|
|
${this._renderMenuStatusContainer('top')}
|
|
${this._renderMenuStatusContainer('overlay')}
|
|
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
|
|
<advanced-camera-card-views
|
|
${ref(this._refViews)}
|
|
.hass=${this._hass}
|
|
.stateWatcher=${this._controller.getHASSManager().getStateWatcher()}
|
|
.viewManagerEpoch=${this._controller.getViewManager().getEpoch()}
|
|
.cameraManager=${cameraManager}
|
|
.foldersManager=${this._controller.getFoldersManager()}
|
|
.viewItemManager=${this._controller.getViewItemManager()}
|
|
.resolvedMediaCache=${this._controller.getResolvedMediaCache()}
|
|
.config=${this._controller.getConfigManager().getConfig()}
|
|
.cardWideConfig=${this._controller.getConfigManager().getCardWideConfig()}
|
|
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
|
|
.configManager=${this._controller.getConfigManager()}
|
|
.hide=${!!fullCardIssue}
|
|
.microphoneManager=${this._controller.getMicrophoneManager()}
|
|
.microphoneState=${this._controller.getMicrophoneManager().getState()}
|
|
.call=${this._controller.getCallManager().getCall() ?? undefined}
|
|
.locked=${this._controller.getLockManager().isLocked()}
|
|
.conditionStateManager=${this._controller.getConditionStateManager()}
|
|
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
|
? this._controller.getCameraTriggersManager().getTriggeredCameraIDs()
|
|
: undefined}
|
|
.deviceRegistryManager=${this._controller.getDeviceRegistryManager()}
|
|
.issues=${this._controller
|
|
.getIssueManager()
|
|
.getStateManager()
|
|
.getIssuePresence()}
|
|
></advanced-camera-card-views>
|
|
${fullCardIssue ? renderNotificationBlock(fullCardIssue.notification) : ''}
|
|
</div>
|
|
${this._renderMenuStatusContainer('bottom')}
|
|
${this._config?.elements &&
|
|
this._controller.getInitializationManager().areMandatoryAspectsInitialized()
|
|
? // Elements need to render after the main views so it can render 'on
|
|
// top'. They are held until the card is initialized: the template
|
|
// renderer loads lazily as a mandatory init aspect (when the
|
|
// config uses templates), so rendering elements earlier could
|
|
// emit raw, unrendered templates or evaluate their visibility
|
|
// conditions before the renderer is available.
|
|
html` <advanced-camera-card-elements
|
|
${ref(this._refElements)}
|
|
.hass=${this._hass}
|
|
.elements=${this._config?.elements}
|
|
.conditionStateManager=${this._controller.getConditionStateManager()}
|
|
.templateRenderer=${this._controller.getTemplateManager()}
|
|
@advanced-camera-card:menu:add=${(ev: CustomEvent<MenuItem>) => {
|
|
this._menuButtonController.addDynamicMenuButton(ev.detail);
|
|
this.requestUpdate();
|
|
}}
|
|
@advanced-camera-card:menu:remove=${(ev: CustomEvent<MenuItem>) => {
|
|
this._menuButtonController.removeDynamicMenuButton(ev.detail);
|
|
this.requestUpdate();
|
|
}}
|
|
@advanced-camera-card:status-bar:add=${(
|
|
ev: CustomEvent<StatusBarItem>,
|
|
) => {
|
|
this._controller
|
|
.getStatusBarItemManager()
|
|
.addDynamicStatusBarItem(ev.detail);
|
|
}}
|
|
@advanced-camera-card:status-bar:remove=${(
|
|
ev: CustomEvent<StatusBarItem>,
|
|
) => {
|
|
this._controller
|
|
.getStatusBarItemManager()
|
|
.removeDynamicStatusBarItem(ev.detail);
|
|
}}
|
|
@advanced-camera-card:condition-state-manager:get=${(
|
|
ev: ConditionStateManagerGetEvent,
|
|
) => {
|
|
ev.conditionStateManager = this._controller.getConditionStateManager();
|
|
}}
|
|
@advanced-camera-card:template-renderer:get=${(
|
|
ev: TemplateRendererGetEvent,
|
|
) => {
|
|
ev.templateRenderer = this._controller.getTemplateManager();
|
|
}}
|
|
>
|
|
</advanced-camera-card-elements>`
|
|
: ``}
|
|
${this._controller.getNotificationManager().getNotification()
|
|
? html`<advanced-camera-card-notification
|
|
.notification=${this._controller
|
|
.getNotificationManager()
|
|
.getNotification()}
|
|
></advanced-camera-card-notification>`
|
|
: ''}
|
|
</ha-card>`,
|
|
);
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return unsafeCSS(cardStyle);
|
|
}
|
|
|
|
public getCardSize(): number {
|
|
// This method is called before the card is rendered. As such, we don't
|
|
// actually know what height the card will end up being, and for this card
|
|
// it may change significantly with usage. As such, we just return a fixed
|
|
// size guess (stock HA cards, such as the picture glance card, do similar).
|
|
|
|
// Lovelace card size is expressed in units of 50px. A 16:9 aspect-ratio
|
|
// camera will likely render as a 276.75px height masonary card => 5.52
|
|
// units of 50, round up to 6.
|
|
return 6;
|
|
}
|
|
}
|
|
|
|
// Keep the old name around for backwards compatibility.
|
|
@customElement('frigate-card')
|
|
class FrigateCard extends AdvancedCameraCard {}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'advanced-camera-card': AdvancedCameraCard;
|
|
'frigate-card': FrigateCard;
|
|
}
|
|
interface Window {
|
|
advancedCameraCards?: AdvancedCameraCard[];
|
|
}
|
|
}
|