feat(forecast): integrate full forecast charts and complete SDL visualization
Tests / test (push) Failing after 52s

This commit is contained in:
dh
2026-10-06 17:40:45 +00:00
parent 5687596799
commit 372b997876
30 changed files with 4377 additions and 53 deletions
@@ -162,6 +162,7 @@ def optimize(steps,batteries,*,at,limits=None,observed_peaks=None,peak_prices=No
elif battery_w< -1:intent='export' if p_export>.001 else 'discharge'
else:intent='hold'
points.append({'time':utc(step.start).isoformat(),'validUntil':step.end.isoformat(),
'pvW':float(step.pv_w),'loadW':float(step.base_load_w),'externalW':float(step.external_w),
'baselineGridW':float(step.residual_w),'gridTargetW':(p_import-p_export)*1000,
'batteryTargetW':battery_w,'assetTargetsW':targets,'socEndPercent':soc_end,
'pvCurtailmentW':float(x[curtail[i]]*1000),'intent':intent,
+8 -2
View File
@@ -176,7 +176,7 @@ def price_at(store,plant,side,config,start,end,now):
class AwaitingInput(ValueError):pass
def assemble(store,plant,family,now):
def assemble(store,plant,family,now,*,include_forecast_points=False):
values={k:latest(store,plant,k) for k in ('operation','forecast','tariffs')}
for k,v in values.items():
if not v:raise AwaitingInput('Missing '+k+' input')
@@ -206,6 +206,11 @@ def assemble(store,plant,family,now):
external=p.get('externalW',0.) if source['loadBasis']=='base_load' else 0.
steps.append(Step(start,p['loadW'],p['pvW'],price_at(store,plant,'import',tariffs['import'],start,end,decision),price_at(store,plant,'export',tariffs['export'],start,end,decision),external,int((end-start).total_seconds())))
if not steps or steps[0].start!=decision:raise AwaitingInput('Forecast has no current interval')
if include_forecast_points:
# Preserve the exact selected input horizon separately from price-limited dispatch.
input_quality['forecastPoints']=[{'time':s.start.isoformat(),'validUntil':s.end.isoformat(),
'pvW':float(s.pv_w),'loadW':float(s.base_load_w),
'externalW':float(s.external_w) if source['loadBasis']=='base_load' else None} for s in steps]
full_end=steps[-1].end;steps=priced_prefix(steps,decision)
if not steps:raise AwaitingInput('No complete published-price billing quarter')
q=quarter_start(decision);elapsed=int((decision-q).total_seconds());past={}
@@ -291,10 +296,11 @@ def run_once(store,now):
selection=choose_family(settings['family'],current,economic_replay.scores(store,plant,now),registry=store.registry,now=now,
lookback_days=settings['autoLookbackDays'],minimum_days=settings['autoMinimumDays'],
minimum_coverage=settings['autoMinimumCoverage'],margin_chf=settings['autoSwitchMarginChf'])
data,full_end,quality=assemble(store,plant,selection['family'],now)
data,full_end,quality=assemble(store,plant,selection['family'],now,include_forecast_points=True)
data['batteries']=[replace(b,roundtrip_efficiency=settings['roundtripEfficiency']) for b in data['batteries']]
result=optimize(**data,config_revision=settings['revision'],family=selection['family'])
if result['executable']:
result['forecastPoints']=quality.pop('forecastPoints')
result.update({'installationId':plant,'inputRefs':quality.pop('inputRefs'),'controlContext':quality.pop('controlContext'),'runMode':'shadow','liveEnabled':False,'sourceSelection':selection,'forecastUntil':full_end.isoformat(),'pricesKnownUntil':result['validUntil'],'inputQuality':quality,'warnings':quality['warnings']+([] if quality['loadBasis']=='base_load' else ['Aggregate house forecast: base-load/SDL separation not verified; shadow only'])})
store.publish_shadow(plant,result,settings['revision'],now,claim['sequence'],claim['lease_token'])
economic_replay.capture(store,plant,now,assemble,optimize)