Files
2026-11-02 01:23:50 +03:00

207 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""BLE sensor status / log / names / settings (collector UI bridge)."""
import json
from flask import Blueprint, jsonify, request
from auth import login_required
from paths import install_root, run_dir
sensors_bp = Blueprint("sensors", __name__)
STATUS_NAME = "sensors-status.json"
LOG_NAME = "sensors-ui.log"
CMD_NAME = "sensors-cmd"
NAMES_NAME = "sensors-names.json"
SETTINGS_NAME = "sensors-settings.json"
LOG_LIMIT = 200
DEFAULT_INTERVAL_MIN = 30
def _config_dir():
p = install_root() / "config"
try:
p.mkdir(parents=True, exist_ok=True)
except OSError:
pass
return p
def _normalize_addr(addr: str) -> str:
return (addr or "").strip().upper()
def _read_status():
path = run_dir() / STATUS_NAME
if not path.exists():
return {
"n": 4,
"pool_full": False,
"period_s": DEFAULT_INTERVAL_MIN * 60,
"report_interval_min": DEFAULT_INTERVAL_MIN,
"slots": [],
"ts": 0,
"available": False,
}
try:
data = json.loads(path.read_text(encoding="utf-8"))
data["available"] = True
if "report_interval_min" not in data and "period_s" in data:
try:
data["report_interval_min"] = max(1, int(data["period_s"]) // 60)
except (TypeError, ValueError):
data["report_interval_min"] = DEFAULT_INTERVAL_MIN
return data
except (OSError, ValueError):
return {"available": False, "slots": [], "error": "bad status file"}
def _read_log(limit=LOG_LIMIT):
path = run_dir() / LOG_NAME
if not path.exists():
return []
try:
lines = path.read_text(encoding="utf-8").splitlines()
except OSError:
return []
entries = []
for line in lines[-limit:]:
line = line.strip()
if not line:
continue
try:
entries.append(json.loads(line))
except ValueError:
continue
entries.reverse()
return entries
def _write_cmd(payload: dict) -> bool:
path = run_dir() / CMD_NAME
try:
path.write_text(json.dumps(payload), encoding="utf-8")
return True
except OSError:
return False
def _load_names():
path = _config_dir() / NAMES_NAME
if not path.exists():
return {}
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return {}
if not isinstance(raw, dict):
return {}
out = {}
for k, v in raw.items():
addr = _normalize_addr(str(k))
name = str(v).strip()
if addr and name:
out[addr] = name
return out
def _save_names(names: dict) -> dict:
cleaned = {}
for k, v in (names or {}).items():
addr = _normalize_addr(str(k))
name = str(v).strip()
if addr and name:
cleaned[addr] = name
path = _config_dir() / NAMES_NAME
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(cleaned, indent=2, ensure_ascii=False), encoding="utf-8")
tmp.replace(path)
return cleaned
def _load_settings():
path = _config_dir() / SETTINGS_NAME
if not path.exists():
return {"report_interval_min": DEFAULT_INTERVAL_MIN}
try:
raw = json.loads(path.read_text(encoding="utf-8"))
v = int(raw.get("report_interval_min", DEFAULT_INTERVAL_MIN))
except (OSError, ValueError, TypeError):
return {"report_interval_min": DEFAULT_INTERVAL_MIN}
return {"report_interval_min": max(1, min(65535, v))}
def _save_settings(minutes: int) -> dict:
v = max(1, min(65535, int(minutes)))
data = {"report_interval_min": v}
path = _config_dir() / SETTINGS_NAME
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
tmp.replace(path)
_write_cmd({"op": "reload_settings", "report_interval_min": v})
return data
@sensors_bp.route("/api/sensors", methods=["GET"])
@login_required
def api_sensors():
return jsonify(_read_status())
@sensors_bp.route("/api/sensors/log", methods=["GET"])
@login_required
def api_sensors_log():
return jsonify({"entries": _read_log()})
@sensors_bp.route("/api/sensors/advertise", methods=["POST"])
@login_required
def api_sensors_advertise():
if not _write_cmd({"op": "advertise"}):
return jsonify({"ok": False, "error": "cannot write command"}), 500
return jsonify({"ok": True})
@sensors_bp.route("/api/sensors/names", methods=["GET"])
@login_required
def api_sensors_names_get():
return jsonify({"names": _load_names()})
@sensors_bp.route("/api/sensors/names", methods=["PUT"])
@login_required
def api_sensors_names_put():
body = request.get_json(silent=True) or {}
names = body.get("names", body)
if not isinstance(names, dict):
return jsonify({"ok": False, "error": "names must be an object"}), 400
try:
cleaned = _save_names(names)
except OSError as e:
return jsonify({"ok": False, "error": str(e)}), 500
_write_cmd({"op": "reload_settings"})
return jsonify({"ok": True, "names": cleaned})
@sensors_bp.route("/api/sensors/settings", methods=["GET"])
@login_required
def api_sensors_settings_get():
return jsonify(_load_settings())
@sensors_bp.route("/api/sensors/settings", methods=["PUT"])
@login_required
def api_sensors_settings_put():
body = request.get_json(silent=True) or {}
try:
minutes = int(body.get("report_interval_min", DEFAULT_INTERVAL_MIN))
except (TypeError, ValueError):
return jsonify({"ok": False, "error": "report_interval_min must be int"}), 400
if minutes < 1 or minutes > 65535:
return jsonify({"ok": False, "error": "report_interval_min out of range"}), 400
try:
data = _save_settings(minutes)
except OSError as e:
return jsonify({"ok": False, "error": str(e)}), 500
return jsonify({"ok": True, **data})