Keba: fix Shutter/PV-Edition parser + add phase switching (x2/x2src UDP)

- Fix: product code parser fails for Shutter connector type (KC-P30-ESS400U2-E00-PV)
  Current rating was compared against connectorTypeValue ('S') instead of
  connectorCurrentValue ('4') -> m_isValid = false for all PV-Edition models
- Add: phase switching support via UDP x2src/x2 commands (doc V2.04 §3.2.14-15)
  New states: phaseSwitchSource, phaseSwitchActive, desiredPhaseCount
  New action: setPhaseCount (1 or 3 phases, 5 min cooldown per IEC 61851)
  Added KeContact::setPhaseSwitch() queuing both commands with built-in 200ms pause
master
Patrick Schurig 2026-03-31 14:59:04 +02:00
parent 22656bdeec
commit e7be9b51af
5 changed files with 106 additions and 1 deletions

View File

@ -289,6 +289,9 @@ void IntegrationPluginKeba::executeAction(ThingActionInfo *info)
requestId = keba->displayMessage(action.param(kebaDisplayActionMessageParamTypeId).value().toByteArray());
} else if (action.actionTypeId() == kebaOutputX2ActionTypeId) {
requestId = keba->setOutputX2(action.param(kebaOutputX2ActionOutputX2ParamTypeId).value().toBool());
} else if (action.actionTypeId() == kebaSetPhaseCountActionTypeId) {
int phaseCount = action.paramValue(kebaSetPhaseCountActionPhaseCountParamTypeId).toInt();
requestId = keba->setPhaseSwitch(phaseCount);
} else if (action.actionTypeId() == kebaFailsafeModeActionTypeId) {
int timeout = 0;
if (action.param(kebaFailsafeModeActionFailsafeModeParamTypeId).value().toBool()) {
@ -426,6 +429,7 @@ void IntegrationPluginKeba::setupKeba(ThingSetupInfo *info, const QHostAddress &
case KebaProductInfo::SeriesX4G:
case KebaProductInfo::SeriesSpecial:
qCDebug(dcKeba()) << "The keba" << productInformation.series() << "is capable of communicating using UDP";
qCDebug(dcKeba()) << "Series Special detected:" << productInformation.productString().right(2) << "(PV-Edition ou autre variante spéciale)";
supported = true;
break;
default:
@ -623,6 +627,12 @@ void IntegrationPluginKeba::onReportTwoReceived(const KeContact::ReportTwo &repo
thing->setStateValue("outputX2", reportTwo.output);
thing->setStateValue("input", reportTwo.input);
// Phase switch state (PV-Edition / SeriesSpecial)
thing->setStateValue("phaseSwitchSource", reportTwo.x2PhaseSwitchSource);
thing->setStateValue("phaseSwitchActive", reportTwo.x2PhaseSwitchSource == 4);
// x2PhaseSwitch: 0 = 1 phase, 1 = 3 phases
thing->setStateValue("desiredPhaseCount", reportTwo.x2PhaseSwitch == 1 ? 3 : 1);
thing->setStateValue("uptime", reportTwo.seconds / 60);
} else {
qCWarning(dcKeba()) << "Received report but the serial number didn't match";

View File

@ -299,6 +299,35 @@
"writable": true,
"defaultValue": false
},
{
"id": "f8e39ab6-53a7-45a4-96df-8644cbd57036",
"name": "phaseSwitchSource",
"displayName": "Phase switch source",
"displayNameEvent": "Phase switch source changed",
"type": "int",
"defaultValue": 0,
"cached": true
},
{
"id": "bea5e94f-95e0-44ad-a37b-5111415fc093",
"name": "phaseSwitchActive",
"displayName": "Phase switch active",
"displayNameEvent": "Phase switch active changed",
"type": "bool",
"defaultValue": false,
"cached": true
},
{
"id": "c8830dc8-4f12-4aeb-be07-e5024bbe34b4",
"name": "desiredPhaseCount",
"displayName": "Desired phase count",
"displayNameEvent": "Desired phase count changed",
"type": "uint",
"defaultValue": 3,
"minValue": 1,
"maxValue": 3,
"cached": true
},
{
"id": "ba600276-8b36-4404-b8ec-415245e5bc15",
"name": "input",
@ -376,6 +405,23 @@
"defaultValue": ""
}
]
},
{
"id": "f6bedafe-ade4-47ec-894a-b9114da9ad38",
"name": "setPhaseCount",
"displayName": "Set phase count",
"paramTypes": [
{
"id": "39ff68f6-1cd9-4395-8d87-ccb2c69436b3",
"name": "phaseCount",
"displayName": "Phase count",
"type": "int",
"defaultValue": 3,
"minValue": 1,
"maxValue": 3,
"allowedValues": [1, 3]
}
]
}
],
"eventTypes": [

View File

@ -35,6 +35,7 @@ KebaProductInfo::KebaProductInfo(const QString &productString) :
// KC-P30-EC220112-000-DE
// KC-P30-EC2404B2-M0A-GE
// KC-P30-EC2204U2-E00-PV
// KC-P30-ESS400U2-E00-PV (PV-Edition, Shutter T2, 32A, triphasé, marché FR)
qCDebug(dcKeba()) << "Parsing product information from" << productString.count() << productString;
if (m_productString.count() < 19) {
@ -95,7 +96,27 @@ KebaProductInfo::KebaProductInfo(const QString &productString) :
qCDebug(dcKeba()) << "Connector type:" << m_connectorType;
QChar connectorCurrentValue = descriptor.at(3);
if (connectorCurrentValue.isDigit() && connectorTypeValue == QChar('1')) {
if (m_connectorType == Shutter) {
// Shutter: le courant est encodé directement en position 3
// (indépendant de connectorTypeValue qui vaut 'S')
if (connectorCurrentValue == QChar('4')) {
m_current = Current32A;
qCDebug(dcKeba()) << "Current (Shutter): 32A";
} else if (connectorCurrentValue == QChar('3')) {
m_current = Current20A;
qCDebug(dcKeba()) << "Current (Shutter): 20A";
} else if (connectorCurrentValue == QChar('2')) {
m_current = Current16A;
qCDebug(dcKeba()) << "Current (Shutter): 16A";
} else if (connectorCurrentValue == QChar('1')) {
m_current = Current13A;
qCDebug(dcKeba()) << "Current (Shutter): 13A";
} else {
qCWarning(dcKeba()) << "Current (Shutter): valeur inconnue" << connectorCurrentValue;
m_isValid = false;
return;
}
} else if (connectorCurrentValue.isDigit() && connectorTypeValue == QChar('1')) {
m_current = Current13A;
} else if (connectorCurrentValue.isDigit() && connectorTypeValue == QChar('2')) {
m_current = Current16A;

View File

@ -327,6 +327,29 @@ void KeContact::getReport1XX(int reportNumber)
getReport(reportNumber);
}
QUuid KeContact::setPhaseSwitch(int phaseCount)
{
if (!m_dataLayer) {
qCWarning(dcKeba()) << "UDP socket not initialized";
setReachable(false);
return QUuid();
}
// 1. Activate UDP phase control source
KeContactRequest activateRequest(QUuid::createUuid(), "x2src 4");
m_requestQueue.enqueue(activateRequest);
// 2. Set phase count: x2 0 = 1 phase, x2 1 = 3 phases
// Note: IEC 61851 requires a 5 min cooldown between phase switches
QByteArray cmd = QString("x2 %1").arg(phaseCount == 3 ? 1 : 0).toLatin1();
KeContactRequest phaseRequest(QUuid::createUuid(), cmd);
qCDebug(dcKeba()) << "Phase switch:" << phaseCount << "phases. Datagram:" << cmd;
m_requestQueue.enqueue(phaseRequest);
sendNextCommand();
return phaseRequest.requestId();
}
QUuid KeContact::setOutputX2(bool state)
{
if (!m_dataLayer) {
@ -500,6 +523,8 @@ void KeContact::onReceivedDatagram(const QHostAddress &address, const QByteArray
reportTwo.setEnergy = data.value("Setenergy").toInt() / 10000.00;
reportTwo.output = data.value("Output").toInt();
reportTwo.input= data.value("Input").toInt();
reportTwo.x2PhaseSwitchSource = data.value("X2 phaseSwitch source").toInt();
reportTwo.x2PhaseSwitch = data.value("X2 phaseSwitch").toInt();
reportTwo.serialNumber = data.value("Serial").toString();
reportTwo.seconds = data.value("Sec").toInt();
// Not documented:

View File

@ -138,6 +138,8 @@ public:
double setEnergy; //Shows the set energy limit
bool output; //State of the output X2.
bool input; //State of the potential free Enable input X1. When using the input, please pay attention to the information in the installation manual.
int x2PhaseSwitchSource = 0; //Source of phase switching control (4 = UDP)
int x2PhaseSwitch = 0; //Current phase state (0 = 1 phase, 1 = 3 phases)
QString serialNumber; //Serial number
int seconds; //Current system clock since restart of the charging station.
};
@ -198,6 +200,7 @@ public:
// Command “currtime”
QUuid setOutputX2(bool state); // Command “output”
QUuid setPhaseSwitch(int phaseCount); // Commands “x2src 4” + “x2 0/1” (IEC 61851: 5 min cooldown between switches)
private:
KeContactDataLayer *m_dataLayer = nullptr;