104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
|
|
"""手术室站点配置:单一 JSON 文件,严格结构,无历史格式分支。"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from loguru import logger
|
||
|
|
|
||
|
|
from app.services.voice_terminal_binding import VoiceTerminalBindingIndex
|
||
|
|
|
||
|
|
_ALLOWED_TOP_LEVEL = frozenset({"video_rtsp_urls", "voice_or_room_bindings"})
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class OrSiteConfig:
|
||
|
|
"""根对象须含 ``video_rtsp_urls`` 与 ``voice_or_room_bindings`` 两个键。"""
|
||
|
|
|
||
|
|
video_rtsp_urls: dict[str, str]
|
||
|
|
voice_bindings: VoiceTerminalBindingIndex
|
||
|
|
|
||
|
|
|
||
|
|
def parse_or_site_config_object(data: Any, *, source: str | Path = "") -> OrSiteConfig:
|
||
|
|
label = str(source) if source else "JSON"
|
||
|
|
if not isinstance(data, dict):
|
||
|
|
raise ValueError(f"{label}: OR site config must be a JSON object")
|
||
|
|
extra = set(data.keys()) - _ALLOWED_TOP_LEVEL
|
||
|
|
if extra:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}: unknown top-level keys {sorted(extra)}; "
|
||
|
|
f"allowed: {sorted(_ALLOWED_TOP_LEVEL)}"
|
||
|
|
)
|
||
|
|
if "video_rtsp_urls" not in data or "voice_or_room_bindings" not in data:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}: must include video_rtsp_urls and voice_or_room_bindings"
|
||
|
|
)
|
||
|
|
urls = data["video_rtsp_urls"]
|
||
|
|
if not isinstance(urls, dict):
|
||
|
|
raise ValueError(f"{label}: video_rtsp_urls must be a JSON object")
|
||
|
|
raw_bindings = data["voice_or_room_bindings"]
|
||
|
|
if not isinstance(raw_bindings, list):
|
||
|
|
raise ValueError(f"{label}: voice_or_room_bindings must be a JSON array")
|
||
|
|
for k, v in urls.items():
|
||
|
|
if not isinstance(v, str):
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}: video_rtsp_urls[{k!r}] must be a string (RTSP URL)"
|
||
|
|
)
|
||
|
|
idx = VoiceTerminalBindingIndex.from_binding_list(raw_bindings)
|
||
|
|
if idx is None:
|
||
|
|
raise ValueError(f"{label}: invalid voice_or_room_bindings content")
|
||
|
|
return OrSiteConfig(
|
||
|
|
video_rtsp_urls={str(k): str(v) for k, v in urls.items()},
|
||
|
|
voice_bindings=idx,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def load_or_site_config_from_path(path: Path) -> OrSiteConfig:
|
||
|
|
p = path.expanduser()
|
||
|
|
try:
|
||
|
|
raw_text = p.read_text(encoding="utf-8")
|
||
|
|
except OSError as exc:
|
||
|
|
raise ValueError(f"Cannot read OR site config {p}: {exc}") from exc
|
||
|
|
try:
|
||
|
|
data: Any = json.loads(raw_text)
|
||
|
|
except json.JSONDecodeError as exc:
|
||
|
|
raise ValueError(f"Invalid JSON in OR site config {p}: {exc}") from exc
|
||
|
|
return parse_or_site_config_object(data, source=p)
|
||
|
|
|
||
|
|
|
||
|
|
def merge_video_rtsp_urls_into_file(
|
||
|
|
path: Path,
|
||
|
|
url_map: dict[str, str],
|
||
|
|
*,
|
||
|
|
replace_host: str,
|
||
|
|
) -> None:
|
||
|
|
"""写入/更新站点配置中的 ``video_rtsp_urls``,保留 ``voice_or_room_bindings``。"""
|
||
|
|
if replace_host in ("", "127.0.0.1"):
|
||
|
|
out_urls = dict(url_map)
|
||
|
|
else:
|
||
|
|
out_urls = {
|
||
|
|
k: v.replace("127.0.0.1", replace_host, 1)
|
||
|
|
for k, v in url_map.items()
|
||
|
|
}
|
||
|
|
path = path.expanduser()
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
if path.is_file():
|
||
|
|
raw_text = path.read_text(encoding="utf-8")
|
||
|
|
data: Any = json.loads(raw_text)
|
||
|
|
parse_or_site_config_object(data, source=path)
|
||
|
|
bindings_list = data["voice_or_room_bindings"]
|
||
|
|
else:
|
||
|
|
bindings_list = []
|
||
|
|
doc = {
|
||
|
|
"video_rtsp_urls": out_urls,
|
||
|
|
"voice_or_room_bindings": bindings_list,
|
||
|
|
}
|
||
|
|
text = json.dumps(doc, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||
|
|
temp = path.with_name(path.name + ".tmp")
|
||
|
|
temp.write_text(text, encoding="utf-8")
|
||
|
|
temp.replace(path)
|
||
|
|
logger.info("Updated video_rtsp_urls in OR site config {}", path)
|