// 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 "integrationpluginvestel.h" #include "plugininfo.h" #include "evc04discovery.h" #include #include IntegrationPluginVestel::IntegrationPluginVestel() { } void IntegrationPluginVestel::discoverThings(ThingDiscoveryInfo *info) { if (!hardwareManager()->networkDeviceDiscovery()->available()) { qCWarning(dcVestel()) << "The network discovery is not available on this platform."; info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("Unable to discover devices in the network. The system may not be installed correctly.")); return; } if (info->thingClassId() == evc04ThingClassId) { EVC04Discovery *discovery = new EVC04Discovery(hardwareManager()->networkDeviceDiscovery(), dcVestel(), info); connect(discovery, &EVC04Discovery::discoveryFinished, info, [=](){ foreach (const EVC04Discovery::Result &result, discovery->discoveryResults()) { QString name = result.chargepointId; QString description = result.brand + " " + result.model; ThingDescriptor descriptor(evc04ThingClassId, name, description); qCDebug(dcVestel()) << "Discovered:" << descriptor.title() << descriptor.description(); ParamList params; params << Param(evc04ThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress()); params << Param(evc04ThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName()); params << Param(evc04ThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress()); descriptor.setParams(params); // Check if we already have set up this device Thing *existingThing = myThings().findByParams(params); if (existingThing) { qCDebug(dcVestel()) << "This thing already exists in the system:" << existingThing << existingThing->params(); descriptor.setThingId(existingThing->id()); } info->addThingDescriptor(descriptor); } info->finish(Thing::ThingErrorNoError); }); discovery->startDiscovery(); } } void IntegrationPluginVestel::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); qCDebug(dcVestel()) << "Setting up" << thing << thing->params(); if (thing->thingClassId() == evc04ThingClassId) { if (m_evc04Connections.contains(thing)) { qCDebug(dcVestel()) << "Reconfiguring existing thing" << thing->name(); m_evc04Connections.take(thing)->deleteLater(); if (m_monitors.contains(thing)) { hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); } } NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing); if (!monitor) { qCWarning(dcVestel()) << "Unable to register monitor with the given params" << thing->params(); info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Unable to set up the connection with this configuration, please reconfigure the connection.")); return; } m_monitors.insert(thing, monitor); connect(info, &ThingSetupInfo::aborted, monitor, [this, thing](){ if (m_monitors.contains(thing)) { qCDebug(dcVestel()) << "Unregistering monitor because setup has been aborted."; hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); } if (m_evc04Connections.contains(thing)) { qCDebug(dcVestel()) << "Clean up connection because setup has been aborted."; m_evc04Connections.take(thing)->deleteLater(); } }); // If this is the initial setup, wait for the monitor to be reachable and make sure // we have an IP address, otherwise let the monitor do his work if (info->isInitialSetup()) { // Continue with setup only if we know that the network device is reachable if (monitor->reachable()) { setupEVC04Connection(info); } else { qCDebug(dcVestel()) << "Waiting for the network monitor to get reachable before continuing to set up the connection" << thing->name() << "..."; connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [this, thing, info, monitor](bool reachable){ if (reachable) { qCDebug(dcVestel()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continuing setup on" << monitor->networkDeviceInfo().address().toString(); setupEVC04Connection(info); } }); } } else { setupEVC04Connection(info); } } } void IntegrationPluginVestel::postSetupThing(Thing *thing) { Q_UNUSED(thing) if (!m_pluginTimer) { qCDebug(dcVestel()) << "Starting plugin timer..."; m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { foreach(EVC04ModbusTcpConnection *connection, m_evc04Connections) { qCDebug(dcVestel()) << "Updating connection" << connection->modbusTcpMaster()->hostAddress().toString(); connection->update(); connection->setAliveRegister(1); } }); m_pluginTimer->start(); } } void IntegrationPluginVestel::executeAction(ThingActionInfo *info) { if (info->thing()->thingClassId() == evc04ThingClassId) { EVC04ModbusTcpConnection *evc04Connection = m_evc04Connections.value(info->thing()); if (info->action().actionTypeId() == evc04PowerActionTypeId) { bool power = info->action().paramValue(evc04PowerActionPowerParamTypeId).toBool(); // If the car is *not* connected, writing a 0 to the charging current register will cause it to go to 6 A instead of 0 // Because of this, we we're not connected, we'll do nothing, but once it get's connected, we'll sync the state over (see below in cableStateChanged) if (!power && evc04Connection->cableState() < EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnected) { qCDebug(dcVestel()) << "Set state to" << false << "but do nothing since the car is not connected"; info->thing()->setStateValue(evc04PowerStateTypeId, false); info->finish(Thing::ThingErrorNoError); return; } uint chargingCurrent = (power ? info->thing()->stateValue(evc04MaxChargingCurrentStateTypeId).toUInt() : 0); qCDebug(dcVestel()) << "Write max charging current" << chargingCurrent; QModbusReply *reply = evc04Connection->setChargingCurrent(chargingCurrent); connect(reply, &QModbusReply::finished, info, [info, reply, power](){ if (reply->error() == QModbusDevice::NoError) { info->thing()->setStateValue(evc04PowerStateTypeId, power); info->finish(Thing::ThingErrorNoError); } else { qCWarning(dcVestel()) << "Error setting power:" << reply->error() << reply->errorString(); info->finish(Thing::ThingErrorHardwareFailure); } }); } if (info->action().actionTypeId() == evc04MaxChargingCurrentActionTypeId) { // Note: only write the register if power is true, otherwise we would start charging. The state represents the desired current, // once the power is true, the current will be written to the corresponding current. quint16 maxChargingCurrent = static_cast(qRound(info->action().paramValue(evc04MaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble())); if (info->thing()->stateValue(evc04PowerStateTypeId).toBool()) { qCDebug(dcVestel()) << "Write max charging current" << maxChargingCurrent; QModbusReply *reply = evc04Connection->setChargingCurrent(maxChargingCurrent); connect(reply, &QModbusReply::finished, info, [info, reply, maxChargingCurrent](){ if (reply->error() == QModbusDevice::NoError) { info->thing()->setStateValue(evc04MaxChargingCurrentStateTypeId, maxChargingCurrent); info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareFailure); } }); } else { qCDebug(dcVestel()) << "Set state to" << maxChargingCurrent << "but do nothing since the power is false"; info->thing()->setStateValue(evc04MaxChargingCurrentStateTypeId, maxChargingCurrent); } } } } void IntegrationPluginVestel::thingRemoved(Thing *thing) { if (thing->thingClassId() == evc04ThingClassId && m_evc04Connections.contains(thing)) { EVC04ModbusTcpConnection *connection = m_evc04Connections.take(thing); delete connection; } // Unregister related hardware resources if (m_monitors.contains(thing)) hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); if (myThings().isEmpty() && m_pluginTimer) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; } } void IntegrationPluginVestel::setupEVC04Connection(ThingSetupInfo *info) { Thing *thing = info->thing(); QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); qCDebug(dcVestel()) << "Setting up EVC04 wallbox on" << address.toString(); EVC04ModbusTcpConnection *evc04Connection = new EVC04ModbusTcpConnection(address, 502, 0xff, this); connect(info, &ThingSetupInfo::aborted, evc04Connection, &EVC04ModbusTcpConnection::deleteLater); // Reconnect on monitor reachable changed NetworkDeviceMonitor *monitor = m_monitors.value(thing); connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ qCDebug(dcVestel()) << "Network device monitor reachable changed for" << thing->name() << reachable; if (!thing->setupComplete()) return; if (reachable && !thing->stateValue("connected").toBool()) { evc04Connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address()); evc04Connection->connectDevice(); } else if (!reachable) { // Note: We disable autoreconnect explicitly and we will // connect the device once the monitor says it is reachable again evc04Connection->disconnectDevice(); } }); connect(evc04Connection, &EVC04ModbusTcpConnection::reachableChanged, thing, [thing, evc04Connection](bool reachable){ qCDebug(dcVestel()) << "Reachable changed to" << reachable << "for" << thing; if (reachable) { evc04Connection->initialize(); } else { thing->setStateValue(evc04ConnectedStateTypeId, false); thing->setStateValue(evc04CurrentPowerStateTypeId, 0); } }); connect(evc04Connection, &EVC04ModbusTcpConnection::initializationFinished, thing, [=](bool success){ if (!thing->setupComplete()) return; if (success) { thing->setStateValue(evc04ConnectedStateTypeId, true); } else { thing->setStateValue(evc04ConnectedStateTypeId, false); thing->setStateValue(evc04CurrentPowerStateTypeId, 0); // Try once to reconnect the device evc04Connection->reconnectDevice(); } }); connect(evc04Connection, &EVC04ModbusTcpConnection::initializationFinished, info, [=](bool success){ if (!success) { qCWarning(dcVestel()) << "Connection init finished with errors" << thing->name() << evc04Connection->modbusTcpMaster()->hostAddress().toString(); hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); evc04Connection->deleteLater(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error communicating with the wallbox.")); return; } qCDebug(dcVestel()) << "Connection init finished successfully" << evc04Connection; m_evc04Connections.insert(thing, evc04Connection); info->finish(Thing::ThingErrorNoError); thing->setStateValue(evc04ConnectedStateTypeId, true); thing->setStateValue(evc04VersionStateTypeId, QString(QString::fromUtf16(evc04Connection->firmwareVersion().data(), evc04Connection->firmwareVersion().length()).toUtf8()).trimmed()); evc04Connection->update(); }); connect(evc04Connection, &EVC04ModbusTcpConnection::updateFinished, thing, [this, evc04Connection, thing](){ qCDebug(dcVestel()) << "EVC04 update finished:" << thing->name() << evc04Connection; qCDebug(dcVestel()) << "Serial:" << QString(QString::fromUtf16(evc04Connection->serialNumber().data(), evc04Connection->serialNumber().length()).toUtf8()).trimmed(); qCDebug(dcVestel()) << "ChargePoint ID:" << QString(QString::fromUtf16(evc04Connection->chargepointId().data(), evc04Connection->chargepointId().length()).toUtf8()).trimmed(); qCDebug(dcVestel()) << "Brand:" << QString(QString::fromUtf16(evc04Connection->brand().data(), evc04Connection->brand().length()).toUtf8()).trimmed(); qCDebug(dcVestel()) << "Model:" << QString(QString::fromUtf16(evc04Connection->model().data(), evc04Connection->model().length()).toUtf8()).trimmed(); updateEVC04MaxCurrent(thing); // I've been observing the wallbox getting stuck on modbus. It is still functional, but modbus keeps on returning the same old values // until the TCP connection is closed and reopened. Checking the wallbox time register to detect that and auto-reconnect. if (m_lastWallboxTime[thing] == evc04Connection->time()) { qCWarning(dcVestel()) << "Wallbox seems stuck and returning outdated values. Reconnecting..."; evc04Connection->reconnectDevice(); } m_lastWallboxTime[thing] = evc04Connection->time(); }); connect(evc04Connection, &EVC04ModbusTcpConnection::chargepointStateChanged, thing, [thing](EVC04ModbusTcpConnection::ChargePointState chargePointState) { qCDebug(dcVestel()) << "Chargepoint state changed" << thing->name() << chargePointState; // switch (chargePointState) { // case EVC04ModbusTcpConnection::ChargePointStateAvailable: // case EVC04ModbusTcpConnection::ChargePointStatePreparing: // case EVC04ModbusTcpConnection::ChargePointStateReserved: // case EVC04ModbusTcpConnection::ChargePointStateUnavailable: // case EVC04ModbusTcpConnection::ChargePointStateFaulted: // thing->setStateValue(evc04PluggedInStateTypeId, false); // break; // case EVC04ModbusTcpConnection::ChargePointStateCharging: // case EVC04ModbusTcpConnection::ChargePointStateSuspendedEVSE: // case EVC04ModbusTcpConnection::ChargePointStateSuspendedEV: // case EVC04ModbusTcpConnection::ChargePointStateFinishing: // thing->setStateValue(evc04PluggedInStateTypeId, true); // break; // } }); connect(evc04Connection, &EVC04ModbusTcpConnection::chargingStateChanged, thing, [thing](EVC04ModbusTcpConnection::ChargingState chargingState) { qCDebug(dcVestel()) << "Charging state changed:" << chargingState; thing->setStateValue(evc04ChargingStateTypeId, chargingState == EVC04ModbusTcpConnection::ChargingStateCharging); }); connect(evc04Connection, &EVC04ModbusTcpConnection::activePowerTotalChanged, thing, [thing](quint16 activePowerTotal) { qCDebug(dcVestel()) << "Total active power:" << activePowerTotal; // The wallbox reports some 5-6W even when there's nothing connected. Let's hide that if we're not charging if (thing->stateValue(evc04ChargingStateTypeId).toBool() == true) { thing->setStateValue(evc04CurrentPowerStateTypeId, activePowerTotal); } else { thing->setStateValue(evc04CurrentPowerStateTypeId, 0); } }); connect(evc04Connection, &EVC04ModbusTcpConnection::meterReadingChanged, thing, [thing](quint32 meterReading) { qCDebug(dcVestel()) << "Meter reading changed:" << meterReading; thing->setStateValue(evc04TotalEnergyConsumedStateTypeId, meterReading / 10.0); }); connect(evc04Connection, &EVC04ModbusTcpConnection::sessionMaxCurrentChanged, thing, [](quint16 sessionMaxCurrent) { // This mostly just reflects what we've been writing to cargingCurrent, so not of much use... qCDebug(dcVestel()) << "Session max current changed:" << sessionMaxCurrent; }); connect(evc04Connection, &EVC04ModbusTcpConnection::cableMaxCurrentChanged, thing, [this, thing](quint16 cableMaxCurrent) { qCDebug(dcVestel()) << "Cable max current changed:" << cableMaxCurrent; updateEVC04MaxCurrent(thing); }); connect(evc04Connection, &EVC04ModbusTcpConnection::evseMinCurrentChanged, thing, [thing](quint16 evseMinCurrent) { qCDebug(dcVestel()) << "EVSE min current changed:" << evseMinCurrent; thing->setStateMinValue(evc04MaxChargingCurrentStateTypeId, evseMinCurrent); }); connect(evc04Connection, &EVC04ModbusTcpConnection::evseMaxCurrentChanged, thing, [this, thing](quint16 evseMaxCurrent) { qCDebug(dcVestel()) << "EVSE max current changed:" << evseMaxCurrent; updateEVC04MaxCurrent(thing); }); connect(evc04Connection, &EVC04ModbusTcpConnection::sessionEnergyChanged, thing, [thing](quint32 sessionEnergy) { qCDebug(dcVestel()) << "Session energy changed:" << sessionEnergy; thing->setStateValue(evc04SessionEnergyStateTypeId, sessionEnergy / 1000.0); }); connect(evc04Connection, &EVC04ModbusTcpConnection::chargingCurrentChanged, thing, [thing](quint16 chargingCurrent) { qCDebug(dcVestel()) << "Charging current changed:" << chargingCurrent; if (chargingCurrent > 0) { thing->setStateValue(evc04PowerStateTypeId, true); thing->setStateValue(evc04MaxChargingCurrentStateTypeId, chargingCurrent); } else { thing->setStateValue(evc04PowerStateTypeId, false); } }); connect(evc04Connection, &EVC04ModbusTcpConnection::numPhasesChanged, thing, [thing](EVC04ModbusTcpConnection::NumPhases numPhases) { switch (numPhases) { case EVC04ModbusTcpConnection::NumPhases1: thing->setStateValue(evc04PhaseCountStateTypeId, 1); break; case EVC04ModbusTcpConnection::NumPhases3: thing->setStateValue(evc04PhaseCountStateTypeId, 3); break; } }); connect(evc04Connection, &EVC04ModbusTcpConnection::cableStateChanged, thing, [evc04Connection, thing](EVC04ModbusTcpConnection::CableState cableState) { switch (cableState) { case EVC04ModbusTcpConnection::CableStateNotConnected: case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleNotConnected: thing->setStateValue(evc04PluggedInStateTypeId, false); break; case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnected: case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnectedCableLocked: thing->setStateValue(evc04PluggedInStateTypeId, true); // The car was plugged in, sync the power state now as the wallbox only allows to set that when the car is connected if (thing->stateValue(evc04PowerStateTypeId).toBool() == false) { qCInfo(dcVestel()) << "Car plugged in. Syncing cached power off state to wallbox"; evc04Connection->setChargingCurrent(0); } else { qCInfo(dcVestel()) << "Car plugged in. Syncing cached current valie to wallbox"; evc04Connection->setChargingCurrent(static_cast(thing->stateValue(evc04MaxChargingCurrentStateTypeId).toUInt())); } break; } }); evc04Connection->connectDevice(); } void IntegrationPluginVestel::updateEVC04MaxCurrent(Thing *thing) { EVC04ModbusTcpConnection *connection = m_evc04Connections.value(thing); quint16 wallboxMax = connection->maxChargePointPower() > 0 ? connection->maxChargePointPower() / 230 : 32; quint16 evseMax = connection->evseMaxCurrent() > 0 ? connection->evseMaxCurrent() : wallboxMax; quint16 cableMax = connection->cableMaxCurrent() > 0 ? connection->cableMaxCurrent() : wallboxMax; quint8 overallMax = qMin(qMin(wallboxMax, evseMax), cableMax); qCDebug(dcVestel()) << "Adjusting max current: Wallbox max:" << wallboxMax << "EVSE max:" << evseMax << "cable max:" << cableMax << "Overall:" << overallMax; thing->setStateMinMaxValues(evc04MaxChargingCurrentStateTypeId, 6, overallMax); }