v1.0
This commit is contained in:
1 parent
0246cbb6ba
commit
0ca4b4bf7a
89 files changed
+7042
-2480
No files matched your search
@@ -7,12 +7,17 @@ Persisted to ``%LOCALAPPDATA%/JavisSlave/slave_config.json`` (or
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import uuid
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Plain stdlib logger: logging_setup imports this module (config_dir).
|
||||
_log = logging.getLogger("javis_slave.config")
|
||||
|
||||
|
||||
def config_dir() -> Path:
|
||||
override = os.environ.get("JAVIS_SLAVE_CONFIG_DIR")
|
||||
@@ -54,6 +59,9 @@ class SlaveConfig:
|
||||
# unreachable by remote controllers). 0.0.0.0 lets other devices reach it.
|
||||
api_host: str = "0.0.0.0"
|
||||
api_port: int = 8766
|
||||
# Bearer token required by the control API for non-loopback callers.
|
||||
# Generated on first start (ensure_control_token); see --show-control-token.
|
||||
control_token: str = ""
|
||||
|
||||
@property
|
||||
def paired(self) -> bool:
|
||||
@@ -64,21 +72,51 @@ class SlaveConfig:
|
||||
return f"http://{self.master_host}:{self.master_port}"
|
||||
|
||||
|
||||
def ensure_control_token(cfg: SlaveConfig) -> bool:
|
||||
"""Generate a control-API token if none is set; returns True when one was created."""
|
||||
if cfg.control_token:
|
||||
return False
|
||||
cfg.control_token = secrets.token_urlsafe(24)
|
||||
return True
|
||||
|
||||
|
||||
def _assign(target, raw: dict) -> None:
|
||||
"""Copy known keys from *raw*, skipping values whose type doesn't match the default."""
|
||||
for f in fields(target):
|
||||
if f.name not in raw or f.name == "wake_word":
|
||||
continue
|
||||
val, cur = raw[f.name], getattr(target, f.name)
|
||||
if cur is not None and val is not None and not isinstance(val, type(cur)):
|
||||
if isinstance(cur, float) and isinstance(val, int) and not isinstance(val, bool):
|
||||
val = float(val)
|
||||
else:
|
||||
_log.warning(f"config: ignoring {f.name}={val!r} (expected {type(cur).__name__})")
|
||||
continue
|
||||
setattr(target, f.name, val)
|
||||
|
||||
|
||||
def load_config() -> SlaveConfig:
|
||||
path = config_path()
|
||||
cfg = SlaveConfig()
|
||||
if path.exists():
|
||||
if not path.exists():
|
||||
return cfg
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("top level is not an object")
|
||||
except (OSError, ValueError) as exc:
|
||||
# Keep the broken file: silently starting fresh would mint a new
|
||||
# device_id and the next save would wipe the pairing for good.
|
||||
backup = path.with_name(path.name + ".corrupt")
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
ww_raw = raw.pop("wake_word", {}) or {}
|
||||
for key, val in raw.items():
|
||||
if hasattr(cfg, key) and key != "wake_word":
|
||||
setattr(cfg, key, val)
|
||||
for key, val in ww_raw.items():
|
||||
if hasattr(cfg.wake_word, key):
|
||||
setattr(cfg.wake_word, key, val)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
path.replace(backup)
|
||||
except OSError:
|
||||
backup = path
|
||||
_log.error(f"config {path} unreadable ({exc}); kept as {backup} — starting unpaired")
|
||||
return cfg
|
||||
_assign(cfg, raw)
|
||||
if isinstance(raw.get("wake_word"), dict):
|
||||
_assign(cfg.wake_word, raw["wake_word"])
|
||||
return cfg
|
||||
|
||||
|
||||
@@ -86,5 +124,11 @@ def save_config(cfg: SlaveConfig) -> None:
|
||||
path = config_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(asdict(cfg), indent=2), encoding="utf-8")
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(asdict(cfg), indent=2))
|
||||
f.flush()
|
||||
# SD cards + power loss: make the data durable before the rename.
|
||||
os.fsync(f.fileno())
|
||||
if os.name == "posix":
|
||||
os.chmod(tmp, 0o600) # holds the master API key and control token
|
||||
tmp.replace(path)
|
||||
Reference in new issue
Block a user