Autodetect motion and occupancy sensors.
This commit is contained in:
+143
-41
@@ -48,23 +48,21 @@ import './patches/ha-camera-stream.js';
|
|||||||
import './patches/ha-hls-player.js';
|
import './patches/ha-hls-player.js';
|
||||||
import './patches/ha-web-rtc-player.ts';
|
import './patches/ha-web-rtc-player.ts';
|
||||||
import cardStyle from './scss/card.scss';
|
import cardStyle from './scss/card.scss';
|
||||||
import type {
|
|
||||||
Entity,
|
|
||||||
ExtendedHomeAssistant,
|
|
||||||
FrigateCardConfig,
|
|
||||||
MediaShowInfo,
|
|
||||||
MenuButton,
|
|
||||||
Message
|
|
||||||
} from './types.js';
|
|
||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
ActionType,
|
ActionType,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
entitySchema,
|
EntityList,
|
||||||
|
ExtendedEntity,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
|
FrigateCardConfig,
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
FrigateCardView,
|
FrigateCardView,
|
||||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
|
MediaShowInfo,
|
||||||
|
MenuButton,
|
||||||
|
Message,
|
||||||
RawFrigateCardConfig
|
RawFrigateCardConfig
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
@@ -81,15 +79,20 @@ import {
|
|||||||
getEntityTitle,
|
getEntityTitle,
|
||||||
getHassDifferences,
|
getHassDifferences,
|
||||||
homeAssistantSignPath,
|
homeAssistantSignPath,
|
||||||
homeAssistantWSRequest,
|
|
||||||
isHassDifferent,
|
isHassDifferent,
|
||||||
isTriggeredState,
|
isTriggeredState,
|
||||||
sideLoadHomeAssistantElements
|
sideLoadHomeAssistantElements
|
||||||
} from './utils/ha';
|
} from './utils/ha';
|
||||||
import { getEventID } from './utils/ha/browse-media.js';
|
import { getEventID } from './utils/ha/browse-media.js';
|
||||||
|
import {
|
||||||
|
ExtendedEntityCache,
|
||||||
|
getAllEntities,
|
||||||
|
getExtendedEntities,
|
||||||
|
getExtendedEntity
|
||||||
|
} from './utils/ha/entity-registry.js';
|
||||||
|
import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
|
||||||
import { supportsFeature } from './utils/ha/update.js';
|
import { supportsFeature } from './utils/ha/update.js';
|
||||||
import { isValidMediaShowInfo } from './utils/media-info.js';
|
import { isValidMediaShowInfo } from './utils/media-info.js';
|
||||||
import { ResolvedMediaCache } from './utils/resolved-media.js';
|
|
||||||
import { View } from './view.js';
|
import { View } from './view.js';
|
||||||
|
|
||||||
/** A note on media callbacks:
|
/** A note on media callbacks:
|
||||||
@@ -579,23 +582,143 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the motion sensor entity for a given camera.
|
||||||
|
* @param cache The ExtendedEntityCache of entity registry information.
|
||||||
|
* @param cameraConfig The camera config in question.
|
||||||
|
* @returns The entity id of the motion sensor or null.
|
||||||
|
*/
|
||||||
|
protected _getMotionSensor(
|
||||||
|
cache: ExtendedEntityCache,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): string | null {
|
||||||
|
if (cameraConfig.camera_name) {
|
||||||
|
return (
|
||||||
|
cache.getMatch(
|
||||||
|
(ent) =>
|
||||||
|
!!ent.unique_id?.match(
|
||||||
|
new RegExp(
|
||||||
|
`:motion_sensor:${cameraConfig.zone || cameraConfig.camera_name}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)?.entity_id ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the occupancy sensor entity for a given camera.
|
||||||
|
* @param cache The ExtendedEntityCache of entity registry information.
|
||||||
|
* @param cameraConfig The camera config in question.
|
||||||
|
* @returns The entity id of the occupancy sensor or null.
|
||||||
|
*/
|
||||||
|
protected _getOccupancySensor(
|
||||||
|
cache: ExtendedEntityCache,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): string | null {
|
||||||
|
if (cameraConfig.camera_name) {
|
||||||
|
return (
|
||||||
|
cache.getMatch(
|
||||||
|
(ent) =>
|
||||||
|
!!ent.unique_id?.match(
|
||||||
|
new RegExp(
|
||||||
|
`:occupancy_sensor:${cameraConfig.zone || cameraConfig.camera_name}_${
|
||||||
|
cameraConfig.label || 'all'
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)?.entity_id ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fully load the configured cameras.
|
* Fully load the configured cameras.
|
||||||
*/
|
*/
|
||||||
protected async _loadCameras(): Promise<void> {
|
protected async _loadCameras(): Promise<void> {
|
||||||
|
if (!this._hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new ExtendedEntityCache();
|
||||||
|
let entityList: EntityList | undefined;
|
||||||
|
try {
|
||||||
|
entityList = await getAllEntities(this._hass);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, (e as Error).stack);
|
||||||
|
}
|
||||||
|
|
||||||
const cameras: Map<string, CameraConfig> = new Map();
|
const cameras: Map<string, CameraConfig> = new Map();
|
||||||
let errorFree = true;
|
let errorFree = true;
|
||||||
|
|
||||||
const addCameraConfig = async (config: CameraConfig) => {
|
const addCameraConfig = async (config: CameraConfig) => {
|
||||||
if (!config.camera_name && config.camera_entity) {
|
if (!this._hass) {
|
||||||
const resolvedName = await this._getFrigateCameraNameFromEntity(
|
return;
|
||||||
config.camera_entity,
|
}
|
||||||
);
|
|
||||||
|
let entity: ExtendedEntity | null = null;
|
||||||
|
if (config.camera_entity) {
|
||||||
|
try {
|
||||||
|
entity = await getExtendedEntity(this._hass, config.camera_entity, cache);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, (e as Error).stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.camera_name && entity) {
|
||||||
|
const resolvedName = this._getFrigateCameraNameFromEntity(entity);
|
||||||
if (resolvedName) {
|
if (resolvedName) {
|
||||||
config.camera_name = resolvedName;
|
config.camera_name = resolvedName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entity && entityList) {
|
||||||
|
// Try to find the correct entities for the motion & occupancy sensors.
|
||||||
|
// We know they are binary_sensors, and that they'll have the same
|
||||||
|
// config entry ID as the camera. Searching via unique_id ensures this
|
||||||
|
// search still works if the user renames the entity_id.
|
||||||
|
const binarySensorEntities = entityList.filter(
|
||||||
|
(ent) =>
|
||||||
|
ent.config_entry_id === entity?.config_entry_id &&
|
||||||
|
!ent.disabled_by &&
|
||||||
|
ent.entity_id.startsWith('binary_sensor.'),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getExtendedEntities(
|
||||||
|
this._hass,
|
||||||
|
binarySensorEntities.map((ent) => ent.entity_id),
|
||||||
|
cache,
|
||||||
|
);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e, (e as Error).stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.trigger_by_motion) {
|
||||||
|
const motionEntity = this._getMotionSensor(cache, config);
|
||||||
|
if (motionEntity) {
|
||||||
|
config.trigger_by_entities.push(motionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.trigger_by_occupancy) {
|
||||||
|
const occupancyEntity = this._getOccupancySensor(cache, config);
|
||||||
|
if (occupancyEntity) {
|
||||||
|
config.trigger_by_entities.push(occupancyEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this auto-detection information.
|
||||||
|
console.info(
|
||||||
|
`Trigger entities sensor for ${entity.entity_id} are ${JSON.stringify(
|
||||||
|
config.trigger_by_entities,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
config.trigger_by_entities = [...new Set(config.trigger_by_entities)];
|
||||||
|
|
||||||
const id = getCameraID(config);
|
const id = getCameraID(config);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
this._setMessageAndUpdate({
|
this._setMessageAndUpdate({
|
||||||
@@ -649,37 +772,16 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Frigate camera name from an entity name.
|
* Get the Frigate camera name from an entity.
|
||||||
* @returns The Frigate camera name or null if unavailable.
|
* @returns The Frigate camera name or null if unavailable.
|
||||||
*/
|
*/
|
||||||
protected async _getFrigateCameraNameFromEntity(
|
protected _getFrigateCameraNameFromEntity(entity: ExtendedEntity): string | null {
|
||||||
entity: string,
|
if (entity.unique_id && entity.platform === 'frigate') {
|
||||||
): Promise<string | null> {
|
const match = entity.unique_id.match(/:camera:(?<camera>[^:]+)$/);
|
||||||
if (!this._hass) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find entity unique_id in registry.
|
|
||||||
const request = {
|
|
||||||
type: 'config/entity_registry/get',
|
|
||||||
entity_id: entity,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const entityResult = await homeAssistantWSRequest<Entity>(
|
|
||||||
this._hass,
|
|
||||||
entitySchema,
|
|
||||||
request,
|
|
||||||
);
|
|
||||||
if (entityResult && entityResult.platform == 'frigate') {
|
|
||||||
const match = entityResult.unique_id.match(/:camera:(?<camera>[^:]+)$/);
|
|
||||||
if (match && match.groups) {
|
if (match && match.groups) {
|
||||||
return match.groups['camera'];
|
return match.groups['camera'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
|
||||||
// Pass.
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1498,7 +1600,7 @@ export class FrigateCard extends LitElement {
|
|||||||
container: true,
|
container: true,
|
||||||
outer: true,
|
outer: true,
|
||||||
triggered: !!this._triggered && this._getConfig().view.scan.trigger_show_border,
|
triggered: !!this._triggered && this._getConfig().view.scan.trigger_show_border,
|
||||||
}
|
};
|
||||||
|
|
||||||
const contentClasses = {
|
const contentClasses = {
|
||||||
'frigate-card-contents': true,
|
'frigate-card-contents': true,
|
||||||
|
|||||||
@@ -1090,7 +1090,8 @@ export class FrigateCardLiveJSMPEG extends LitElement {
|
|||||||
if (!this.cameraConfig?.camera_name) {
|
if (!this.cameraConfig?.camera_name) {
|
||||||
return dispatchErrorMessageEvent(
|
return dispatchErrorMessageEvent(
|
||||||
this,
|
this,
|
||||||
localize('error.no_camera_name') + `: ${JSON.stringify(this.cameraConfig)}`,
|
localize('error.no_camera_name'),
|
||||||
|
this.cameraConfig,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
overrideMultiBrowseMediaQueryParameters
|
overrideMultiBrowseMediaQueryParameters
|
||||||
} from '../utils/ha/browse-media.js';
|
} from '../utils/ha/browse-media.js';
|
||||||
import { createMediaShowInfo } from '../utils/media-info.js';
|
import { createMediaShowInfo } from '../utils/media-info.js';
|
||||||
import { ResolvedMediaCache, resolveMedia } from '../utils/resolved-media.js';
|
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||||
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
|
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
|
||||||
|
|||||||
+10
-2
@@ -23,6 +23,12 @@ export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER =
|
|||||||
`${CONF_CAMERAS}.#.live_provider` as const;
|
`${CONF_CAMERAS}.#.live_provider` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS =
|
export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS =
|
||||||
`${CONF_CAMERAS}.#.dependent_cameras` as const;
|
`${CONF_CAMERAS}.#.dependent_cameras` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_TRIGGER_BY_MOTION =
|
||||||
|
`${CONF_CAMERAS}.#.trigger_by_motion` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_TRIGGER_BY_OCCUPANCY =
|
||||||
|
`${CONF_CAMERAS}.#.trigger_by_occupancy` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_TRIGGER_BY_ENTITIES =
|
||||||
|
`${CONF_CAMERAS}.#.trigger_by_entities` as const;
|
||||||
|
|
||||||
export const CONF_VIEW = 'view' as const;
|
export const CONF_VIEW = 'view' as const;
|
||||||
export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const;
|
export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const;
|
||||||
@@ -35,8 +41,10 @@ export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const
|
|||||||
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
|
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
|
||||||
export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const;
|
export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const;
|
||||||
export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const;
|
export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const;
|
||||||
export const CONF_VIEW_SCAN_TRIGGER_MIN_SECONDS = `${CONF_VIEW_SCAN}.trigger_min_seconds` as const;
|
export const CONF_VIEW_SCAN_TRIGGER_MIN_SECONDS =
|
||||||
export const CONF_VIEW_SCAN_TRIGGER_SHOW_BORDER = `${CONF_VIEW_SCAN}.trigger_show_border` as const;
|
`${CONF_VIEW_SCAN}.trigger_min_seconds` as const;
|
||||||
|
export const CONF_VIEW_SCAN_TRIGGER_SHOW_BORDER =
|
||||||
|
`${CONF_VIEW_SCAN}.trigger_show_border` as const;
|
||||||
|
|
||||||
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
|
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
|
||||||
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS =
|
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS =
|
||||||
|
|||||||
+24
-21
@@ -21,6 +21,9 @@ import {
|
|||||||
CONF_CAMERAS_ARRAY_LABEL,
|
CONF_CAMERAS_ARRAY_LABEL,
|
||||||
CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
|
CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
|
||||||
CONF_CAMERAS_ARRAY_TITLE,
|
CONF_CAMERAS_ARRAY_TITLE,
|
||||||
|
CONF_CAMERAS_ARRAY_TRIGGER_BY_ENTITIES,
|
||||||
|
CONF_CAMERAS_ARRAY_TRIGGER_BY_MOTION,
|
||||||
|
CONF_CAMERAS_ARRAY_TRIGGER_BY_OCCUPANCY,
|
||||||
CONF_CAMERAS_ARRAY_URL,
|
CONF_CAMERAS_ARRAY_URL,
|
||||||
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
|
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
|
||||||
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
|
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
|
||||||
@@ -103,7 +106,7 @@ import {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { arrayMove } from './utils/basic.js';
|
import { arrayMove } from './utils/basic.js';
|
||||||
import { getCameraID, getCameraTitle } from './utils/camera.js';
|
import { getCameraID, getCameraTitle } from './utils/camera.js';
|
||||||
import { sideLoadHomeAssistantElements } from './utils/ha';
|
import { getEntitiesFromHASS, sideLoadHomeAssistantElements } from './utils/ha';
|
||||||
|
|
||||||
const MENU_BUTTONS = 'buttons';
|
const MENU_BUTTONS = 'buttons';
|
||||||
const MENU_CAMERAS = 'cameras';
|
const MENU_CAMERAS = 'cameras';
|
||||||
@@ -403,20 +406,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getEntities(domain: string): string[] {
|
|
||||||
if (!this.hass) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const entities = Object.keys(this.hass.states).filter(
|
|
||||||
(eid) => eid.substr(0, eid.indexOf('.')) === domain,
|
|
||||||
);
|
|
||||||
entities.sort();
|
|
||||||
|
|
||||||
// Add a blank entry to unset a selection.
|
|
||||||
entities.unshift('');
|
|
||||||
return entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render an option set header
|
* Render an option set header
|
||||||
* @param optionSetName The name of the EditorOptionsSet.
|
* @param optionSetName The name of the EditorOptionsSet.
|
||||||
@@ -615,9 +604,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
.key=${true}
|
.key=${true}
|
||||||
>
|
>
|
||||||
<ha-icon .icon=${'mdi:target-account'}></ha-icon>
|
<ha-icon .icon=${'mdi:target-account'}></ha-icon>
|
||||||
<span
|
<span>${localize(`config.${CONF_VIEW_SCAN}.scan_mode`)}</span>
|
||||||
>${localize(`config.${CONF_VIEW_SCAN}.scan_mode`)}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
${this._expandedMenus[MENU_VIEW_SCAN]
|
${this._expandedMenus[MENU_VIEW_SCAN]
|
||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
@@ -743,6 +730,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
protected _renderCamera(
|
protected _renderCamera(
|
||||||
cameras: RawFrigateCardConfigArray,
|
cameras: RawFrigateCardConfigArray,
|
||||||
cameraIndex: number,
|
cameraIndex: number,
|
||||||
|
entities: string[],
|
||||||
addNewCamera?: boolean,
|
addNewCamera?: boolean,
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
const liveProviders: EditorSelectOption[] = [
|
const liveProviders: EditorSelectOption[] = [
|
||||||
@@ -891,6 +879,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
multiple: true,
|
multiple: true,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGER_BY_OCCUPANCY, cameraIndex),
|
||||||
|
frigateCardConfigDefaults.cameras.trigger_by_occupancy,
|
||||||
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGER_BY_MOTION, cameraIndex),
|
||||||
|
frigateCardConfigDefaults.cameras.trigger_by_motion,
|
||||||
|
)}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGER_BY_ENTITIES, cameraIndex),
|
||||||
|
entities,
|
||||||
|
{
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
: ``}
|
: ``}
|
||||||
`;
|
`;
|
||||||
@@ -978,7 +981,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaults = frigateCardConfigDefaults;
|
const defaults = frigateCardConfigDefaults;
|
||||||
|
const entities = getEntitiesFromHASS(this.hass);
|
||||||
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
|
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
|
||||||
[]) as RawFrigateCardConfigArray;
|
[]) as RawFrigateCardConfigArray;
|
||||||
|
|
||||||
@@ -1007,8 +1010,8 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._renderOptionSetHeader('cameras')}
|
${this._renderOptionSetHeader('cameras')}
|
||||||
${this._expandedMenus[MENU_OPTIONS] === 'cameras'
|
${this._expandedMenus[MENU_OPTIONS] === 'cameras'
|
||||||
? html` <div class="submenu">
|
? html` <div class="submenu">
|
||||||
${cameras.map((_, index) => this._renderCamera(cameras, index))}
|
${cameras.map((_, index) => this._renderCamera(cameras, index, entities))}
|
||||||
${this._renderCamera(cameras, cameras.length, true)}
|
${this._renderCamera(cameras, cameras.length, entities, true)}
|
||||||
</div>`
|
</div>`
|
||||||
: ''}
|
: ''}
|
||||||
${this._renderOptionSetHeader('view')}
|
${this._renderOptionSetHeader('view')}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
"frigate-jsmpeg": "Frigate JSMpeg",
|
"frigate-jsmpeg": "Frigate JSMpeg",
|
||||||
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
|
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
|
||||||
},
|
},
|
||||||
|
"trigger_by_entities": "Trigger from other entities",
|
||||||
|
"trigger_by_motion": "Trigger by auto-detecting the motion sensor",
|
||||||
|
"trigger_by_occupancy": "Trigger by auto-detecting the occupancy sensor",
|
||||||
"webrtc_card": {
|
"webrtc_card": {
|
||||||
"entity": "WebRTC Card Camera Entity (Not a Frigate camera)",
|
"entity": "WebRTC Card Camera Entity (Not a Frigate camera)",
|
||||||
"url": "WebRTC Card Camera URL"
|
"url": "WebRTC Card Camera URL"
|
||||||
|
|||||||
+17
-4
@@ -364,6 +364,9 @@ const customSchema = z
|
|||||||
export const cameraConfigDefault = {
|
export const cameraConfigDefault = {
|
||||||
client_id: 'frigate' as const,
|
client_id: 'frigate' as const,
|
||||||
live_provider: 'auto' as const,
|
live_provider: 'auto' as const,
|
||||||
|
trigger_by_motion: true,
|
||||||
|
trigger_by_occupancy: true,
|
||||||
|
trigger_by_entities: [],
|
||||||
};
|
};
|
||||||
const webrtcCardCameraConfigSchema = z.object({
|
const webrtcCardCameraConfigSchema = z.object({
|
||||||
entity: z.string().optional(),
|
entity: z.string().optional(),
|
||||||
@@ -395,9 +398,9 @@ const cameraConfigSchema = z
|
|||||||
// Set of cameras IDs upon which this camera depends.
|
// Set of cameras IDs upon which this camera depends.
|
||||||
dependent_cameras: z.string().array().optional(),
|
dependent_cameras: z.string().array().optional(),
|
||||||
|
|
||||||
trigger_by_motion: z.boolean().optional(),
|
trigger_by_motion: z.boolean().default(cameraConfigDefault.trigger_by_motion),
|
||||||
trigger_by_occupancy: z.boolean().optional(),
|
trigger_by_occupancy: z.boolean().default(cameraConfigDefault.trigger_by_occupancy),
|
||||||
trigger_by_entities: z.string().array().optional(),
|
trigger_by_entities: z.string().array().default(cameraConfigDefault.trigger_by_entities),
|
||||||
})
|
})
|
||||||
.default(cameraConfigDefault);
|
.default(cameraConfigDefault);
|
||||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||||
@@ -1263,8 +1266,18 @@ export const signedPathSchema = z.object({
|
|||||||
export type SignedPath = z.infer<typeof signedPathSchema>;
|
export type SignedPath = z.infer<typeof signedPathSchema>;
|
||||||
|
|
||||||
export const entitySchema = z.object({
|
export const entitySchema = z.object({
|
||||||
|
config_entry_id: z.string().nullable(),
|
||||||
|
disabled_by: z.string().nullable(),
|
||||||
entity_id: z.string(),
|
entity_id: z.string(),
|
||||||
unique_id: z.string(),
|
|
||||||
platform: z.string(),
|
platform: z.string(),
|
||||||
});
|
});
|
||||||
export type Entity = z.infer<typeof entitySchema>;
|
export type Entity = z.infer<typeof entitySchema>;
|
||||||
|
|
||||||
|
export const extendedEntitySchema = entitySchema.extend({
|
||||||
|
// Extended entity results.
|
||||||
|
unique_id: z.string().optional(),
|
||||||
|
})
|
||||||
|
export type ExtendedEntity = z.infer<typeof extendedEntitySchema>;
|
||||||
|
|
||||||
|
export const entityListSchema = entitySchema.array();
|
||||||
|
export type EntityList = z.infer<typeof entityListSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { homeAssistantWSRequest } from '.';
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
EntityList,
|
||||||
|
entityListSchema,
|
||||||
|
ExtendedEntity,
|
||||||
|
extendedEntitySchema
|
||||||
|
} from '../../types.js';
|
||||||
|
|
||||||
|
export class ExtendedEntityCache {
|
||||||
|
protected _cache: Map<string, ExtendedEntity> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the cache has a given entity_id.
|
||||||
|
* @param id
|
||||||
|
* @returns `true` if the id is in the cache, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public has(id: string): boolean {
|
||||||
|
return this._cache.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first value that returns true for the given predicate.
|
||||||
|
* @param func A callback function that returns a boolean.
|
||||||
|
* @returns The first matching value.
|
||||||
|
*/
|
||||||
|
public getMatch(func: (arg: ExtendedEntity) => boolean): ExtendedEntity | null {
|
||||||
|
return [...this._cache.values()].find(func) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entity information given an id.
|
||||||
|
* @param id The entity id.
|
||||||
|
* @returns The `ExtendedEntity` for this id.
|
||||||
|
*/
|
||||||
|
public get(id: string): ExtendedEntity | undefined {
|
||||||
|
return this._cache.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a given ExtendedEntity to the cache.
|
||||||
|
* @param extendedEntity
|
||||||
|
*/
|
||||||
|
public set(extendedEntity: ExtendedEntity): void {
|
||||||
|
this._cache.set(extendedEntity.entity_id, extendedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the extended entity information for an entity. May throw.
|
||||||
|
* @param hass The Home Assistant object.
|
||||||
|
* @param entity The entity id.
|
||||||
|
* @param cache An optional ExtendedEntityCache.
|
||||||
|
* @returns The ExtendedEntity information.
|
||||||
|
*/
|
||||||
|
export const getExtendedEntity = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity: string,
|
||||||
|
cache?: ExtendedEntityCache,
|
||||||
|
): Promise<ExtendedEntity> => {
|
||||||
|
const cachedValue = cache ? cache.get(entity) : undefined;
|
||||||
|
if (cachedValue) {
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
const result = await homeAssistantWSRequest<ExtendedEntity>(
|
||||||
|
hass,
|
||||||
|
extendedEntitySchema,
|
||||||
|
{
|
||||||
|
type: 'config/entity_registry/get',
|
||||||
|
entity_id: entity,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (cache) {
|
||||||
|
cache.set(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the extended entity information for an array of entities.
|
||||||
|
* @param hass The Home Assistant object.
|
||||||
|
* @param entities An array of entity ids.
|
||||||
|
* @param cache An optional ExtendedEntityCache.
|
||||||
|
* @returns A map of entity id to ExtendedEntity objects.
|
||||||
|
*/
|
||||||
|
export const getExtendedEntities = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: string[],
|
||||||
|
cache?: ExtendedEntityCache,
|
||||||
|
): Promise<Map<string, Entity>> => {
|
||||||
|
const output: Map<string, Entity> = new Map();
|
||||||
|
const _storeExtendedEntity = async (entity: string): Promise<void> => {
|
||||||
|
output.set(entity, await getExtendedEntity(hass, entity, cache));
|
||||||
|
};
|
||||||
|
await Promise.all(entities.map(_storeExtendedEntity));
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all entities from the entity registry.
|
||||||
|
* @param hass The Home Assistant object.
|
||||||
|
* @returns An entity list object.
|
||||||
|
*/
|
||||||
|
export const getAllEntities = async (hass: HomeAssistant): Promise<EntityList> => {
|
||||||
|
return await homeAssistantWSRequest<EntityList>(hass, entityListSchema, {
|
||||||
|
type: 'config/entity_registry/list',
|
||||||
|
});
|
||||||
|
};
|
||||||
+18
-2
@@ -4,8 +4,7 @@ import { StyleInfo } from 'lit/directives/style-map.js';
|
|||||||
import { ZodSchema } from 'zod';
|
import { ZodSchema } from 'zod';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import {
|
import {
|
||||||
CardHelpers,
|
CardHelpers, ExtendedHomeAssistant,
|
||||||
ExtendedHomeAssistant,
|
|
||||||
SignedPath,
|
SignedPath,
|
||||||
signedPathSchema,
|
signedPathSchema,
|
||||||
StateParameters
|
StateParameters
|
||||||
@@ -306,3 +305,20 @@ export const sideLoadHomeAssistantElements = async (): Promise<boolean> => {
|
|||||||
export const isTriggeredState = (state?: HassEntity): boolean => {
|
export const isTriggeredState = (state?: HassEntity): boolean => {
|
||||||
return !!state && ['on', 'open'].includes(state.state);
|
return !!state && ['on', 'open'].includes(state.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entities from the HASS object.
|
||||||
|
* @param hass
|
||||||
|
* @param domain
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getEntitiesFromHASS = (hass: HomeAssistant, domain?: string): string[] => {
|
||||||
|
if (!hass) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const entities = Object.keys(hass.states).filter(
|
||||||
|
(eid) => !domain || eid.substr(0, eid.indexOf('.')) === domain,
|
||||||
|
);
|
||||||
|
entities.sort();
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import QuickLRU from 'quick-lru';
|
import QuickLRU from 'quick-lru';
|
||||||
|
import { homeAssistantWSRequest } from '.';
|
||||||
import {
|
import {
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
ResolvedMedia,
|
ResolvedMedia,
|
||||||
resolvedMediaSchema
|
resolvedMediaSchema
|
||||||
} from '../types.js';
|
} from '../../types.js';
|
||||||
import { homeAssistantWSRequest } from './ha';
|
|
||||||
|
|
||||||
// It's important the cache size be at least as large as the largest likely
|
// It's important the cache size be at least as large as the largest likely
|
||||||
// media query or media items will from a given query will be evicted for other
|
// media query or media items will from a given query will be evicted for other
|
||||||
@@ -21,19 +21,42 @@ export class ResolvedMediaCache {
|
|||||||
this._cache = new QuickLRU({ maxSize: RESOLVED_MEDIA_CACHE_SIZE });
|
this._cache = new QuickLRU({ maxSize: RESOLVED_MEDIA_CACHE_SIZE });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the cache has a given id.
|
||||||
|
* @param id
|
||||||
|
* @returns `true` if the id is in the cache, `false` otherwise.
|
||||||
|
*/
|
||||||
public has(id: string): boolean {
|
public has(id: string): boolean {
|
||||||
return this._cache.has(id);
|
return this._cache.has(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get resolved media information given an id.
|
||||||
|
* @param id The id.
|
||||||
|
* @returns The `ResolvedMedia` for this id.
|
||||||
|
*/
|
||||||
public get(id: string): ResolvedMedia | undefined {
|
public get(id: string): ResolvedMedia | undefined {
|
||||||
return this._cache.get(id);
|
return this._cache.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a given ResolvedMedia to the cache.
|
||||||
|
* @param id The id for the object.
|
||||||
|
* @param resolvedMedia The `ResolvedMedia` object.
|
||||||
|
*/
|
||||||
public set(id: string, resolvedMedia: ResolvedMedia): void {
|
public set(id: string, resolvedMedia: ResolvedMedia): void {
|
||||||
this._cache.set(id, resolvedMedia);
|
this._cache.set(id, resolvedMedia);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a given media source item.
|
||||||
|
* @param hass The Home Assistant object.
|
||||||
|
* @param mediaSource The media source object.
|
||||||
|
* @param cache An optional ResolvedMediaCache object.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const resolveMedia = async (
|
export const resolveMedia = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mediaSource?: FrigateBrowseMediaSource,
|
mediaSource?: FrigateBrowseMediaSource,
|
||||||
Reference in New Issue
Block a user