Feature: Persistente History via SQLite (v1.3.0)
Sensorwerte werden in /data/history.db (SQLite, WAL-Modus) persistiert und überleben damit Add-on-Neustarts. Beim Start werden die letzten 300 Messpunkte pro Sensor in die In-Memory-Deque geladen, sodass Sparklines sofort Daten zeigen. Retention: 7 Tage. Neue API: GET /api/history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+26
-2
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional
|
||||
from flask import Flask, jsonify, request, send_from_directory
|
||||
|
||||
from inverters import INVERTERS
|
||||
import history
|
||||
from modbus_client import ModbusReader
|
||||
from mqtt_publisher import MqttPublisher
|
||||
|
||||
@@ -160,9 +161,18 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
|
||||
_publisher.register_inverter(inverter, device_id, prefix,
|
||||
inv_cfg.get("name", inverter.name))
|
||||
|
||||
log.info("[%s] Poll-Loop: %s @ %s:%s alle %ds",
|
||||
# History aus DB in die In-Memory-Deque laden
|
||||
hist_data = history.load_recent(inv_id, limit=300)
|
||||
with State.lock:
|
||||
d = State.inv_data.setdefault(inv_id, {"poll_count": 0})
|
||||
hist = d.setdefault("history", {})
|
||||
for sid, points in hist_data.items():
|
||||
q = hist.setdefault(sid, deque(maxlen=300))
|
||||
for pt in points:
|
||||
q.append(pt)
|
||||
log.info("[%s] Poll-Loop: %s @ %s:%s alle %ds — %d Sensoren aus DB geladen",
|
||||
inv_id, inverter.name, inv_cfg["modbus_ip"],
|
||||
inv_cfg.get("modbus_port", 502), interval)
|
||||
inv_cfg.get("modbus_port", 502), interval, len(hist_data))
|
||||
|
||||
while not stop.is_set():
|
||||
t0 = time.time()
|
||||
@@ -179,6 +189,7 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
|
||||
for sid, val in values.items():
|
||||
q = hist.setdefault(sid, deque(maxlen=300))
|
||||
q.append((now, val))
|
||||
history.write_batch(inv_id, now, values)
|
||||
if _publisher:
|
||||
_publisher.publish_data(values, prefix)
|
||||
_publisher.publish_status("online", prefix)
|
||||
@@ -321,6 +332,18 @@ def api_get_data():
|
||||
aggregates = _compute_aggregates()
|
||||
return jsonify({"inverters": result, "mqtt_ok": mqtt_ok, "aggregates": aggregates})
|
||||
|
||||
@app.get("/api/history")
|
||||
def api_get_history():
|
||||
inv_id = request.args.get("inv_id", "")
|
||||
sensor_id = request.args.get("sensor_id", "")
|
||||
hours = min(float(request.args.get("hours", "24")), 24 * 7)
|
||||
if not inv_id or not sensor_id:
|
||||
return jsonify({"error": "inv_id and sensor_id required"}), 400
|
||||
to_ts = time.time()
|
||||
from_ts = to_ts - hours * 3600
|
||||
rows = history.query(inv_id, sensor_id, from_ts, to_ts)
|
||||
return jsonify([{"ts": t, "value": v} for t, v in rows])
|
||||
|
||||
@app.get("/api/inverter-models")
|
||||
def api_get_models():
|
||||
return jsonify({
|
||||
@@ -343,6 +366,7 @@ def static_files(filename):
|
||||
# ── Startup ───────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
history.init_db()
|
||||
cfg = load_config()
|
||||
with State.lock:
|
||||
State.mqtt_cfg = {k: cfg[k] for k in
|
||||
|
||||
Reference in New Issue
Block a user