diff --git a/src/card.ts b/src/card.ts index 8e919f83..375eb7b9 100644 --- a/src/card.ts +++ b/src/card.ts @@ -48,6 +48,7 @@ import { frigateCardHasAction, getActionConfigGivenAction, getCameraIcon, + getCameraID, getCameraTitle, homeAssistantSignPath, homeAssistantWSRequest, @@ -467,12 +468,7 @@ export class FrigateCard extends LitElement { } } - const id = - config.id || - config.camera_entity || - config.webrtc_card?.entity || - config.camera_name; - + const id = getCameraID(config); if (!id) { this._setMessageAndUpdate({ message: localize('error.no_camera_id'), diff --git a/src/common.ts b/src/common.ts index c9ca7153..de3f9bb8 100644 --- a/src/common.ts +++ b/src/common.ts @@ -28,6 +28,7 @@ import { FrigateEvent, MediaShowInfo, Message, + RawFrigateCardConfig, SignedPath, signedPathSchema, StateParameters, @@ -463,21 +464,53 @@ export function getEntityIcon( return hass && entity ? stateIcon(hass.states[entity]) : undefined; } +/** + * Get a camera id. + * @param config The camera config (either parsed or raw). + * @returns A camera id. + */ +export function getCameraID( + config?: CameraConfig | RawFrigateCardConfig | null, +): string { + return ( + (typeof config?.id === 'string' && config.id) || + (typeof config?.camera_entity === 'string' && config.camera_entity) || + (typeof config?.webrtc_card === 'object' && config.webrtc_card && typeof config.webrtc_card['entity'] === 'string' && config.webrtc_card['entity']) || + (typeof config?.camera_name === 'string' && config.camera_name) || + '' + ); +} + /** * Get a camera text title. * @param hass The Home Assistant object. - * @param config The camera config. + * @param config The camera config (either parsed or raw). * @returns A title string. */ export function getCameraTitle( hass?: HomeAssistant, - config?: CameraConfig | null, + config?: CameraConfig | RawFrigateCardConfig | null, ): string { + // Attempt to render a recognizable name for the camera, + // starting with the most likely to be useful and working our + // ways towards the least useful. Extra type checking here since this is also + // used on raw configuration in the editor. return ( - config?.title || - (config?.camera_entity ? getEntityTitle(hass, config.camera_entity) : '') || - (config?.camera_name ? prettifyFrigateName(config.camera_name) : '') || - config?.id || + (typeof config?.title === 'string' && config.title) || + (typeof config?.id === 'string' && config.id) || + [ + typeof config?.camera_entity === 'string' + ? getEntityTitle(hass, config.camera_entity) + : '', + typeof config?.client_id === 'string' ? config.client_id : '', + typeof config?.camera_name === 'string' + ? prettifyFrigateName(config.camera_name) + : '', + typeof config?.label === 'string' ? prettifyFrigateName(config.label) : '', + typeof config?.zone === 'string' ? prettifyFrigateName(config.zone) : '', + ] + .filter(Boolean) + .join(' / ') || '' ); } diff --git a/src/const.ts b/src/const.ts index 193b9661..aa3915b3 100644 --- a/src/const.ts +++ b/src/const.ts @@ -18,6 +18,8 @@ export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY = export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL = `${CONF_CAMERAS}.#.webrtc_card.url` as const; export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER = `${CONF_CAMERAS}.#.live_provider` as const; +export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS = + `${CONF_CAMERAS}.#.dependent_cameras` as const; export const CONF_VIEW = 'view' as const; export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const; diff --git a/src/editor.ts b/src/editor.ts index 851c2f21..4e708d67 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -6,6 +6,7 @@ import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helper import { localize } from './localize/localize.js'; import { BUTTON_SIZE_MIN, + CameraConfig, RawFrigateCardConfig, RawFrigateCardConfigArray, THUMBNAIL_WIDTH_MAX, @@ -18,6 +19,7 @@ import { CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_CAMERA_NAME, CONF_CAMERAS_ARRAY_CLIENT_ID, + CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS, CONF_CAMERAS_ARRAY_ICON, CONF_CAMERAS_ARRAY_ID, CONF_CAMERAS_ARRAY_LABEL, @@ -88,7 +90,13 @@ import { CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, } from './const.js'; -import { arrayMove, getEntityTitle, prettifyFrigateName } from './common.js'; +import { + arrayMove, + getCameraID, + getCameraTitle, + getEntityTitle, + prettifyFrigateName, +} from './common.js'; import { copyConfig, deleteConfigValue, @@ -119,6 +127,11 @@ interface EditorOptionSetTarget { optionSetName: string; } +interface EditorSelectOption { + value: string; + label: string; +} + const options: EditorOptions = { cameras: { icon: 'video', @@ -193,7 +206,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor @property({ attribute: false }) protected _expandedCameraIndex: number | null = null; - protected _viewModes = [ + protected _viewModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'live', label: localize('config.view.views.live') }, { value: 'clips', label: localize('config.view.views.clips') }, @@ -204,12 +217,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { value: 'timeline', label: localize('config.view.views.timeline') }, ]; - protected _cameraSelectViewModes = [ + protected _cameraSelectViewModes: EditorSelectOption[] = [ ...this._viewModes, { value: 'current', label: localize('config.view.views.current') }, ]; - protected _menuModes = [ + protected _menuModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'none', label: localize('config.menu.modes.none') }, { value: 'hidden-top', label: localize('config.menu.modes.hidden-top') }, @@ -228,7 +241,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { value: 'below', label: localize('config.menu.modes.below') }, ]; - protected _eventViewerNextPreviousControlStyles = [ + protected _eventViewerNextPreviousControlStyles: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'thumbnails', @@ -244,7 +257,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor }, ]; - protected _liveNextPreviousControlStyles = [ + protected _liveNextPreviousControlStyles: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'chevrons', @@ -257,7 +270,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { value: 'none', label: localize('config.live.controls.next_previous.styles.none') }, ]; - protected _aspectRatioModes = [ + protected _aspectRatioModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'dynamic', @@ -270,7 +283,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor }, ]; - protected _thumbnailModes = [ + protected _thumbnailModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'none', @@ -294,7 +307,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor }, ]; - protected _thumbnailMedias = [ + protected _thumbnailMedias: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'clips', label: localize('config.live.controls.thumbnails.medias.clips') }, { @@ -303,7 +316,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor }, ]; - protected _titleModes = [ + protected _titleModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'none', label: localize('config.event_viewer.controls.title.modes.none') }, { @@ -324,27 +337,27 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor }, ]; - protected _transitionEffects = [ + protected _transitionEffects: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'none', label: localize('config.event_viewer.transition_effects.none') }, { value: 'slide', label: localize('config.event_viewer.transition_effects.slide') }, ]; - protected _imageModes = [ + protected _imageModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'camera', label: localize('config.image.modes.camera') }, { value: 'screensaver', label: localize('config.image.modes.screensaver') }, { value: 'url', label: localize('config.image.modes.url') }, ]; - protected _timelineMediaTypes = [ + protected _timelineMediaTypes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'all', label: localize('config.timeline.medias.all') }, { value: 'clips', label: localize('config.timeline.medias.clips') }, { value: 'snapshots', label: localize('config.timeline.medias.snapshots') }, ]; - protected _darkModes = [ + protected _darkModes: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'on', label: localize('config.view.dark_modes.on') }, { value: 'off', label: localize('config.view.dark_modes.off') }, @@ -456,6 +469,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor protected _renderOptionSelector( configPath: string, options: string[] | { value: string; label: string }[], + multiple?: boolean, ): TemplateResult | void { if (!this._config) { return; @@ -464,7 +478,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor return html` ${info}`; } + /** + * Get an editor title for the camera. + * @param cameraIndex The index of the camera in the cameras array. + * @param cameraConfig The raw camera configuration object. + * @returns A string title. + */ + protected _getEditorCameraTitle( + cameraIndex: number, + cameraConfig: RawFrigateCardConfig, + ): string { + return ( + getCameraTitle(this.hass, cameraConfig) || + localize('editor.camera') + ' #' + cameraIndex + ); + } + /** * Render a camera header. * @param cameraIndex The index of the camera to edit/add. @@ -540,31 +572,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ? html` [${localize('editor.add_new_camera')}...] ` - : // Attempt to render a recognizable name for the camera, - // starting with the most likely to be useful and working our - // ways towards the least useful. - html` - ${cameraConfig?.title || - cameraConfig?.id || - [ - cameraConfig?.camera_entity - ? getEntityTitle(this.hass, String(cameraConfig.camera_entity)) - : '', - cameraConfig?.client_id, - cameraConfig?.camera_name - ? prettifyFrigateName(String(cameraConfig.camera_name)) - : '', - cameraConfig?.label - ? prettifyFrigateName(String(cameraConfig.label)) - : '', - cameraConfig?.zone - ? prettifyFrigateName(String(cameraConfig.zone)) - : '', - ] - .filter(Boolean) - .join(' / ') || - localize('editor.camera') + ' #' + cameraIndex} - `} + : html`${this._getEditorCameraTitle(cameraIndex, cameraConfig || {})}`} `; @@ -582,7 +592,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor cameraIndex: number, addNewCamera?: boolean, ): TemplateResult | void { - const liveProviders = [ + const liveProviders: EditorSelectOption[] = [ { value: '', label: '' }, { value: 'auto', label: localize('config.cameras.live_providers.auto') }, { value: 'ha', label: localize('config.cameras.live_providers.ha') }, @@ -596,6 +606,16 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor }, ]; + const dependentCameras: EditorSelectOption[] = []; + cameras.forEach((camera, index) => { + if (index !== cameraIndex) { + dependentCameras.push({ + value: getCameraID(camera), + label: this._getEditorCameraTitle(index, camera), + }); + } + }); + // Make a new config and update the editor with changes on it, const modifyConfig = (func: (config: RawFrigateCardConfig) => boolean): void => { if (this._config) { @@ -711,6 +731,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, cameraIndex), )} + ${this._renderOptionSelector( + getArrayConfigPath(CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS, cameraIndex), + dependentCameras, + true, + )} ` : ``} `; @@ -841,10 +866,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_VIEW_CAMERA_SELECT, this._cameraSelectViewModes, )} - ${this._renderOptionSelector( - CONF_VIEW_DARK_MODE, - this._darkModes, - )} + ${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)} ${this._renderNumberInput(CONF_VIEW_TIMEOUT_SECONDS)} ${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)} ${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)} diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 6d3dfa2f..662f6694 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -14,6 +14,7 @@ "camera_entity": "Camera Entity", "camera_name": "Frigate camera name (Autodetected from entity)", "client_id": "Frigate client id (For >1 Frigate server)", + "dependent_cameras": "Dependent cameras to also show events for", "id": "Unique id for this camera in this card", "label": "Frigate label/object filter", "frigate_url": "Frigate server URL", diff --git a/src/scss/editor.scss b/src/scss/editor.scss index 7dfc161e..8384b604 100644 --- a/src/scss/editor.scss +++ b/src/scss/editor.scss @@ -28,10 +28,6 @@ display: grid; margin-bottom: 10px; } -ha-formfield { - padding-bottom: 8px; -} - div.upgrade { width: auto; border: 1px dotted var(--primary-color); @@ -78,4 +74,9 @@ div.upgrade span { } span.info { padding: 4px; -} \ No newline at end of file +} + +ha-selector { + padding: 10px; + border: 1px solid var(--divider-color); +}