Add support for multiple zones/labels per Frigate camera.

This commit is contained in:
Dermot Duffy
2023-03-12 15:35:53 -07:00
parent d9190ade01
commit 342d988bbd
10 changed files with 125 additions and 63 deletions
+23 -15
View File
@@ -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:
+53 -27
View File
@@ -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,
});
+1 -1
View File
@@ -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,`,
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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": "",
+2 -2
View File
@@ -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
View File
@@ -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),