feat(application): integrate measured-load ingestion training and planner source
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace Belevo\EnelixEMS;
|
||||
use InvalidArgumentException;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/** Pure protocol/control helper. No IPS calls, no device output. */
|
||||
final class NetzfahrplanV4
|
||||
{
|
||||
private static function stamp(string $value): int
|
||||
{
|
||||
if (!preg_match('/(?:Z|[+-]\d{2}:\d{2})$/', $value)) {
|
||||
throw new InvalidArgumentException('Explicit timezone required');
|
||||
}
|
||||
return (new DateTimeImmutable($value))->getTimestamp();
|
||||
}
|
||||
private static function number(mixed $value): float
|
||||
{
|
||||
if ((!is_float($value) && !is_int($value)) || !is_finite((float)$value)) {
|
||||
throw new InvalidArgumentException('Finite numeric value required');
|
||||
}
|
||||
return (float)$value;
|
||||
}
|
||||
public static function currentPoint(array $plan, int $now, int $revision, bool $shadowInspection=false): ?array
|
||||
{
|
||||
if (($plan['schemaVersion']??null)!==2 || ($plan['configRevision']??null)!==$revision
|
||||
|| ($plan['executable']??false)!==true || !is_array($plan['points']??null)
|
||||
|| !in_array($plan['status']??'', ['optimal','feasible_time_limit'], true)
|
||||
|| !is_string($plan['planId']??null) || $plan['planId']==='') return null;
|
||||
if ($shadowInspection ? ($plan['runMode']??'')!=='shadow'
|
||||
: (($plan['runMode']??'')!=='live' || ($plan['liveEnabled']??false)!==true)) return null;
|
||||
try {
|
||||
$age=$now-self::stamp($plan['generatedAt']);
|
||||
if ($age < -30 || $age>900 || self::stamp($plan['validFrom'])>$now || self::stamp($plan['validUntil'])<=$now) return null;
|
||||
$selected=null; $previousEnd=null;
|
||||
foreach ($plan['points'] as $p) {
|
||||
$start=self::stamp($p['time']); $end=self::stamp($p['validUntil']);
|
||||
self::number($p['gridTargetW']);
|
||||
if ($end<=$start || $end-$start>300 || ($previousEnd!==null && $previousEnd!==$start)) return null;
|
||||
$previousEnd=$end;
|
||||
if ($start<=$now && $now<$end) $selected=$p;
|
||||
}
|
||||
return $selected;
|
||||
} catch (\Throwable $e) { return null; }
|
||||
}
|
||||
public static function correction(array $point, float $grid, float $battery, float $chargeAvailable, float $dischargeAvailable, ?float $hardImportLimit=null): array
|
||||
{
|
||||
foreach ([$grid,$battery,$chargeAvailable,$dischargeAvailable] as $v) self::number($v);
|
||||
if ($chargeAvailable<0 || $dischargeAvailable<0) throw new InvalidArgumentException('Invalid available power');
|
||||
$target=self::number($point['gridTargetW']??null);
|
||||
if ($hardImportLimit!==null) {
|
||||
if (self::number($hardImportLimit)<0) throw new InvalidArgumentException('Negative hard limit');
|
||||
$target=min($target,$hardImportLimit);
|
||||
}
|
||||
$raw=$battery+$target-$grid;
|
||||
if (abs($target)>1) {
|
||||
$raw=match ($point['intent']??'') {
|
||||
'gridCharge'=>max(0.,$raw),
|
||||
'pvCharge'=>max(0.,min($battery-$grid,$raw)),
|
||||
'discharge','export'=>min(0.,$raw),
|
||||
'hold'=>0.,
|
||||
default=>throw new InvalidArgumentException('Unknown plan intent'),
|
||||
};
|
||||
}
|
||||
if ($hardImportLimit!==null) $raw=min($raw,$battery+$hardImportLimit-$grid);
|
||||
$wanted=min($chargeAvailable,max(-$dischargeAvailable,$raw));
|
||||
$expectedGrid=$grid+$wanted-$battery;
|
||||
return ['batteryTargetW'=>$wanted,'gridTargetW'=>$target,'limited'=>abs($wanted-$raw)>1,
|
||||
'expectedGridW'=>$expectedGrid,'trackingErrorW'=>$expectedGrid-$target];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user