Skip to content

Commit de7b4b2

Browse files
authored
Refactor app.py into modules and add new ML solar features
Refactor app.py into modules and add new ML solar features
2 parents 2818b9a + b80b13d commit de7b4b2

8 files changed

Lines changed: 1624 additions & 1553 deletions

File tree

app.py

Lines changed: 17 additions & 1551 deletions
Large diffs are not rendered by default.

config.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import os
2+
import time
3+
from dotenv import load_dotenv
4+
5+
# Zeitzone setzen
6+
os.environ['TZ'] = 'Europe/Berlin'
7+
time.tzset()
8+
9+
# Umgebungsvariablen laden
10+
load_dotenv()
11+
12+
# ================= KONFIGURATION =================
13+
DB_FILE = "solar_data.db"
14+
ADMIN_PASS = os.getenv("ADMIN_PASS")
15+
LATITUDE = float(os.getenv("LATITUDE", 0.0))
16+
LONGITUDE = float(os.getenv("LONGITUDE", 0.0))
17+
GAP_THRESHOLD = 45 # 3 x 15 Sekunden Logging
18+
MODEL_FILE = "pv_model.pkl"
19+
20+
# MQTT Konfiguration (für die Flask-App Initialisierung)
21+
MQTT_CONFIG = {
22+
'MQTT_BROKER_URL': os.getenv("MQTT_BROKER_URL"),
23+
'MQTT_BROKER_PORT': int(os.getenv("MQTT_BROKER_PORT", 1883)),
24+
'MQTT_USERNAME': os.getenv("MQTT_USERNAME"),
25+
'MQTT_PASSWORD': os.getenv("MQTT_PASSWORD"),
26+
'MQTT_TLS_ENABLED': False
27+
}
28+
29+
# ================= SQL SCHNIPSEL =================
30+
TRAPEZOID_SQL = f"""
31+
CASE
32+
WHEN prev_t IS NOT NULL
33+
AND dt > 0
34+
AND dt <= {GAP_THRESHOLD}
35+
THEN ((prev_w + w) / 2.0) * (dt / 3600.0)
36+
ELSE 0
37+
END
38+
"""

