diff --git a/pcelectric/EV11.3-registers.json b/pcelectric/EV11.3-registers.json index 936b0d3..c6f44b1 100644 --- a/pcelectric/EV11.3-registers.json +++ b/pcelectric/EV11.3-registers.json @@ -306,7 +306,7 @@ "registerType": "holdingRegister", "description": "Write charging current", "unit": "mA", - "access": "WO" + "access": "RW" }, { "id": "chargingCurrentOffline", diff --git a/pcelectric/integrationpluginpcelectric.cpp b/pcelectric/integrationpluginpcelectric.cpp index 6b4d3bd..99130a2 100644 --- a/pcelectric/integrationpluginpcelectric.cpp +++ b/pcelectric/integrationpluginpcelectric.cpp @@ -139,6 +139,7 @@ void IntegrationPluginPcElectric::setupThing(ThingSetupInfo *info) void IntegrationPluginPcElectric::postSetupThing(Thing *thing) { qCDebug(dcPcElectric()) << "Post setup thing" << thing->name(); + if (!m_refreshTimer) { m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(1); connect(m_refreshTimer, &PluginTimer::timeout, this, [this] { @@ -152,6 +153,14 @@ void IntegrationPluginPcElectric::postSetupThing(Thing *thing) qCDebug(dcPcElectric()) << "Starting refresh timer..."; m_refreshTimer->start(); } + + + PceWallbox::ChargingCurrentState chargingCurrentState; + chargingCurrentState.power = thing->stateValue(ev11PowerStateTypeId).toBool(); + chargingCurrentState.maxChargingCurrent = thing->stateValue(ev11MaxChargingCurrentStateTypeId).toDouble(); + chargingCurrentState.desiredPhaseCount = thing->stateValue(ev11DesiredPhaseCountStateTypeId).toDouble(); + qCDebug(dcPcElectric()) << "Initialize charging current state with cached values" << chargingCurrentState; + m_chargingCurrentStateBuffer[thing] = chargingCurrentState; } void IntegrationPluginPcElectric::thingRemoved(Thing *thing) @@ -167,6 +176,9 @@ void IntegrationPluginPcElectric::thingRemoved(Thing *thing) if (m_initialUpdate.contains(thing)) m_initialUpdate.remove(thing); + if (m_chargingCurrentStateBuffer.contains(thing)) + m_chargingCurrentStateBuffer.remove(thing); + // Unregister related hardware resources if (m_monitors.contains(thing)) hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); @@ -190,91 +202,81 @@ void IntegrationPluginPcElectric::executeAction(ThingActionInfo *info) } if (info->action().actionTypeId() == ev11PowerActionTypeId) { - bool power = info->action().paramValue(ev11PowerActionPowerParamTypeId).toBool(); - quint16 chargingCurrent = 0; - if (power) { - chargingCurrent = thing->stateValue(ev11MaxChargingCurrentStateTypeId).toUInt() * 1000; - if (thing->stateValue(ev11DesiredPhaseCountStateTypeId).toUInt() == 3) { - // If 3 phase charging is enabled, we set the first bit - chargingCurrent |= static_cast(1) << 15; - } - } - qCDebug(dcPcElectric()) << "Writing charging current register" << chargingCurrent << "mA"; - QueuedModbusReply *reply = connection->setChargingCurrent(chargingCurrent); - connect(reply, &QueuedModbusReply::finished, info, [reply, info, thing, power, chargingCurrent](){ + bool power = info->action().paramValue(ev11PowerActionPowerParamTypeId).toBool(); + qCDebug(dcPcElectric()) << "Set charging enabled to" << power; + + // Update buffer + m_chargingCurrentStateBuffer[thing].power = power; + + quint16 registerValue = PceWallbox::deriveRegisterFromStates(m_chargingCurrentStateBuffer.value(thing)); + qCDebug(dcPcElectric()) << "Writing charging current register" << registerValue; + QueuedModbusReply *reply = connection->setChargingCurrent(registerValue); + connect(reply, &QueuedModbusReply::finished, info, [reply, info, thing, power, registerValue](){ if (reply->error() != QModbusDevice::NoError) { - qCWarning(dcPcElectric()) << "Could not set power state to" << power << "(" << chargingCurrent << "mA)" << reply->errorString(); + qCWarning(dcPcElectric()) << "Could not set power state to" << power << "(" << registerValue << ")" << reply->errorString(); info->finish(Thing::ThingErrorHardwareFailure); return; } - qCDebug(dcPcElectric()) << "Successfully set power state to" << power << "(" << chargingCurrent << "mA)"; + qCDebug(dcPcElectric()) << "Successfully set power state to" << power << "(" << registerValue << ")"; thing->setStateValue(ev11PowerStateTypeId, power); info->finish(Thing::ThingErrorNoError); }); + return; + } else if (info->action().actionTypeId() == ev11MaxChargingCurrentActionTypeId) { + uint desiredChargingCurrent = info->action().paramValue(ev11MaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); qCDebug(dcPcElectric()) << "Set max charging current to" << desiredChargingCurrent << "A"; - if (thing->stateValue(ev11PowerStateTypeId).toBool()) { - // The charging is enabled, let's write the value to the wallbox - quint16 finalChargingCurrent = static_cast(desiredChargingCurrent * 1000); - if (thing->stateValue(ev11DesiredPhaseCountStateTypeId).toUInt() == 3) { - // If 3 phase charging is enabled, we set the first bit - finalChargingCurrent |= static_cast(1) << 15; + + // Update buffer + m_chargingCurrentStateBuffer[thing].maxChargingCurrent = desiredChargingCurrent; + + quint16 registerValue = PceWallbox::deriveRegisterFromStates(m_chargingCurrentStateBuffer.value(thing)); + qCDebug(dcPcElectric()) << "Writing charging current register" << registerValue; + QueuedModbusReply *reply = connection->setChargingCurrent(registerValue); + connect(reply, &QueuedModbusReply::finished, info, [reply, info, thing, desiredChargingCurrent](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set charging current to" << desiredChargingCurrent << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + return; } - qCDebug(dcPcElectric()) << "Writing charging current register" << finalChargingCurrent << "mA"; - QueuedModbusReply *reply = connection->setChargingCurrent(finalChargingCurrent); - connect(reply, &QueuedModbusReply::finished, info, [reply, info, thing, desiredChargingCurrent](){ - if (reply->error() != QModbusDevice::NoError) { - qCWarning(dcPcElectric()) << "Could not set charging current to" << desiredChargingCurrent << "mA" << reply->errorString(); - info->finish(Thing::ThingErrorHardwareFailure); - return; - } - - qCDebug(dcPcElectric()) << "Successfully set charging current to" << desiredChargingCurrent << "mA"; - thing->setStateValue(ev11MaxChargingCurrentStateTypeId, desiredChargingCurrent); - info->finish(Thing::ThingErrorNoError); - }); - } else { - // Save the value in the state, but do not send the value to the wallbox since the power state is reflected using the charging current... - qCDebug(dcPcElectric()) << "Setting charging current to" << desiredChargingCurrent << "without synching to wallbox since the power state is false"; + qCDebug(dcPcElectric()) << "Successfully set charging current (" << desiredChargingCurrent << ")"; thing->setStateValue(ev11MaxChargingCurrentStateTypeId, desiredChargingCurrent); info->finish(Thing::ThingErrorNoError); - } - return; - } else if (info->action().actionTypeId() == ev11DesiredPhaseCountActionTypeId) { - uint desiredPhaseCount = info->action().paramValue(ev11DesiredPhaseCountActionDesiredPhaseCountParamTypeId).toUInt(); - qCDebug(dcPcElectric()) << "Desried phase count changed" << desiredPhaseCount; - thing->setStateValue(ev11DesiredPhaseCountStateTypeId, desiredPhaseCount); - info->finish(Thing::ThingErrorNoError); + }); - // Update the max charging current according to the new desired phase count - if (thing->stateValue(ev11PowerStateTypeId).toBool()) { - uint chargingCurrent = thing->stateValue(ev11MaxChargingCurrentStateTypeId).toUInt(); - quint16 finalChargingCurrent = static_cast(chargingCurrent * 1000); - if (thing->stateValue(ev11DesiredPhaseCountStateTypeId).toUInt() == 3) { - // If 3 phase charging is enabled, we set the first bit - finalChargingCurrent |= static_cast(1) << 15; + return; + + } else if (info->action().actionTypeId() == ev11DesiredPhaseCountActionTypeId) { + + uint desiredPhaseCount = info->action().paramValue(ev11DesiredPhaseCountActionDesiredPhaseCountParamTypeId).toUInt(); + qCDebug(dcPcElectric()) << "Set desried phase count to" << desiredPhaseCount; + + // Update buffer + m_chargingCurrentStateBuffer[thing].desiredPhaseCount = desiredPhaseCount; + + quint16 registerValue = PceWallbox::deriveRegisterFromStates(m_chargingCurrentStateBuffer.value(thing)); + qCDebug(dcPcElectric()) << "Writing charging current register" << registerValue; + QueuedModbusReply *reply = connection->setChargingCurrent(registerValue); + connect(reply, &QueuedModbusReply::finished, info, [reply, info, thing, desiredPhaseCount](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set desired phase count to" << desiredPhaseCount << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + return; } - qCDebug(dcPcElectric()) << "Writing charging current register" << finalChargingCurrent << "mA"; - QueuedModbusReply *reply = connection->setChargingCurrent(finalChargingCurrent); - connect(reply, &QueuedModbusReply::finished, info, [reply, finalChargingCurrent](){ - if (reply->error() != QModbusDevice::NoError) { - qCWarning(dcPcElectric()) << "Could not set charging current to" << finalChargingCurrent << "mA" << reply->errorString(); - return; - } + qCDebug(dcPcElectric()) << "Successfully set phase count (" << desiredPhaseCount << ")"; + thing->setStateValue(ev11DesiredPhaseCountStateTypeId, desiredPhaseCount); + info->finish(Thing::ThingErrorNoError); + }); - qCDebug(dcPcElectric()) << "Successfully set charging current to" << finalChargingCurrent << "mA"; - }); - } return; } - Q_ASSERT_X(false, "IntegrationPluginPcElectric::executeAction", QString("Unhandled action: %1").arg(info->action().actionTypeId().toString()).toLocal8Bit()); } @@ -336,7 +338,7 @@ void IntegrationPluginPcElectric::setupConnection(ThingSetupInfo *info) thing->setStateMaxValue(ev11MaxChargingCurrentStateTypeId, connection->maxChargingCurrentDip() / 1000); thing->setStateValue(ev11PluggedInStateTypeId, connection->chargingState() >= PceWallbox::ChargingStateB1 && - connection->chargingState() < PceWallbox::ChargingStateError); + connection->chargingState() < PceWallbox::ChargingStateError); thing->setStateValue(ev11ChargingStateTypeId, connection->chargingState() == PceWallbox::ChargingStateC2); if (connection->chargingRelayState() != EV11ModbusTcpConnection::ChargingRelayStateNoCharging) { @@ -383,6 +385,19 @@ void IntegrationPluginPcElectric::setupConnection(ThingSetupInfo *info) if (m_initialUpdate.value(thing)) { m_initialUpdate[thing] = false; + + qCDebug(dcPcElectric()) << "Update initial charger states from charging current register..."; + + PceWallbox::ChargingCurrentState chargingCurrentState = PceWallbox::deriveStatesFromRegister(connection->chargingCurrent()); + qCDebug(dcPcElectric()) << chargingCurrentState; + thing->setStateValue(ev11PowerStateTypeId, chargingCurrentState.power); + thing->setStateValue(ev11DesiredPhaseCountStateTypeId, chargingCurrentState.desiredPhaseCount); + if (chargingCurrentState.power) { + thing->setStateValue(ev11MaxChargingCurrentStateTypeId, chargingCurrentState.maxChargingCurrent); + } + + m_chargingCurrentStateBuffer[thing] = chargingCurrentState; + qCDebug(dcPcElectric()) << "Updating initial settings after connecting..."; thing->setSettingValue(ev11SettingsLedBrightnessParamTypeId, connection->ledBrightness()); @@ -451,3 +466,4 @@ void IntegrationPluginPcElectric::setupConnection(ThingSetupInfo *info) if (monitor->reachable()) connection->connectDevice(); } + diff --git a/pcelectric/integrationpluginpcelectric.h b/pcelectric/integrationpluginpcelectric.h index 036989b..6a726f3 100644 --- a/pcelectric/integrationpluginpcelectric.h +++ b/pcelectric/integrationpluginpcelectric.h @@ -32,6 +32,7 @@ #define INTEGRATIONPLUGINPCELECTRIC_H #include +#include #include #include @@ -59,12 +60,22 @@ public: private: PluginTimer *m_refreshTimer = nullptr; + QHash m_connections; QHash m_monitors; QHash m_initialUpdate; + // We need to buffer the desired power / current / phase count states because all 3 states + // will be represented by one register (200 - chaegingCurrent). If all 3 actions get executed, they might + // overwrite each other, since the action gets started right the way, but the request gets queued. + // If the actions would be queued, there would be still the issue with the order of the actions + // (set power to false and then set charging current would always enable charging in the end). + QHash m_chargingCurrentStateBuffer; + void setupConnection(ThingSetupInfo *info); }; + + #endif // INTEGRATIONPLUGINPCELECTRIC_H diff --git a/pcelectric/pcewallbox.cpp b/pcelectric/pcewallbox.cpp index 0ea2f01..7705364 100644 --- a/pcelectric/pcewallbox.cpp +++ b/pcelectric/pcewallbox.cpp @@ -107,6 +107,41 @@ bool PceWallbox::update() enqueueRequest(reply); + // charging current register. Contains + // - power state + // - chargingcurrent (if power is true) + // - phases (if power is true) + bool chargingCurrentQueued = false; + foreach (QueuedModbusReply *r, m_queue) { + if (r->dataUnit().startAddress() == chargingCurrentDataUnit().startAddress()) { + chargingCurrentQueued = true; + break; + } + } + + if (!chargingCurrentQueued) { + reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, chargingCurrentDataUnit(), this); + connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); + connect(reply, &QueuedModbusReply::finished, this, [this, reply](){ + + if (m_currentReply == reply) + m_currentReply = nullptr; + + if (reply->error() != QModbusDevice::NoError) { + QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); + return; + } + + const QModbusDataUnit unit = reply->reply()->result(); + const QVector values = unit.values(); + processChargingCurrentRegisterValues(values); + + QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); + }); + + enqueueRequest(reply); + } + // Digital input bool digitalInputAlreadyQueued = false; foreach (QueuedModbusReply *r, m_queue) { @@ -255,6 +290,38 @@ void PceWallbox::gracefullDeleteLater() } } +quint16 PceWallbox::deriveRegisterFromStates(PceWallbox::ChargingCurrentState state) +{ + quint16 registerValue = 0; + if (!state.power) + return registerValue; // 0 + + registerValue = state.maxChargingCurrent * 1000; // convert to mA + if (state.desiredPhaseCount > 1) { + registerValue |= static_cast(1) << 15; + } + + return registerValue; +} + +PceWallbox::ChargingCurrentState PceWallbox::deriveStatesFromRegister(quint16 registerValue) +{ + PceWallbox::ChargingCurrentState chargingCurrentState; + chargingCurrentState.power = (registerValue != 0); + + // Only set max charging current if power, otherwise we use default 6A + if (chargingCurrentState.power) { + + bool threePhaseCharging = (registerValue & (1 << 15)); + chargingCurrentState.desiredPhaseCount = (threePhaseCharging ? 3 : 1); + + chargingCurrentState.maxChargingCurrent = (registerValue & 0x7FFF) / 1000.0; + } + + return chargingCurrentState; +} + + void PceWallbox::sendHeartbeat() { if (m_aboutToDelete) @@ -349,3 +416,10 @@ void PceWallbox::cleanupQueue() qDeleteAll(m_queue); m_queue.clear(); } + +QDebug operator<<(QDebug debug, const PceWallbox::ChargingCurrentState &chargingCurrentState) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "ChargingCurrentState(" << chargingCurrentState.power << ", " << chargingCurrentState.maxChargingCurrent << " [A], " << chargingCurrentState.desiredPhaseCount << ')'; + return debug; +} diff --git a/pcelectric/pcewallbox.h b/pcelectric/pcewallbox.h index 6650cfd..dbd9d39 100644 --- a/pcelectric/pcewallbox.h +++ b/pcelectric/pcewallbox.h @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -43,6 +44,12 @@ class PceWallbox : public EV11ModbusTcpConnection { Q_OBJECT public: + typedef struct ChargingCurrentState { + bool power = false; + double maxChargingCurrent = 6; + uint desiredPhaseCount = 3; + } ChargingCurrentState; + explicit PceWallbox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr); bool update() override; @@ -54,7 +61,6 @@ public: QueuedModbusReply *setDigitalInputMode(DigitalInputMode digitalInputMode); - // Note: the modbus implementation of the wallbox gets stuck if a Modbus request has been sent // and we disconnect the socket before the response has arrived. Only a reboot of the wallbox // fixes the broken communication afterwards. This method waits for the current request before closing the @@ -62,6 +68,9 @@ public: // IMPORTNAT: do not use the object after this call, this is a temporary workaround void gracefullDeleteLater(); + static quint16 deriveRegisterFromStates(PceWallbox::ChargingCurrentState state); + static PceWallbox::ChargingCurrentState deriveStatesFromRegister(quint16 registerValue); + private slots: void sendHeartbeat(); void sendNextRequest(); @@ -78,4 +87,7 @@ private: void cleanupQueue(); }; +QDebug operator<<(QDebug debug, const PceWallbox::ChargingCurrentState &chargingCurrentState); + + #endif // PCEWALLBOX_H