// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-plugins-modbus. * * nymea-plugins-modbus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-plugins-modbus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-plugins-modbus. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "pcewallbox.h" #include "extern-plugininfo.h" #include PceWallbox::PceWallbox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent) : EV11ModbusTcpConnection{hostAddress, port, slaveId, parent} { // Timer for resetting the heartbeat register (watchdog) m_timer.setInterval(30000); m_timer.setSingleShot(false); connect(&m_timer, &QTimer::timeout, this, &PceWallbox::sendHeartbeat); connect(this, &EV11ModbusTcpConnection::reachableChanged, this, [this](bool reachable) { if (!reachable) { m_timer.stop(); cleanupQueues(); if (m_currentReply) { m_currentReply = nullptr; } } else { initialize(); } }); connect(this, &EV11ModbusTcpConnection::initializationFinished, this, [this](bool success) { if (success) { qCDebug(dcPcElectric()) << "Connection initialized successfully" << m_modbusTcpMaster->hostAddress().toString(); m_timer.start(); sendHeartbeat(); update(); } else { qCWarning(dcPcElectric()) << "Connection initialization failed for" << m_modbusTcpMaster->hostAddress().toString(); } }); } bool PceWallbox::update() { if (m_aboutToDelete) return false; if (!reachable()) return false; // Make sure we only have one update call in the queue foreach (QueuedModbusReply *r, m_readQueue) { if (r->dataUnit().startAddress() == readBlockInitInfosDataUnit().startAddress()) { return true; } } QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, readBlockStatusDataUnit(), 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 blockValues = unit.values(); processBlockStatusRegisterValues(blockValues); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); }); 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_readQueue) { 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_readQueue) { if (r->dataUnit().startAddress() == digitalInputModeDataUnit().startAddress()) { digitalInputAlreadyQueued = true; break; } } if (!digitalInputAlreadyQueued) { reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, digitalInputModeDataUnit(), 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(); processDigitalInputModeRegisterValues(values); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); }); enqueueRequest(reply); } // Led brightness bool ledBrightnessAlreadyQueued = false; foreach (QueuedModbusReply *r, m_readQueue) { if (r->dataUnit().startAddress() == ledBrightnessDataUnit().startAddress()) { ledBrightnessAlreadyQueued = true; break; } } if (!ledBrightnessAlreadyQueued) { reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, ledBrightnessDataUnit(), 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(); processLedBrightnessRegisterValues(values); if (firmwareRevision() < "0025") emit updateFinished(); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); }); enqueueRequest(reply); } if (firmwareRevision() < "0025") return true; // --------------------------------------------------------------------------------------- // Registers since 0025 (V 0.25) // Make sure we only have one update 2 call in the queue bool update2Queued = false; foreach (QueuedModbusReply *r, m_readQueue) { if (r->dataUnit().startAddress() == readBlockUpdate2DataUnit().startAddress()) { update2Queued = true; break; } } if (!update2Queued) { reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, readBlockUpdate2DataUnit(), 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) { qCWarning(dcPcElectric()) << "Failed to fetch update 2 block" << reply->error() << reply->errorString(); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; } const QModbusDataUnit unit = reply->reply()->result(); const QVector blockValues = unit.values(); processBlockUpdate2RegisterValues(blockValues); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); }); enqueueRequest(reply); } bool phaseAutoSwitchPauseQueued = false; foreach (QueuedModbusReply *r, m_readQueue) { if (r->dataUnit().startAddress() == phaseAutoSwitchPauseDataUnit().startAddress()) { phaseAutoSwitchPauseQueued = true; break; } } if (!phaseAutoSwitchPauseQueued) { reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, phaseAutoSwitchPauseDataUnit(), 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(); processPhaseAutoSwitchPauseRegisterValues(values); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); }); enqueueRequest(reply); } // Phase auto switch pause (since firmware version 0.25 ...) bool phaseAutoSwitchMinChargingTimeQueued = false; foreach (QueuedModbusReply *r, m_readQueue) { if (r->dataUnit().startAddress() == phaseAutoSwitchMinChargingTimeDataUnit().startAddress()) { phaseAutoSwitchMinChargingTimeQueued = true; break; } } if (!phaseAutoSwitchMinChargingTimeQueued) { reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, phaseAutoSwitchMinChargingTimeDataUnit(), 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(); processPhaseAutoSwitchMinChargingTimeRegisterValues(values); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); }); enqueueRequest(reply); } // Phase auto switch pause (since firmware version 0.25 ...) bool forceChargingResumeQueued = false; foreach (QueuedModbusReply *r, m_readQueue) { if (r->dataUnit().startAddress() == forceChargingResumeDataUnit().startAddress()) { forceChargingResumeQueued = true; break; } } if (!forceChargingResumeQueued) { reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, forceChargingResumeDataUnit(), 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(); processForceChargingResumeRegisterValues(values); emit updateFinished(); QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); }); enqueueRequest(reply); } return true; } QueuedModbusReply *PceWallbox::setChargingCurrentAsync(quint16 chargingCurrent) { if (m_aboutToDelete) return nullptr; QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setChargingCurrentDataUnit(chargingCurrent), this); connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); connect(reply, &QueuedModbusReply::finished, this, [this, reply]() { if (m_currentReply == reply) m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; }); enqueueRequest(reply); return reply; } QueuedModbusReply *PceWallbox::setLedBrightnessAsync(quint16 percentage) { if (m_aboutToDelete) return nullptr; QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setLedBrightnessDataUnit(percentage), this); connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); connect(reply, &QueuedModbusReply::finished, this, [this, reply]() { if (m_currentReply == reply) m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; }); enqueueRequest(reply); return reply; } QueuedModbusReply *PceWallbox::setPhaseAutoSwitchPauseAsync(quint16 seconds) { if (m_aboutToDelete) return nullptr; QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setPhaseAutoSwitchPauseDataUnit(seconds), this); connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); connect(reply, &QueuedModbusReply::finished, this, [this, reply]() { if (m_currentReply == reply) m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; }); enqueueRequest(reply); return reply; } QueuedModbusReply *PceWallbox::setPhaseAutoSwitchMinChargingTimeAsync(quint16 seconds) { if (m_aboutToDelete) return nullptr; QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setPhaseAutoSwitchMinChargingTimeDataUnit(seconds), this); connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); connect(reply, &QueuedModbusReply::finished, this, [this, reply]() { if (m_currentReply == reply) m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; }); enqueueRequest(reply); return reply; } QueuedModbusReply *PceWallbox::setForceChargingResumeAsync(quint16 value) { if (m_aboutToDelete) return nullptr; QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setForceChargingResumeDataUnit(value), this); connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); connect(reply, &QueuedModbusReply::finished, this, [this, reply]() { if (m_currentReply == reply) m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; }); enqueueRequest(reply); return reply; } QueuedModbusReply *PceWallbox::setDigitalInputModeAsync(DigitalInputMode digitalInputMode) { if (m_aboutToDelete) return nullptr; QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setDigitalInputModeDataUnit(digitalInputMode), this); connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); connect(reply, &QueuedModbusReply::finished, this, [this, reply]() { if (m_currentReply == reply) m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; }); enqueueRequest(reply); return reply; } void PceWallbox::gracefullDeleteLater() { // Clean up the queue m_aboutToDelete = true; cleanupQueues(); m_timer.stop(); if (!m_currentReply) { qCDebug(dcPcElectric()) << "Deleting object without pending request..."; // No pending request, we can close the connection and delete the object disconnect(this, nullptr, nullptr, nullptr); disconnectDevice(); deleteLater(); } else { qCDebug(dcPcElectric()) << "Pending request, deleting object once the request is finished..."; } } 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) return; QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setHeartbeatDataUnit(m_heartbeat++), 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) { qCWarning(dcPcElectric()) << "Failed to send heartbeat to" << m_modbusTcpMaster->hostAddress().toString() << reply->errorString(); } else { qCDebug(dcPcElectric()) << "Successfully sent heartbeat to" << m_modbusTcpMaster->hostAddress().toString(); } QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; }); enqueueRequest(reply); } void PceWallbox::sendNextRequest() { if (m_writeQueue.isEmpty() && m_readQueue.isEmpty()) return; if (m_currentReply) return; if (m_aboutToDelete) { disconnect(this, nullptr, nullptr, nullptr); disconnectDevice(); deleteLater(); return; } // Note: due to the fact that we have one register which controls 3 states, // the order of the execution is critical at this point. We have to make sure // the register gets written in the same order as they where requested by the action // execution (and the dedicated ChargingCurrentState buffer) if (!m_writeQueue.isEmpty()) { // Prioritize write requests m_currentReply = m_writeQueue.dequeue(); qCDebug(dcPcElectric()) << "Dequeued write request. Queue count: W" << m_writeQueue.length() << "| R:" << m_readQueue.length(); } else { m_currentReply = m_readQueue.dequeue(); qCDebug(dcPcElectric()) << "Dequeued read request. Queue count: W" << m_writeQueue.length() << "| R:" << m_readQueue.length(); } switch (m_currentReply->requestType()) { case QueuedModbusReply::RequestTypeRead: qCDebug(dcPcElectric()) << "--> Reading" << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType()) << "register:" << m_currentReply->dataUnit().startAddress() << "length" << m_currentReply->dataUnit().valueCount(); m_currentReply->setReply(m_modbusTcpMaster->sendReadRequest(m_currentReply->dataUnit(), m_slaveId)); break; case QueuedModbusReply::RequestTypeWrite: qCDebug(dcPcElectric()) << "--> Writing" << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType()) << "register:" << m_currentReply->dataUnit().startAddress() << "length:" << m_currentReply->dataUnit().valueCount() << "values:" << m_currentReply->dataUnit().values(); m_currentReply->setReply(m_modbusTcpMaster->sendWriteRequest(m_currentReply->dataUnit(), m_slaveId)); break; } if (!m_currentReply->reply()) { qCWarning(dcPcElectric()) << "Error occurred while sending" << m_currentReply->requestType() << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType()) << "register:" << m_currentReply->dataUnit().startAddress() << "length:" << m_currentReply->dataUnit().valueCount() << "to" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); m_currentReply->deleteLater(); m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; } if (m_currentReply->reply()->isFinished()) { qCWarning(dcPcElectric()) << "Reply immediatly finished"; m_currentReply->deleteLater(); m_currentReply = nullptr; QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); return; } } void PceWallbox::enqueueRequest(QueuedModbusReply *reply) { switch (reply->requestType()) { case QueuedModbusReply::RequestTypeRead: m_readQueue.enqueue(reply); break; case QueuedModbusReply::RequestTypeWrite: m_writeQueue.enqueue(reply); break; } QTimer::singleShot(0, this, &PceWallbox::sendNextRequest); } void PceWallbox::cleanupQueues() { qDeleteAll(m_readQueue); m_readQueue.clear(); qDeleteAll(m_writeQueue); m_writeQueue.clear(); } QDebug operator<<(QDebug debug, const PceWallbox::ChargingCurrentState &chargingCurrentState) { QDebugStateSaver saver(debug); debug.nospace() << "ChargingCurrentState(" << chargingCurrentState.power << ", " << chargingCurrentState.maxChargingCurrent << " [A], " << chargingCurrentState.desiredPhaseCount << ')'; return debug; }