feat(application): integrate measured-load ingestion training and planner source

This commit is contained in:
ENELIX Agent
2026-10-02 21:00:05 +00:00
parent 44362e4bf5
commit fc024fcfba
96 changed files with 8933 additions and 0 deletions
@@ -0,0 +1,100 @@
"""Read-only probe executed INSIDE the existing forecast-engine container.
No get_configs() (it may migrate SQL), no training/prediction, no forecast writing,
no manual /run_now request, and no user credentials in output. Reads one plant's
stored raw 5-minute forecast values and the input frames used by the engine.
"""
import contextlib
from datetime import datetime, timezone
import hashlib
import io
import json
import math
import os
from pathlib import Path
import sqlite3
from urllib.parse import quote
from uuid import UUID
class Discard(io.TextIOBase):
def write(self,text):return len(text)
def clean_time(value):
if hasattr(value,'to_pydatetime'):value=value.to_pydatetime()
if value.tzinfo is None:value=value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc).isoformat()
def run():
aid=str(UUID(os.environ['ENELIX_ACCEPTANCE_PLANT']))
os.environ['FORECAST_INFLUX_TIMEOUT_MS']='20000'
# These assignments affect only this diagnostic process, not the service.
with contextlib.redirect_stdout(Discard()),contextlib.redirect_stderr(Discard()):
import main
import numpy as np
import pandas as pd
path=Path(main.SQLITE_DB_PATH)
if not path.is_file():raise RuntimeError('Existing configuration database missing')
con=sqlite3.connect('file:'+quote(str(path))+'?mode=ro',uri=True)
con.row_factory=sqlite3.Row
try:row=con.execute('SELECT * FROM anlagen_meta WHERE anlagen_id=?',(aid,)).fetchone()
finally:con.close()
if row is None:raise RuntimeError('Installation not found')
cfg=dict(row)
cfg['daecher']=json.loads(cfg.get('daecher') or '[]')
for key,default in [('ac_leistung',10.),('batt_capacity_kwh',0.),('batt_power_kw',0.),('tarif_bezug_fest',.3),('tarif_einspeisung_fest',.1),('tarif_peak_fest',5.)]:
value=cfg.get(key);cfg[key]=float(default if value is None or value=='' else value)
data=main.build_data_object(cfg,training=False)
frames={}
for key in ('df_hist','df_recent_raw','df_load_training','df_fut'):
frame=data.get(key)
if frame is None:frames[key]={'available':False};continue
item={'rows':len(frame),'from':clean_time(frame.index.min()) if len(frame) else None,'until':clean_time(frame.index.max()) if len(frame) else None,'columns':{}}
for col in ('Hausverbrauch','PV','Netzleistung','SOC'):
if col not in frame.columns:continue
values=pd.to_numeric(frame[col],errors='coerce');valid=values[np.isfinite(values)]
item['columns'][col]={'finite':len(valid),'zeros':int((valid==0).sum()),'median':float(valid.median()) if len(valid) else None,'maximum':float(valid.max()) if len(valid) else None,'lastFiniteAt':clean_time(valid.index[-1]) if len(valid) else None,'recentValues':[{'time':clean_time(t),'value':float(v)} for t,v in valid.tail(12).items()]}
frames[key]=item
start=datetime.now(timezone.utc).replace(second=0,microsecond=0)
from datetime import timedelta
end=start+timedelta(hours=48)
fields=('prog_var_1','prog_var_2','prog_var_10','prog_var_11','prog_var_21','prog_var_22')
field_filter=' or '.join('r["_field"] == '+json.dumps(f) for f in fields)
query='''from(bucket: %s)
|> range(start: %s, stop: %s)
|> filter(fn: (r) => r["_measurement"] == "api_telemetry")
|> filter(fn: (r) => r["anlagen_id"] == %s)
|> filter(fn: (r) => r["data_type"] == "forecast")
|> filter(fn: (r) => %s)
|> keep(columns: ["_time", "_field", "_value"])
''' % (json.dumps(main.INFLUX_BUCKET),start.isoformat(),end.isoformat(),json.dumps(aid),field_filter)
client=main.InfluxDBClient(url=main.INFLUX_URL,token=main.INFLUX_TOKEN,org=main.INFLUX_ORG,timeout=20000)
series={field:[] for field in fields}
try:
tables=client.query_api().query(org=main.INFLUX_ORG,query=query)
for table in tables:
for record in table.records:
value=record.get_value();field=record.get_field()
if field in series and isinstance(value,(int,float)) and math.isfinite(value):
series[field].append({'time':clean_time(record.get_time()),'value':float(value)})
finally:client.close()
for field in series:series[field].sort(key=lambda p:p['time'])
versions={}
for relative in ('main.py','methods/var_1.py','methods/var_2.py','methods/var_10.py','methods/var_11.py','methods/var_21.py','methods/var_22.py'):
source=Path('/app')/relative
if source.is_file():versions[relative]=hashlib.sha256(source.read_bytes()).hexdigest()
summary={}
for field,points in series.items():
values=[p['value'] for p in points]
summary[field]={'points':len(values),'allZero':bool(values) and max(abs(v) for v in values)==0,'maximumW':max(values) if values else None,'from':points[0]['time'] if points else None,'until':points[-1]['time'] if points else None}
native=True
for points in series.values():
stamps=[datetime.fromisoformat(p['time']).timestamp() for p in points]
if not stamps or any(t%300 for t in stamps) or any(b-a!=300 for a,b in zip(stamps,stamps[1:])):native=False
return {'status':'read_only_acquired','installationId':aid,'observedAt':datetime.now(timezone.utc).isoformat(),'queryResolution':'raw_no_chart_resampling','nativeFiveMinuteForecast':native,'generationTimeVerified':False,'inputFrames':frames,'forecastSummary':summary,'forecastSeries':series,'runningSourceHashes':versions,'modelWrite':False,'liveControlChanged':False}
try:
result=run()
except Exception as error:
# Never echo exception details containing SQL payloads, credentials or URLs.
result={'status':'read_only_probe_failed','errorType':type(error).__name__,'liveControlChanged':False}
print(json.dumps(result,allow_nan=False))