Initial containerized Frigate camera control bridge
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!*.env.example
|
||||||
|
camera-control.env
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.mo
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!.env.example
|
||||||
|
camera-control.env
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
ARG BASE_IMAGE=python:3.11-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV LOCALE_DIR=/app/locale
|
||||||
|
ENV HTTP_PORT=5011
|
||||||
|
ENV UI_LANGUAGE=en
|
||||||
|
|
||||||
|
COPY frigate-camera-control-bridge.py /app/frigate-camera-control-bridge.py
|
||||||
|
COPY locale /app/locale
|
||||||
|
COPY tools/compile_gettext.py /app/tools/compile_gettext.py
|
||||||
|
|
||||||
|
RUN python3 /app/tools/compile_gettext.py /app/locale
|
||||||
|
|
||||||
|
EXPOSE 5011
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 CMD python3 -c "import os,urllib.request; urllib.request.urlopen('http://127.0.0.1:%s/readyz' % os.environ.get('HTTP_PORT', '5011'), timeout=3).read()"
|
||||||
|
|
||||||
|
ENTRYPOINT ["python3", "/app/frigate-camera-control-bridge.py"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ARG BASE_IMAGE=python:3.11-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV LOCALE_DIR=/workspace/locale
|
||||||
|
ENV HTTP_PORT=5011
|
||||||
|
ENV UI_LANGUAGE=en
|
||||||
|
|
||||||
|
COPY tools/compile_gettext.py /usr/local/bin/compile_gettext.py
|
||||||
|
|
||||||
|
CMD ["sh"]
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# Frigate Camera Control Bridge
|
||||||
|
|
||||||
|
## PL
|
||||||
|
|
||||||
|
Ten projekt jest uniwersalnym sidecarem dla pojedynczej instancji Frigate.
|
||||||
|
Kontener pobiera konfiguracje Frigate przez `/api/config`, wykrywa kamery z
|
||||||
|
`go2rtc` oraz z bezposrednich URL-i `ffmpeg.inputs[].path`, a potem wystawia
|
||||||
|
sterowanie swiatlem/syrena przez MQTT Discovery dla Home Assistant i awaryjne
|
||||||
|
GUI HTTP.
|
||||||
|
|
||||||
|
Najwazniejsze zalozenia:
|
||||||
|
- Ten sam obraz obsluguje Podman dzisiaj i Kubernetes pozniej.
|
||||||
|
- Konfiguracja runtime jest przez zmienne srodowiskowe, bez montowania pliku
|
||||||
|
konfiguracyjnego Frigate.
|
||||||
|
- Sidecar czeka na Frigate przy starcie i ponawia odczyt `/api/config`.
|
||||||
|
- Jesli `ffmpeg.inputs[].path` wskazuje na `rtsp://127.0.0.1:8554/...`,
|
||||||
|
sidecar traktuje to jako brame go2rtc i szuka prawdziwego URL-a kamery w
|
||||||
|
`go2rtc.streams`.
|
||||||
|
- Aktualny Podman uzywa pliku env per sidecar, np.
|
||||||
|
`/etc/frigate-camera-control-bridge/frigate-main.env`.
|
||||||
|
- Kubernetes powinien podawac `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`,
|
||||||
|
`DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD` przez `Secret`/`ConfigMap`.
|
||||||
|
- W trybie env brak tych wartosci jest bledem startu; sidecar nie szuka
|
||||||
|
credentiali w Quadletach ani w konfiguracji Frigate.
|
||||||
|
- GUI fallback domyslnie slucha na `5011`, bo `5001` jest juz uzywany przez
|
||||||
|
czesc instancji Frigate.
|
||||||
|
- Jezyki GUI sa standardowym gettext: `locale/<lang>/LC_MESSAGES/*.po` i `.mo`.
|
||||||
|
|
||||||
|
Podstawowe zmienne:
|
||||||
|
|
||||||
|
| Zmienna | Znaczenie |
|
||||||
|
| --- | --- |
|
||||||
|
| `FRIGATE_NAME` | Nazwa tej instancji, np. `frigate-main`. |
|
||||||
|
| `FRIGATE_URL` | URL API Frigate, np. `http://frigate-main.example.test:5000`. |
|
||||||
|
| `MQTT_HOST`, `MQTT_PORT` | Broker MQTT. |
|
||||||
|
| `MQTT_USER`, `MQTT_PASSWORD` | Credentiale MQTT. |
|
||||||
|
| `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD` | Domyslne credentiale kamer odkrytych z Frigate. |
|
||||||
|
| `BASE_TOPIC` | Bazowy topic MQTT, np. `frigate_camera_control/frigate-main`. |
|
||||||
|
| `DISCOVERY_PREFIX` | Prefix Home Assistant MQTT Discovery, zwykle `homeassistant`. |
|
||||||
|
| `UI_LANGUAGE` | Domyslny jezyk GUI i encji HA, domyslnie `en`. |
|
||||||
|
| `HTTP_PORT` | Port GUI fallback, domyslnie `5011`. |
|
||||||
|
| `HTTP_TOKEN` | Opcjonalny token dla GUI/API HTTP. |
|
||||||
|
| `SNAPSHOT_RETRY_SECONDS` | Co ile probowac ponownie, gdy migawki brakuja, domyslnie `600`. |
|
||||||
|
| `SNAPSHOT_REFRESH_SECONDS` | Co ile odswiezac poprawnie pobrane migawki, domyslnie `3600`. |
|
||||||
|
| `SNAPSHOT_LIVE_ON_GUI` | Czy pobierac swieza migawke z Frigate przy otwieraniu GUI, domyslnie `true`. |
|
||||||
|
|
||||||
|
Przyklad `/etc/frigate-camera-control-bridge/frigate-main.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MQTT_HOST=mqtt.example.test
|
||||||
|
MQTT_PORT=1883
|
||||||
|
MQTT_USER=...
|
||||||
|
MQTT_PASSWORD=...
|
||||||
|
DEFAULT_CAMERA_USER=admin
|
||||||
|
DEFAULT_CAMERA_PASSWORD=...
|
||||||
|
```
|
||||||
|
|
||||||
|
Budowanie:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t frigate-camera-control-bridge:1.0 .
|
||||||
|
docker build -f Dockerfile.dev -t frigate-camera-control-bridge:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
Przyklady uruchomienia:
|
||||||
|
- Podman Quadlet: `examples/podman-quadlet/`
|
||||||
|
- Kubernetes sidecar: `examples/kubernetes-sidecar.yaml`
|
||||||
|
|
||||||
|
Lokalny test kontenera:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-e FRIGATE_NAME=example \
|
||||||
|
-e FRIGATE_URL=http://frigate:5000 \
|
||||||
|
-e MQTT_HOST=mqtt.local \
|
||||||
|
-e MQTT_USER=user \
|
||||||
|
-e MQTT_PASSWORD=password \
|
||||||
|
-e DEFAULT_CAMERA_USER=admin \
|
||||||
|
-e DEFAULT_CAMERA_PASSWORD=password \
|
||||||
|
-p 5011:5011 \
|
||||||
|
frigate-camera-control-bridge:1.0 --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Jezyki:
|
||||||
|
|
||||||
|
1. Dodaj katalog `locale/de/LC_MESSAGES/`.
|
||||||
|
2. Skopiuj `frigate_camera_control_bridge.po` z `locale/en/LC_MESSAGES/`.
|
||||||
|
3. Przetlumacz `msgstr`.
|
||||||
|
4. Uruchom `python3 tools/compile_gettext.py locale`.
|
||||||
|
5. Po przebudowaniu obrazu GUI samo pokaze nowy jezyk.
|
||||||
|
|
||||||
|
Manualne komendy w kontenerze:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/app/frigate-camera-control-bridge.py --list
|
||||||
|
/app/frigate-camera-control-bridge.py --check
|
||||||
|
/app/frigate-camera-control-bridge.py --all-off
|
||||||
|
/app/frigate-camera-control-bridge.py --action frigate_main_camera_a_red_blue off
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP fallback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://<ip-frigate>:5011/
|
||||||
|
http://<ip-frigate>:5011/?lang=pl
|
||||||
|
http://<ip-frigate>:5011/api/actions
|
||||||
|
http://<ip-frigate>:5011/snapshot/<action_id>.jpg
|
||||||
|
http://<ip-frigate>:5011/healthz
|
||||||
|
http://<ip-frigate>:5011/readyz
|
||||||
|
```
|
||||||
|
|
||||||
|
Jesli ustawiono `HTTP_TOKEN`, dodaj `?token=<token>` albo naglowek
|
||||||
|
`X-Bridge-Token: <token>`.
|
||||||
|
|
||||||
|
## EN
|
||||||
|
|
||||||
|
This project is a generic sidecar for one Frigate instance. The container reads
|
||||||
|
Frigate configuration from `/api/config`, discovers cameras from `go2rtc` and
|
||||||
|
direct `ffmpeg.inputs[].path` stream URLs, then exposes light/siren controls
|
||||||
|
through Home Assistant MQTT Discovery plus an emergency HTTP fallback GUI.
|
||||||
|
|
||||||
|
Main design points:
|
||||||
|
- One image works for Podman now and Kubernetes later.
|
||||||
|
- Runtime configuration is environment-based; no Frigate config mount is needed.
|
||||||
|
- The sidecar waits for Frigate startup and retries `/api/config`.
|
||||||
|
- If `ffmpeg.inputs[].path` points at `rtsp://127.0.0.1:8554/...`, the sidecar
|
||||||
|
treats it as a go2rtc gateway and resolves the real camera URL from
|
||||||
|
`go2rtc.streams`.
|
||||||
|
- Current Podman deployment uses one env file per sidecar, for example:
|
||||||
|
`/etc/frigate-camera-control-bridge/frigate-main.env`.
|
||||||
|
- Kubernetes should pass `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`,
|
||||||
|
`DEFAULT_CAMERA_USER`, and `DEFAULT_CAMERA_PASSWORD` from a `Secret`/`ConfigMap`.
|
||||||
|
- In env mode, missing values are startup errors; the sidecar does not look up
|
||||||
|
credentials in Quadlets or Frigate config.
|
||||||
|
- The fallback GUI listens on `5011` by default because `5001` is already used
|
||||||
|
by some Frigate instances.
|
||||||
|
- GUI languages use standard gettext files:
|
||||||
|
`locale/<lang>/LC_MESSAGES/*.po` and `.mo`.
|
||||||
|
|
||||||
|
Core env vars:
|
||||||
|
|
||||||
|
| Variable | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `FRIGATE_NAME` | Instance name, for example `frigate-main`. |
|
||||||
|
| `FRIGATE_URL` | Frigate API URL, for example `http://frigate-main.example.test:5000`. |
|
||||||
|
| `MQTT_HOST`, `MQTT_PORT` | MQTT broker. |
|
||||||
|
| `MQTT_USER`, `MQTT_PASSWORD` | MQTT credentials. |
|
||||||
|
| `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD` | Default credentials for cameras discovered from Frigate. |
|
||||||
|
| `BASE_TOPIC` | MQTT base topic, for example `frigate_camera_control/frigate-main`. |
|
||||||
|
| `DISCOVERY_PREFIX` | Home Assistant MQTT Discovery prefix, usually `homeassistant`. |
|
||||||
|
| `UI_LANGUAGE` | Default GUI and HA entity language, defaults to `en`. |
|
||||||
|
| `HTTP_PORT` | Fallback GUI port, defaults to `5011`. |
|
||||||
|
| `HTTP_TOKEN` | Optional HTTP GUI/API token. |
|
||||||
|
| `SNAPSHOT_RETRY_SECONDS` | Retry interval while a snapshot is missing, defaults to `600`. |
|
||||||
|
| `SNAPSHOT_REFRESH_SECONDS` | Refresh interval after snapshots are available, defaults to `3600`. |
|
||||||
|
| `SNAPSHOT_LIVE_ON_GUI` | Fetch a fresh Frigate snapshot when the GUI loads, defaults to `true`. |
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t frigate-camera-control-bridge:1.0 .
|
||||||
|
docker build -f Dockerfile.dev -t frigate-camera-control-bridge:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run examples:
|
||||||
|
- Podman Quadlet: `examples/podman-quadlet/`
|
||||||
|
- Kubernetes sidecar: `examples/kubernetes-sidecar.yaml`
|
||||||
|
|
||||||
|
Development container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -it \
|
||||||
|
-v "$PWD":/workspace \
|
||||||
|
frigate-camera-control-bridge:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP fallback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://<frigate-ip>:5011/
|
||||||
|
http://<frigate-ip>:5011/?lang=pl
|
||||||
|
http://<frigate-ip>:5011/api/actions
|
||||||
|
http://<frigate-ip>:5011/snapshot/<action_id>.jpg
|
||||||
|
http://<frigate-ip>:5011/healthz
|
||||||
|
http://<frigate-ip>:5011/readyz
|
||||||
|
```
|
||||||
|
|
||||||
|
Kubernetes sidecar notes:
|
||||||
|
- Put the bridge container in the same pod as Frigate, or point `FRIGATE_URL`
|
||||||
|
at the Frigate service.
|
||||||
|
- Use `readinessProbe` on `/readyz`.
|
||||||
|
- Pass secrets through env vars from `Secret`.
|
||||||
|
- Keep `/var/lib/frigate-camera-control-bridge` on a small persistent volume if
|
||||||
|
you want stale Home Assistant discovery cleanup to survive pod recreation.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Per-instance runtime env for one Frigate camera-control sidecar.
|
||||||
|
# Example target on Podman: /etc/frigate-camera-control-bridge/frigate-main.env
|
||||||
|
# Kubernetes equivalent: env values from Secret/ConfigMap for that Frigate pod.
|
||||||
|
|
||||||
|
MQTT_HOST=mqtt.example.test
|
||||||
|
MQTT_PORT=1883
|
||||||
|
MQTT_USER=change-me
|
||||||
|
MQTT_PASSWORD=change-me
|
||||||
|
|
||||||
|
DEFAULT_CAMERA_USER=admin
|
||||||
|
DEFAULT_CAMERA_PASSWORD=change-me
|
||||||
|
|
||||||
|
# Optional. If set, HTTP GUI/API require ?token=<value> or X-Bridge-Token.
|
||||||
|
# HTTP_TOKEN=change-me
|
||||||
|
|
||||||
|
# Optional. Snapshot refresh is intentionally slow; it is only for GUI context.
|
||||||
|
SNAPSHOT_ENABLED=true
|
||||||
|
SNAPSHOT_HEIGHT=180
|
||||||
|
SNAPSHOT_CACHE_SECONDS=300
|
||||||
|
SNAPSHOT_RETRY_SECONDS=600
|
||||||
|
SNAPSHOT_REFRESH_SECONDS=3600
|
||||||
|
SNAPSHOT_LIVE_ON_GUI=true
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: frigate-with-camera-control
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: frigate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: frigate
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: frigate
|
||||||
|
image: ghcr.io/blakeblackshear/frigate:stable
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 5000
|
||||||
|
- name: camera-control
|
||||||
|
image: registry.example.local/frigate-camera-control-bridge:1.0
|
||||||
|
env:
|
||||||
|
- name: FRIGATE_NAME
|
||||||
|
value: frigate-main
|
||||||
|
- name: FRIGATE_URL
|
||||||
|
value: http://127.0.0.1:5000
|
||||||
|
- name: MQTT_HOST
|
||||||
|
value: mqtt.default.svc.cluster.local
|
||||||
|
- name: BASE_TOPIC
|
||||||
|
value: frigate_camera_control/frigate-main
|
||||||
|
- name: UI_LANGUAGE
|
||||||
|
value: en
|
||||||
|
- name: SNAPSHOT_RETRY_SECONDS
|
||||||
|
value: "600"
|
||||||
|
- name: SNAPSHOT_REFRESH_SECONDS
|
||||||
|
value: "3600"
|
||||||
|
- name: SNAPSHOT_LIVE_ON_GUI
|
||||||
|
value: "true"
|
||||||
|
- name: MQTT_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: frigate-camera-control
|
||||||
|
key: mqtt-user
|
||||||
|
- name: MQTT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: frigate-camera-control
|
||||||
|
key: mqtt-password
|
||||||
|
- name: DEFAULT_CAMERA_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: frigate-camera-control
|
||||||
|
key: camera-user
|
||||||
|
- name: DEFAULT_CAMERA_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: frigate-camera-control
|
||||||
|
key: camera-password
|
||||||
|
- name: HTTP_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: frigate-camera-control
|
||||||
|
key: http-token
|
||||||
|
optional: true
|
||||||
|
ports:
|
||||||
|
- name: control-http
|
||||||
|
containerPort: 5011
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /readyz
|
||||||
|
port: control-http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: control-http
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
|
volumeMounts:
|
||||||
|
- name: camera-control-state
|
||||||
|
mountPath: /var/lib/frigate-camera-control-bridge
|
||||||
|
volumes:
|
||||||
|
- name: camera-control-state
|
||||||
|
emptyDir: {}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Podman Quadlet Example
|
||||||
|
|
||||||
|
## PL
|
||||||
|
|
||||||
|
To jest generyczny przyklad uruchomienia kontenera
|
||||||
|
`frigate-camera-control-bridge` przez Podman Quadlet. Pliki sa przykladem dla
|
||||||
|
jednej instancji Frigate. Dla wielu instancji skopiuj `.container` oraz `.env`
|
||||||
|
i zmien `FRIGATE_NAME`, `FRIGATE_URL`, `BASE_TOPIC`, port publikowany przez
|
||||||
|
`PublishPort` oraz credentiale MQTT.
|
||||||
|
|
||||||
|
Przykladowa instalacja:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
install -d /etc/frigate-camera-control-bridge
|
||||||
|
install -m 0600 frigate-camera-control-bridge.env.example \
|
||||||
|
/etc/frigate-camera-control-bridge/frigate-main.env
|
||||||
|
install -m 0644 frigate-camera-control-bridge.container \
|
||||||
|
/etc/containers/systemd/frigate-camera-control-bridge.container
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now frigate-camera-control-bridge.service
|
||||||
|
```
|
||||||
|
|
||||||
|
W pliku env uzupelnij:
|
||||||
|
- `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`
|
||||||
|
- `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD`
|
||||||
|
- `FRIGATE_NAME`, `FRIGATE_URL`, `BASE_TOPIC`
|
||||||
|
|
||||||
|
Jesli sidecar ma startowac razem z istniejaca usluga Frigate, dopasuj w
|
||||||
|
`.container` nazwe `frigate.service` w polach `After`, `Requires`, `BindsTo`
|
||||||
|
i `PartOf`.
|
||||||
|
|
||||||
|
## EN
|
||||||
|
|
||||||
|
This is a generic example for running `frigate-camera-control-bridge` with
|
||||||
|
Podman Quadlet. The files model one Frigate instance. For multiple instances,
|
||||||
|
copy the `.container` and `.env` files and change `FRIGATE_NAME`,
|
||||||
|
`FRIGATE_URL`, `BASE_TOPIC`, the `PublishPort` binding, and MQTT credentials.
|
||||||
|
|
||||||
|
Example install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
install -d /etc/frigate-camera-control-bridge
|
||||||
|
install -m 0600 frigate-camera-control-bridge.env.example \
|
||||||
|
/etc/frigate-camera-control-bridge/frigate-main.env
|
||||||
|
install -m 0644 frigate-camera-control-bridge.container \
|
||||||
|
/etc/containers/systemd/frigate-camera-control-bridge.container
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now frigate-camera-control-bridge.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill these env values:
|
||||||
|
- `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`
|
||||||
|
- `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD`
|
||||||
|
- `FRIGATE_NAME`, `FRIGATE_URL`, `BASE_TOPIC`
|
||||||
|
|
||||||
|
If the sidecar should follow an existing Frigate service, adjust the
|
||||||
|
`frigate.service` references in `After`, `Requires`, `BindsTo`, and `PartOf`.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Frigate camera control bridge sidecar
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target frigate.service
|
||||||
|
Requires=frigate.service
|
||||||
|
BindsTo=frigate.service
|
||||||
|
PartOf=frigate.service
|
||||||
|
RequiresMountsFor=/var/lib/frigate-camera-control-bridge
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=frigate-camera-control-bridge
|
||||||
|
Image=localhost/frigate-camera-control-bridge:1.0
|
||||||
|
PublishPort=0.0.0.0:5011:5011
|
||||||
|
Volume=/var/lib/frigate-camera-control-bridge:/var/lib/frigate-camera-control-bridge
|
||||||
|
Volume=/etc/localtime:/etc/localtime:ro
|
||||||
|
PodmanArgs=--env-file=/etc/frigate-camera-control-bridge/frigate-main.env
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
TimeoutStartSec=180
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
WantedBy=frigate.service
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
FRIGATE_NAME=frigate-main
|
||||||
|
FRIGATE_URL=http://127.0.0.1:5000
|
||||||
|
BASE_TOPIC=frigate_camera_control/frigate-main
|
||||||
|
|
||||||
|
MQTT_HOST=mqtt.local
|
||||||
|
MQTT_PORT=1883
|
||||||
|
MQTT_USER=change-me
|
||||||
|
MQTT_PASSWORD=change-me
|
||||||
|
|
||||||
|
DEFAULT_CAMERA_USER=admin
|
||||||
|
DEFAULT_CAMERA_PASSWORD=change-me
|
||||||
|
|
||||||
|
HTTP_PORT=5011
|
||||||
|
UI_LANGUAGE=en
|
||||||
|
WAIT_FOR_FRIGATE=true
|
||||||
|
FRIGATE_RETRY_SECONDS=5
|
||||||
|
FRIGATE_STARTUP_WAIT_SECONDS=0
|
||||||
|
|
||||||
|
SNAPSHOT_ENABLED=true
|
||||||
|
SNAPSHOT_HEIGHT=180
|
||||||
|
SNAPSHOT_CACHE_SECONDS=300
|
||||||
|
SNAPSHOT_RETRY_SECONDS=600
|
||||||
|
SNAPSHOT_REFRESH_SECONDS=3600
|
||||||
|
SNAPSHOT_LIVE_ON_GUI=true
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: frigate-camera-control-bridge 1.0\n"
|
||||||
|
"Language: en\n"
|
||||||
|
"Language-Name: English\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
|
msgid "title"
|
||||||
|
msgstr "Frigate Camera Control"
|
||||||
|
|
||||||
|
msgid "language"
|
||||||
|
msgstr "Language"
|
||||||
|
|
||||||
|
msgid "id"
|
||||||
|
msgstr "ID"
|
||||||
|
|
||||||
|
msgid "name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
msgid "type"
|
||||||
|
msgstr "Type"
|
||||||
|
|
||||||
|
msgid "state"
|
||||||
|
msgstr "State"
|
||||||
|
|
||||||
|
msgid "actions"
|
||||||
|
msgstr "Actions"
|
||||||
|
|
||||||
|
msgid "snapshot"
|
||||||
|
msgstr "Snapshot"
|
||||||
|
|
||||||
|
msgid "button.on"
|
||||||
|
msgstr "ON"
|
||||||
|
|
||||||
|
msgid "button.off"
|
||||||
|
msgstr "OFF"
|
||||||
|
|
||||||
|
msgid "button.press"
|
||||||
|
msgstr "PRESS"
|
||||||
|
|
||||||
|
msgid "action.all_off"
|
||||||
|
msgstr "{target} all off"
|
||||||
|
|
||||||
|
msgid "action.manual_white"
|
||||||
|
msgstr "{target} manual white light"
|
||||||
|
|
||||||
|
msgid "action.red_blue"
|
||||||
|
msgstr "{target} red-blue light"
|
||||||
|
|
||||||
|
msgid "action.siren"
|
||||||
|
msgstr "{target} siren"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: frigate-camera-control-bridge 1.0\n"
|
||||||
|
"Language: pl\n"
|
||||||
|
"Language-Name: Polski\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
|
msgid "title"
|
||||||
|
msgstr "Frigate Camera Control"
|
||||||
|
|
||||||
|
msgid "language"
|
||||||
|
msgstr "Jezyk"
|
||||||
|
|
||||||
|
msgid "id"
|
||||||
|
msgstr "ID"
|
||||||
|
|
||||||
|
msgid "name"
|
||||||
|
msgstr "Nazwa"
|
||||||
|
|
||||||
|
msgid "type"
|
||||||
|
msgstr "Typ"
|
||||||
|
|
||||||
|
msgid "state"
|
||||||
|
msgstr "Stan"
|
||||||
|
|
||||||
|
msgid "actions"
|
||||||
|
msgstr "Akcje"
|
||||||
|
|
||||||
|
msgid "snapshot"
|
||||||
|
msgstr "Migawka"
|
||||||
|
|
||||||
|
msgid "button.on"
|
||||||
|
msgstr "WLACZ"
|
||||||
|
|
||||||
|
msgid "button.off"
|
||||||
|
msgstr "WYLACZ"
|
||||||
|
|
||||||
|
msgid "button.press"
|
||||||
|
msgstr "URUCHOM"
|
||||||
|
|
||||||
|
msgid "action.all_off"
|
||||||
|
msgstr "{target} wylacz wszystko"
|
||||||
|
|
||||||
|
msgid "action.manual_white"
|
||||||
|
msgstr "{target} biale swiatlo reczne"
|
||||||
|
|
||||||
|
msgid "action.red_blue"
|
||||||
|
msgstr "{target} czerwono-niebieskie"
|
||||||
|
|
||||||
|
msgid "action.siren"
|
||||||
|
msgstr "{target} syrena"
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import importlib.util
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load_bridge_module():
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
path = root / "frigate-camera-control-bridge.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("bridge", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryMappingTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.bridge = load_bridge_module()
|
||||||
|
|
||||||
|
def test_loopback_ffmpeg_path_resolves_through_go2rtc(self):
|
||||||
|
camera_cfg = {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://127.0.0.1:8554/stream-a?video=copy"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streams = {
|
||||||
|
"stream-a": [
|
||||||
|
"rtsp://camera-a.example.test:554/Streaming/Channels/101",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
urls = self.bridge.CameraControlBridge.camera_candidate_urls("camera-a", camera_cfg, streams)
|
||||||
|
|
||||||
|
self.assertEqual(urls, ["rtsp://camera-a.example.test:554/Streaming/Channels/101"])
|
||||||
|
self.assertEqual(self.bridge.CameraControlBridge.hosts_from_urls(urls), ["camera-a.example.test"])
|
||||||
|
|
||||||
|
def test_direct_ffmpeg_path_is_used_without_go2rtc(self):
|
||||||
|
camera_cfg = {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://camera-b.example.test:554/cam/realmonitor?channel=1&subtype=0"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls = self.bridge.CameraControlBridge.camera_candidate_urls("camera-b", camera_cfg, {})
|
||||||
|
|
||||||
|
self.assertEqual(urls, ["rtsp://camera-b.example.test:554/cam/realmonitor?channel=1&subtype=0"])
|
||||||
|
self.assertEqual(self.bridge.CameraControlBridge.hosts_from_urls(urls), ["camera-b.example.test"])
|
||||||
|
|
||||||
|
def test_direct_path_wins_when_loopback_is_also_present(self):
|
||||||
|
camera_cfg = {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://127.0.0.1:8554/stream-a"},
|
||||||
|
{"path": "rtsp://direct-camera.example.test:554/Streaming/Channels/102"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streams = {
|
||||||
|
"stream-a": {
|
||||||
|
"streams": [
|
||||||
|
"rtsp://camera-a.example.test:554/Streaming/Channels/101",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls = self.bridge.CameraControlBridge.camera_candidate_urls("camera-a", camera_cfg, streams)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
urls,
|
||||||
|
[
|
||||||
|
"rtsp://direct-camera.example.test:554/Streaming/Channels/102",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.bridge.CameraControlBridge.hosts_from_urls(urls),
|
||||||
|
[
|
||||||
|
"direct-camera.example.test",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import ast
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def read_po(path):
|
||||||
|
messages = {}
|
||||||
|
current = None
|
||||||
|
buffers = {"msgid": [], "msgstr": []}
|
||||||
|
|
||||||
|
def flush():
|
||||||
|
if buffers["msgid"] or buffers["msgstr"]:
|
||||||
|
messages["".join(buffers["msgid"])] = "".join(buffers["msgstr"])
|
||||||
|
buffers["msgid"] = []
|
||||||
|
buffers["msgstr"] = []
|
||||||
|
|
||||||
|
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if line.startswith("msgid "):
|
||||||
|
flush()
|
||||||
|
current = "msgid"
|
||||||
|
buffers[current].append(ast.literal_eval(line[6:].strip()))
|
||||||
|
continue
|
||||||
|
if line.startswith("msgstr "):
|
||||||
|
current = "msgstr"
|
||||||
|
buffers[current].append(ast.literal_eval(line[7:].strip()))
|
||||||
|
continue
|
||||||
|
if line.startswith('"') and current:
|
||||||
|
buffers[current].append(ast.literal_eval(line))
|
||||||
|
flush()
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def write_mo(messages, path):
|
||||||
|
keys = sorted(messages)
|
||||||
|
ids = b"\x00".join(key.encode("utf-8") for key in keys) + b"\x00"
|
||||||
|
strs = b"\x00".join(messages[key].encode("utf-8") for key in keys) + b"\x00"
|
||||||
|
|
||||||
|
keystart = 7 * 4 + len(keys) * 16
|
||||||
|
valuestart = keystart + len(ids)
|
||||||
|
koffsets = []
|
||||||
|
voffsets = []
|
||||||
|
offset = keystart
|
||||||
|
for key in keys:
|
||||||
|
encoded = key.encode("utf-8")
|
||||||
|
koffsets.append((len(encoded), offset))
|
||||||
|
offset += len(encoded) + 1
|
||||||
|
offset = valuestart
|
||||||
|
for key in keys:
|
||||||
|
encoded = messages[key].encode("utf-8")
|
||||||
|
voffsets.append((len(encoded), offset))
|
||||||
|
offset += len(encoded) + 1
|
||||||
|
|
||||||
|
output = [
|
||||||
|
struct.pack("Iiiiiii", 0x950412DE, 0, len(keys), 7 * 4, 7 * 4 + len(keys) * 8, 0, 0)
|
||||||
|
]
|
||||||
|
output.extend(struct.pack("ii", length, offset) for length, offset in koffsets)
|
||||||
|
output.extend(struct.pack("ii", length, offset) for length, offset in voffsets)
|
||||||
|
output.append(ids)
|
||||||
|
output.append(strs)
|
||||||
|
path.write_bytes(b"".join(output))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = Path(sys.argv[1] if len(sys.argv) > 1 else "locale")
|
||||||
|
for po in root.glob("*/LC_MESSAGES/*.po"):
|
||||||
|
mo = po.with_suffix(".mo")
|
||||||
|
write_mo(read_po(po), mo)
|
||||||
|
print(mo)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user