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:
+30
-11
@@ -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}
|
||||
|
||||
+24
-13
@@ -18,6 +18,7 @@ class Camera:
|
||||
password: str
|
||||
main_path: str
|
||||
sub_path: str | None = None
|
||||
record: bool = True # REC: писать на диск. enabled — камера вообще включена (live+запись)
|
||||
|
||||
def rtsp_url(self, path: str) -> str:
|
||||
return f"rtsp://{self.user}:{self.password}@{self.ip}:554{path}"
|
||||
@@ -70,27 +71,37 @@ def load_config(path: str | None = None) -> Config:
|
||||
|
||||
storage = Storage(**(raw.get("storage") or {}))
|
||||
live = Live(**(raw.get("live") or {}))
|
||||
cameras = [
|
||||
Camera(
|
||||
id=c["id"],
|
||||
name=c.get("name", c["id"]),
|
||||
enabled=c.get("enabled", True),
|
||||
ip=c["ip"],
|
||||
user=c["user"],
|
||||
password=c["password"],
|
||||
main_path=c["main_path"],
|
||||
sub_path=c.get("sub_path"),
|
||||
)
|
||||
for c in (raw.get("cameras") or [])
|
||||
]
|
||||
cameras = [_camera_from_raw(c) for c in (raw.get("cameras") or [])]
|
||||
return Config(storage=storage, live=live, cameras=cameras)
|
||||
|
||||
|
||||
def _camera_from_raw(c: dict) -> Camera:
|
||||
# Миграция со старой схемы: раньше `enabled` означал «писать на диск» (live был всегда),
|
||||
# отдельного «камера выключена» не существовало. Старый файл (без ключа record) трактуем
|
||||
# так: запись = старый enabled, а сама камера включена. Новый файл (с record) — как есть.
|
||||
if "record" in c:
|
||||
enabled, record = bool(c.get("enabled", True)), bool(c["record"])
|
||||
else:
|
||||
enabled, record = True, bool(c.get("enabled", True))
|
||||
return Camera(
|
||||
id=c["id"],
|
||||
name=c.get("name", c["id"]),
|
||||
enabled=enabled,
|
||||
record=record,
|
||||
ip=c["ip"],
|
||||
user=c["user"],
|
||||
password=c["password"],
|
||||
main_path=c["main_path"],
|
||||
sub_path=c.get("sub_path"),
|
||||
)
|
||||
|
||||
|
||||
def camera_to_yaml(c: Camera) -> dict:
|
||||
return {
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"enabled": c.enabled,
|
||||
"record": c.record,
|
||||
"ip": c.ip,
|
||||
"user": c.user,
|
||||
"password": c.password,
|
||||
|
||||
@@ -26,11 +26,11 @@ class Supervisor:
|
||||
task = RecorderTask(cam, self.config.storage, on_status=self.on_status,
|
||||
input_url=self._input_url(cam))
|
||||
self.tasks[cam.id] = task
|
||||
if cam.enabled:
|
||||
if cam.enabled and cam.record:
|
||||
task.start()
|
||||
log.info("recorder started: %s", cam.id)
|
||||
else:
|
||||
log.info("recorder skipped (disabled): %s", cam.id)
|
||||
log.info("recorder skipped (enabled=%s record=%s): %s", cam.enabled, cam.record, cam.id)
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
for task in self.tasks.values():
|
||||
@@ -60,7 +60,7 @@ class Supervisor:
|
||||
task = RecorderTask(cam, self.config.storage, on_status=self.on_status,
|
||||
input_url=self._input_url(cam))
|
||||
self.tasks[cam.id] = task
|
||||
if cam.enabled:
|
||||
if cam.enabled and cam.record:
|
||||
task.start()
|
||||
log.info("recorder added: %s", cam.id)
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ def _streams_for(cam: Camera) -> dict[str, str]:
|
||||
def build_streams(config: Config) -> dict:
|
||||
streams: dict = {}
|
||||
for c in config.cameras:
|
||||
if not c.enabled: # выключенная камера — без потоков (ни live, ни записи)
|
||||
continue
|
||||
for name, src in _streams_for(c).items():
|
||||
streams[name] = [src]
|
||||
return streams
|
||||
@@ -120,6 +122,8 @@ async def sync_all(config: Config) -> None:
|
||||
write_go2rtc_yaml(config)
|
||||
base = config.live.go2rtc_url.rstrip("/")
|
||||
for cam in config.cameras:
|
||||
if not cam.enabled:
|
||||
continue
|
||||
for name, src in _streams_for(cam).items():
|
||||
await _call("PUT", _put(base, name, src))
|
||||
|
||||
@@ -151,6 +155,8 @@ async def reconcile(config: Config) -> int:
|
||||
return 0
|
||||
added = 0
|
||||
for cam in config.cameras:
|
||||
if not cam.enabled: # выключенные камеры в go2rtc не держим
|
||||
continue
|
||||
for name, src in _streams_for(cam).items():
|
||||
if name not in existing:
|
||||
await _call("PUT", _put(base, name, src))
|
||||
|
||||
Reference in New Issue
Block a user