72 lines
3.5 KiB
PHP
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];
|
|
}
|
|
}
|