Datenspeicherung¶
Dateiübersicht¶
| Pfad | Verwaltet von | Zweck |
|---|---|---|
/data/config.json |
miniEMS | Persistente Konfiguration mit Schema-Version |
/data/miniems.db |
miniEMS (store.py) |
SQLite-Tagesenergie-Historie |
| HA Core Entity Registry | Home Assistant | sensor.miniems_* Langzeit-Zustandshistorie |
In-Memory status_store |
miniEMS Runtime | Live-Werte für Dashboard und Publisher |
In-Memory CostOptimizer |
miniEMS Runtime | Intraday-Akkumulatoren (täglich um Mitternacht gespeichert) |
In-Memory EventLog |
miniEMS Runtime | Ringpuffer mit 100 Ereignissen (gestützt durch SQLite-Tabelle event_log) |
options.json und config.json
options.json wird beim Start gelesen und kann config.json für Werte
überschreiben, die von den Dataclass-Standardwerten abweichen. config.json
ist jedoch die dauerhaft maßgebliche Konfigurationsquelle — bei jedem Start
wird das zusammengeführte Ergebnis zurückgeschrieben, sodass es jeden
Supervisor-Reset von options.json überlebt.
Beide Dateien können über die Tabs config.json und options.json
in der miniEMS-Oberfläche eingesehen und bearbeitet werden.
/data/config.json¶
Erstellt und gepflegt von config_loader.py. Überlebt Add-on-Updates,
Supervisor-Neuladen und Neustarts.
{
"_version": 10,
"pv_power_entity": "sensor.deye_pv_total_power",
"battery_soc_entity": "sensor.deye_battery_soc",
"battery_capacity_kwh": 25.0,
"battery_min_soc": 10,
"battery_max_soc": 95,
"cheap_rate_threshold_eur": 0.28,
"medium_rate_threshold_eur": 0.20,
"feed_in_tariff_eur_kwh": 0.08,
"fix_price": 0.30,
"battery_control_enabled": false,
"battery_control_simulation": true,
"grid_charge_switch_entity": "switch.deye8k_battery_grid_charging",
"battery_discharging_power_entity": "number.deye8k_battery_discharging_power",
"battery_max_charge_power_w": 5500,
"battery_max_discharge_power_w": 5500,
"default_discharge_power_w": 185,
"solcast_remaining_today_entity": "sensor.solcast_pv_forecast_prognose_verbleibende_leistung_heute",
"solcast_today_entity": "sensor.solcast_pv_forecast_prognose_heute",
"solcast_tomorrow_entity": "sensor.solcast_pv_forecast_prognose_morgen",
"weather_entity": "weather.openweathermap",
"update_interval_sec": 30,
"event_log_retention_days": 30,
"long_lived_token": ""
}
Schema-Versionierung¶
_version wird von migration.py geschrieben. Die aktuelle Version ist 10.
Bei einer Schema-Änderung:
CONFIG_SCHEMA_VERSIONinconst.pyerhöhen- Eine Migrationsfunktion
_vN_to_vN1(data: dict) -> dicthinzufügen - In
migration.pyregistrieren
Migrationskette: v0 → v1 → v2 → v3 → v4 → v5 → v6 → v7 → v8 → v9 → v10
/data/miniems.db¶
SQLite-Datenbank, verwaltet von store.py. Enthält zwei Tabellen:
Tabelle daily_stats¶
CREATE TABLE daily_stats (
date TEXT PRIMARY KEY, -- ISO-Datum 'YYYY-MM-DD'
grid_import_kwh REAL DEFAULT 0,
grid_cost_eur REAL DEFAULT 0,
pv_used_kwh REAL DEFAULT 0,
pv_savings_eur REAL DEFAULT 0,
load_total_kwh REAL DEFAULT 0,
load_cost_eur REAL DEFAULT 0,
avg_price_eur_kwh REAL DEFAULT 0,
avg_outdoor_temp_c REAL,
ticks INTEGER DEFAULT 0,
-- per ALTER TABLE beim Upgrade hinzugefügt:
peak_pv_w REAL DEFAULT 0,
grid_charge_kwh REAL DEFAULT 0,
grid_charge_cost_eur REAL DEFAULT 0,
feed_in_kwh REAL DEFAULT 0,
feed_in_revenue_eur REAL DEFAULT 0,
last_flush_ts TEXT, -- UTC-ISO-Zeitstempel des letzten Schreibvorgangs
kwh_high_rate REAL DEFAULT 0, -- Last bei hohem Preisklasse
kwh_medium_rate REAL DEFAULT 0, -- Last bei mittlerer Preisklasse
kwh_low_rate REAL DEFAULT 0 -- Last bei niedriger Preisklasse
);
Neue Spalten werden beim ersten Start nach einem Upgrade automatisch per ALTER TABLE hinzugefügt.
Flush-Verhalten¶
CostOptimizer akkumuliert Werte den ganzen Tag im Speicher. Um Mitternacht
(erkannt durch Wechsel von date.today()) ruft er store.flush_day() auf,
um die fertige Tageszeile in SQLite zu schreiben. Die Spalte last_flush_ts
wird bei jedem Flush aktualisiert und beim Start zur Ausfallzeiten-Erkennung
verwendet.
Aggregation¶
store._aggregate() berechnet rollende/kalendarische Aggregate direkt in SQL:
-- Monat (Kalendermonat)
SELECT SUM(grid_cost_eur), SUM(kwh_high_rate), SUM(kwh_medium_rate), SUM(kwh_low_rate)
FROM daily_stats
WHERE strftime('%Y-%m', date) = strftime('%Y-%m', 'now')
Tabelle event_log¶
Speichert jedes Moduszwechsel- und Preisänderungsereignis, sodass das Log Neustarts und Updates überlebt.
CREATE TABLE event_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL, -- ISO 8601 Ortszeit
entry_type TEXT NOT NULL, -- "mode_change" | "price_change"
state TEXT NOT NULL, -- "on" | "off" | "price_change"
battery_kwh_freetochange REAL DEFAULT 0,
battery_kwh_useable REAL DEFAULT 0,
predicted_load_kwh REAL,
price_eur_kwh REAL -- NULL bei mode_change-Einträgen
);
Schreibpfad¶
EventLog.append() wird von EMSController bei jedem Moduszwechsel oder
Preisänderung aufgerufen. Es:
- Fügt den Eintrag zur In-Memory-
deque(maxlen=100)hinzu - Schreibt sofort eine Zeile über
store.append_event()inevent_log
Startup-Wiederherstellung¶
EventLog.restore_from_db() wird in main.py nach store.open() aufgerufen.
Es liest die letzten 100 Zeilen (neueste zuerst), kehrt die Reihenfolge um und
füllt den In-Memory-Puffer — sodass to_list() sofort befüllt ist.
Tägliche Bereinigung¶
Einmal täglich (beim ersten EMS-Tick nach Mitternacht) ruft EMSController
EventLog.cleanup_old_entries(retention_days) auf, das Folgendes ausführt:
Das Aufbewahrungsfenster wird durch event_log_retention_days in der Konfiguration
festgelegt (Standard: 30 Tage).
HA-Entity-Zustände (sensor.miniems_*)¶
Zustände werden über zwei Pfade geschrieben:
MQTT Discovery (bevorzugt)¶
miniEMS veröffentlicht Config-Topics an homeassistant/sensor/miniems_*/config
beim Start, dann Daten an homeassistant/sensor/miniems_*/state bei jedem Tick.
Sensoren erscheinen unter dem miniEMS-Gerät in HA.
REST API (Fallback)¶
POST http://hassio/homeassistant/api/states/sensor.miniems_<name>
{
"state": "<wert>",
"attributes": { "unit_of_measurement": "...", "state_class": "..." }
}
HA speichert diese Zustände in seiner eigenen Datenbank. Das Add-on pflegt
keine lokale Kopie über status_store hinaus.
In-Memory-Laufzeitzustand¶
status_store (shared dict)¶
Nach jedem EMS-Tick von ems_loop aktualisiert. Gelesen von:
web_server→/api/status→ Dashboard-Auto-Refreshmqtt_publisher→ MQTT-Zustands-Topicsha_sensor_publisher→ REST POST an HA Core
CostOptimizer-Akkumulatoren¶
Pro-Tag-defaultdict(float) nach date.today() geordnet:
_grid_import_kwh : {date: float}
_pv_used_kwh : {date: float}
_grid_cost_eur : {date: float}
_pv_saved_eur : {date: float}
_load_total_kwh : {date: float}
_grid_charge_kwh : {date: float}
_grid_charge_cost_eur: {date: float}
_feed_in_kwh : {date: float}
_feed_in_revenue_eur : {date: float}
_kwh_high_rate : {date: float} # Preisklassen-Akkumulatoren
_kwh_medium_rate : {date: float}
_kwh_low_rate : {date: float}
In-Memory-Akkumulatoren werden beim Add-on-Neustart zurückgesetzt. Langzeit-Persistenz
wird durch die total_increasing HA-Sensoren (HA zeichnet diese in seiner eigenen DB auf)
und die daily_stats-Wiederherstellung beim Start bereitgestellt.
EventLog-Ringpuffer + SQLite¶
event_log.py pflegt eine deque(maxlen=100), gestützt durch die SQLite-Tabelle event_log.
Zwei Ereignistypen teilen denselben Puffer:
@dataclass
class LogEntry:
timestamp: str # ISO 8601 String
state: str # "on" | "off" | "price_change"
battery_kwh_freetochange: float
battery_kwh_useable: float
predicted_load_kwh: float | None
entry_type: str # "mode_change" | "price_change"
price_eur_kwh: float | None # gesetzt bei price_change-Einträgen
entry_type |
Ausgelöst wenn | state-Wert |
|---|---|---|
mode_change |
EMS wechselt den Betriebsmodus | "on" (Netzladung gestartet) oder "off" |
price_change |
Strompreis-Sensor meldet einen neuen Wert | "price_change" |
Preisänderungs-Einträge werden nur aufgezeichnet, wenn der Preis vom zuvor beobachteten Wert abweicht. Der allererste Wert nach dem Start wird nicht geloggt.
Das Log wird über /api/status → log-Array bereitgestellt und auf der
/log-Seite dargestellt. Der Puffer hält die letzten 100 Ereignisse beider
Typen zusammen. Ereignisse überleben Neustarts — sie werden in SQLite
gespeichert und beim Start wiederhergestellt. Siehe das
event_log-Tabellen-Schema oben.