feat(control): connect physical feedback to bounded V4 command and fallback paths

This commit is contained in:
ENELIX Agent
2026-10-03 11:40:54 +00:00
parent 8eb9688942
commit 9878197aea
24 changed files with 891 additions and 7 deletions
@@ -0,0 +1,65 @@
{
"scope": "corrected_feedback_trial_disabled",
"managerId": 17004,
"batteryInstanceId": 44234,
"files": {
"libs/BatterieNetzfahrplanV4RueckmeldungTrait.php": {
"before": null,
"after": "24cf0654f1a6bd545d64ce5f115f309b16d8ffca5172ec64fb37c390c6f001a7"
},
"libs/BatterieNetzfahrplanV4TestTrait.php": {
"before": null,
"after": "80474ff24cf9b2d32dfe481f0ce26bb94584c458b0fa3de232dec3897b64a4b2"
},
"libs/BatterieRegler.php": {
"before": "035988c54d4d0cc3a59d234f1c08a8edbf5fcac42e09a6bcea61d3dd8270771b",
"after": "beecc9c817b722336367e1c574c1a38afc8f3f5346442170d67c101b980adf7b"
},
"libs/ManagerNetzfahrplanV4EmpfangTrait.php": {
"before": "d083cfa6ae8e8026c134d2b53f34bb6fbe342fb7332a3e7da1b881de33e13e97",
"after": "936939a3e4a5a5d54fdff68f6fc689a9400e8406aecd5ac09ebc8a6b569c0b66"
},
"libs/ManagerNetzfahrplanV4TestTrait.php": {
"before": null,
"after": "c0d9b6336574dd8acf5f84adb12a32cad3b42ae91c5a31d513d0bc638a560f18"
},
"libs/NetzfahrplanV4Regeltest.php": {
"before": null,
"after": "bdac3e2e54c50a4c1541da8918ddf3ab2f665bb5cc6696848f546e7b05f5f249"
},
"libs/NetzfahrplanV4Rueckmeldung.php": {
"before": null,
"after": "a7459a208c4dd84ffe3a166d266dcd00216f3660fe60e5ee61d5042d0b840631"
},
"Batterie/module.php": {
"before": "34503645f7e21dd1e5bf0a3f754f6592175dd67f9a263bf431f3801209555401",
"after": "3d5fcd83e4e7d999d5ac949fe357b492d5661ffa76639d2ca57a3f3934eabdbe"
},
"Manager/module.php": {
"before": "83e80883c8f9b817c0af5e239b20bcb019c7cf7f7943ee5bcc6493ac5a743532",
"after": "cbbb33a8f71dda8cc80a4fc01cd3d6154da8153b5523306b2da723bb44f2dd79"
}
},
"configurationSha256": "f66cfb9ebf5e02259b902f876a4c78756a51b3f2f2c240fd92c0f01161268c56",
"dependencies": {
"libs/NetzfahrplanV4Planpruefung.php": "24b935d80bad4ce2d9047b6777aaf74a18773236ca91a2844a0907012f4fadf4",
"libs/Nachrichtenvertrag.php": "215ad7d4c5240b39b48cb7672f5057afa532aafdb34039862a1a22985a9ee839",
"libs/VerbraucherBasisTrait.php": "10fd9f54283c378104bce11d623d5581558355d26fc2f85bf04b316542874507",
"libs/VerbraucherSchnittstelle.php": "6329d423dca7b551f01fd884204be45be43f92cd6da6e4faedf2c3c9b4654a15",
"libs/ManagerNetzfahrplanV4Trait.php": "e27a3595e4ed8cdb62e2561149ef6a50adb9194e0239eb25c1216aee2716fa88",
"libs/ManagerNetzfahrplanV4DatenTrait.php": "59ae0ae283839ca82c58db9c071dcf28d0ab95e44f7a74c6473aca837ce416c4",
"libs/NetzfahrplanV4Datenarchiv.php": "57ef7a60c273c5270b3e93ff5528d7d294e708725c9edfe81ac7ed819587f983",
"libs/NetzfahrplanV4Messaufnahme.php": "ebe873bb714041e505a4f500b34c9bae3a5a0b93ac459161584ced758507a6e9",
"libs/NetzfahrplanV4Bilanzierung.php": "ffda2a03dbb226a4405a2ad66704c080a9dc475ce64282ad3f0d7d60034ab199",
"libs/NetzfahrplanV4Betriebsdaten.php": "6ff7d5710995778e7f941020a6f18555307ef51f16867f13efc204915e6dc9d9",
"libs/NetzfahrplanV4Bezugszaehler.php": "7aa01ce83a343eb767a889575fa04cece7f1c65cda347723e24dd68da40cea9a",
"libs/ManagerEnergieTrait.php": "2f89b0ad31ee6dd99180a7225173036eb6212b2ea7157e57fee23f0f8b441a51",
"libs/EnergieMessung.php": "16c64e1cbefde81c25f913aaca6546f0d30cc8f2fddecea0768a3760a98e97d2",
"libs/StoerungsSnapshot.php": "c871c1dd58d7fb1ef6c06985de066518688ef080f3cbdadda192d66cedcf49e1",
"libs/Anlagentopologie.php": "1b7e95d103a3048ed38c464a424f2f12821a147b269e149fd6b89bca84878c47",
"libs/Lizenzpruefung.php": "cb93cb49b93ed3ebb2060dba28b5b2dec2f29548d6ccff1afd9c7cb4624ad77e",
"libs/EinspeiseRegler.php": "8b982be5b95275c6dd6611a2bf2b1f89b991ba53cf94e46df0eed3249add65e0",
"libs/ManagerRegler.php": "608a4abebbe45da37a16316a530fa3f5ab21cb880bb341acbdcd3a9aaa93c6ee",
"libs/ManagerSchnittstelle.php": "0aae8c8c85eb252c577fcb0b5a50a12f3247e4a0bb083579122854f6c35878b3"
}
}
@@ -0,0 +1,25 @@
"""Recreate a reviewed Lihrenmoos package from this commit; never install in Symcon."""
from pathlib import Path
import hashlib,json,sys
here=Path(__file__).resolve().parent;repo=here.parents[1]
if len(sys.argv)!=2:raise SystemExit('Usage: python3 build_stage.py NEW_PRIVATE_STAGING_DIRECTORY')
target=Path(sys.argv[1]).resolve()
allowed=[Path('/srv/agent'),Path('/home/agent/services/qa')]
if not any(target!=p and target.is_relative_to(p) for p in allowed):raise SystemExit('Use a private agent staging directory, not live module paths')
if target.exists():raise SystemExit('Destination must not exist')
m=json.loads((here/'MANIFEST.json').read_text());contents={}
for n,v in m['files'].items():
p=repo/n
if p.is_symlink() or not p.resolve().is_relative_to(repo):raise SystemExit('Unsafe source')
data=p.read_bytes()
if hashlib.sha256(data).hexdigest()!=v['after']:raise SystemExit('Reviewed source changed: '+n)
contents['source/'+n]=data
for n,v in m['dependencies'].items():
if hashlib.sha256((repo/n).read_bytes()).hexdigest()!=v:raise SystemExit('Dependency changed: '+n)
for n in ['install.php','file_installer.php','MANIFEST.json','feedback-config.json']:contents[n]=(here/n).read_bytes()
if hashlib.sha256(contents['feedback-config.json']).hexdigest()!=m['configurationSha256']:raise SystemExit('Configuration changed')
target.mkdir(parents=True,mode=0o700)
for n,data in contents.items():
p=target/n;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(data);p.chmod(0o640)
(target/'PACKAGE_HASHES.json').write_text(json.dumps({n:hashlib.sha256(v).hexdigest() for n,v in contents.items()},indent=2)+'\n')
print('Private staging package created:',target,'; no running module, settings or control changes.')
@@ -0,0 +1,71 @@
{
"version": 1,
"installationId": "e3a08f9e-af12-4695-99bd-8b51c0520021",
"assetId": "anlage01-virtual-ev",
"managerId": 17004,
"batteryInstanceId": 44234,
"mode": "virtual_split",
"allowEstimatedForTrial": false,
"maxSkewSeconds": 30,
"maxAgeSeconds": 60,
"idleToleranceW": 50.0,
"trackingToleranceW": 200.0,
"sources": [
{
"key": "grid",
"variableId": 40348,
"parentId": 11490,
"ident": "Power_8",
"factorToW": 1000,
"role": "grid"
},
{
"key": "physical_goodwe1",
"variableId": 47725,
"parentId": 19742,
"ident": "A_6_3_35182",
"factorToW": -1,
"role": "physical"
},
{
"key": "physical_goodwe2",
"variableId": 35724,
"parentId": 57658,
"ident": "A_6_3_35182",
"factorToW": -1,
"role": "physical"
},
{
"key": "physical_solaredge",
"variableId": 21447,
"parentId": 30789,
"ident": "Value",
"factorToW": 1,
"role": "physical"
},
{
"key": "ev_requested",
"variableId": 19651,
"parentId": 58448,
"ident": "Nennleistung_Soll_EV",
"factorToW": 1.0,
"role": "ev_request"
},
{
"key": "sdl_requested",
"variableId": 38943,
"parentId": 58448,
"ident": "Nennleistung_Soll_SDL",
"factorToW": 1.0,
"role": "sdl_request"
},
{
"key": "gateway_active",
"role": "gateway_active",
"variableId": 23483,
"parentId": 58448,
"ident": "State",
"factorToW": 1.0
}
]
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/** File-only installation helpers. No module classes loaded from the staging tree. */
function v4FeedbackWrite(string $path, string $content, int $mode=0644): void
{
if (is_link($path)) throw new RuntimeException('Symlink wird nicht ersetzt.');
$temp=tempnam(dirname($path),'.v4-feedback-');
if ($temp===false) throw new RuntimeException('Temporaere Datei nicht verfuegbar.');
try {
if (file_put_contents($temp,$content,LOCK_EX)!==strlen($content) || !chmod($temp,$mode) || !rename($temp,$path)) throw new RuntimeException('Atomarer Dateitausch fehlgeschlagen.');
} finally { if(is_file($temp))unlink($temp); }
}
function v4FeedbackFiles(string $stage,string $target): array
{
$allowed=['libs/NetzfahrplanV4Rueckmeldung.php','libs/BatterieNetzfahrplanV4RueckmeldungTrait.php','libs/NetzfahrplanV4Regeltest.php',
'libs/BatterieNetzfahrplanV4TestTrait.php','libs/ManagerNetzfahrplanV4TestTrait.php','libs/ManagerNetzfahrplanV4EmpfangTrait.php','libs/BatterieRegler.php','Batterie/module.php','Manager/module.php'];
$m=json_decode((string)file_get_contents($stage.'/MANIFEST.json'),true,32,JSON_THROW_ON_ERROR);
if (($m['scope']??null)!=='corrected_feedback_trial_disabled' || count($m['files']??[])!==count($allowed) || array_diff(array_keys($m['files']),$allowed)) throw new RuntimeException('Unerwarteter Dateiumfang.');
if (realpath($target)!==$target || is_link($target)) throw new RuntimeException('Modulpfad unerwartet.');
foreach($m['dependencies'] as $name=>$hash) {
if (!preg_match('~^libs/[A-Za-z0-9]+\.php$~D',$name) || is_link($target.'/'.$name) || hash_file('sha256',$target.'/'.$name)!==$hash) throw new RuntimeException('Abhaengigkeit wurde parallel geaendert: '.$name);
}
$before=[];$source=[];$already=true;
// Explicit dependency order. Both top-level modules are replaced only after all dependencies.
foreach($allowed as$name) {
$path=$target.'/'.$name;$candidate=$stage.'/source/'.$name;
if (is_link($path)||is_link($candidate)||realpath(dirname($path))!==$target.'/'.dirname($name)) throw new RuntimeException('Unerwarteter Dateipfad.');
$source[$name]=(string)file_get_contents($candidate);
if (hash('sha256',$source[$name])!==$m['files'][$name]['after']) throw new RuntimeException('Paketpruefsumme geaendert: '.$name);
token_get_all($source[$name],TOKEN_PARSE);
$before[$name]=is_file($path)?file_get_contents($path):null;
$hash=$before[$name]===null?null:hash('sha256',$before[$name]);
if (!in_array($hash,[$m['files'][$name]['before'],$m['files'][$name]['after']],true)) throw new RuntimeException('Paralleler Modulstand; nichts ueberschrieben: '.$name);
if($hash!==$m['files'][$name]['after'])$already=false;
}
if($already)return ['status'=>'already_installed','backup'=>null];
$backup=$stage.'/backups/'.gmdate('Ymd\THis\Z').'-'.bin2hex(random_bytes(4));
if(!mkdir($backup,0700,true))throw new RuntimeException('Sicherung fehlgeschlagen.');
foreach($allowed as$name) {
if($before[$name]!==null) {
$dest=$backup.'/'.$name;if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);
v4FeedbackWrite($dest,$before[$name],0600);
}
}
v4FeedbackWrite($backup.'/MANIFEST.json',json_encode($m,JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT),0600);
$written=[];
try {
foreach($allowed as$name) {
if((is_file($target.'/'.$name)?file_get_contents($target.'/'.$name):null)!==$before[$name])throw new RuntimeException('Parallele Aenderung waehrend Installation.');
if($before[$name]===$source[$name])continue;
v4FeedbackWrite($target.'/'.$name,$source[$name]);$written[]=$name;
}
} catch(Throwable $e) {
foreach(array_reverse($written)as$name) {
$p=$target.'/'.$name;
if(!is_link($p)&&is_file($p)&&hash_file('sha256',$p)===$m['files'][$name]['after']) {
if($before[$name]===null)unlink($p);else v4FeedbackWrite($p,$before[$name]);
}
}
throw $e;
}
return ['status'=>'installed','backup'=>$backup,'filesChanged'=>$written];
}
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
// Execute only in Lihrenmoos Symcon. Installs code and readonly feedback mapping; never starts a trial.
if(!function_exists('IPS_GetKernelVersion'))throw new RuntimeException('Nur im IP-Symcon-Skripteditor ausfuehren.');
require_once __DIR__.'/file_installer.php';
$manager=17004;$battery=44234;$locked=false;$changedMapping=false;$oldMapping=null;
$report=['scope'=>'corrected_feedback_trial_disabled','startedAt'=>gmdate('c'),'actuatorPermissionGranted'=>false,'serverUpdateRequired'=>false];
try {
$hashes=json_decode((string)file_get_contents(__DIR__.'/PACKAGE_HASHES.json'),true,32,JSON_THROW_ON_ERROR);
foreach($hashes as$name=>$hash) {
if(str_contains($name,'..')||str_starts_with($name,'/')||is_link(__DIR__.'/'.$name)||hash_file('sha256',__DIR__.'/'.$name)!==$hash)throw new RuntimeException('Vorbereitetes Paket veraendert.');
}
foreach([$manager=>'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',$battery=>'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}']as$id=>$module) {
if(!IPS_InstanceExists($id)||IPS_GetInstance($id)['ModuleInfo']['ModuleID']!==$module)throw new RuntimeException('Modulzuordnung ungueltig.');
if(IPS_HasChanges($id))throw new RuntimeException('Offene Aenderungen zuerst speichern oder verwerfen.');
}
$beforeM=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
$beforeB=json_decode(IPS_GetConfiguration($battery),true,128,JSON_THROW_ON_ERROR);
if(($beforeM['NetzfahrplanAktiv']??null)!==false || ($beforeM['NetzfahrplanV4RegeltestErlaubt']??false)!==false || ($beforeB['NetzfahrplanV4RegeltestErlaubt']??false)!==false)throw new RuntimeException('Alte Fahrplanausgabe und beide Regeltestfreigaben muessen AUS bleiben.');
$c=json_decode((string)file_get_contents(__DIR__.'/feedback-config.json'),true,32,JSON_THROW_ON_ERROR);
$manifest=json_decode((string)file_get_contents(__DIR__.'/MANIFEST.json'),true,32,JSON_THROW_ON_ERROR);
if(($c['managerId']??null)!==$manager || ($c['batteryInstanceId']??null)!==$battery || ($c['allowEstimatedForTrial']??null)!==false
|| hash_file('sha256',__DIR__.'/feedback-config.json')!==$manifest['configurationSha256'])throw new RuntimeException('Falsche Rueckmeldekonfiguration.');
$desired=json_encode($c,JSON_THROW_ON_ERROR|JSON_PRESERVE_ZERO_FRACTION);
$oldMapping=$beforeB['NetzfahrplanV4RueckmeldungKonfiguration']??'{}';
if(!in_array($oldMapping,['','{}',$desired],true))throw new RuntimeException('Abweichende Rueckmeldekonfiguration nicht ueberschrieben.');
$controls=[];foreach(IPS_GetModuleList()as$module)if((IPS_GetModule($module)['Prefix']??'')==='MC')$controls=array_merge($controls,IPS_GetInstanceListByModuleID($module));
if(count($controls)!==1||!function_exists('MC_ReloadModule'))throw new RuntimeException('Module Control nicht eindeutig.');
$locked=IPS_SemaphoreEnter('ENELIX.V4.FeedbackInstall',3000);if(!$locked)throw new RuntimeException('Installation bereits aktiv.');
$report['files']=v4FeedbackFiles(__DIR__,'/var/lib/symcon/modules/Enelix-EMS');
if(MC_ReloadModule($controls[0],'Enelix-EMS')===false)throw new RuntimeException('Bibliotheks-Reload fehlgeschlagen.');
$afterM=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
$afterB=json_decode(IPS_GetConfiguration($battery),true,128,JSON_THROW_ON_ERROR);
foreach([[$beforeM,$afterM],[$beforeB,$afterB]]as$pair)foreach($pair[0]as$key=>$value)if(!array_key_exists($key,$pair[1])||$pair[1][$key]!==$value)throw new RuntimeException('Bestehender Parameter beim Reload veraendert: '.$key);
if(!array_key_exists('NetzfahrplanV4RueckmeldungKonfiguration',$afterB)||!array_key_exists('NetzfahrplanV4RegeltestErlaubt',$afterM)
||!function_exists('ENELIX_GetV4BatterieRueckmeldung')||!function_exists('ENELIX_GetV4ManagerTestStatus')) {
$report['status']='waiting_for_registration';
echo "Dateien installiert. Modulregistrierung wird abgeschlossen; diesen Aufruf einmal wiederholen. Keine Stellfreigabe.\n";
} else {
if($afterM['NetzfahrplanV4RegeltestErlaubt']!==false||$afterB['NetzfahrplanV4RegeltestErlaubt']!==false)throw new RuntimeException('Unerwartete Testfreigabe.');
if($afterB['NetzfahrplanV4RueckmeldungKonfiguration']!==$desired) {
IPS_SetProperty($battery,'NetzfahrplanV4RueckmeldungKonfiguration',$desired);$changedMapping=true;IPS_ApplyChanges($battery);
}
$finalM=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
$finalB=json_decode(IPS_GetConfiguration($battery),true,128,JSON_THROW_ON_ERROR);
foreach([[$beforeM,$finalM],[$beforeB,$finalB]]as$pair)foreach($pair[0]as$key=>$value)if($key!=='NetzfahrplanV4RueckmeldungKonfiguration'&&(!array_key_exists($key,$pair[1])||$pair[1][$key]!==$value))throw new RuntimeException('Bestehender Parameter unerwartet veraendert: '.$key);
if(($finalM['NetzfahrplanAktiv']??null)!==false||($finalM['NetzfahrplanV4RegeltestErlaubt']??null)!==false||($finalB['NetzfahrplanV4RegeltestErlaubt']??null)!==false)throw new RuntimeException('Unerwartete Stellfreigabe nach Installation.');
$f=json_decode(ENELIX_GetV4BatterieRueckmeldung($battery),true,32,JSON_THROW_ON_ERROR);
$report['feedback']=array_intersect_key($f,array_flip(['status','kind','method','reason','reasons','estimated','batteryW','gridW','sourceOldestAt','checkedAt','configHash','usableForTrial','canDispatch']));
$report['status']='corrected_feedback_installed_trial_disabled';$report['originalParametersPreserved']=true;
echo "OK: Korrigierte physische Rueckmeldung mit Manager-Vorschau und Batterietreiber verbunden.\n";
echo 'Rueckmeldung: '.($f['status']??'available').'; Modellanteil: '.(($f['estimated']??false)?'ja':'nein').".\n";
if(isset($f['reason']))echo 'Aktueller Hinweis: '.$f['reason']."\n";
echo "Beide Regeltestfreigaben AUS. Keine V4-Ausgabe aktiviert; vorhandene Regelung, Energiekonten und Datensender bleiben bestehen.\n";
}
} catch(Throwable $e) {
$report['status']='needs_review';$report['reason']=substr($e->getMessage(),0,260);
if($changedMapping && $oldMapping!==null) {
try{IPS_SetProperty($battery,'NetzfahrplanV4RueckmeldungKonfiguration',$oldMapping);IPS_ApplyChanges($battery);$report['mappingRestored']=true;}catch(Throwable $ignored){$report['mappingRestored']=false;}
}
echo 'FEHLER: '.$report['reason']."\nKeine V4-Stellfreigabe erteilt.\n";
} finally {
$report['finishedAt']=gmdate('c');v4FeedbackWrite(__DIR__.'/INSTALL_RESULT.json',json_encode($report,JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT),0644);
if($locked)IPS_SemaphoreLeave('ENELIX.V4.FeedbackInstall');
echo 'FEEDBACK INSTALL REPORT: '.__DIR__."/INSTALL_RESULT.json\n";
}