RegisterPropertyString('OCPPVersionMode', 'auto'); $this->RegisterPropertyString('DeviceProfile', 'generic_ocpp'); $this->RegisterPropertyString('ChargePointId', ''); $this->RegisterPropertyInteger('EVSEId', 1); $this->RegisterPropertyInteger('ConnectorId', 1); $this->RegisterPropertyInteger('OCPPServerInstance', 0); $this->RegisterPropertyString('AuthorizeIdTag', 'Symcon'); $this->RegisterPropertyFloat('MaxCurrentAbs', 32.0); $this->RegisterPropertyFloat('MinCurrent', 6.0); $this->RegisterPropertyFloat('SafeCurrent', 6.0); $this->RegisterPropertyString('SafeOffStrategy', 'SafeCurrent'); $this->RegisterPropertyInteger('EMSWatchdogSeconds', 120); $this->RegisterPropertyInteger('OCPPHeartbeatIntervalSeconds', 30); $this->RegisterPropertyInteger('OCPPHeartbeatTimeoutSeconds', 90); $this->RegisterPropertyInteger('MeterValuesTimeoutSeconds', 180); $this->RegisterPropertyInteger('CommandAckTimeoutSeconds', 30); $this->RegisterPropertyInteger('SmartChargingRetryLimit', 3); $this->RegisterPropertyInteger('MeterPowerIntervalSeconds', 1); $this->RegisterPropertyInteger('MeterEnergyIntervalSeconds', 60); $this->RegisterPropertyInteger('IdleCounterMax', 2); $this->RegisterPropertyInteger('Interval', 2); $this->RegisterPropertyInteger('PhaseSwitchHoldSeconds', 120); $this->RegisterPropertyInteger('PhaseSwitchPauseSeconds', 30); $this->RegisterPropertyBoolean('AllowAutomaticPhaseSwitch', false); $this->RegisterPropertyBoolean('AllowDataTransfer', false); $this->RegisterPropertyString('GroupId', ''); $this->RegisterPropertyFloat('GroupLimitA', 0.0); $this->RegisterPropertyFloat('GroupLimitW', 0.0); $this->RegisterPropertyInteger('VNBInputMode', 0); $this->RegisterPropertyInteger('DebugLevel', 0); $this->RegisterPropertyFloat('SetpointTolerancePercent', 5.0); $this->RegisterPropertyInteger('SetpointToleranceW', 300); $this->RegisterAttributeString(self::ATTR_TRANSACTION, TransactionStore::toJson(TransactionStore::empty())); $this->RegisterAttributeString(self::ATTR_CAPABILITIES, json_encode(CapabilityModel::defaults())); $this->RegisterAttributeInteger(self::ATTR_LAST_EMS_UPDATE, 0); $this->RegisterAttributeInteger(self::ATTR_LAST_OCPP_HEARTBEAT, 0); $this->RegisterAttributeString(self::ATTR_PENDING_COMMANDS, json_encode([])); $this->RegisterAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION, 0); $this->registerEmsVariables(); $this->registerControlVariables(); $this->registerStatusVariables(); $this->registerMeterVariables(); $this->registerDiagnosticVariables(); $this->RegisterTimer('Timer_Do_UserCalc', 2000, 'IPS_RequestAction(' . $this->InstanceID . ', "Do_UserCalc", "");'); $this->RegisterTimer('Timer_StatusWatchdog', 5000, 'IPS_RequestAction(' . $this->InstanceID . ', "StatusWatchdog", "");'); $this->RegisterTimer('Timer_EMSWatchdog', 5000, 'IPS_RequestAction(' . $this->InstanceID . ', "EMSWatchdog", "");'); $this->RegisterTimer('Timer_OCPPWatchdog', 5000, 'IPS_RequestAction(' . $this->InstanceID . ', "OCPPWatchdog", "");'); $this->RegisterTimer('Timer_MeterAggregation', 60000, 'IPS_RequestAction(' . $this->InstanceID . ', "MeterAggregation", "");'); $this->setInitialDefaults(); } public function ApplyChanges() { parent::ApplyChanges(); $interval = max(2, $this->ReadPropertyInteger('Interval')); $this->SetTimerInterval('Timer_Do_UserCalc', $interval * 1000); $this->SetTimerInterval('Timer_StatusWatchdog', 5000); $this->SetTimerInterval('Timer_EMSWatchdog', 5000); $this->SetTimerInterval('Timer_OCPPWatchdog', 5000); $this->SetTimerInterval('Timer_MeterAggregation', max(10, $this->ReadPropertyInteger('MeterEnergyIntervalSeconds')) * 1000); $this->EnableAction('Ladebereit'); $this->EnableAction('Managementmodus'); $this->EnableAction('Mindestladestrom'); $this->EnableAction('Konstantstrom'); $this->EnableAction('Max_Current_abs'); $this->EnableAction('SafeCurrent'); $this->EnableAction('AutomatischePhasenumschaltung'); $this->EnableAction('ManuelleSperre'); $this->EnableAction('VNB_Mode'); $this->EnableAction('VNB_Limit_A'); $this->EnableAction('VNB_Limit_W'); $this->EnableAction('Sperre_Prio'); $this->EnableAction('PV_Prio'); $this->applyDeviceProfileCapabilities(); if ($this->GetValue('Max_Current_abs') <= 0) { $this->SetValue('Max_Current_abs', $this->ReadPropertyFloat('MaxCurrentAbs')); } if ($this->GetValue('Mindestladestrom') <= 0) { $this->SetValue('Mindestladestrom', $this->ReadPropertyFloat('MinCurrent')); } if ($this->GetValue('SafeCurrent') <= 0) { $this->SetValue('SafeCurrent', $this->ReadPropertyFloat('SafeCurrent')); } $chargePointId = trim($this->ReadPropertyString('ChargePointId')); $this->SetValue('ChargePointId', $chargePointId); $this->SetValue('EVSEId', $this->ReadPropertyInteger('EVSEId')); $this->SetValue('ConnectorId', $this->ReadPropertyInteger('ConnectorId')); $server = $this->ReadPropertyInteger('OCPPServerInstance'); if ($server > 0) { $this->RegisterReference($server); } if ($chargePointId === '') { $this->SetStatus(201); $this->setDiagnostic('Warnung', 'Konfiguration', 'ChargePointId ist noch nicht gesetzt.'); $this->SetTimerInterval('Timer_Do_UserCalc', 0); $this->SetSummary('OCPP Scaffold: ChargePointId fehlt'); return; } $this->SetStatus(102); $this->SetSummary('OCPP Scaffold: ' . $chargePointId); } public function RequestAction($Ident, $Value) { switch ($Ident) { case 'SetAktuelle_Leistung': $this->SetAktuelle_Leistung((float)$Value); break; case 'GetCurrentData': $this->SetValue('Is_Peak_Shaving', (bool)$Value); $this->GetCurrentData((bool)$Value); break; case 'Do_UserCalc': $this->Do_UserCalc(); break; case 'Ladebereit': case 'AutomatischePhasenumschaltung': case 'ManuelleSperre': $this->SetValue($Ident, (bool)$Value); $this->markPolicyChanged($Ident); break; case 'Managementmodus': case 'VNB_Mode': case 'Sperre_Prio': case 'PV_Prio': $this->SetValue($Ident, (int)$Value); $this->markPolicyChanged($Ident); break; case 'Mindestladestrom': case 'Konstantstrom': case 'Max_Current_abs': case 'SafeCurrent': case 'VNB_Limit_A': case 'VNB_Limit_W': $this->SetValue($Ident, (float)$Value); $this->markPolicyChanged($Ident); break; case 'Reset': $this->resetDiagnostics(); break; case 'ClearChargingProfile': $this->queueOcppCommand('ClearChargingProfile', $this->buildClearProfilePayload()); break; case 'RemoteStartTransaction': $this->queueOcppCommand('RemoteStartTransaction', $this->buildRemoteStartPayload()); break; case 'RemoteStopTransaction': $payload = $this->buildRemoteStopPayload(); if (!empty($payload)) { $this->queueOcppCommand('RemoteStopTransaction', $payload); } break; case 'ChangeAvailabilityOperative': $this->queueOcppCommand('ChangeAvailability', $this->buildAvailabilityPayload('Operative')); break; case 'ChangeAvailabilityInoperative': $this->queueOcppCommand('ChangeAvailability', $this->buildAvailabilityPayload('Inoperative')); break; case 'ProbeSmartCharging': $this->probeSmartCharging(); break; case 'HandleInboundFrame': $this->HandleInboundFrame((string)$Value); break; case 'StatusWatchdog': case 'EMSWatchdog': case 'OCPPWatchdog': $this->runWatchdogs(); break; case 'MeterAggregation': $this->aggregateEnergyFallback(); break; default: throw new Exception('Invalid Ident'); } } public function SetAktuelle_Leistung(float $power) { $lastPower = (float)$this->GetValue('Power'); $this->SetValue('Power', $power); $this->WriteAttributeInteger(self::ATTR_LAST_EMS_UPDATE, time()); if (abs($lastPower - $power) > 1) { $this->SetValue('Idle', false); $this->SetValue('IdleCounter', $this->ReadPropertyInteger('IdleCounterMax')); } $this->setDiagnostic('Info', 'EMS', 'Neue EMS-Leistungsvorgabe gespeichert: ' . round($power) . ' W'); } public function GetCurrentData(bool $Peak) { $this->SetValue('Is_Peak_Shaving', $Peak); $calculator = new PowerStepCalculator(); $steps = $calculator->calculate($this->buildPowerStepInput()); $this->SetValue('PowerSteps', json_encode($steps)); $this->SetValue('Leistung_Delta', (float)$this->GetValue('Aktuelle_Leistung') - (float)$this->GetValue('Ladeleistung_Effektiv')); $this->SetValue('SollIstAbweichung', (float)$this->GetValue('Leistung_Delta')); return $steps; } public function Do_UserCalc() { $this->runWatchdogs(); $targetPower = $this->calculateEffectivePower(); $oldPower = (float)$this->GetValue('Aktuelle_Leistung'); $this->SetValue('Aktuelle_Leistung', $targetPower); $this->SetValue('AktuellerEffektivSollwert', $targetPower); $this->SetValue('Leistung_Delta', $targetPower - (float)$this->GetValue('Ladeleistung_Effektiv')); $this->SetValue('SollIstAbweichung', $this->GetValue('Leistung_Delta')); if ($this->shouldSendChargingProfile($oldPower, $targetPower)) { $setpoint = $this->buildEffectiveSetpoint($targetPower); $profile = $this->buildChargingProfilePayload($setpoint); $this->queueOcppCommand('SetChargingProfile', $profile); } $this->ProcessIdleCounter(); $this->GetCurrentData($this->GetValue('Is_Peak_Shaving')); } public function HandleInboundFrame(string $json) { $message = OCPPMessage::fromJson($json, $this->effectiveOcppVersion(), $this->ReadPropertyString('ChargePointId')); if ($message === null) { $this->setDiagnostic('Fehler', 'OCPP', 'Ungueltiger OCPP Frame empfangen.'); return; } $this->SetValue('LetzteMeldungZeit', time()); $this->SetValue('LetzteMeldung', $message->action); $this->SetValue('OCPP_Online', true); $this->SetValue('OCPP_Connected', true); $this->SetValue('OCPP_LastSeen', time()); $this->SetValue('OCPP_Error', ''); $this->WriteAttributeInteger(self::ATTR_LAST_OCPP_HEARTBEAT, time()); if ($message->messageTypeId === 3) { $this->handleCallResult($message); return; } if ($message->messageTypeId === 4) { $this->handleCallError($message); return; } switch ($message->action) { case 'BootNotification': $this->handleBootNotification($message); $this->respondToCall($message, $this->buildBootResponse()); break; case 'Heartbeat': $this->setDiagnostic('Info', 'OCPP', 'Heartbeat empfangen.'); $this->respondToCall($message, ['currentTime' => gmdate('Y-m-d\TH:i:s\Z')]); break; case 'StatusNotification': $this->handleStatusNotification($message->payload); $this->respondToCall($message, []); break; case 'MeterValues': $this->handleMeterValues($message->payload); $this->respondToCall($message, []); break; case 'StartTransaction': case 'StopTransaction': case 'TransactionEvent': $this->handleTransaction($message); $this->respondToCall($message, $this->buildTransactionResponse($message)); break; case 'Authorize': $this->SetValue('LetztesIdToken', $this->extractIdToken($message->payload)); $this->respondToCall($message, ['idTagInfo' => ['status' => 'Accepted']]); break; case 'DataTransfer': $this->handleDataTransfer($message->payload); $this->respondToCall($message, $this->buildDataTransferResponse($message->payload)); break; default: $this->setDiagnostic('Info', 'OCPP', 'OCPP Aktion empfangen: ' . $message->action); $this->respondToCall($message, []); break; } } private function registerEmsVariables(): void { $this->RegisterVariableInteger('Sperre_Prio', 'Sperre_Prio', '', 10); $this->RegisterVariableInteger('PV_Prio', 'PV_Prio', '', 11); $this->RegisterVariableBoolean('Idle', 'Idle', '', 12); $this->RegisterVariableFloat('Aktuelle_Leistung', 'Aktuelle_Leistung', '', 13); $this->RegisterVariableFloat('Bezogene_Energie', 'Bezogene_Energie', '', 14); $this->RegisterVariableString('PowerSteps', 'PowerSteps', '', 15); $this->RegisterVariableFloat('Power', 'Power', '', 16); $this->RegisterVariableBoolean('Is_Peak_Shaving', 'Is_Peak_Shaving', '', 17); $this->RegisterVariableFloat('Leistung_Delta', 'Leistung_Delta', '', 18); $this->RegisterVariableInteger('IdleCounter', 'IdleCounter', '', 19); } private function registerControlVariables(): void { $this->RegisterVariableBoolean('Ladebereit', 'Ladebereit', '~Switch', 30); $this->RegisterVariableInteger('Managementmodus', 'Managementmodus', '', 31); $this->RegisterVariableFloat('Mindestladestrom', 'Mindestladestrom', '', 32); $this->RegisterVariableFloat('Konstantstrom', 'Konstantstrom', '', 33); $this->RegisterVariableFloat('Max_Current_abs', 'Max_Current_abs', '', 34); $this->RegisterVariableFloat('SafeCurrent', 'SafeCurrent', '', 35); $this->RegisterVariableFloat('SafeOff', 'SafeOff', '', 36); $this->RegisterVariableBoolean('AutomatischePhasenumschaltung', 'AutomatischePhasenumschaltung', '~Switch', 37); $this->RegisterVariableBoolean('ManuelleSperre', 'ManuelleSperre', '~Switch', 38); $this->RegisterVariableInteger('VNB_Mode', 'VNB_Mode', '', 39); $this->RegisterVariableFloat('VNB_Limit_A', 'VNB_Limit_A', '', 40); $this->RegisterVariableFloat('VNB_Limit_W', 'VNB_Limit_W', '', 41); $this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 42); } private function registerStatusVariables(): void { $this->RegisterVariableBoolean('OCPP_Online', 'OCPP_Online', '~Switch', 60); $this->RegisterVariableString('OCPP_Version', 'OCPP_Version', '', 61); $this->RegisterVariableString('ChargePointId', 'ChargePointId', '', 62); $this->RegisterVariableInteger('EVSEId', 'EVSEId', '', 63); $this->RegisterVariableInteger('ConnectorId', 'ConnectorId', '', 64); $this->RegisterVariableString('ConnectorStatus', 'ConnectorStatus', '', 65); $this->RegisterVariableString('ChargingState', 'ChargingState', '', 66); $this->RegisterVariableString('TransactionId', 'TransactionId', '', 67); $this->RegisterVariableBoolean('Car_detected', 'Car_detected', '~Switch', 68); $this->RegisterVariableBoolean('Car_is_full', 'Car_is_full', '~Switch', 69); $this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 70); $this->RegisterVariableBoolean('Is_1_ph', 'Is_1_ph', '~Switch', 71); $this->RegisterVariableInteger('NumberPhases', 'NumberPhases', '', 72); $this->RegisterVariableInteger('PhaseToUse', 'PhaseToUse', '', 73); $this->RegisterVariableFloat('AktuellerEffektivSollwert', 'AktuellerEffektivSollwert', '', 74); $this->RegisterVariableString('AktiveFreigabequelle', 'AktiveFreigabequelle', '', 75); $this->RegisterVariableString('AktiverSperrgrund', 'AktiverSperrgrund', '', 76); $this->RegisterVariableString('AktiverReduktionsgrund', 'AktiverReduktionsgrund', '', 77); $this->RegisterVariableInteger('LetzterFreigabewechsel', 'LetzterFreigabewechsel', '', 78); $this->RegisterVariableString('LetztesIdToken', 'LetztesIdToken', '', 79); $this->RegisterVariableString('DeviceProfile', 'DeviceProfile', '', 80); $this->RegisterVariableBoolean('OCPP_Connected', 'OCPP_Connected', '~Switch', 81); $this->RegisterVariableInteger('OCPP_LastSeen', 'OCPP_LastSeen', '', 82); $this->RegisterVariableString('OCPP_Error', 'OCPP_Error', '', 83); } private function registerMeterVariables(): void { $this->RegisterVariableFloat('Ladeleistung_Effektiv', 'Ladeleistung_Effektiv', '', 90); $this->RegisterVariableFloat('Entladeleistung_Effektiv', 'Entladeleistung_Effektiv', '', 91); $this->RegisterVariableFloat('Strom_L1', 'Strom_L1', '', 92); $this->RegisterVariableFloat('Strom_L2', 'Strom_L2', '', 93); $this->RegisterVariableFloat('Strom_L3', 'Strom_L3', '', 94); $this->RegisterVariableFloat('Spannung_L1', 'Spannung_L1', '', 95); $this->RegisterVariableFloat('Spannung_L2', 'Spannung_L2', '', 96); $this->RegisterVariableFloat('Spannung_L3', 'Spannung_L3', '', 97); $this->RegisterVariableFloat('Leistung_L1', 'Leistung_L1', '', 98); $this->RegisterVariableFloat('Leistung_L2', 'Leistung_L2', '', 99); $this->RegisterVariableFloat('Leistung_L3', 'Leistung_L3', '', 100); $this->RegisterVariableFloat('Abgegebene_Energie', 'Abgegebene_Energie', '', 101); $this->RegisterVariableFloat('SoC', 'SoC', '', 102); $this->RegisterVariableFloat('Temperatur', 'Temperatur', '', 103); $this->RegisterVariableString('MesswertQualitaet', 'MesswertQualitaet', '', 104); $this->RegisterVariableInteger('LetzterMeterValueZeitpunkt', 'LetzterMeterValueZeitpunkt', '', 105); } private function registerDiagnosticVariables(): void { $this->RegisterVariableString('Kommunikationsstatus', 'Kommunikationsstatus', '', 120); $this->RegisterVariableString('LetzteMeldung', 'LetzteMeldung', '', 121); $this->RegisterVariableInteger('LetzteMeldungZeit', 'LetzteMeldungZeit', '', 122); $this->RegisterVariableString('Fehlerklasse', 'Fehlerklasse', '', 123); $this->RegisterVariableString('AktuelleStoerung', 'AktuelleStoerung', '', 124); $this->RegisterVariableBoolean('WarnungAktiv', 'WarnungAktiv', '~Switch', 125); $this->RegisterVariableBoolean('FehlerAktiv', 'FehlerAktiv', '~Switch', 126); $this->RegisterVariableBoolean('StoerungAktiv', 'StoerungAktiv', '~Switch', 127); $this->RegisterVariableString('LetzterOCPPFehlercode', 'LetzterOCPPFehlercode', '', 128); $this->RegisterVariableBoolean('LetztesChargingProfileAccepted', 'LetztesChargingProfileAccepted', '~Switch', 129); $this->RegisterVariableInteger('LetzterCommandTimestamp', 'LetzterCommandTimestamp', '', 130); $this->RegisterVariableString('LetzterCommandStatus', 'LetzterCommandStatus', '', 131); $this->RegisterVariableFloat('SollIstAbweichung', 'SollIstAbweichung', '', 132); $this->RegisterVariableBoolean('Capability_SmartCharging', 'Capability_SmartCharging', '~Switch', 133); $this->RegisterVariableBoolean('Capability_MeterValues', 'Capability_MeterValues', '~Switch', 134); $this->RegisterVariableBoolean('Capability_RemoteStartStop', 'Capability_RemoteStartStop', '~Switch', 135); $this->RegisterVariableBoolean('Capability_Availability', 'Capability_Availability', '~Switch', 136); $this->RegisterVariableBoolean('Capability_PhaseValues', 'Capability_PhaseValues', '~Switch', 137); $this->RegisterVariableBoolean('Capability_SimpleAmpereProfiles', 'Capability_SimpleAmpereProfiles', '~Switch', 138); $this->RegisterVariableInteger('WattpilotProfileRetries', 'WattpilotProfileRetries', '', 139); $this->RegisterVariableBoolean('WattpilotSmartChargingBlocked', 'WattpilotSmartChargingBlocked', '~Switch', 140); } private function setInitialDefaults(): void { $defaults = [ 'Idle' => true, 'PowerSteps' => json_encode([0]), 'Ladebereit' => true, 'Managementmodus' => PowerStepCalculator::MODE_SOLAR, 'Mindestladestrom' => 6.0, 'Konstantstrom' => 6.0, 'Max_Current_abs' => 32.0, 'SafeCurrent' => 6.0, 'SafeOff' => 0.0, 'DeviceProfile' => 'generic_ocpp', 'OCPP_Version' => 'auto', 'OCPP_Connected' => false, 'OCPP_LastSeen' => 0, 'OCPP_Error' => '', 'ConnectorStatus' => 'Unknown', 'ChargingState' => 'Unknown', 'NumberPhases' => 3, 'PhaseToUse' => 0, 'AktiveFreigabequelle' => 'Symcon', 'AktiverSperrgrund' => '', 'AktiverReduktionsgrund' => '', 'MesswertQualitaet' => 'unbekannt', 'Kommunikationsstatus' => 'Scaffold', 'LetzteMeldung' => 'Initialisiert', 'Fehlerklasse' => '', 'AktuelleStoerung' => '', 'LetzterCommandStatus' => 'Noch kein OCPP-Transport verifiziert', 'WattpilotProfileRetries' => 0, 'WattpilotSmartChargingBlocked' => false ]; foreach ($defaults as $ident => $value) { $this->SetValue($ident, $value); } } private function buildPowerStepInput(): array { return [ 'idle' => (bool)$this->GetValue('Idle'), 'currentPowerW' => (float)$this->GetValue('Aktuelle_Leistung'), 'ladebereit' => (bool)$this->GetValue('Ladebereit'), 'manualLock' => (bool)$this->GetValue('ManuelleSperre'), 'carDetected' => (bool)$this->GetValue('Car_detected'), 'carFull' => (bool)$this->GetValue('Car_is_full'), 'mode' => (int)$this->GetValue('Managementmodus'), 'minCurrentA' => (float)$this->GetValue('Mindestladestrom'), 'maxCurrentA' => (float)$this->GetValue('Max_Current_abs'), 'safeCurrentA' => (float)$this->GetValue('SafeCurrent'), 'constantCurrentA' => (float)$this->GetValue('Konstantstrom'), 'numberPhases' => (int)$this->GetValue('NumberPhases'), 'groupLimitA' => $this->ReadPropertyFloat('GroupLimitA'), 'groupLimitW' => $this->ReadPropertyFloat('GroupLimitW'), 'vnbLimitA' => (float)$this->GetValue('VNB_Limit_A'), 'vnbLimitW' => (float)$this->GetValue('VNB_Limit_W'), 'voltage' => $this->averageVoltage() ]; } private function calculateEffectivePower(): float { $steps = json_decode($this->GetValue('PowerSteps'), true); if (!is_array($steps) || empty($steps)) { $steps = [0]; } $requested = (float)$this->GetValue('Power'); $mode = (int)$this->GetValue('Managementmodus'); if ($mode === PowerStepCalculator::MODE_NEVER || !$this->GetValue('Ladebereit') || $this->GetValue('ManuelleSperre')) { $this->SetValue('AktiverSperrgrund', 'Bedienung/Sperre'); return 0.0; } if ($mode === PowerStepCalculator::MODE_CONSTANT) { $requested = PhaseManager::wattsFromCurrent((float)$this->GetValue('Konstantstrom'), (int)$this->GetValue('NumberPhases'), $this->averageVoltage()); } $vnbMode = (int)$this->GetValue('VNB_Mode'); if ($vnbMode === 1) { $this->SetValue('AktiverSperrgrund', 'VNB harte Sperre'); return 0.0; } if ($vnbMode === 2) { $this->SetValue('AktiverReduktionsgrund', 'VNB Reduktion'); } else { $this->SetValue('AktiverReduktionsgrund', ''); } $best = 0.0; foreach ($steps as $step) { $step = (float)$step; if ($step <= $requested && $step >= $best) { $best = $step; } } return $best; } private function buildEffectiveSetpoint(float $powerW): array { $phases = (int)$this->GetValue('NumberPhases'); $currentA = PhaseManager::currentFromPower($powerW, $phases, $this->averageVoltage()); $this->SetValue('Ladestrom', $currentA); return [ 'effectivePowerW' => $powerW, 'effectiveCurrentA' => $currentA, 'numberPhases' => $phases, 'phaseToUse' => (int)$this->GetValue('PhaseToUse'), 'evseId' => $this->ReadPropertyInteger('EVSEId'), 'connectorId' => $this->ReadPropertyInteger('ConnectorId'), 'transactionId' => $this->GetValue('TransactionId'), 'reason' => $this->GetValue('AktiverReduktionsgrund'), 'activeReleaseSource' => $this->GetValue('AktiveFreigabequelle'), 'activeBlockingReason' => $this->GetValue('AktiverSperrgrund'), 'activeReductionReason' => $this->GetValue('AktiverReduktionsgrund') ]; } private function buildClearProfilePayload(): array { if ($this->effectiveOcppVersion() === '1.6') { return ['connectorId' => $this->ReadPropertyInteger('ConnectorId')]; } return ['evseId' => $this->ReadPropertyInteger('EVSEId')]; } private function buildRemoteStartPayload(): array { return [ 'connectorId' => $this->ReadPropertyInteger('ConnectorId'), 'idTag' => $this->ReadPropertyString('AuthorizeIdTag') ]; } private function buildRemoteStopPayload(): array { $state = TransactionStore::fromJson($this->ReadAttributeString(self::ATTR_TRANSACTION)); $transactionId = (string)($state['activeTransactionId'] ?? $this->GetValue('TransactionId')); if ($transactionId === '') { $this->setDiagnostic('Warnung', 'OCPP', 'RemoteStop ohne aktive Transaction angefordert.'); return []; } return ['transactionId' => is_numeric($transactionId) ? (int)$transactionId : $transactionId]; } private function buildAvailabilityPayload(string $type): array { return [ 'connectorId' => $this->ReadPropertyInteger('ConnectorId'), 'type' => $type ]; } private function shouldSendChargingProfile(float $oldPower, float $newPower): bool { $delta = abs($oldPower - $newPower); $toleranceW = max(1, $this->ReadPropertyInteger('SetpointToleranceW')); $tolerancePercent = max(0.0, $this->ReadPropertyFloat('SetpointTolerancePercent')); $percentLimit = max($oldPower, 1.0) * ($tolerancePercent / 100.0); return $delta >= max($toleranceW, $percentLimit); } private function queueOcppCommand(string $action, array $payload): void { $this->queueOcppCommandInternal($action, $payload, 0, false); } private function queueOcppCommandInternal(string $action, array $payload, int $retryCount, bool $force): void { $this->processPendingCommands(); $version = $this->effectiveOcppVersion(); if ($action === 'SetChargingProfile' && !$force && !$this->canUseSmartCharging()) { $this->SetValue('LetzterCommandStatus', 'SetChargingProfile blockiert: Capability nicht bestaetigt.'); return; } $message = OCPPMessage::call($action, $payload, $version, $this->ReadPropertyString('ChargePointId')); $this->SetValue('LetzterCommandTimestamp', time()); $this->SetValue('LetzterCommandStatus', 'Queued OCPP: ' . $action); $pendingCommands = $this->readPendingCommands(); $pendingCommands[$message->uniqueId] = [ 'uniqueId' => $message->uniqueId, 'action' => $action, 'payload' => $payload, 'retryCount' => $retryCount, 'timestamp' => time(), 'ackDeadline' => time() + max(5, $this->ReadPropertyInteger('CommandAckTimeoutSeconds')), 'status' => 'pending' ]; $this->writePendingCommands($pendingCommands); $this->queueOcppMessage($message); } private function queueOcppMessage(OCPPMessage $message): void { $server = $this->ReadPropertyInteger('OCPPServerInstance'); if ($server > 0 && IPS_InstanceExists($server)) { IPS_RequestAction($server, 'QueueOutboundFrame', json_encode(OCPPTransport::buildEnvelope($message, $this->InstanceID))); return; } $this->setDiagnostic('Warnung', 'OCPP Transport', 'Kein OCPP_Server verbunden. Frame nur lokal vorgemerkt: ' . $message->action); } private function runWatchdogs(): void { $this->processPendingCommands(); $manager = new FailSafeManager(); $result = $manager->evaluate([ 'lastEmsUpdate' => $this->ReadAttributeInteger(self::ATTR_LAST_EMS_UPDATE), 'emsWatchdogSeconds' => $this->ReadPropertyInteger('EMSWatchdogSeconds'), 'lastOcppHeartbeat' => $this->ReadAttributeInteger(self::ATTR_LAST_OCPP_HEARTBEAT), 'ocppHeartbeatTimeoutSeconds' => $this->ReadPropertyInteger('OCPPHeartbeatTimeoutSeconds'), 'ocppOnline' => (bool)$this->GetValue('OCPP_Online'), 'criticalFault' => (bool)$this->GetValue('StoerungAktiv'), 'safeOffStrategy' => $this->ReadPropertyString('SafeOffStrategy'), 'safeCurrentA' => (float)$this->GetValue('SafeCurrent'), 'numberPhases' => (int)$this->GetValue('NumberPhases') ]); if (!empty($result['warnings'])) { $this->SetValue('WarnungAktiv', true); $this->SetValue('Fehlerklasse', implode(', ', $result['warnings'])); } if ($result['blockingReason'] !== '') { $this->SetValue('AktiverSperrgrund', $result['blockingReason']); $this->SetValue('StoerungAktiv', true); } $lastMeter = (int)$this->GetValue('LetzterMeterValueZeitpunkt'); if ($lastMeter > 0 && (time() - $lastMeter) > max(30, $this->ReadPropertyInteger('MeterValuesTimeoutSeconds'))) { $this->SetValue('WarnungAktiv', true); $this->SetValue('OCPP_Error', 'MeterValues Timeout'); $this->setDiagnostic('Warnung', 'OCPP MeterValues', 'MeterValues sind veraltet.'); } $lastSeen = (int)$this->GetValue('OCPP_LastSeen'); if ($lastSeen > 0 && (time() - $lastSeen) > max(60, $this->ReadPropertyInteger('OCPPHeartbeatTimeoutSeconds'))) { $this->SetValue('OCPP_Connected', false); } } private function ProcessIdleCounter(): void { $counter = (int)$this->GetValue('IdleCounter'); if ($counter > 0) { $this->SetValue('IdleCounter', $counter - 1); $this->SetValue('Idle', false); return; } $this->SetValue('Idle', true); } private function handleBootNotification(OCPPMessage $message): void { $version = $this->effectiveOcppVersion(); $this->SetValue('OCPP_Version', $version); $capabilities = CapabilityModel::detectFromBoot($version, $message->payload); if ($this->isWattpilotProfile() || WattpilotGen1Profile::isWattpilotBoot($message->payload)) { $this->SetValue('DeviceProfile', WattpilotGen1Profile::DEVICE_PROFILE); $capabilities = CapabilityModel::merge($capabilities, WattpilotGen1Profile::capabilities()); } $this->writeCapabilities($capabilities); $this->setDiagnostic('Info', 'OCPP', 'BootNotification verarbeitet.'); } private function handleStatusNotification(array $payload): void { $status = (string)($payload['status'] ?? $payload['connectorStatus'] ?? 'Unknown'); $this->SetValue('ConnectorStatus', $status); $this->SetValue('ChargingState', (string)($payload['chargingState'] ?? $status)); $this->SetValue('Car_detected', in_array($status, ['Preparing', 'Charging', 'SuspendedEV', 'SuspendedEVSE', 'Finishing', 'Occupied'], true)); $this->SetValue('Car_is_full', in_array($status, ['Finishing', 'SuspendedEV'], true)); $statusMap = [ 'Available' => 1, 'Preparing' => 2, 'Charging' => 3, 'SuspendedEV' => 4, 'SuspendedEVSE' => 5, 'Finishing' => 6, 'Reserved' => 7, 'Unavailable' => 0, 'Faulted' => -1 ]; $this->SetValue('Fahrzeugstatus', $statusMap[$status] ?? 0); if ($status === 'Faulted') { $this->setDiagnostic('Stoerung', 'OCPP Status', 'Ladestation meldet Faulted.', (string)($payload['errorCode'] ?? '')); } } private function handleMeterValues(array $payload): void { $values = (new MeterValueNormalizer())->normalize($payload); if ($values['powerImportW'] !== null) { $this->SetValue('Ladeleistung_Effektiv', (float)$values['powerImportW']); } if ($values['powerExportW'] !== null) { $this->SetValue('Entladeleistung_Effektiv', (float)$values['powerExportW']); } if ($values['energyImportWh'] !== null) { $this->SetValue('Bezogene_Energie', (float)$values['energyImportWh']); } if ($values['energyExportWh'] !== null) { $this->SetValue('Abgegebene_Energie', (float)$values['energyExportWh']); } foreach ([1, 2, 3] as $phase) { if ($values['currentA'][$phase] !== null) { $this->SetValue('Strom_L' . $phase, (float)$values['currentA'][$phase]); } if ($values['voltageV'][$phase] !== null) { $this->SetValue('Spannung_L' . $phase, (float)$values['voltageV'][$phase]); } if ($values['powerPhaseW'][$phase] !== null) { $this->SetValue('Leistung_L' . $phase, (float)$values['powerPhaseW'][$phase]); } } if ($values['soc'] !== null) { $this->SetValue('SoC', (float)$values['soc']); } if ($values['temperature'] !== null) { $this->SetValue('Temperatur', (float)$values['temperature']); } $this->SetValue('MesswertQualitaet', $values['quality']); $this->SetValue('LetzterMeterValueZeitpunkt', (int)$values['timestamp']); $this->updateMeterCapabilities($values); $this->updatePhaseStateFromMeters($values); } private function handleTransaction(OCPPMessage $message): void { $state = TransactionStore::fromJson($this->ReadAttributeString(self::ATTR_TRANSACTION)); $payload = $message->payload; $transactionId = (string)($payload['transactionId'] ?? ($payload['transactionInfo']['transactionId'] ?? $state['activeTransactionId'])); if ($transactionId !== '') { $state['activeTransactionId'] = $transactionId; $this->SetValue('TransactionId', $transactionId); } $state['lastTransactionState'] = $message->action; $state['lastIdToken'] = $this->extractIdToken($payload); if ($state['transactionStartTime'] === 0 && ($message->action === 'StartTransaction' || $message->action === 'TransactionEvent')) { $state['transactionStartTime'] = time(); } if ($message->action === 'StopTransaction') { $state['sessionStopReason'] = (string)($payload['reason'] ?? 'Unknown'); $state['activeTransactionId'] = ''; $this->SetValue('TransactionId', ''); } $this->WriteAttributeString(self::ATTR_TRANSACTION, TransactionStore::toJson($state)); $this->SetValue('LetztesIdToken', $state['lastIdToken']); } private function handleDataTransfer(array $payload): void { $registry = new DataTransferRegistry($this->ReadPropertyBoolean('AllowDataTransfer')); $vendorId = (string)($payload['vendorId'] ?? ''); $messageId = (string)($payload['messageId'] ?? ''); if (!$registry->isAllowed($vendorId, $messageId)) { $this->setDiagnostic('Warnung', 'DataTransfer', 'DataTransfer blockiert: ' . $vendorId . '/' . $messageId); return; } $this->setDiagnostic('Info', 'DataTransfer', 'DataTransfer empfangen: ' . $vendorId . '/' . $messageId); } private function aggregateEnergyFallback(): void { if ($this->GetValue('LetzterMeterValueZeitpunkt') > 0) { return; } $now = time(); $lastAggregation = $this->ReadAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION); if ($lastAggregation <= 0) { $this->WriteAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION, $now); return; } $seconds = max(0, $now - $lastAggregation); if ($seconds <= 0) { return; } $energy = (float)$this->GetValue('Bezogene_Energie'); $energy += ((float)$this->GetValue('Ladeleistung_Effektiv') * $seconds) / 3600.0; $this->SetValue('Bezogene_Energie', $energy); $this->WriteAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION, $now); } private function extractIdToken(array $payload): string { if (isset($payload['idTag'])) { return (string)$payload['idTag']; } if (isset($payload['idToken']['idToken'])) { return (string)$payload['idToken']['idToken']; } return ''; } private function averageVoltage(): float { $values = []; foreach (['Spannung_L1', 'Spannung_L2', 'Spannung_L3'] as $ident) { $value = (float)$this->GetValue($ident); if ($value > 0) { $values[] = $value; } } if (empty($values)) { return 230.0; } return array_sum($values) / count($values); } private function effectiveOcppVersion(): string { $version = $this->ReadPropertyString('OCPPVersionMode'); if ($version === 'auto') { $current = $this->GetValue('OCPP_Version'); return ($current === '' || $current === 'auto') ? '1.6' : $current; } return $version; } private function markPolicyChanged(string $ident): void { $this->SetValue('LetzterFreigabewechsel', time()); $this->setDiagnostic('Info', 'Bedienung', 'Policy geaendert: ' . $ident); $this->GetCurrentData($this->GetValue('Is_Peak_Shaving')); } private function resetDiagnostics(): void { $this->SetValue('WarnungAktiv', false); $this->SetValue('FehlerAktiv', false); $this->SetValue('StoerungAktiv', false); $this->SetValue('Fehlerklasse', ''); $this->SetValue('AktuelleStoerung', ''); $this->SetValue('LetzterOCPPFehlercode', ''); $this->setDiagnostic('Info', 'Diagnose', 'Diagnose zurueckgesetzt.'); } private function setDiagnostic(string $severity, string $source, string $text, string $code = ''): void { $this->SetValue('LetzteMeldung', $text); $this->SetValue('LetzteMeldungZeit', time()); if ($severity === 'Fehler' || $severity === 'Stoerung') { $this->SetValue('OCPP_Error', $text); } $this->SetValue('Kommunikationsstatus', Diagnostics::statusFromFlags((bool)$this->GetValue('OCPP_Online'), (bool)$this->GetValue('StoerungAktiv'))); if ($severity === 'Warnung') { $this->SetValue('WarnungAktiv', true); } elseif ($severity === 'Fehler') { $this->SetValue('FehlerAktiv', true); $this->SetValue('Fehlerklasse', $source); } elseif ($severity === 'Stoerung') { $this->SetValue('StoerungAktiv', true); $this->SetValue('AktuelleStoerung', $text); } if ($code !== '') { $this->SetValue('LetzterOCPPFehlercode', $code); } if ($this->ReadPropertyInteger('DebugLevel') > 0) { $this->SendDebug($source, $severity . ': ' . $text, 0); } } private function buildChargingProfilePayload(array $setpoint): array { if ($this->isWattpilotProfile()) { return WattpilotGen1Profile::buildChargingProfile($setpoint, (int)(time() % 100000)); } return (new ChargingProfileBuilder())->build($setpoint, $this->effectiveOcppVersion()); } private function canUseSmartCharging(): bool { $capabilities = $this->readCapabilities(); return (bool)($capabilities['smartCharging'] ?? false) && !$this->GetValue('WattpilotSmartChargingBlocked'); } private function probeSmartCharging(): void { $power = PhaseManager::wattsFromCurrent((float)$this->GetValue('SafeCurrent'), (int)$this->GetValue('NumberPhases'), $this->averageVoltage()); $setpoint = $this->buildEffectiveSetpoint($power); $this->SetValue('WattpilotSmartChargingBlocked', false); $this->queueOcppCommandInternal('SetChargingProfile', $this->buildChargingProfilePayload($setpoint), 0, true); } private function respondToCall(OCPPMessage $request, array $payload): void { if ($request->messageTypeId !== 2) { return; } $this->queueOcppMessage(OCPPMessage::callResult($request->uniqueId, $payload, $request->version, $request->chargePointId)); } private function buildBootResponse(): array { return [ 'status' => 'Accepted', 'currentTime' => gmdate('Y-m-d\TH:i:s\Z'), 'interval' => max(10, $this->ReadPropertyInteger('OCPPHeartbeatIntervalSeconds')) ]; } private function buildTransactionResponse(OCPPMessage $message): array { if ($message->action === 'StartTransaction') { $state = TransactionStore::fromJson($this->ReadAttributeString(self::ATTR_TRANSACTION)); $transactionId = $state['activeTransactionId'] !== '' ? $state['activeTransactionId'] : (string)time(); $state['activeTransactionId'] = $transactionId; $state['lastTransactionState'] = 'StartTransaction'; $this->WriteAttributeString(self::ATTR_TRANSACTION, TransactionStore::toJson($state)); $this->SetValue('TransactionId', $transactionId); return [ 'transactionId' => is_numeric($transactionId) ? (int)$transactionId : $transactionId, 'idTagInfo' => ['status' => 'Accepted'] ]; } if ($message->action === 'StopTransaction') { return ['idTagInfo' => ['status' => 'Accepted']]; } return []; } private function buildDataTransferResponse(array $payload): array { $registry = new DataTransferRegistry($this->ReadPropertyBoolean('AllowDataTransfer')); $vendorId = (string)($payload['vendorId'] ?? ''); $messageId = (string)($payload['messageId'] ?? ''); return [ 'status' => $registry->isAllowed($vendorId, $messageId) ? 'Accepted' : 'Rejected' ]; } private function handleCallResult(OCPPMessage $message): void { $pendingCommands = $this->readPendingCommands(); if (!isset($pendingCommands[$message->uniqueId])) { $this->SetValue('LetzterCommandStatus', 'CallResult ohne passenden Pending Command: ' . $message->uniqueId); return; } $pending = $pendingCommands[$message->uniqueId]; unset($pendingCommands[$message->uniqueId]); $this->writePendingCommands($pendingCommands); $action = (string)($pending['action'] ?? ''); $status = (string)($message->payload['status'] ?? 'Accepted'); $this->SetValue('LetzterCommandStatus', $action . ' bestaetigt: ' . $status); if ($action === 'SetChargingProfile') { if ($status === 'Accepted') { $capabilities = $this->readCapabilities(); $capabilities['smartCharging'] = true; $this->writeCapabilities($capabilities); $this->SetValue('LetztesChargingProfileAccepted', true); $this->SetValue('WattpilotProfileRetries', 0); $this->SetValue('WattpilotSmartChargingBlocked', false); return; } $this->retryOrBlockSmartCharging($pending, 'CallResult status ' . $status); } } private function handleCallError(OCPPMessage $message): void { $pendingCommands = $this->readPendingCommands(); $pending = $pendingCommands[$message->uniqueId] ?? []; if (isset($pendingCommands[$message->uniqueId])) { unset($pendingCommands[$message->uniqueId]); $this->writePendingCommands($pendingCommands); } $action = (string)($pending['action'] ?? 'Unknown'); $code = (string)($message->payload['errorCode'] ?? $message->action); $description = (string)($message->payload['errorDescription'] ?? ''); $this->SetValue('LetzterOCPPFehlercode', $code); $this->SetValue('LetzterCommandStatus', $action . ' abgelehnt: ' . $code); $this->setDiagnostic('Warnung', 'OCPP Command', $action . ' abgelehnt: ' . $description, $code); if ($action === 'SetChargingProfile') { $this->retryOrBlockSmartCharging($pending, $code); return; } $this->writePendingCommands($pendingCommands); } private function retryOrBlockSmartCharging(array $pending, string $reason): void { $retryCount = (int)($pending['retryCount'] ?? 0); $limit = max(0, $this->ReadPropertyInteger('SmartChargingRetryLimit')); if ($retryCount < $limit) { $next = $retryCount + 1; $delay = min(300, (int)pow(2, $next) * 5); $retryKey = 'retry-' . str_replace('.', '', uniqid('', true)); $pendingCommands = $this->readPendingCommands(); $pendingCommands[$retryKey] = [ 'uniqueId' => $retryKey, 'action' => 'SetChargingProfile', 'payload' => (array)($pending['payload'] ?? []), 'retryCount' => $next, 'timestamp' => time(), 'nextRetryAt' => time() + $delay, 'status' => 'retry_wait', 'reason' => $reason ]; $this->writePendingCommands($pendingCommands); $this->SetValue('WattpilotProfileRetries', $next); $this->SetValue('LetzterCommandStatus', 'SetChargingProfile Retry ' . $next . '/' . $limit . ' in ' . $delay . 's: ' . $reason); return; } $capabilities = $this->readCapabilities(); $capabilities['smartCharging'] = false; $this->writeCapabilities($capabilities); $this->SetValue('LetztesChargingProfileAccepted', false); $this->SetValue('WattpilotSmartChargingBlocked', true); $this->setDiagnostic('Warnung', 'Wattpilot Gen1', 'SmartCharging nach ' . $limit . ' Retries deaktiviert: ' . $reason); } private function applyDeviceProfileCapabilities(): void { $profile = $this->ReadPropertyString('DeviceProfile'); $this->SetValue('DeviceProfile', $profile); if ($profile !== WattpilotGen1Profile::DEVICE_PROFILE) { $this->setCapabilityVariables($this->readCapabilities()); return; } $capabilities = CapabilityModel::merge($this->readCapabilities(), WattpilotGen1Profile::capabilities()); $this->writeCapabilities($capabilities); $this->SetValue('OCPP_Version', '1.6'); $this->SetValue('AutomatischePhasenumschaltung', false); $this->SetValue('Capability_SimpleAmpereProfiles', true); } private function isWattpilotProfile(): bool { return $this->ReadPropertyString('DeviceProfile') === WattpilotGen1Profile::DEVICE_PROFILE || $this->GetValue('DeviceProfile') === WattpilotGen1Profile::DEVICE_PROFILE; } private function readCapabilities(): array { $data = json_decode($this->ReadAttributeString(self::ATTR_CAPABILITIES), true); if (!is_array($data)) { return CapabilityModel::defaults(); } return CapabilityModel::merge(CapabilityModel::defaults(), $data); } private function writeCapabilities(array $capabilities): void { $capabilities = CapabilityModel::merge(CapabilityModel::defaults(), $capabilities); $this->WriteAttributeString(self::ATTR_CAPABILITIES, json_encode($capabilities)); $this->setCapabilityVariables($capabilities); } private function setCapabilityVariables(array $capabilities): void { $this->SetValue('Capability_SmartCharging', (bool)($capabilities['smartCharging'] ?? false)); $this->SetValue('Capability_MeterValues', (bool)($capabilities['meterValues'] ?? false)); $this->SetValue('Capability_RemoteStartStop', (bool)($capabilities['remoteStartStop'] ?? false)); $this->SetValue('Capability_Availability', (bool)($capabilities['availability'] ?? false)); $this->SetValue('Capability_PhaseValues', (bool)($capabilities['phaseMetering'] ?? false)); $this->SetValue('Capability_SimpleAmpereProfiles', (bool)($capabilities['simpleAmpereProfiles'] ?? false)); } private function readPendingCommands(): array { $pending = json_decode($this->ReadAttributeString(self::ATTR_PENDING_COMMANDS), true); if (!is_array($pending)) { return []; } if (isset($pending['uniqueId'])) { return [(string)$pending['uniqueId'] => $pending]; } return $pending; } private function writePendingCommands(array $pendingCommands): void { $this->WriteAttributeString(self::ATTR_PENDING_COMMANDS, json_encode($pendingCommands)); } private function processPendingCommands(): void { $pendingCommands = $this->readPendingCommands(); if (empty($pendingCommands)) { return; } $now = time(); $changed = false; foreach ($pendingCommands as $uniqueId => $pending) { $status = (string)($pending['status'] ?? 'pending'); if ($status === 'retry_wait') { if ($now >= (int)($pending['nextRetryAt'] ?? 0)) { unset($pendingCommands[$uniqueId]); $this->writePendingCommands($pendingCommands); $this->queueOcppCommandInternal( (string)($pending['action'] ?? 'SetChargingProfile'), (array)($pending['payload'] ?? []), (int)($pending['retryCount'] ?? 0), true ); $pendingCommands = $this->readPendingCommands(); $changed = true; } continue; } if ($status === 'pending' && $now > (int)($pending['ackDeadline'] ?? 0)) { unset($pendingCommands[$uniqueId]); $changed = true; if ((string)($pending['action'] ?? '') === 'SetChargingProfile') { $this->writePendingCommands($pendingCommands); $this->retryOrBlockSmartCharging($pending, 'ACK timeout'); $pendingCommands = $this->readPendingCommands(); } else { $this->SetValue('LetzterCommandStatus', (string)($pending['action'] ?? 'Command') . ' ACK timeout'); } } if ($now - (int)($pending['timestamp'] ?? $now) > 3600) { unset($pendingCommands[$uniqueId]); $changed = true; } } if ($changed) { $this->writePendingCommands($pendingCommands); } } private function updateMeterCapabilities(array $values): void { $capabilities = $this->readCapabilities(); $capabilities['meterValues'] = true; $capabilities['powerImport'] = $capabilities['powerImport'] || $values['powerImportW'] !== null; $capabilities['powerExport'] = $capabilities['powerExport'] || $values['powerExportW'] !== null; $capabilities['energyImport'] = $capabilities['energyImport'] || $values['energyImportWh'] !== null; $capabilities['energyExport'] = $capabilities['energyExport'] || $values['energyExportWh'] !== null; $capabilities['soc'] = $capabilities['soc'] || $values['soc'] !== null; $capabilities['temperature'] = $capabilities['temperature'] || $values['temperature'] !== null; foreach ([1, 2, 3] as $phase) { if ($values['currentA'][$phase] !== null) { $capabilities['currentImportPerPhase'] = true; $capabilities['phaseMetering'] = true; } if ($values['voltageV'][$phase] !== null) { $capabilities['voltagePerPhase'] = true; $capabilities['phaseMetering'] = true; } } $this->writeCapabilities($capabilities); } private function updatePhaseStateFromMeters(array $values): void { $activePhases = 0; foreach ([1, 2, 3] as $phase) { if ($values['currentA'][$phase] !== null && (float)$values['currentA'][$phase] > 0.5) { $activePhases++; } } if ($activePhases === 1) { $this->SetValue('Is_1_ph', true); $this->SetValue('NumberPhases', 1); return; } if ($activePhases >= 2) { $this->SetValue('Is_1_ph', false); $this->SetValue('NumberPhases', 3); } } } ?>