Add support for multiple zones/labels per Frigate camera.
This commit is contained in:
@@ -160,8 +160,8 @@ cameras:
|
||||
| - | - | - | - |
|
||||
| `camera_name` | Autodetected from `camera_entity` if that is specified. | :white_check_mark: | The Frigate camera name to use when communicating with the Frigate server, e.g. for viewing clips/snapshots or the JSMPEG live view.|
|
||||
| `url` | | :white_check_mark: | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. All other communication with Frigate goes via Home Assistant. |
|
||||
| `label` | | :white_check_mark: | A Frigate label / object filter used to filter events (clips & snapshots), e.g. `person`.|
|
||||
| `zone` | | :white_check_mark: | A Frigate zone used to filter events (clips & snapshots), e.g. `front_door`.|
|
||||
| `labels` | | :white_check_mark: | An array of Frigate labels used to filter events (clips & snapshots), e.g. [`person`, `car`].|
|
||||
| `zones` | | :white_check_mark: | An array of Frigates zones used to filter events (clips & snapshots), e.g. [`front_door`, `front_steps`].|
|
||||
| `client_id` | `frigate` | :white_check_mark: | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://docs.frigate.video/integrations/home-assistant/#multiple-instance-support).|
|
||||
|
||||
#### Live Provider: Camera go2rtc configuration
|
||||
@@ -255,7 +255,7 @@ cameras:
|
||||
| Option | Default | Overridable | Description |
|
||||
| - | - | - | - |
|
||||
| `motion` | `false` | :white_check_mark: | Whether to not to trigger the camera by automatically detecting and using the motion `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the motion `binary_sensor` entity has been enabled in Home Assistant.|
|
||||
| `occupancy` | `true` | :white_check_mark: | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant.|
|
||||
| `occupancy` | `true` | :white_check_mark: | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera and its configured zones and labels. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant. If this camera has configured zones, only occupancy sensors for those zones are used -- if the overall _camera_ occupancy sensor is also required, it can be manually added to `entities`. If this camera has configured labels, only occupancy sensors for those labels are used.|
|
||||
| `entities` | | :white_check_mark: | Whether to not to trigger the camera when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). This works for Frigate or non-Frigate cameras.|
|
||||
|
||||
<a name="camera-ids"></a>
|
||||
@@ -1120,10 +1120,10 @@ This card supports several different views:
|
||||
| Key | Description |
|
||||
| ------------- | --------------------------------------------- |
|
||||
|`live` (default)| Shows the live camera view with the configured live provider.|
|
||||
|`snapshots`|Shows a gallery of snapshots for this camera/zone/label.|
|
||||
|`snapshot`|Shows a viewer for the most recent snapshot for this camera/zone/label. Can also be accessed by holding down the `snapshots` menu icon.|
|
||||
|`clips`|Shows a gallery of clips for this camera/zone/label.|
|
||||
|`clip`|Shows a viewer for the most recent clip for this camera/zone/label. Can also be accessed by holding down the `clips` menu icon.|
|
||||
|`snapshots`|Shows a gallery of snapshots for this camera.|
|
||||
|`snapshot`|Shows a viewer for the most recent snapshot for this camera. Can also be accessed by holding down the `snapshots` menu icon.|
|
||||
|`clips`|Shows a gallery of clips for this camera.|
|
||||
|`clip`|Shows a viewer for the most recent clip for this camera. Can also be accessed by holding down the `clips` menu icon.|
|
||||
|`recordings`|Shows a gallery of recent (last day) recordings for this camera and its dependents.|
|
||||
|`recording`|Shows a viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon.|
|
||||
|`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.timeout_seconds`).|
|
||||
@@ -1361,8 +1361,10 @@ cameras:
|
||||
url: http://my.frigate.local
|
||||
client_id: frigate
|
||||
camera_name: front_door
|
||||
label: person
|
||||
zone: steps
|
||||
labels:
|
||||
- person
|
||||
zones:
|
||||
- steps
|
||||
# Show events for camera-2 when this camera is viewed.
|
||||
dependencies:
|
||||
all_cameras: false
|
||||
@@ -1380,8 +1382,10 @@ cameras:
|
||||
url: http://my-other.frigate.local
|
||||
client_id: frigate-other
|
||||
camera_name: entrance
|
||||
label: car
|
||||
zone: driveway
|
||||
labels:
|
||||
- car
|
||||
zones:
|
||||
- driveway
|
||||
icon: 'mdi:car'
|
||||
title: 'Front entrance'
|
||||
# Custom identifier for the camera to refer to it above.
|
||||
@@ -1450,8 +1454,10 @@ cameras_global:
|
||||
url: http://my.frigate.local
|
||||
client_id: frigate
|
||||
camera_name: front_door
|
||||
label: person
|
||||
zone: steps
|
||||
labels:
|
||||
- person
|
||||
zones:
|
||||
- steps
|
||||
dependencies:
|
||||
all_cameras: false
|
||||
cameras:
|
||||
@@ -2213,8 +2219,10 @@ overrides:
|
||||
url: http://my.frigate.local
|
||||
client_id: frigate
|
||||
camera_name: front_door
|
||||
label: person
|
||||
zone: steps
|
||||
labels:
|
||||
- person
|
||||
zones:
|
||||
- steps
|
||||
dependencies:
|
||||
all_cameras: false
|
||||
cameras:
|
||||
|
||||
@@ -56,8 +56,10 @@ import {
|
||||
NativeFrigateRecordingSegmentsQuery,
|
||||
retainEvent,
|
||||
} from './requests';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import uniqWith from 'lodash-es/uniqWith';
|
||||
import {
|
||||
allPromises,
|
||||
formatDate,
|
||||
@@ -194,11 +196,11 @@ export class FrigateCameraManagerEngine
|
||||
}
|
||||
|
||||
if (cameraConfig.triggers.occupancy) {
|
||||
const occupancyEntity = this._getOccupancySensor(cameraConfig, [
|
||||
const occupancyEntities = this._getOccupancySensor(cameraConfig, [
|
||||
...binarySensorEntities.values(),
|
||||
]);
|
||||
if (occupancyEntity) {
|
||||
cameraConfig.triggers.entities.push(occupancyEntity);
|
||||
if (occupancyEntities) {
|
||||
cameraConfig.triggers.entities.push(...occupancyEntities);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,11 +240,7 @@ export class FrigateCameraManagerEngine
|
||||
entities.find(
|
||||
(ent) =>
|
||||
!!ent.unique_id?.match(
|
||||
new RegExp(
|
||||
`:motion_sensor:${
|
||||
cameraConfig.frigate.zone || cameraConfig.frigate.camera_name
|
||||
}`,
|
||||
),
|
||||
new RegExp(`:motion_sensor:${cameraConfig.frigate.camera_name}`),
|
||||
),
|
||||
)?.entity_id ?? null
|
||||
);
|
||||
@@ -259,20 +257,39 @@ export class FrigateCameraManagerEngine
|
||||
protected _getOccupancySensor(
|
||||
cameraConfig: CameraConfig,
|
||||
entities: Entity[],
|
||||
): string | null {
|
||||
if (cameraConfig.frigate.camera_name) {
|
||||
return (
|
||||
): string[] | null {
|
||||
const entityIDs: string[] = [];
|
||||
const addEntityIDIfFound = (cameraOrZone: string, label: string): void => {
|
||||
const entityID =
|
||||
entities.find(
|
||||
(ent) =>
|
||||
!!ent.unique_id?.match(
|
||||
new RegExp(
|
||||
`:occupancy_sensor:${
|
||||
cameraConfig.frigate.zone || cameraConfig.frigate.camera_name
|
||||
}_${cameraConfig.frigate.label || 'all'}`,
|
||||
),
|
||||
new RegExp(`:occupancy_sensor:${cameraOrZone}_${label}`),
|
||||
),
|
||||
)?.entity_id ?? null
|
||||
);
|
||||
)?.entity_id ?? null;
|
||||
if (entityID) {
|
||||
entityIDs.push(entityID);
|
||||
}
|
||||
};
|
||||
|
||||
if (cameraConfig.frigate.camera_name) {
|
||||
// If zone(s) are specified, the master occupancy sensor for the overall
|
||||
// camera is not used by default (but could be manually added by the
|
||||
// user).
|
||||
const camerasAndZones = cameraConfig.frigate.zones
|
||||
? cameraConfig.frigate.zones
|
||||
: [cameraConfig.frigate.camera_name];
|
||||
|
||||
const labels = cameraConfig.frigate.labels ? cameraConfig.frigate.labels : ['all'];
|
||||
for (const cameraOrZone of camerasAndZones) {
|
||||
for (const label of labels) {
|
||||
addEntityIDIfFound(cameraOrZone, label);
|
||||
}
|
||||
}
|
||||
|
||||
if (entityIDs.length) {
|
||||
return entityIDs;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -308,17 +325,26 @@ export class FrigateCameraManagerEngine
|
||||
cameras.get(cameraID),
|
||||
);
|
||||
|
||||
// If there isn't a label or zone specified, we can come up with a single
|
||||
// batch query for Frigate that will match across all cameras.
|
||||
const canDoBatchQuery = relevantCameraConfigs.every(
|
||||
(cameraConfig) => !cameraConfig?.frigate.label && !cameraConfig?.frigate.zone,
|
||||
// If all cameras specify exactly the same zones or labels (incl. none), we
|
||||
// can use a single batch query which will be better performance wise,
|
||||
// otherwise we must fan out to multiple queries in order to precisely match
|
||||
// the user's intent.
|
||||
const uniqueZoneArrays = uniqWith(
|
||||
relevantCameraConfigs.map((config) => config?.frigate.zones),
|
||||
isEqual,
|
||||
);
|
||||
const uniqueLabelArrays = uniqWith(
|
||||
relevantCameraConfigs.map((config) => config?.frigate.labels),
|
||||
isEqual,
|
||||
);
|
||||
|
||||
if (canDoBatchQuery) {
|
||||
if (uniqueZoneArrays.length === 1 && uniqueLabelArrays.length === 1) {
|
||||
return [
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: cameraIDs,
|
||||
...(uniqueLabelArrays[0] && { what: new Set(uniqueLabelArrays[0]) }),
|
||||
...(uniqueZoneArrays[0] && { where: new Set(uniqueZoneArrays[0]) }),
|
||||
...query,
|
||||
},
|
||||
];
|
||||
@@ -331,11 +357,11 @@ export class FrigateCameraManagerEngine
|
||||
output.push({
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set([cameraID]),
|
||||
...(cameraConfig.frigate.label && {
|
||||
what: new Set([cameraConfig.frigate.label]),
|
||||
...(cameraConfig.frigate.labels && {
|
||||
what: new Set(cameraConfig.frigate.labels),
|
||||
}),
|
||||
...(cameraConfig.frigate.zone && {
|
||||
where: new Set([cameraConfig.frigate.zone]),
|
||||
...(cameraConfig.frigate.zones && {
|
||||
where: new Set(cameraConfig.frigate.zones),
|
||||
}),
|
||||
...query,
|
||||
});
|
||||
|
||||
@@ -233,7 +233,7 @@ export class CameraManager {
|
||||
log(
|
||||
this._cardWideConfig,
|
||||
'Frigate Card CameraManager initialized (Cameras: ',
|
||||
camerasConfig,
|
||||
this._store.getCameras(),
|
||||
`, Duration: ${
|
||||
(new Date().getTime() - initializationStartTime.getTime()) / 1000
|
||||
}s,`,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
THUMBNAIL_WIDTH_MAX,
|
||||
THUMBNAIL_WIDTH_MIN,
|
||||
} from './types';
|
||||
import { arrayify } from './utils/basic';
|
||||
|
||||
/**
|
||||
* Set a configuration value.
|
||||
@@ -588,4 +589,16 @@ const UPGRADES = [
|
||||
upgradeMoveToWithOverrides('live.image', CONF_CAMERAS_GLOBAL_IMAGE),
|
||||
upgradeMoveToWithOverrides('live.jsmpeg', CONF_CAMERAS_GLOBAL_JSMPEG),
|
||||
upgradeMoveToWithOverrides('live.webrtc_card', CONF_CAMERAS_GLOBAL_WEBRTC_CARD),
|
||||
upgradeArrayValue(
|
||||
CONF_CAMERAS,
|
||||
upgradeMoveToWithOverrides('frigate.zone', 'frigate.zones', {
|
||||
transform: (zone) => arrayify(zone),
|
||||
}),
|
||||
),
|
||||
upgradeArrayValue(
|
||||
CONF_CAMERAS,
|
||||
upgradeMoveToWithOverrides('frigate.label', 'frigate.labels', {
|
||||
transform: (label) => arrayify(label),
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@ export const CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME =
|
||||
`${CONF_CAMERAS}.#.frigate.camera_name` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID =
|
||||
`${CONF_CAMERAS}.#.frigate.client_id` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_LABEL =
|
||||
`${CONF_CAMERAS}.#.frigate.label` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_LABELS =
|
||||
`${CONF_CAMERAS}.#.frigate.labels` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_URL = `${CONF_CAMERAS}.#.frigate.url` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_ZONE = `${CONF_CAMERAS}.#.frigate.zone` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_ZONES = `${CONF_CAMERAS}.#.frigate.zones` as const;
|
||||
export const CONF_CAMERAS_ARRAY_GO2RTC_MODES = `${CONF_CAMERAS}.#.go2rtc.modes` as const;
|
||||
export const CONF_CAMERAS_ARRAY_GO2RTC_STREAM =
|
||||
`${CONF_CAMERAS}.#.go2rtc.stream` as const;
|
||||
|
||||
+24
-9
@@ -18,9 +18,9 @@ import {
|
||||
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_LABEL,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_URL,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_ZONE,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_ZONES,
|
||||
CONF_CAMERAS_ARRAY_ICON,
|
||||
CONF_CAMERAS_ARRAY_ID,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
|
||||
@@ -620,7 +620,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
*/
|
||||
protected _renderOptionSelector(
|
||||
configPath: string,
|
||||
options: string[] | { value: string; label: string }[],
|
||||
options: string[] | { value: string; label: string }[] = [],
|
||||
params?: {
|
||||
multiple?: boolean;
|
||||
label?: string;
|
||||
@@ -634,7 +634,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${{
|
||||
select: { mode: 'dropdown', multiple: !!params?.multiple, options: options },
|
||||
select: {
|
||||
mode: 'dropdown',
|
||||
multiple: !!params?.multiple,
|
||||
custom_value: !options.length,
|
||||
options: options,
|
||||
},
|
||||
}}
|
||||
.label=${params?.label || this._getLabel(configPath)}
|
||||
.value=${getConfigValue(this._config, configPath, '')}
|
||||
@@ -1370,11 +1375,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
${this._renderStringInput(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_URL, cameraIndex),
|
||||
)}
|
||||
${this._renderStringInput(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_LABEL, cameraIndex),
|
||||
${this._renderOptionSelector(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_LABELS, cameraIndex),
|
||||
[],
|
||||
{
|
||||
multiple: true,
|
||||
label: localize('config.cameras.frigate.labels'),
|
||||
},
|
||||
)}
|
||||
${this._renderStringInput(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_ZONE, cameraIndex),
|
||||
${this._renderOptionSelector(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_ZONES, cameraIndex),
|
||||
[],
|
||||
{
|
||||
multiple: true,
|
||||
label: localize('config.cameras.frigate.zones'),
|
||||
},
|
||||
)}
|
||||
${this._renderStringInput(
|
||||
getArrayConfigPath(
|
||||
@@ -1657,7 +1672,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
${this._renderMenuButton('download')}
|
||||
${this._renderMenuButton('camera_ui')}
|
||||
${this._renderMenuButton('fullscreen')}
|
||||
${this._renderMenuButton('expand')}
|
||||
${this._renderMenuButton('expand') /* */}
|
||||
${this._renderMenuButton('timeline')}
|
||||
${this._renderMenuButton('media_player')}
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
"frigate": {
|
||||
"camera_name": "Frigate camera name (Autodetected from entity)",
|
||||
"client_id": "Frigate client id (For >1 Frigate server)",
|
||||
"label": "Frigate label/object filter",
|
||||
"labels": "Frigate labels/object filters",
|
||||
"editor_label": "Frigate Options",
|
||||
"url": "Frigate server URL",
|
||||
"zone": "Frigate zone"
|
||||
"zones": "Frigate zones"
|
||||
},
|
||||
"go2rtc": {
|
||||
"editor_label": "go2rtc Options",
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
"frigate": {
|
||||
"camera_name": "Nome della telecamera frigate (autodificato dall'entità)",
|
||||
"client_id": "ID client Frigate (per > 1 Frigate server)",
|
||||
"label": "Filtro etichetta/oggetto Frigate",
|
||||
"labels": "",
|
||||
"editor_label": "Frigate Opzione",
|
||||
"url": "Frigate URL del server",
|
||||
"zone": "Frigate zona"
|
||||
"zones": ""
|
||||
},
|
||||
"go2rtc": {
|
||||
"editor_label": "",
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
"frigate": {
|
||||
"camera_name": "Nome da câmera do Frigate (detectado automaticamente pela entidade)",
|
||||
"client_id": "ID do cliente do Frigate (para >1 servidor Frigate)",
|
||||
"label": "Filtro de rótulo/objeto do Frigate",
|
||||
"label": "",
|
||||
"editor_label": "Opções do Frigate",
|
||||
"url": "URL do servidor Frigate",
|
||||
"zone": "Zona do Frigate"
|
||||
"zone": ""
|
||||
},
|
||||
"go2rtc": {
|
||||
"editor_label": "",
|
||||
|
||||
+2
-2
@@ -506,8 +506,8 @@ const cameraConfigSchema = z
|
||||
url: z.string().optional(),
|
||||
client_id: z.string().default(cameraConfigDefault.frigate.client_id),
|
||||
camera_name: z.string().optional(),
|
||||
label: z.string().optional(),
|
||||
zone: z.string().optional(),
|
||||
labels: z.string().array().optional(),
|
||||
zones: z.string().array().optional(),
|
||||
})
|
||||
.default(cameraConfigDefault.frigate),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user