feat(workflow): publish verified forecast controls and consolidated docs
Tests / test (push) Successful in 1m1s

Approved by Daniel Haefliger for develop and beta. Author dh_Agent, authenticated account dh. Preserve published battery, charging and overall Energy Pie changes. No deployment or plant control authorization.
This commit is contained in:
dh
2026-10-08 10:02:58 +00:00
parent af69151165
commit 74a2d6c685
82 changed files with 5054 additions and 617 deletions
@@ -4,10 +4,12 @@ Legacy UTC-naive forecast indices are explicitly interpreted as UTC here.
"""
from datetime import datetime,timezone
from hashlib import sha256
from uuid import uuid4
from uuid import UUID,uuid4
from urllib.parse import urlparse
from urllib.request import Request,urlopen
import json,logging,math,os
import json,logging,math,os,time
_enrollment_cache = {'key': None, 'checked': 0., 'plants': set()}
def timestamp(value):
if hasattr(value,'to_pydatetime'):value=value.to_pydatetime()
@@ -71,13 +73,42 @@ def ckw_payload(label,rows,publication_timestamp=None,at=None):
result['periods']=periods
return result
def enabled(plant):return bool(os.getenv('NETPLAN_V4_URL') and plant in {p.strip() for p in os.getenv('NETPLAN_V4_PLANTS','').split(',') if p.strip()})
def planning_plants():
"""Discover enrolled planning tenants; this registry has no actuator authority."""
base=os.getenv('NETPLAN_V4_URL','').rstrip('/');token=os.getenv('PROGNOSIS_SERVICE_TOKEN','')
if not base:return set()
explicit={p.strip() for p in os.getenv('NETPLAN_V4_PLANTS','').split(',') if p.strip()}
if not token:return explicit
url=urlparse(base)
if url.scheme not in ('http','https') or url.username or url.password:return explicit
key=(base,token);now=time.monotonic()
if _enrollment_cache['key']==key and now-_enrollment_cache['checked']<60:
return explicit|_enrollment_cache['plants']
plants=set()
try:
req=Request(base+'/internal/v2/planner/installations',headers={'X-Enelix-Service-Token':token})
with urlopen(req,timeout=5) as response:
raw=response.read(500001)
if len(raw)>500000:raise ValueError('Oversized enrollment response')
data=json.loads(raw)
values=data['installationIds']
if not isinstance(values,list) or len(values)>10000 or data.get('controlEnabled') is not False:
raise ValueError('Invalid planning enrollment response')
if any(not isinstance(p,str) or str(UUID(p))!=p for p in values):
raise ValueError('Invalid planning installation ID')
plants=set(values)
except Exception as exc:
logging.getLogger(__name__).warning('V4 planning enrollment unavailable: %s',type(exc).__name__)
_enrollment_cache.update(key=key,checked=now,plants=plants)
return explicit|plants
def enabled(plant):return plant in planning_plants()
def send(plant,kind,payload):
if not enabled(plant):return {'status':'disabled'}
token=os.getenv('PROGNOSIS_SERVICE_TOKEN','');base=os.environ['NETPLAN_V4_URL'].rstrip('/');url=urlparse(base)
if not token or url.scheme not in ('http','https') or url.username or url.password:raise ValueError('Private V4 transport not configured')
UUID=__import__('uuid').UUID;UUID(plant)
if str(UUID(plant))!=plant:raise ValueError('Canonical installation UUID required')
if kind not in ('forecast','tariffs','prices'):raise ValueError('Unsupported publisher input kind')
req=Request(base+'/internal/v2/prognosis/'+plant+'/planner/inputs/'+kind,
json.dumps(payload,allow_nan=False).encode(),{'Content-Type':'application/json','X-Enelix-Service-Token':token},method='POST')
@@ -99,7 +130,7 @@ def publish_tariffs(plant,config):
except Exception as exc:logging.getLogger(__name__).warning('V4 shadow tariffs for %s: %s',plant,type(exc).__name__)
def publish_ckw(label,rows,publication_timestamp=None,tariff_type='integrated'):
plants=[p.strip() for p in os.getenv('NETPLAN_V4_PLANTS','').split(',') if p.strip()]
plants=sorted(planning_plants())
if not os.getenv('NETPLAN_V4_URL') or not plants:return
try:
if tariff_type!='integrated':raise ValueError('Full integrated tariff required')