database.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import sqlite3
2+
import datetime
3+
from config import DB_FILE, TRAPEZOID_SQL
4+
from utils import get_historical_weather_data, calculate_eur
5+
6+
def get_db_connection(timeout=None):
7+
"""Hilfsfunktion für eine saubere DB-Verbindung."""
8+
if timeout is not None:
9+
conn = sqlite3.connect(DB_FILE, timeout=timeout)
10+
else:
11+
conn = sqlite3.connect(DB_FILE)
12+
13+
conn.row_factory = sqlite3.Row
14+
return conn
15+
16+
def init_db():
17+
conn = sqlite3.connect(DB_FILE)
18+
c = conn.cursor()
19+
20+
# 1. Haupttabelle für Rohdaten
21+
c.execute('''
22+
CREATE TABLE IF NOT EXISTS data (
23+
t TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
24+
w REAL,
25+
a REAL,
26+
v REAL,
27+
clouds REAL,
28+
ac_power_w REAL,
29+
dc_power_w REAL,
30+
panel1_w REAL,
31+
panel2_w REAL,
32+
inverter_temp_c REAL
33+
)
34+
''')
35+
c.execute('CREATE INDEX IF NOT EXISTS idx_data_t ON data(t)')
36+
37+
# 2. Tabelle für aggregierte Tagesstatistiken
38+
c.execute('''
39+
CREATE TABLE IF NOT EXISTS daily_stats (
40+
day TEXT PRIMARY KEY,
41+
kwh REAL,
42+
eur REAL,
43+
avg_clouds REAL,
44+
max_w REAL,
45+
avg_temp REAL,
46+
daylight_duration REAL,
47+
sunshine_duration REAL,
48+
max_w_panel1 REAL,
49+
max_w_panel2 REAL,
50+
kwh_panel1 REAL,
51+
kwh_panel2 REAL,
52+
kwh_dc_total REAL
53+
)
54+
''')
55+
56+
# 2. AUTOMATISCHES UPGRADE für neue Spalten
57+
c.execute("PRAGMA table_info(daily_stats)")
58+
existing_columns = [col[1] for col in c.fetchall()]
59+
60+
if "daylight_duration" not in existing_columns:
61+
print("Migriere Datenbank: Spalte daylight_duration wird hinzugefügt...")
62+
c.execute("ALTER TABLE daily_stats ADD COLUMN daylight_duration REAL")
63+
64+
if "sunshine_duration" not in existing_columns:
65+
print("Migriere Datenbank: Spalte sunshine_duration wird hinzugefügt...")
66+
c.execute("ALTER TABLE daily_stats ADD COLUMN sunshine_duration REAL")
67+
68+
# 3. Restliche Spalten (max_w_panel1, etc.) sicherstellen
69+
# Falls du die auch noch nicht hast, kannst du das Muster einfach fortsetzen:
70+
for col in ["max_w_panel1", "max_w_panel2", "kwh_panel1", "kwh_panel2", "kwh_dc_total"]:
71+
if col not in existing_columns:
72+
c.execute(f"ALTER TABLE daily_stats ADD COLUMN {col} REAL")
73+
74+
# 3. Globale Gesamt-Statistiken
75+
c.execute('''
76+
CREATE TABLE IF NOT EXISTS stats (
77+
id INTEGER PRIMARY KEY CHECK (id = 1),
78+
total_kwh REAL,
79+
total_eur REAL
80+
)
81+
''')
82+
c.execute("INSERT OR IGNORE INTO stats (id, total_kwh, total_eur) VALUES (1, 0, 0)")
83+
84+
# 4. Preis-Tabelle
85+
c.execute('''
86+
CREATE TABLE IF NOT EXISTS prices (
87+
valid_from DATE PRIMARY KEY,
88+
price REAL
89+
)
90+
''')
91+
92+
# Initialen Preis setzen, falls Tabelle leer
93+
c.execute("SELECT COUNT(*) FROM prices")
94+
if c.fetchone()[0] == 0:
95+
c.execute("INSERT INTO prices (valid_from, price) VALUES ('2026-01-01', 0.329)")
96+
97+
conn.commit()
98+
conn.close()
99+
print("Datenbank erfolgreich initialisiert.")
100+
101+
def finalize_day(day):
102+
# Heute niemals finalisieren
103+
today = datetime.date.today().strftime("%Y-%m-%d")
104+
if day >= today:
105+
return
106+
107+
conn = sqlite3.connect(DB_FILE)
108+
c = conn.cursor()
109+
110+
# --- NEU: Trapez-Regel dynamisch für die anderen Spalten klonen ---
111+
trap_p1 = TRAPEZOID_SQL.replace("prev_w", "prev_p1").replace("+ w", "+ p1")
112+
trap_p2 = TRAPEZOID_SQL.replace("prev_w", "prev_p2").replace("+ w", "+ p2")
113+
trap_dc = TRAPEZOID_SQL.replace("prev_w", "prev_dc").replace("+ w", "+ dc")
114+
115+
c.execute(f"""
116+
WITH base AS (
117+
SELECT
118+
t,
119+
w,
120+
panel1_w as p1,
121+
panel2_w as p2,
122+
dc_power_w as dc,
123+
LAG(t) OVER (ORDER BY t) as prev_t,
124+
LAG(w) OVER (ORDER BY t) as prev_w,
125+
LAG(panel1_w) OVER (ORDER BY t) as prev_p1,
126+
LAG(panel2_w) OVER (ORDER BY t) as prev_p2,
127+
LAG(dc_power_w) OVER (ORDER BY t) as prev_dc,
128+
(strftime('%s', t) - strftime('%s', LAG(t) OVER (ORDER BY t))) as dt,
129+
clouds
130+
FROM data
131+
WHERE date(t) = ?
132+
)
133+
SELECT
134+
SUM({TRAPEZOID_SQL}) as total_wh,
135+
AVG(clouds),
136+
MAX(w),
137+
MAX(p1),
138+
MAX(p2),
139+
SUM({trap_p1}) as wh_p1,
140+
SUM({trap_p2}) as wh_p2,
141+
SUM({trap_dc}) as wh_dc
142+
FROM base
143+
""", (day,))
144+
145+
row = c.fetchone()
146+
147+
if row and row[0] is not None:
148+
149+
total_wh = float(row[0])
150+
avg_clouds = float(row[1]) if row[1] is not None else 0.0
151+
max_w = float(row[2]) if row[2] is not None else 0.0
152+
max_w_p1 = float(row[3]) if row[3] is not None else 0.0
153+
max_w_p2 = float(row[4]) if row[4] is not None else 0.0
154+
wh_p1 = float(row[5]) if row[5] is not None else 0.0
155+
wh_p2 = float(row[6]) if row[6] is not None else 0.0
156+
wh_dc = float(row[7]) if row[7] is not None else 0.0
157+
weather = get_historical_weather_data(day)
158+
avg_temp = weather["temp"]
159+
daylight_s = weather["daylight_duration"]
160+
sunshine_s = weather["sunshine_duration"]
161+
162+
kwh = total_wh / 1000.0
163+
kwh_p1 = wh_p1 / 1000.0
164+
kwh_p2 = wh_p2 / 1000.0
165+
kwh_dc = wh_dc / 1000.0
166+
167+
# Preis sauber aus prices-Tabelle holen
168+
c.execute("""
169+
SELECT valid_from, price
170+
FROM prices
171+
ORDER BY valid_from DESC
172+
""")
173+
prices = c.fetchall()
174+
175+
def get_price_for_date(date_str):
176+
for p in prices:
177+
if date_str >= p[0]:
178+
return p[1]
179+
return 0.35
180+
181+
price = get_price_for_date(day)
182+
prices_list = [{"date": p[0], "price": p[1]} for p in prices]
183+
eur = calculate_eur(kwh, day, prices_list)
184+
185+
# 🔒 Speicherung mit hoher Präzision (DB)
186+
kwh_db = round(kwh, 6)
187+
eur_db = round(eur, 6)
188+
kwh_p1_db = round(kwh_p1, 6)
189+
kwh_p2_db = round(kwh_p2, 6)
190+
kwh_dc_db = round(kwh_dc, 6)
191+
192+
c.execute("""
193+
INSERT OR REPLACE INTO daily_stats
194+
(day, kwh, eur, avg_clouds, avg_temp, daylight_duration, sunshine_duration, max_w, max_w_panel1, max_w_panel2, kwh_panel1, kwh_panel2, kwh_dc_total)
195+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
196+
""", (
197+
day,
198+
kwh_db,
199+
eur_db,
200+
round(avg_clouds, 2),
201+
round(avg_temp, 2),
202+
round(daylight_s, 1),
203+
round(sunshine_s, 1), # In Sekunden
204+
round(max_w, 1),
205+
round(max_w_p1, 1),
206+
round(max_w_p2, 1),
207+
kwh_p1_db,
208+
kwh_p2_db,
209+
kwh_dc_db
210+
))
211+
212+
conn.commit()
213+
214+
conn.close()
215+
216+
# Modell neu trainieren nach Tagesabschluss
217+
from ml_logic import train_model
218+
train_model()
219+
220+
221+
def self_heal_daily_stats():
222+
conn = sqlite3.connect(DB_FILE)
223+
c = conn.cursor()
224+
225+
# Alle Tage aus Rohdaten holen außer heute
226+
today = datetime.date.today().strftime("%Y-%m-%d")
227+
c.execute("SELECT DISTINCT date(t) FROM data WHERE date(t) < ? ORDER BY date(t)", (today,))
228+
data_days = [row[0] for row in c.fetchall()]
229+
230+
# Alle Tage aus daily_stats holen
231+
c.execute("SELECT day FROM daily_stats")
232+
existing_days = {row[0] for row in c.fetchall()}
233+
conn.close()
234+
235+
missing_days = [d for d in data_days if d not in existing_days]
236+
if missing_days:
237+
print(f"Self-Heal: {len(missing_days)} fehlende Tage werden berechnet...")
238+
for day in missing_days:
239+
finalize_day(day)
240+
print("Self-Heal abgeschlossen.")
241+
242+
def force_rebuild_daily_stats():
243+
conn = sqlite3.connect(DB_FILE)
244+
c = conn.cursor()
245+
print("Starte kompletten Neuaufbau von daily_stats...")
246+
247+
# daily_stats komplett leeren
248+
c.execute("DELETE FROM daily_stats")
249+
250+
# stats sauber zurücksetzen
251+
c.execute("UPDATE stats SET total_kwh = 0, total_eur = 0 WHERE id = 1")
252+
conn.commit()
253+
254+
# Alle Tage aus Rohdaten holen außer heute
255+
today = datetime.date.today().strftime("%Y-%m-%d")
256+
c.execute("SELECT DISTINCT date(t) FROM data WHERE date(t) < ?", (today,))
257+
days = [row[0] for row in c.fetchall()]
258+
conn.close()
259+
260+
# Für jeden Tag neu berechnen
261+
for d in days:
262+
finalize_day(d)
263+
print(f"Rebuild abgeschlossen. {len(days)} Tage neu berechnet.")

0 commit comments

Comments
 (0)