Neues Ladestationsmodul mit ocpp Modul erstellt.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
class CapabilityModel
|
||||
{
|
||||
public static function defaults(): array
|
||||
{
|
||||
return [
|
||||
'smartCharging' => false,
|
||||
'meterValues' => false,
|
||||
'phaseMetering' => false,
|
||||
'currentImportPerPhase' => false,
|
||||
'voltagePerPhase' => false,
|
||||
'powerImport' => false,
|
||||
'powerExport' => false,
|
||||
'energyImport' => false,
|
||||
'energyExport' => false,
|
||||
'numberPhases' => false,
|
||||
'phaseToUse' => false,
|
||||
'phaseSwitching' => false,
|
||||
'dataTransfer' => false,
|
||||
'getVariables' => false,
|
||||
'setVariables' => false,
|
||||
'transactionEvent' => false,
|
||||
'soc' => false,
|
||||
'temperature' => false,
|
||||
'signedMeterValue' => false,
|
||||
'bidirectional' => false
|
||||
];
|
||||
}
|
||||
|
||||
public static function detectFromBoot(string $version, array $payload): array
|
||||
{
|
||||
$capabilities = self::defaults();
|
||||
$capabilities['meterValues'] = true;
|
||||
|
||||
if ($version === '2.0.1' || $version === '2.1') {
|
||||
$capabilities['getVariables'] = true;
|
||||
$capabilities['setVariables'] = true;
|
||||
$capabilities['transactionEvent'] = true;
|
||||
$capabilities['smartCharging'] = true;
|
||||
}
|
||||
|
||||
if ($version === '2.1') {
|
||||
$capabilities['bidirectional'] = true;
|
||||
$capabilities['powerExport'] = true;
|
||||
$capabilities['energyExport'] = true;
|
||||
}
|
||||
|
||||
if (isset($payload['chargingStation']['model']) || isset($payload['chargePointModel'])) {
|
||||
$capabilities['dataTransfer'] = true;
|
||||
}
|
||||
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
public static function merge(array $base, array $updates): array
|
||||
{
|
||||
return array_replace($base, array_intersect_key($updates, $base));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
class ChargingProfileBuilder
|
||||
{
|
||||
public function build(array $setpoint, string $version): array
|
||||
{
|
||||
$currentA = max(0.0, (float)($setpoint['effectiveCurrentA'] ?? 0));
|
||||
$validTo = gmdate('Y-m-d\TH:i:s\Z', time() + 120);
|
||||
|
||||
if ($version === '1.6') {
|
||||
return [
|
||||
'connectorId' => (int)($setpoint['connectorId'] ?? 1),
|
||||
'csChargingProfiles' => [
|
||||
'chargingProfileId' => (int)(time() % 100000),
|
||||
'stackLevel' => 0,
|
||||
'chargingProfilePurpose' => 'TxDefaultProfile',
|
||||
'chargingProfileKind' => 'Absolute',
|
||||
'validTo' => $validTo,
|
||||
'chargingSchedule' => [
|
||||
'chargingRateUnit' => 'A',
|
||||
'chargingSchedulePeriod' => [
|
||||
['startPeriod' => 0, 'limit' => $currentA]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'evseId' => (int)($setpoint['evseId'] ?? 1),
|
||||
'chargingProfile' => [
|
||||
'id' => (int)(time() % 100000),
|
||||
'stackLevel' => 0,
|
||||
'chargingProfilePurpose' => 'TxDefaultProfile',
|
||||
'chargingProfileKind' => 'Absolute',
|
||||
'validTo' => $validTo,
|
||||
'chargingSchedule' => [
|
||||
[
|
||||
'id' => 1,
|
||||
'chargingRateUnit' => 'A',
|
||||
'chargingSchedulePeriod' => [
|
||||
['startPeriod' => 0, 'limit' => $currentA]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
class DataTransferRegistry
|
||||
{
|
||||
private bool $allowed;
|
||||
private array $entries;
|
||||
|
||||
public function __construct(bool $allowed, array $entries = [])
|
||||
{
|
||||
$this->allowed = $allowed;
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
public function isAllowed(string $vendorId, string $messageId = ''): bool
|
||||
{
|
||||
if (!$this->allowed) {
|
||||
return false;
|
||||
}
|
||||
if (empty($this->entries)) {
|
||||
return true;
|
||||
}
|
||||
foreach ($this->entries as $entry) {
|
||||
if (($entry['vendorId'] ?? '') === $vendorId && (($entry['messageId'] ?? '') === '' || ($entry['messageId'] ?? '') === $messageId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
class Diagnostics
|
||||
{
|
||||
public static function message(string $severity, string $source, string $text, string $code = ''): array
|
||||
{
|
||||
return [
|
||||
'timestamp' => time(),
|
||||
'severity' => $severity,
|
||||
'source' => $source,
|
||||
'text' => $text,
|
||||
'code' => $code
|
||||
];
|
||||
}
|
||||
|
||||
public static function statusFromFlags(bool $online, bool $fault): string
|
||||
{
|
||||
if ($fault) {
|
||||
return 'Stoerung';
|
||||
}
|
||||
return $online ? 'Online' : 'Offline';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
class FailSafeManager
|
||||
{
|
||||
public function evaluate(array $state): array
|
||||
{
|
||||
$now = time();
|
||||
$warnings = [];
|
||||
$blockingReason = '';
|
||||
$safePowerW = null;
|
||||
|
||||
$emsAge = $now - (int)($state['lastEmsUpdate'] ?? 0);
|
||||
if (($state['lastEmsUpdate'] ?? 0) > 0 && $emsAge > (int)($state['emsWatchdogSeconds'] ?? 120)) {
|
||||
$warnings[] = 'EMS watchdog expired';
|
||||
$safePowerW = $this->safePower($state);
|
||||
}
|
||||
|
||||
$heartbeatAge = $now - (int)($state['lastOcppHeartbeat'] ?? 0);
|
||||
if (($state['ocppOnline'] ?? false) && ($state['lastOcppHeartbeat'] ?? 0) > 0 && $heartbeatAge > (int)($state['ocppHeartbeatTimeoutSeconds'] ?? 90)) {
|
||||
$warnings[] = 'OCPP heartbeat timeout';
|
||||
$blockingReason = 'OCPP timeout';
|
||||
}
|
||||
|
||||
if (($state['criticalFault'] ?? false) === true) {
|
||||
$warnings[] = 'Critical fault';
|
||||
$blockingReason = 'Critical fault';
|
||||
$safePowerW = 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'warnings' => $warnings,
|
||||
'blockingReason' => $blockingReason,
|
||||
'safePowerW' => $safePowerW
|
||||
];
|
||||
}
|
||||
|
||||
private function safePower(array $state): int
|
||||
{
|
||||
$strategy = (string)($state['safeOffStrategy'] ?? 'SafeCurrent');
|
||||
if ($strategy === '0A' || $strategy === 'Unavailable') {
|
||||
return 0;
|
||||
}
|
||||
$phases = PhaseManager::normalizePhaseCount((int)($state['numberPhases'] ?? 3));
|
||||
$safeCurrent = (float)($state['safeCurrentA'] ?? 6.0);
|
||||
return PhaseManager::wattsFromCurrent($safeCurrent, $phases);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
class MeterValueNormalizer
|
||||
{
|
||||
public function normalize(array $payload): array
|
||||
{
|
||||
$result = [
|
||||
'powerImportW' => null,
|
||||
'powerExportW' => null,
|
||||
'energyImportWh' => null,
|
||||
'energyExportWh' => null,
|
||||
'currentA' => [1 => null, 2 => null, 3 => null],
|
||||
'voltageV' => [1 => null, 2 => null, 3 => null],
|
||||
'powerPhaseW' => [1 => null, 2 => null, 3 => null],
|
||||
'soc' => null,
|
||||
'temperature' => null,
|
||||
'quality' => 'OCPP-MeterValue',
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
foreach ($this->extractSamples($payload) as $sample) {
|
||||
$measurand = (string)($sample['measurand'] ?? 'Energy.Active.Import.Register');
|
||||
$value = $this->normalizeValue($sample);
|
||||
$phase = $this->phaseIndex((string)($sample['phase'] ?? ''));
|
||||
|
||||
switch ($measurand) {
|
||||
case 'Power.Active.Import':
|
||||
$this->writePower($result, 'powerImportW', $value, $phase);
|
||||
break;
|
||||
case 'Power.Active.Export':
|
||||
$this->writePower($result, 'powerExportW', $value, $phase);
|
||||
break;
|
||||
case 'Energy.Active.Import.Register':
|
||||
case 'Energy.Active.Import.Interval':
|
||||
$result['energyImportWh'] = $value;
|
||||
break;
|
||||
case 'Energy.Active.Export.Register':
|
||||
case 'Energy.Active.Export.Interval':
|
||||
$result['energyExportWh'] = $value;
|
||||
break;
|
||||
case 'Current.Import':
|
||||
if ($phase > 0) {
|
||||
$result['currentA'][$phase] = $value;
|
||||
}
|
||||
break;
|
||||
case 'Voltage':
|
||||
if ($phase > 0) {
|
||||
$result['voltageV'][$phase] = $value;
|
||||
}
|
||||
break;
|
||||
case 'SoC':
|
||||
$result['soc'] = $value;
|
||||
break;
|
||||
case 'Temperature':
|
||||
$result['temperature'] = $value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($sample['timestamp'])) {
|
||||
$ts = strtotime((string)$sample['timestamp']);
|
||||
if ($ts !== false) {
|
||||
$result['timestamp'] = $ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($result['powerImportW'] === null) {
|
||||
$result['powerImportW'] = $this->sumPhases($result['powerPhaseW']);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function extractSamples(array $payload): array
|
||||
{
|
||||
$samples = [];
|
||||
$containers = $payload['meterValue'] ?? $payload['meterValues'] ?? [$payload];
|
||||
if (!is_array($containers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($containers as $container) {
|
||||
if (!is_array($container)) {
|
||||
continue;
|
||||
}
|
||||
$timestamp = $container['timestamp'] ?? null;
|
||||
$sampled = $container['sampledValue'] ?? $container['sampledValues'] ?? [];
|
||||
if (!is_array($sampled)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($sampled as $sample) {
|
||||
if (!is_array($sample)) {
|
||||
continue;
|
||||
}
|
||||
if ($timestamp !== null && !isset($sample['timestamp'])) {
|
||||
$sample['timestamp'] = $timestamp;
|
||||
}
|
||||
$samples[] = $sample;
|
||||
}
|
||||
}
|
||||
|
||||
return $samples;
|
||||
}
|
||||
|
||||
private function normalizeValue(array $sample): float
|
||||
{
|
||||
$value = (float)($sample['value'] ?? 0);
|
||||
$unit = $sample['unit'] ?? ($sample['unitOfMeasure']['unit'] ?? '');
|
||||
$unit = strtolower((string)$unit);
|
||||
|
||||
if ($unit === 'kwh') {
|
||||
return $value * 1000.0;
|
||||
}
|
||||
if ($unit === 'kw') {
|
||||
return $value * 1000.0;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function phaseIndex(string $phase): int
|
||||
{
|
||||
if (stripos($phase, 'L1') !== false) {
|
||||
return 1;
|
||||
}
|
||||
if (stripos($phase, 'L2') !== false) {
|
||||
return 2;
|
||||
}
|
||||
if (stripos($phase, 'L3') !== false) {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function writePower(array &$result, string $key, float $value, int $phase): void
|
||||
{
|
||||
if ($phase > 0) {
|
||||
$result['powerPhaseW'][$phase] = $value;
|
||||
return;
|
||||
}
|
||||
$result[$key] = $value;
|
||||
}
|
||||
|
||||
private function sumPhases(array $values): ?float
|
||||
{
|
||||
$sum = 0.0;
|
||||
$found = false;
|
||||
foreach ($values as $value) {
|
||||
if ($value !== null) {
|
||||
$sum += (float)$value;
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
return $found ? $sum : null;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
class OCPP16Adapter
|
||||
{
|
||||
public function normalizeInbound(OCPPMessage $message): array
|
||||
{
|
||||
return [
|
||||
'version' => '1.6',
|
||||
'action' => $message->action,
|
||||
'uniqueId' => $message->uniqueId,
|
||||
'payload' => $message->payload
|
||||
];
|
||||
}
|
||||
|
||||
public function buildSetChargingProfile(array $profile): OCPPMessage
|
||||
{
|
||||
return OCPPMessage::call('SetChargingProfile', $profile, '1.6');
|
||||
}
|
||||
|
||||
public function buildClearChargingProfile(int $connectorId): OCPPMessage
|
||||
{
|
||||
return OCPPMessage::call('ClearChargingProfile', ['connectorId' => $connectorId], '1.6');
|
||||
}
|
||||
|
||||
public function buildReset(string $type = 'Soft'): OCPPMessage
|
||||
{
|
||||
return OCPPMessage::call('Reset', ['type' => $type], '1.6');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
class OCPP201Adapter
|
||||
{
|
||||
public function normalizeInbound(OCPPMessage $message): array
|
||||
{
|
||||
return [
|
||||
'version' => '2.0.1',
|
||||
'action' => $message->action,
|
||||
'uniqueId' => $message->uniqueId,
|
||||
'payload' => $message->payload
|
||||
];
|
||||
}
|
||||
|
||||
public function buildSetChargingProfile(array $profile): OCPPMessage
|
||||
{
|
||||
return OCPPMessage::call('SetChargingProfile', $profile, '2.0.1');
|
||||
}
|
||||
|
||||
public function buildClearChargingProfile(int $evseId): OCPPMessage
|
||||
{
|
||||
return OCPPMessage::call('ClearChargingProfile', ['evseId' => $evseId], '2.0.1');
|
||||
}
|
||||
|
||||
public function buildReset(string $type = 'Immediate'): OCPPMessage
|
||||
{
|
||||
return OCPPMessage::call('Reset', ['type' => $type], '2.0.1');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
class OCPP21Adapter extends OCPP201Adapter
|
||||
{
|
||||
public function normalizeInbound(OCPPMessage $message): array
|
||||
{
|
||||
$data = parent::normalizeInbound($message);
|
||||
$data['version'] = '2.1';
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function supportsBidirectionalPreparation(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
class OCPPMessage
|
||||
{
|
||||
public string $version;
|
||||
public string $direction;
|
||||
public string $action;
|
||||
public string $uniqueId;
|
||||
public array $payload;
|
||||
public int $timestamp;
|
||||
public string $chargePointId;
|
||||
public int $messageTypeId;
|
||||
|
||||
public function __construct(
|
||||
string $version,
|
||||
string $direction,
|
||||
string $action,
|
||||
string $uniqueId,
|
||||
array $payload,
|
||||
string $chargePointId = '',
|
||||
int $messageTypeId = 0
|
||||
) {
|
||||
$this->version = $version;
|
||||
$this->direction = $direction;
|
||||
$this->action = $action;
|
||||
$this->uniqueId = $uniqueId;
|
||||
$this->payload = $payload;
|
||||
$this->timestamp = time();
|
||||
$this->chargePointId = $chargePointId;
|
||||
$this->messageTypeId = $messageTypeId;
|
||||
}
|
||||
|
||||
public static function fromJson(string $json, string $version = 'auto', string $chargePointId = ''): ?self
|
||||
{
|
||||
$frame = json_decode($json, true);
|
||||
if (!is_array($frame) || count($frame) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = (int)($frame[0] ?? 0);
|
||||
$uniqueId = (string)($frame[1] ?? '');
|
||||
|
||||
if ($type === 2) {
|
||||
return new self($version, 'in', (string)($frame[2] ?? ''), $uniqueId, (array)($frame[3] ?? []), $chargePointId, $type);
|
||||
}
|
||||
|
||||
if ($type === 3) {
|
||||
return new self($version, 'in_result', 'CallResult', $uniqueId, (array)($frame[2] ?? []), $chargePointId, $type);
|
||||
}
|
||||
|
||||
if ($type === 4) {
|
||||
return new self($version, 'in_error', (string)($frame[2] ?? 'CallError'), $uniqueId, [
|
||||
'errorCode' => (string)($frame[2] ?? ''),
|
||||
'errorDescription' => (string)($frame[3] ?? ''),
|
||||
'errorDetails' => (array)($frame[4] ?? [])
|
||||
], $chargePointId, $type);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function call(string $action, array $payload, string $version = 'auto', string $chargePointId = ''): self
|
||||
{
|
||||
return new self($version, 'out', $action, self::newUniqueId(), $payload, $chargePointId, 2);
|
||||
}
|
||||
|
||||
public static function callResult(string $uniqueId, array $payload, string $version = 'auto', string $chargePointId = ''): self
|
||||
{
|
||||
return new self($version, 'out_result', 'CallResult', $uniqueId, $payload, $chargePointId, 3);
|
||||
}
|
||||
|
||||
public static function callError(string $uniqueId, string $code, string $description, array $details = [], string $version = 'auto', string $chargePointId = ''): self
|
||||
{
|
||||
return new self($version, 'out_error', $code, $uniqueId, [
|
||||
'errorCode' => $code,
|
||||
'errorDescription' => $description,
|
||||
'errorDetails' => $details
|
||||
], $chargePointId, 4);
|
||||
}
|
||||
|
||||
public function toFrame(): array
|
||||
{
|
||||
if ($this->messageTypeId === 2) {
|
||||
return [2, $this->uniqueId, $this->action, $this->payload];
|
||||
}
|
||||
if ($this->messageTypeId === 3) {
|
||||
return [3, $this->uniqueId, $this->payload];
|
||||
}
|
||||
if ($this->messageTypeId === 4) {
|
||||
return [
|
||||
4,
|
||||
$this->uniqueId,
|
||||
(string)($this->payload['errorCode'] ?? $this->action),
|
||||
(string)($this->payload['errorDescription'] ?? ''),
|
||||
(array)($this->payload['errorDetails'] ?? [])
|
||||
];
|
||||
}
|
||||
return [$this->messageTypeId, $this->uniqueId, $this->action, $this->payload];
|
||||
}
|
||||
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toFrame());
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'version' => $this->version,
|
||||
'direction' => $this->direction,
|
||||
'action' => $this->action,
|
||||
'uniqueId' => $this->uniqueId,
|
||||
'payload' => $this->payload,
|
||||
'timestamp' => $this->timestamp,
|
||||
'chargePointId' => $this->chargePointId,
|
||||
'messageTypeId' => $this->messageTypeId
|
||||
];
|
||||
}
|
||||
|
||||
private static function newUniqueId(): string
|
||||
{
|
||||
return str_replace('.', '', uniqid('ips-', true));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
class OCPPTransport
|
||||
{
|
||||
public static function buildEnvelope(OCPPMessage $message, int $targetInstance = 0): array
|
||||
{
|
||||
return [
|
||||
'TargetInstance' => $targetInstance,
|
||||
'ChargePointId' => $message->chargePointId,
|
||||
'Version' => $message->version,
|
||||
'Frame' => $message->toJson(),
|
||||
'Timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
public static function decodeEnvelope(string $json): array
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'TargetInstance' => (int)($data['TargetInstance'] ?? 0),
|
||||
'ChargePointId' => (string)($data['ChargePointId'] ?? ''),
|
||||
'Version' => (string)($data['Version'] ?? 'auto'),
|
||||
'Frame' => (string)($data['Frame'] ?? ''),
|
||||
'Timestamp' => (int)($data['Timestamp'] ?? time())
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
class PhaseManager
|
||||
{
|
||||
public static function normalizePhaseCount(int $phases): int
|
||||
{
|
||||
if ($phases <= 1) {
|
||||
return 1;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
public static function wattsFromCurrent(float $currentA, int $phases, float $voltage = 230.0): int
|
||||
{
|
||||
$phaseCount = self::normalizePhaseCount($phases);
|
||||
return (int)round($currentA * $voltage * $phaseCount);
|
||||
}
|
||||
|
||||
public static function currentFromPower(float $powerW, int $phases, float $voltage = 230.0): float
|
||||
{
|
||||
$phaseCount = self::normalizePhaseCount($phases);
|
||||
if ($voltage <= 0 || $phaseCount <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
return round($powerW / ($voltage * $phaseCount), 2);
|
||||
}
|
||||
|
||||
public static function choosePhaseToUse(array $phaseCurrents): int
|
||||
{
|
||||
$bestPhase = 1;
|
||||
$bestCurrent = null;
|
||||
foreach ([1, 2, 3] as $phase) {
|
||||
$value = isset($phaseCurrents[$phase]) ? (float)$phaseCurrents[$phase] : null;
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
if ($bestCurrent === null || $value < $bestCurrent) {
|
||||
$bestCurrent = $value;
|
||||
$bestPhase = $phase;
|
||||
}
|
||||
}
|
||||
return $bestPhase;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
class PowerStepCalculator
|
||||
{
|
||||
public const MODE_NEVER = 0;
|
||||
public const MODE_ALWAYS = 1;
|
||||
public const MODE_CONSTANT = 2;
|
||||
public const MODE_SOLAR = 3;
|
||||
public const MODE_MINIMAL_SOLAR = 4;
|
||||
public const MODE_SOLAR_TARIFF = 5;
|
||||
public const MODE_MINIMUM_ENERGY = 6;
|
||||
public const MODE_TARGET_SOC = 7;
|
||||
public const MODE_BIDIRECTIONAL = 8;
|
||||
|
||||
public function calculate(array $input): array
|
||||
{
|
||||
$currentPower = (int)round((float)($input['currentPowerW'] ?? 0));
|
||||
if (!($input['idle'] ?? true)) {
|
||||
return [$currentPower];
|
||||
}
|
||||
|
||||
if (!($input['ladebereit'] ?? false) || ($input['manualLock'] ?? false)) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
if (!($input['carDetected'] ?? false) || ($input['carFull'] ?? false)) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
$mode = (int)($input['mode'] ?? self::MODE_NEVER);
|
||||
if ($mode === self::MODE_NEVER) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
$minCurrent = max(0.0, (float)($input['minCurrentA'] ?? 6.0));
|
||||
$maxCurrent = max($minCurrent, (float)($input['maxCurrentA'] ?? 6.0));
|
||||
$safeCurrent = max(0.0, (float)($input['safeCurrentA'] ?? 6.0));
|
||||
$constantCurrent = max(0.0, (float)($input['constantCurrentA'] ?? $minCurrent));
|
||||
$phaseCount = PhaseManager::normalizePhaseCount((int)($input['numberPhases'] ?? 3));
|
||||
$voltage = (float)($input['voltage'] ?? 230.0);
|
||||
|
||||
$limitCurrent = $maxCurrent;
|
||||
if (($input['groupLimitA'] ?? 0) > 0) {
|
||||
$limitCurrent = min($limitCurrent, (float)$input['groupLimitA']);
|
||||
}
|
||||
if (($input['vnbLimitA'] ?? 0) > 0) {
|
||||
$limitCurrent = min($limitCurrent, (float)$input['vnbLimitA']);
|
||||
}
|
||||
if (($input['vnbLimitW'] ?? 0) > 0) {
|
||||
$limitCurrent = min($limitCurrent, PhaseManager::currentFromPower((float)$input['vnbLimitW'], $phaseCount, $voltage));
|
||||
}
|
||||
|
||||
if ($limitCurrent < $minCurrent) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
if ($mode === self::MODE_CONSTANT) {
|
||||
$target = min(max($constantCurrent, $safeCurrent), $limitCurrent);
|
||||
return $this->uniqueSorted([0, PhaseManager::wattsFromCurrent($target, $phaseCount, $voltage)]);
|
||||
}
|
||||
|
||||
$steps = [0];
|
||||
for ($current = (int)ceil($minCurrent); $current <= (int)floor($limitCurrent); $current++) {
|
||||
$steps[] = PhaseManager::wattsFromCurrent((float)$current, $phaseCount, $voltage);
|
||||
}
|
||||
|
||||
return $this->uniqueSorted($steps);
|
||||
}
|
||||
|
||||
private function uniqueSorted(array $steps): array
|
||||
{
|
||||
$steps = array_map(static function ($value) {
|
||||
return (int)round((float)$value);
|
||||
}, $steps);
|
||||
$steps = array_values(array_unique($steps));
|
||||
sort($steps, SORT_NUMERIC);
|
||||
return $steps;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
class TransactionStore
|
||||
{
|
||||
public static function empty(): array
|
||||
{
|
||||
return [
|
||||
'activeTransactionId' => '',
|
||||
'transactionStartTime' => 0,
|
||||
'transactionStartEnergyImport' => 0.0,
|
||||
'transactionStartEnergyExport' => 0.0,
|
||||
'lastTransactionState' => '',
|
||||
'lastIdToken' => '',
|
||||
'sessionEnergyImport' => 0.0,
|
||||
'sessionEnergyExport' => 0.0,
|
||||
'sessionPeakPower' => 0.0,
|
||||
'sessionStopReason' => ''
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromJson(string $json): array
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
return self::empty();
|
||||
}
|
||||
return array_replace(self::empty(), $data);
|
||||
}
|
||||
|
||||
public static function toJson(array $state): string
|
||||
{
|
||||
return json_encode(array_replace(self::empty(), $state));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user