cameras: split camera enable (Вкл) from REC (record-to-disk)

Add a separate `record` field so a camera can be registered but fully
off, or on but not recording:
- enabled (Вкл): camera on/off. Disabled = no go2rtc stream (no live),
  no recording; entry is just stored. build_streams/sync_all/reconcile
  skip disabled cameras; live grid shows "камера выключена".
- record (REC): write segments to disk. Recorder runs only when
  enabled AND record.

Unified POST /api/cameras/{id}/toggle {enabled?, record?} (replaces the
/record endpoint); go2rtc is touched only when `enabled` changes, so
flipping REC doesn't blip live. Camera table gets Вкл + Запись toggle
columns; add/edit forms get both switches.

load_config migrates the old schema (no `record` key): old `enabled`
meant "record" with live always on -> enabled=True, record=old enabled.

VERSION -> 0.0.126.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 18:35:26 +05:00
parent 7060f26e86
commit 24155a8492
7 changed files with 97 additions and 44 deletions
+30 -11
View File
@@ -33,6 +33,7 @@ def _camera_from_body(body: dict, cam_id: str) -> Camera:
id=cam_id,
name=(body.get("name") or cam_id).strip(),
enabled=bool(body.get("enabled", True)),
record=bool(body.get("record", True)),
ip=ip,
user=user,
password=password,
@@ -50,6 +51,7 @@ def _camera_payload(cam_id: str) -> dict:
"id": cam.id,
"name": cam.name,
"enabled": cam.enabled,
"record": cam.record,
"ip": cam.ip,
"user": cam.user,
"password": cam.password,
@@ -255,7 +257,8 @@ async def create_camera(request: Request) -> dict:
runtime.config.cameras.append(cam)
save_config(runtime.config)
runtime.supervisor.add_camera(cam)
await go2rtc.add_camera_streams(runtime.config, cam)
if cam.enabled:
await go2rtc.add_camera_streams(runtime.config, cam)
return _camera_payload(cam_id)
@@ -270,7 +273,10 @@ async def update_camera(cam_id: str, request: Request) -> dict:
runtime.config.cameras[idx] = cam
save_config(runtime.config)
await runtime.supervisor.replace_camera(cam)
await go2rtc.add_camera_streams(runtime.config, cam)
if cam.enabled:
await go2rtc.add_camera_streams(runtime.config, cam)
else:
await go2rtc.remove_camera_streams(runtime.config, cam_id)
return _camera_payload(cam_id)
@@ -301,21 +307,34 @@ async def clear_cameras(request: Request) -> dict:
return {"ok": True, "removed": len(ids)}
@router.post("/{cam_id}/record")
async def set_record(cam_id: str, request: Request) -> dict:
"""Включает/выключает запись камеры на диск (тумблер REC).
@router.post("/{cam_id}/toggle")
async def toggle_camera(cam_id: str, request: Request) -> dict:
"""Тумблеры камеры: enabled (вкл/выкл камеру) и/или record (REC — запись на диск).
enabled=False — запись не идёт, но go2rtc/live не трогаем → просмотр работает.
Меняет только рекордер (без пересоздания go2rtc-потоков, чтобы live не мигал)."""
enabled=False — камера выключена полностью: убираем go2rtc-потоки → нет live и записи
(камера остаётся зарегистрированной). record управляет только записью. go2rtc трогаем
лишь при смене enabled, поэтому переключение REC не мигает live."""
require_role(request, "admin")
cam = runtime.config.camera(cam_id)
if not cam:
raise HTTPException(404, "camera not found")
body = await request.json()
on = bool(body.get("enabled", True))
kw = {}
if "enabled" in body:
kw["enabled"] = bool(body["enabled"])
if "record" in body:
kw["record"] = bool(body["record"])
if not kw:
raise HTTPException(400, "ожидается enabled и/или record")
enabled_changed = "enabled" in kw and kw["enabled"] != cam.enabled
new = replace(cam, **kw)
idx = next(i for i, c in enumerate(runtime.config.cameras) if c.id == cam_id)
new = replace(cam, enabled=on)
runtime.config.cameras[idx] = new
save_config(runtime.config)
await runtime.supervisor.replace_camera(new) # старт/стоп записи; go2rtc не трогаем
return {"ok": True, "enabled": on}
if enabled_changed: # потоки go2rtc только при смене вкл/выкл камеры
if new.enabled:
await go2rtc.add_camera_streams(runtime.config, new)
else:
await go2rtc.remove_camera_streams(runtime.config, cam_id)
await runtime.supervisor.replace_camera(new) # рекордер запускается при enabled AND record
return {"ok": True, "enabled": new.enabled, "record": new.record}