Filter hidden media players out of the media player menu.

This commit is contained in:
Dermot Duffy
2023-02-12 17:30:30 -08:00
parent 1d826cf415
commit 8c1fa511b0
4 changed files with 143 additions and 55 deletions
+15 -1
View File
@@ -1,8 +1,11 @@
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { localize } from '../localize/localize';
import { CameraConfig, CardWideConfig } from '../types'; import { CameraConfig, CardWideConfig } from '../types';
import { EntityRegistryManager } from '../utils/ha/entity-registry'; import { EntityRegistryManager } from '../utils/ha/entity-registry';
import { Entity } from '../utils/ha/entity-registry/types';
import { RecordingSegmentsCache, RequestCache } from './cache'; import { RecordingSegmentsCache, RequestCache } from './cache';
import { CameraManagerEngine } from './engine'; import { CameraManagerEngine } from './engine';
import { CameraInitializationError } from './error';
import { FrigateCameraManagerEngine } from './frigate/engine-frigate'; import { FrigateCameraManagerEngine } from './frigate/engine-frigate';
import { GenericCameraManagerEngine } from './generic/engine-generic'; import { GenericCameraManagerEngine } from './generic/engine-generic';
import { Engine } from './types'; import { Engine } from './types';
@@ -51,7 +54,18 @@ export class CameraManagerEngineFactory {
const cameraEntity = cameraConfig.camera_entity; const cameraEntity = cameraConfig.camera_entity;
if (cameraEntity) { if (cameraEntity) {
const entity = await this._entityRegistryManager.getEntity(hass, cameraEntity); let entity: Entity | null;
try {
entity = await this._entityRegistryManager.getEntity(hass, cameraEntity);
} catch (e) {
// Throw a slightly friendlier exception (as a typo in the entity is
// likely to be a common failure mode).
throw new CameraInitializationError(
localize('error.no_camera_entity'),
cameraConfig,
);
}
switch (entity?.platform) { switch (entity?.platform) {
case 'frigate': case 'frigate':
engine = Engine.Frigate; engine = Engine.Frigate;
+104 -51
View File
@@ -210,6 +210,8 @@ class FrigateCard extends LitElement {
protected _conditionManager: CardConditionManager | null = null; protected _conditionManager: CardConditionManager | null = null;
protected _mediaPlayers?: string[];
constructor() { constructor() {
super(); super();
this._entityRegistryManager = new EntityRegistryManager( this._entityRegistryManager = new EntityRegistryManager(
@@ -551,29 +553,12 @@ class FrigateCard extends LitElement {
}); });
} }
const isValidMediaPlayer = (entity: string): boolean => {
if (entity.startsWith('media_player.')) {
const stateObj = this._hass?.states[entity];
if ( if (
stateObj && this._mediaPlayers?.length &&
stateObj.state !== 'unavailable' &&
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
) {
return true;
}
}
return false;
};
const mediaPlayers = Object.keys(this._hass?.states || {}).filter(
isValidMediaPlayer,
);
if (
mediaPlayers.length &&
(this._view?.isViewerView() || (this._view?.isViewerView() ||
(this._view?.is('live') && selectedCameraConfig?.camera_entity)) (this._view?.is('live') && selectedCameraConfig?.camera_entity))
) { ) {
const mediaPlayerItems = mediaPlayers.map((playerEntityID) => { const mediaPlayerItems = this._mediaPlayers.map((playerEntityID) => {
const title = getEntityTitle(this._hass, playerEntityID) || playerEntityID; const title = getEntityTitle(this._hass, playerEntityID) || playerEntityID;
const state = this._hass?.states[playerEntityID]; const state = this._hass?.states[playerEntityID];
const playAction = createFrigateCardCustomAction('media_player', { const playAction = createFrigateCardCustomAction('media_player', {
@@ -865,16 +850,45 @@ class FrigateCard extends LitElement {
}); });
} }
protected async _initializeCameras(): Promise<void> { protected async _initialize(
if (!this._hass || !this._cameraManager) { hass: HomeAssistant,
return; config: FrigateCardConfig,
cardWideConfig: CardWideConfig,
): Promise<void> {
// Above arguments are taken (vs usage of `this`) as they must exist prior
// to initialization and this ensures it is the callers responsibility to
// verify that.
await Promise.all([
// Side load Home Assistant elements used in the UI.
sideLoadHomeAssistantElements(),
// Load dynamic language imports.
loadLanguages(),
]);
await this._initializeCameras(hass, config, cardWideConfig);
// Don't reset the message which may be set to an error above. This sets the
// first view using the newly loaded cameras.
this._changeView({ resetMessage: false });
} }
protected async _initializeCameras(
hass: HomeAssistant,
config: FrigateCardConfig,
cardWideConfig: CardWideConfig,
): Promise<void> {
this._cameraManager = new CameraManager(
new CameraManagerEngineFactory(this._entityRegistryManager, cardWideConfig),
this._cardWideConfig,
);
try { try {
await this._cameraManager.initializeCameras( await this._cameraManager.initializeCameras(
this._hass, hass,
this._entityRegistryManager, this._entityRegistryManager,
this._getConfig().cameras, config.cameras,
); );
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
@@ -888,36 +902,74 @@ class FrigateCard extends LitElement {
}); });
} }
} }
}
// Don't reset the message which may be set to an error above. This sets the protected async _initializeMediaPlayers(hass: HomeAssistant): Promise<void> {
// first view using the newly loaded cameras. const isValidMediaPlayer = (entityID: string): boolean => {
this._changeView({ resetMessage: false }); if (entityID.startsWith('media_player.')) {
const stateObj = this._hass?.states[entityID];
if (
stateObj &&
stateObj.state !== 'unavailable' &&
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
) {
return true;
}
}
return false;
};
const mediaPlayers = Object.keys(this._hass?.states || {}).filter(
isValidMediaPlayer,
);
let mediaPlayerEntities: Map<string, Entity>;
try {
mediaPlayerEntities = await this._entityRegistryManager.getEntities(
hass,
mediaPlayers,
);
} catch (e) {
// Failing to fetch media player information is not considered
// sufficiently serious to block card startup.
errorToConsole(e as Error);
return;
}
// Filter out entities that are marked as hidden (this information is not
// available in the HA state, only in the registry).
this._mediaPlayers = [...mediaPlayerEntities.values()]
.filter((entity) => !entity.hidden_by)
.map((entity) => entity.entity_id);
} }
/** /**
* Called before each update. * Called before each update.
*/ */
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
if (
this._cardWideConfig &&
(!this._cameraManager ||
changedProps.has('_config') ||
changedProps.has('_cardWideConfig'))
) {
this._cameraManager = new CameraManager(
new CameraManagerEngineFactory(
this._entityRegistryManager,
this._cardWideConfig,
),
this._cardWideConfig,
);
this._initializeCameras().then(() => this.requestUpdate());
}
if (changedProps.has('_cardWideConfig')) { if (changedProps.has('_cardWideConfig')) {
setPerformanceCSSStyles(this, this._cardWideConfig?.performance); setPerformanceCSSStyles(this, this._cardWideConfig?.performance);
} }
if (
this._hass &&
!this._mediaPlayers &&
this._getConfig().menu.buttons.media_player.enabled
) {
// Media players are initialized outside the main initialization code (the
// `initialize` method) since they may be required depending on an
// overridable configuration value.
// We also want to initialize media players after since the main camera
// initialization since that may have fetched entity information that will
// be cached and re-used here (minor performance optimization). Only do
// this if the media player button is enabled to further limit the
// performance implications.
// Prevent a double initialization.
this._mediaPlayers = [];
this._initializeMediaPlayers(this._hass);
}
if (this._view?.is('live')) { if (this._view?.is('live')) {
import('./components/live/live.js'); import('./components/live/live.js');
} else if (this._view?.isGalleryView()) { } else if (this._view?.isGalleryView()) {
@@ -1039,12 +1091,6 @@ class FrigateCard extends LitElement {
} }
} }
/**
* Initialize the card.
*/
protected async _initialize(): Promise<void> {
await Promise.all([sideLoadHomeAssistantElements(), loadLanguages()]);
}
/** /**
* Determine whether the element should be updated. * Determine whether the element should be updated.
* @param changedProps The changed properties if any. * @param changedProps The changed properties if any.
@@ -1053,8 +1099,15 @@ class FrigateCard extends LitElement {
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
// Load the relevant languages. Cannot do anything until then. // Load the relevant languages. Cannot do anything until then.
if (this._initialized !== 'initialized') { if (this._initialized !== 'initialized') {
if (this._initialized !== 'initializing') { const config = this._getConfig();
this._initialize().then(() => { if (
this._initialized !== 'initializing' &&
this._hass &&
config &&
this._cardWideConfig
) {
this._initialized = 'initializing';
this._initialize(this._hass, config, this._cardWideConfig).then(() => {
this._initialized = 'initialized'; this._initialized = 'initialized';
this.requestUpdate(); this.requestUpdate();
}); });
+4 -1
View File
@@ -95,9 +95,12 @@ const LOW_PROFILE_DEFAULTS = {
// Hide several buttons that are otherwise visible by default. // Hide several buttons that are otherwise visible by default.
[`${CONF_MENU_BUTTONS_FRIGATE}.enabled`]: false, [`${CONF_MENU_BUTTONS_FRIGATE}.enabled`]: false,
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false, [`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
[`${CONF_MENU_BUTTONS_MEDIA_PLAYER}.enabled`]: false,
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false, [`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
// If the media player button is present media player entity fetches are
// required on initialization.
[`${CONF_MENU_BUTTONS_MEDIA_PLAYER}.enabled`]: false,
// Disable all options in thumbnails. // Disable all options in thumbnails.
[CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false, [CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
+19 -1
View File
@@ -42,7 +42,10 @@ export class EntityRegistryManager {
return await this.getExtendedEntity(hass, entityID); return await this.getExtendedEntity(hass, entityID);
} }
public async getMatchingEntities(hass: HomeAssistant, func: (arg: Entity) => boolean): Promise<Entity[]> { public async getMatchingEntities(
hass: HomeAssistant,
func: (arg: Entity) => boolean,
): Promise<Entity[]> {
await this.fetchEntityList(hass); await this.fetchEntityList(hass);
return this._cache.getMatches(func); return this._cache.getMatches(func);
} }
@@ -67,6 +70,21 @@ export class EntityRegistryManager {
return extendedEntity; return extendedEntity;
} }
public async getEntities(
hass: HomeAssistant,
entityIDs: string[],
): Promise<Map<string, Entity>> {
const output: Map<string, Entity> = new Map();
const _storeEntity = async (entityID: string): Promise<void> => {
const entity = await this.getEntity(hass, entityID);
if (entity) {
output.set(entityID, entity);
}
};
await Promise.all(entityIDs.map(_storeEntity));
return output;
}
public async getExtendedEntities( public async getExtendedEntities(
hass: HomeAssistant, hass: HomeAssistant,
entityIDs: string[], entityIDs: string[],