Files
Enelix-EMS/services/netplan-v4/integrations/NetzfahrplanV4.php
T

72 lines
3.5 KiB
PHP

<?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];
}
}