diff --git a/debian/control b/debian/control index ab10b9a..05f8045 100644 --- a/debian/control +++ b/debian/control @@ -246,6 +246,15 @@ Description: nymea integration plugin for UniPi devices This package contains the nymea integration plugin for UniPi devices. +Package: nymea-plugin-pcelectric +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for PCE wallboxes + This package contains the nymea integration plugin for wallboxes made by PC Electric. + + Package: nymea-plugin-phoenixconnect Architecture: any Section: libs diff --git a/debian/nymea-plugin-pcelectric.install.in b/debian/nymea-plugin-pcelectric.install.in new file mode 100644 index 0000000..2645b3f --- /dev/null +++ b/debian/nymea-plugin-pcelectric.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginpcelectric.so +pcelectric/translations/*qm usr/share/nymea/translations/ diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index bb9149a..bc1a5c8 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -17,6 +17,7 @@ PLUGIN_DIRS = \ modbuscommander \ mtec \ mypv \ + pcelectric \ phoenixconnect \ schrack \ senseair \ diff --git a/pcelectric/EV11.3-registers.json b/pcelectric/EV11.3-registers.json new file mode 100644 index 0000000..518103f --- /dev/null +++ b/pcelectric/EV11.3-registers.json @@ -0,0 +1,334 @@ +{ + "className": "EV11", + "protocol": "TCP", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 10, + "checkReachableRegister": "chargingState", + "enums": [ + { + "name": "ChargingState", + "values": [ + { + "key": "Initializing", + "value": 0 + }, + { + "key": "A1", + "value": 1 + }, + { + "key": "A2", + "value": 2 + }, + { + "key": "B1", + "value": 3 + }, + { + "key": "B2", + "value": 4 + }, + { + "key": "C1", + "value": 5 + }, + { + "key": "C2", + "value": 6 + }, + { + "key": "Error", + "value": 7 + } + ] + }, + { + "name": "ChargingRelayState", + "values": [ + { + "key": "NoCharging", + "value": 0 + }, + { + "key": "SinglePhase", + "value": 1 + }, + { + "key": "TheePhase", + "value": 2 + } + ] + }, + { + "name": "Error", + "values": [ + { + "key": "NoError", + "value": 0 + }, + { + "key": "Overheating", + "value": 1 + }, + { + "key": "DCFaultCurrent", + "value": 2 + }, + { + "key": "ChargingWithVentilation", + "value": 3 + }, + { + "key": "CPErrorEF", + "value": 4 + }, + { + "key": "CPErrorBypass", + "value": 5 + }, + { + "key": "CPErrorDiodFault", + "value": 6 + }, + { + "key": "DCFaultCurrentCalibrating", + "value": 7 + }, + { + "key": "DCFaultCurrentCommunication", + "value": 8 + }, + { + "key": "DCFaultCurrentError", + "value": 9 + } + ] + } + ], + "blocks": [ + { + "id": "status", + "readSchedule": "update", + "registers": [ + { + "id": "chargingState", + "address": 100, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "enum": "ChargingState", + "description": "Current charging state", + "defaultValue": "ChargingStateInitializing", + "access": "R" + }, + { + "id": "chargingRelayState", + "address": 101, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "enum": "ChargingRelayState", + "description": "Charging relay state", + "defaultValue": "ChargingRelayStateNoCharging", + "access": "R" + }, + { + "id": "maxChargingCurrentDip", + "address": 102, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Maximum charging current (DIP)", + "unit": "mA", + "defaultValue": "6000", + "access": "R" + }, + { + "id": "phaseAutoSwitch", + "address": 103, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Automatic phase switching", + "defaultValue": "0", + "access": "R" + }, + { + "id": "activeChargingCurrent", + "address": 104, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Active charging current", + "unit": "mA", + "defaultValue": "0", + "access": "R" + }, + { + "id": "sessionDuration", + "address": 105, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Session durration", + "unit": "10 seconds", + "defaultValue": "0", + "access": "R" + }, + { + "id": "powerMeter0", + "address": 106, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Current session energy", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "R" + }, + { + "id": "powerMeter1", + "address": 107, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Last session energy", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "R" + }, + { + "id": "powerMeter3", + "address": 108, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Penultimate session energy", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "R" + }, + { + "id": "temperature", + "address": 109, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Onboard temperature", + "unit": "°C", + "staticScaleFactor": -1, + "defaultValue": "0", + "access": "R" + }, + { + "id": "error", + "address": 110, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "enum": "Error", + "description": "Error", + "defaultValue": "ErrorNoError", + "access": "R" + } + ] + }, + { + "id": "initInfos", + "readSchedule": "init", + "registers": [ + { + "id": "firmwareRevision", + "address": 135, + "size": 2, + "type": "string", + "registerType": "holdingRegister", + "description": "Firmware revision (ASCII)", + "access": "R" + }, + { + "id": "hardwareRevision", + "address": 137, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Hardware revision", + "defaultValue": "0", + "access": "R" + }, + { + "id": "serialNumber", + "address": 138, + "size": 3, + "type": "raw", + "registerType": "holdingRegister", + "description": "Serial number", + "access": "R" + }, + { + "id": "macAddress", + "address": 141, + "size": 3, + "type": "raw", + "registerType": "holdingRegister", + "description": "MAC address", + "access": "R" + } + ] + } + ], + "registers": [ + { + "id": "chargingCurrent", + "address": 200, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Write charging current", + "unit": "mA", + "access": "WO" + }, + { + "id": "chargingCurrentOffline", + "address": 201, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Write charging current", + "unit": "mA", + "access": "WO" + }, + { + "id": "maxChargingTime", + "address": 202, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Max charging time", + "unit": "Minutes", + "access": "WO" + }, + { + "id": "heartbeat", + "address": 203, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Heartbeat (write < 60s to keep alive)", + "access": "WO" + }, + { + "id": "ledBrightness", + "address": 204, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "LED brightness", + "unit": "%", + "access": "WO" + } + ] +} diff --git a/pcelectric/README.md b/pcelectric/README.md new file mode 100644 index 0000000..753d65c --- /dev/null +++ b/pcelectric/README.md @@ -0,0 +1,3 @@ +# PC Electric + + diff --git a/pcelectric/integrationpluginpcelectric.cpp b/pcelectric/integrationpluginpcelectric.cpp new file mode 100644 index 0000000..421379f --- /dev/null +++ b/pcelectric/integrationpluginpcelectric.cpp @@ -0,0 +1,398 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginpcelectric.h" +#include "pcelectricdiscovery.h" +#include "plugininfo.h" + +#include +#include + +IntegrationPluginPcElectric::IntegrationPluginPcElectric() +{ + +} + +void IntegrationPluginPcElectric::init() +{ + +} + +void IntegrationPluginPcElectric::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcPcElectric()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; + } + + // Create a discovery with the info as parent for auto deleting the object once the discovery info is done + PcElectricDiscovery *discovery = new PcElectricDiscovery(hardwareManager()->networkDeviceDiscovery(), 502, 1, info); + connect(discovery, &PcElectricDiscovery::discoveryFinished, info, [=](){ + foreach (const PcElectricDiscovery::Result &result, discovery->results()) { + + ThingDescriptor descriptor(ev11ThingClassId, "PCE EV11.3 (" + result.serialNumber + ")", "Version: " + result.firmwareRevision + " - " + result.networkDeviceInfo.address().toString()); + qCDebug(dcPcElectric()) << "Discovered:" << descriptor.title() << descriptor.description(); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(ev11ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcPcElectric()) << "This PCE wallbox already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(ev11ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + // Note: if we discover also the port and modbusaddress, we must fill them in from the discovery here, for now everywhere the defaults... + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + + // Start the discovery process + discovery->startDiscovery(); +} + +void IntegrationPluginPcElectric::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcPcElectric()) << "Setup thing" << thing << thing->params(); + + if (m_connections.contains(thing)) { + qCDebug(dcPcElectric()) << "Reconfiguring existing thing" << thing->name(); + m_connections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + MacAddress macAddress = MacAddress(thing->paramValue(ev11ThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcPcElectric()) << "The configured mac address is not valid" << thing->params(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing.")); + return; + } + + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); + m_monitors.insert(thing, monitor); + + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + if (m_monitors.contains(thing)) { + qCDebug(dcPcElectric()) << "Unregistering monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + // Only make sure the connection is working in the initial setup, otherwise we let the monitor do the work + if (info->isInitialSetup()) { + // Continue with setup only if we know that the network device is reachable + if (monitor->reachable()) { + setupConnection(info); + } else { + // otherwise wait until we reach the networkdevice before setting up the device + qCDebug(dcPcElectric()) << "Network device" << thing->name() << "is not reachable yet. Continue with the setup once reachable."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcPcElectric()) << "Network device" << thing->name() << "is now reachable. Continue with the setup..."; + setupConnection(info); + } + }); + } + } else { + setupConnection(info); + } + + return; +} + +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] { + foreach (PceWallbox *connection, m_connections) { + if (connection->reachable()) { + connection->update(); + } + } + }); + + qCDebug(dcPcElectric()) << "Starting refresh timer..."; + m_refreshTimer->start(); + } +} + +void IntegrationPluginPcElectric::thingRemoved(Thing *thing) +{ + qCDebug(dcPcElectric()) << "Thing removed" << thing->name(); + + if (m_connections.contains(thing)) { + PceWallbox *connection = m_connections.take(thing); + connection->disconnectDevice(); + connection->deleteLater(); + } + + // Unregister related hardware resources + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + + if (myThings().isEmpty() && m_refreshTimer) { + qCDebug(dcPcElectric()) << "Stopping reconnect timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +void IntegrationPluginPcElectric::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + + PceWallbox *connection = m_connections.value(thing); + if (!connection->reachable()) { + qCWarning(dcPcElectric()) << "Could not execute action because the connection is not available."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + 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](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set power state to" << power << "(" << chargingCurrent << "mA)" << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + qCDebug(dcPcElectric()) << "Successfully set power state to" << power << "(" << chargingCurrent << "mA)"; + 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; + } + + 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"; + 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; + } + + 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 charging current to" << finalChargingCurrent << "mA"; + }); + } + return; + } + + + Q_ASSERT_X(false, "IntegrationPluginPcElectric::executeAction", QString("Unhandled action: %1").arg(info->action().actionTypeId().toString()).toLocal8Bit()); +} + +void IntegrationPluginPcElectric::setupConnection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + + qCDebug(dcPcElectric()) << "Setting up PCE wallbox finished successfully" << monitor->networkDeviceInfo().address().toString(); + + PceWallbox *connection = new PceWallbox(monitor->networkDeviceInfo().address(), 502, 1, this); + connect(info, &ThingSetupInfo::aborted, connection, &PceWallbox::deleteLater); + + // Monitor reachability + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + if (!thing->setupComplete()) + return; + + qCDebug(dcPcElectric()) << "Network device monitor for" << thing->name() << (reachable ? "is now reachable" : "is not reachable any more" ); + if (reachable && !thing->stateValue("connected").toBool()) { + connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address()); + connection->connectDevice(); + } else if (!reachable) { + // Note: We disable autoreconnect explicitly and we will + // connect the device once the monitor says it is reachable again + connection->disconnectDevice(); + } + }); + + // Connection reachability + connect(connection, &PceWallbox::reachableChanged, thing, [thing](bool reachable){ + qCInfo(dcPcElectric()) << "Reachable changed to" << reachable << "for" << thing; + thing->setStateValue("connected", reachable); + }); + + connect(connection, &PceWallbox::updateFinished, thing, [thing, connection](){ + qCDebug(dcPcElectric()) << "Update finished for" << thing; + qCDebug(dcPcElectric()) << connection; + if (!connection->phaseAutoSwitch()) { + // Note: if auto phase switching is disabled, the wallbox forces 3 phase charging + thing->setStatePossibleValues(ev11DesiredPhaseCountStateTypeId, { 3 }); // Disable phase switching (default 3) + thing->setStateValue(ev11DesiredPhaseCountStateTypeId, 3); + thing->setStateValue(ev11PhaseCountStateTypeId, 3); + } else { + thing->setStatePossibleValues(ev11DesiredPhaseCountStateTypeId, { 1, 3 }); // Enable phase switching + } + + if (connection->chargingRelayState() != EV11ModbusTcpConnection::ChargingRelayStateNoCharging) { + if (connection->chargingRelayState() == EV11ModbusTcpConnection::ChargingRelayStateSinglePhase) { + thing->setStateValue(ev11PhaseCountStateTypeId, 1); + } else if (connection->chargingRelayState() == EV11ModbusTcpConnection::ChargingRelayStateTheePhase) { + thing->setStateValue(ev11PhaseCountStateTypeId, 3); + } + } + + thing->setStateMaxValue(ev11MaxChargingCurrentStateTypeId, connection->maxChargingCurrentDip() / 1000); + thing->setStateValue(ev11PluggedInStateTypeId, connection->chargingState() >= PceWallbox::ChargingStateB1 && + connection->chargingState() < PceWallbox::ChargingStateError); + + thing->setStateValue(ev11ChargingStateTypeId, connection->chargingState() == PceWallbox::ChargingStateC2); + if (connection->chargingRelayState() != EV11ModbusTcpConnection::ChargingRelayStateNoCharging) { + thing->setStateValue(ev11PhaseCountStateTypeId, connection->chargingRelayState() == EV11ModbusTcpConnection::ChargingRelayStateSinglePhase ? 1 : 3); + } + + thing->setStateValue(ev11CurrentVersionStateTypeId, connection->firmwareRevision()); + thing->setStateValue(ev11SessionEnergyStateTypeId, connection->powerMeter0()); + thing->setStateValue(ev11TemperatureStateTypeId, connection->temperature()); + + switch (connection->error()) { + case EV11ModbusTcpConnection::ErrorNoError: + thing->setStateValue(ev11ErrorStateTypeId, "Kein Fehler aktiv"); + break; + case EV11ModbusTcpConnection::ErrorOverheating: + thing->setStateValue(ev11ErrorStateTypeId, "Übertemperatur. Ladevorgang wird automatisch fortgesetzt."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrent: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor ausgelöst."); + break; + case EV11ModbusTcpConnection::ErrorChargingWithVentilation: + thing->setStateValue(ev11ErrorStateTypeId, "Ladeanforderung mit Belüftung."); + break; + case EV11ModbusTcpConnection::ErrorCPErrorEF: + thing->setStateValue(ev11ErrorStateTypeId, "CP Signal, Fehlercode E oder F."); + break; + case EV11ModbusTcpConnection::ErrorCPErrorBypass: + thing->setStateValue(ev11ErrorStateTypeId, "CP Signal, bypass."); + break; + case EV11ModbusTcpConnection::ErrorCPErrorDiodFault: + thing->setStateValue(ev11ErrorStateTypeId, "CP Signal, Diode defekt."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrentCalibrating: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor, Kalibrirung."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrentCommunication: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor, Kommunikationsfehler."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrentError: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor, Fehler."); + break; + } + }); + + connect(thing, &Thing::settingChanged, connection, [thing, connection](const ParamTypeId ¶mTypeId, const QVariant &value){ + if (paramTypeId == ev11SettingsLedBrightnessParamTypeId) { + quint16 percentage = value.toUInt(); + qCDebug(dcPcElectric()) << "Set LED brightness" << percentage << "%"; + QueuedModbusReply *reply = connection->setLedBrightness(percentage); + connect(reply, &QueuedModbusReply::finished, thing, [reply, percentage](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set led brightness to" << percentage << "%" << reply->errorString(); + return; + } + + qCDebug(dcPcElectric()) << "Successfully set led brightness to" << percentage << "%"; + }); + } + }); + + m_connections.insert(thing, connection); + info->finish(Thing::ThingErrorNoError); + + // Connect reight the way if the monitor indicates reachable, otherwise the connect will handle the connect later + if (monitor->reachable()) + connection->connectDevice(); +} diff --git a/pcelectric/integrationpluginpcelectric.h b/pcelectric/integrationpluginpcelectric.h new file mode 100644 index 0000000..9fb1519 --- /dev/null +++ b/pcelectric/integrationpluginpcelectric.h @@ -0,0 +1,69 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINPCELECTRIC_H +#define INTEGRATIONPLUGINPCELECTRIC_H + +#include + +#include +#include +#include + +#include "pcewallbox.h" +#include "extern-plugininfo.h" + +class IntegrationPluginPcElectric : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginpcelectric.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginPcElectric(); + void init() override; + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private: + PluginTimer *m_refreshTimer = nullptr; + QHash m_connections; + QHash m_monitors; + + void setupConnection(ThingSetupInfo *info); + +}; + +#endif // INTEGRATIONPLUGINPCELECTRIC_H diff --git a/pcelectric/integrationpluginpcelectric.json b/pcelectric/integrationpluginpcelectric.json new file mode 100644 index 0000000..eed7855 --- /dev/null +++ b/pcelectric/integrationpluginpcelectric.json @@ -0,0 +1,148 @@ +{ + "name": "PcElectric", + "displayName": "PC Electric", + "id": "aa7ff833-a8e0-45cc-a1ef-65f05871f272", + "paramTypes":[ ], + "vendors": [ + { + "name": "PcElectric", + "displayName": "PC Electric GmbH", + "id": "b365937b-f1d6-46bf-9ff1-e787373b8aa6", + "thingClasses": [ + { + "name": "ev11", + "displayName": "PCE EV11.3", + "id": "88d96940-a940-4a07-8176-5e6aba7ca832", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "connectable"], + "paramTypes": [ + { + "id": "0a3f8d12-9d33-4ae2-b763-9568f32e8da1", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "settingsTypes": [ + { + "id": "3a1329a2-84cc-47b9-a6c2-e96fdfd0c454", + "name": "ledBrightness", + "displayName": "LED brightness", + "type": "uint", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": 50 + } + ], + "stateTypes": [ + { + "id": "ca8d680c-c2f8-456a-a246-9c6cd64e25a7", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "c12a7a27-fa56-450c-a1ec-717c868554f2", + "name": "power", + "displayName": "Charging enabled", + "displayNameEvent": "Charging enabled or disabled", + "displayNameAction": "Enable or disable charging", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "b5bbf23c-06db-463b-bb5c-3aea38e18818", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameEvent": "Maximum charging current changed", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "defaultValue": 6, + "minValue": 6, + "maxValue": 16, + "writable": true + }, + { + "id": "50164bbd-9802-4cf6-82de-626b74293a1b", + "name": "pluggedIn", + "displayName": "Plugged in", + "displayNameEvent": "Plugged or unplugged", + "type": "bool", + "defaultValue": false + }, + { + "id": "b7972cd7-471a-46bd-ab99-f49997f12309", + "name": "charging", + "displayName": "Charging", + "displayNameEvent": "Charging started or stopped", + "type": "bool", + "defaultValue": false + }, + { + "id": "bca88c23-e940-40c1-afca-eb511fd17aab", + "name": "phaseCount", + "displayName": "Active phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 3 + }, + { + "id": "d91f7d96-2599-400a-91da-d164477098b7", + "name": "desiredPhaseCount", + "displayName": "Desired phase count", + "displayNameAction": "Set desired phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "possibleValues": [1, 3], + "defaultValue": 3, + "writable": true + }, + { + "id": "3da3ee80-e9e7-4237-85a6-b4adcb2f483b", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "bb092562-377e-458e-bb8a-735af9036652", + "name": "temperature", + "displayName": "Onboard temperature", + "displayNameEvent": "Onboard temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "2ea1a53f-b2b0-452d-8060-cdb114db05a7", + "name": "error", + "displayName": "Error", + "type": "QString", + "defaultValue": "Kein Fehler", + "suggestLogging": true + }, + { + "id": "142b4276-e2e9-4149-adc4-89d9d3e31117", + "name": "currentVersion", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + } + ] +} diff --git a/pcelectric/meta.json b/pcelectric/meta.json new file mode 100644 index 0000000..6e433a8 --- /dev/null +++ b/pcelectric/meta.json @@ -0,0 +1,13 @@ +{ + "title": "PC Electric", + "tagline": "Integrate the PCE EV11.3 wallbox with nymea.", + "icon": "pce.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "modbus" + ], + "categories": [ + "energy" + ] +} diff --git a/pcelectric/pce.png b/pcelectric/pce.png new file mode 100644 index 0000000..db773ed Binary files /dev/null and b/pcelectric/pce.png differ diff --git a/pcelectric/pcelectric.pro b/pcelectric/pcelectric.pro new file mode 100644 index 0000000..61740a5 --- /dev/null +++ b/pcelectric/pcelectric.pro @@ -0,0 +1,17 @@ +include(../plugins.pri) + +# Generate modbus connection +MODBUS_CONNECTIONS += EV11.3-registers.json +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + integrationpluginpcelectric.h \ + pcelectricdiscovery.h \ + pcewallbox.h + +SOURCES += \ + integrationpluginpcelectric.cpp \ + pcelectricdiscovery.cpp \ + pcewallbox.cpp + diff --git a/pcelectric/pcelectricdiscovery.cpp b/pcelectric/pcelectricdiscovery.cpp new file mode 100644 index 0000000..427834a --- /dev/null +++ b/pcelectric/pcelectricdiscovery.cpp @@ -0,0 +1,169 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "pcelectricdiscovery.h" +#include "extern-plugininfo.h" + +PcElectricDiscovery::PcElectricDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent) + : QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery}, + m_port{port}, + m_modbusAddress{modbusAddress} +{ + +} + +QList PcElectricDiscovery::results() const +{ + return m_results; +} + + +void PcElectricDiscovery::startDiscovery() +{ + qCInfo(dcPcElectric()) << "Discovery: Start searching for PCE wallboxes in the network..."; + m_startDateTime = QDateTime::currentDateTime(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &PcElectricDiscovery::checkNetworkDevice); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + // Finish with some delay so the last added network device information objects still can be checked. + QTimer::singleShot(3000, this, [this](){ + qCDebug(dcPcElectric()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); + }); +} + +void PcElectricDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + EV11ModbusTcpConnection *connection = new EV11ModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this); + m_connections.append(connection); + + connect(connection, &EV11ModbusTcpConnection::reachableChanged, this, [=](bool reachable){ + if (!reachable) { + // Disconnected ... done with this connection + cleanupConnection(connection); + return; + } + + // Modbus TCP connected...ok, let's try to initialize it! + connect(connection, &EV11ModbusTcpConnection::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcPcElectric()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + return; + } + + // Parse the mac address from the registers and compair with the network device info mac address. + // If they match, we most likly found a PCE wallbox + + QByteArray macRawData; + QDataStream stream(&macRawData, QIODevice::WriteOnly); + for (int i = 0; i < connection->macAddress().count(); i++) + stream << connection->macAddress().at(i); + + MacAddress registerMacAddress(macRawData); + qCDebug(dcPcElectric()) << "Fetched mac address" << macRawData.toHex() << registerMacAddress; + + // According to PCE the HW revision must be 0 + if (registerMacAddress == MacAddress(networkDeviceInfo.macAddress()) && connection->hardwareRevision() == 0) { + + // Parse the serial number + QByteArray serialRawData; + QDataStream stream(&serialRawData, QIODevice::ReadWrite); + stream << static_cast(0); + for (int i = 0; i < connection->serialNumber().count(); i++) + stream << connection->serialNumber().at(i); + + quint64 serialNumber = serialRawData.toHex().toULongLong(nullptr, 16); + qCDebug(dcPcElectric()) << "Serial number" << serialRawData.toHex() << serialNumber; + + Result result; + result.serialNumber = QString::number(serialNumber); + result.firmwareRevision = connection->firmwareRevision(); + result.networkDeviceInfo = networkDeviceInfo; + m_results.append(result); + + qCInfo(dcPcElectric()) << "Discovery: --> Found" + << "Serial number:" << result.serialNumber + << "Firmware revision:" << result.firmwareRevision + << result.networkDeviceInfo; + } + + // Done with this connection + cleanupConnection(connection); + }); + + // Initializing... + if (!connection->initialize()) { + qCDebug(dcPcElectric()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If we get any error...skip this host... + connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [=](QModbusDevice::Error error){ + if (error != QModbusDevice::NoError) { + qCDebug(dcPcElectric()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If check reachability failed...skip this host... + connect(connection, &EV11ModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(dcPcElectric()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + }); + + // Try to connect, maybe it works, maybe not... + connection->connectDevice(); +} + +void PcElectricDiscovery::cleanupConnection(EV11ModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void PcElectricDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (EV11ModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcPcElectric()) << "Discovery: Finished the discovery process. Found" << m_results.count() + << "PCE wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + emit discoveryFinished(); +} diff --git a/pcelectric/pcelectricdiscovery.h b/pcelectric/pcelectricdiscovery.h new file mode 100644 index 0000000..bdee2f1 --- /dev/null +++ b/pcelectric/pcelectricdiscovery.h @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef PCELECTRICDISCOVERY_H +#define PCELECTRICDISCOVERY_H + +#include + +#include + +#include "ev11modbustcpconnection.h" + +class PcElectricDiscovery : public QObject +{ + Q_OBJECT +public: + explicit PcElectricDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent = nullptr); + + typedef struct Result { + QString serialNumber; + QString firmwareRevision; + NetworkDeviceInfo networkDeviceInfo; + } Result; + + QList results() const; + +public slots: + void startDiscovery(); + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + quint16 m_port; + quint16 m_modbusAddress; + QDateTime m_startDateTime; + + QList m_connections; + + QList m_results; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(EV11ModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // PCELECTRICDISCOVERY_H diff --git a/pcelectric/pcewallbox.cpp b/pcelectric/pcewallbox.cpp new file mode 100644 index 0000000..dc0b914 --- /dev/null +++ b/pcelectric/pcewallbox.cpp @@ -0,0 +1,267 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#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(); + + qDeleteAll(m_queue); + m_queue.clear(); + + 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_queue) { + 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) { + emit updateFinished(); + sendNextRequest(); + return; + } + + const QModbusDataUnit unit = reply->reply()->result(); + const QVector blockValues = unit.values(); + processBlockStatusRegisterValues(blockValues); + + emit updateFinished(); + sendNextRequest(); + }); + + enqueueRequest(reply); + return true; +} + +QueuedModbusReply *PceWallbox::setChargingCurrent(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; + + sendNextRequest(); + return; + }); + + enqueueRequest(reply, true); + return reply; +} + +QueuedModbusReply *PceWallbox::setLedBrightness(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; + + sendNextRequest(); + return; + }); + + enqueueRequest(reply, true); + return reply; +} + +void PceWallbox::gracefullDeleteLater() +{ + // Clean up the queue + m_aboutToDelete = true; + cleanupQueue(); + + 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..."; + } +} + +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(); + } + + sendNextRequest(); + return; + }); + + enqueueRequest(reply, true); +} + +void PceWallbox::sendNextRequest() +{ + if (m_queue.isEmpty()) + return; + + if (m_currentReply) + return; + + if (m_aboutToDelete) { + disconnect(this, nullptr, nullptr, nullptr); + disconnectDevice(); + deleteLater(); + return; + } + + m_currentReply = m_queue.dequeue(); + 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; + sendNextRequest(); + return; + } + + if (m_currentReply->reply()->isFinished()) { + qCWarning(dcPcElectric()) << "Reply immediatly finished"; + m_currentReply->deleteLater(); + m_currentReply = nullptr; + sendNextRequest(); + return; + } +} + +void PceWallbox::enqueueRequest(QueuedModbusReply *reply, bool prepend) +{ + if (prepend) { + m_queue.prepend(reply); + } else { + m_queue.enqueue(reply); + } + + sendNextRequest(); +} + +void PceWallbox::cleanupQueue() +{ + qDeleteAll(m_queue); + m_queue.clear(); +} diff --git a/pcelectric/pcewallbox.h b/pcelectric/pcewallbox.h new file mode 100644 index 0000000..9954e59 --- /dev/null +++ b/pcelectric/pcewallbox.h @@ -0,0 +1,77 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef PCEWALLBOX_H +#define PCEWALLBOX_H + +#include +#include +#include + +#include + +#include "ev11modbustcpconnection.h" + +class PceWallbox : public EV11ModbusTcpConnection +{ + Q_OBJECT +public: + explicit PceWallbox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr); + + bool update() override; + + QueuedModbusReply *setChargingCurrent(quint16 chargingCurrent); // mA + + QueuedModbusReply *setLedBrightness(quint16 percentage); + + // 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 + // socket and deletes it self. + // IMPORTNAT: do not use the object after this call, this is a temporary workaround + void gracefullDeleteLater(); + +private slots: + void sendHeartbeat(); + +private: + QTimer m_timer; + quint16 m_heartbeat = 1; + QueuedModbusReply *m_currentReply = nullptr; + QQueue m_queue; + bool m_aboutToDelete = false; + + void sendNextRequest(); + void enqueueRequest(QueuedModbusReply *reply, bool prepend = false); + + void cleanupQueue(); +}; + +#endif // PCEWALLBOX_H diff --git a/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-de_DE.ts b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-de_DE.ts new file mode 100644 index 0000000..82ee749 --- /dev/null +++ b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-de_DE.ts @@ -0,0 +1,117 @@ + + + + + IntegrationPluginPcElectric + + + The network device discovery is not available. + Die Netzwerk Suche ist nicht verfügbar. + + + + PcElectric + + + Active phases + The name of the StateType ({bca88c23-e940-40c1-afca-eb511fd17aab}) of ThingClass ev11 + Aktive Phasen + + + + Charging + The name of the StateType ({b7972cd7-471a-46bd-ab99-f49997f12309}) of ThingClass ev11 + Lädt + + + + + Charging enabled + The name of the ParamType (ThingClass: ev11, ActionType: power, ID: {c12a7a27-fa56-450c-a1ec-717c868554f2}) +---------- +The name of the StateType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + Laden eingeschalten + + + + Connected + The name of the StateType ({ca8d680c-c2f8-456a-a246-9c6cd64e25a7}) of ThingClass ev11 + Verbunden + + + + Enable or disable charging + The name of the ActionType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + Laden starten/stoppen + + + + Firmware version + The name of the StateType ({142b4276-e2e9-4149-adc4-89d9d3e31117}) of ThingClass ev11 + Firmware Version + + + + Hardware version + The name of the StateType ({b6e65baf-6dcd-4db1-a3dc-962a4c33d157}) of ThingClass ev11 + Hardware Version + + + + LED brightness + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {3a1329a2-84cc-47b9-a6c2-e96fdfd0c454}) + LED Helligkeit + + + + MAC address + The name of the ParamType (ThingClass: ev11, Type: thing, ID: {0a3f8d12-9d33-4ae2-b763-9568f32e8da1}) + MAC Adresse + + + + + Maximum charging current + The name of the ParamType (ThingClass: ev11, ActionType: maxChargingCurrent, ID: {b5bbf23c-06db-463b-bb5c-3aea38e18818}) +---------- +The name of the StateType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + Maximaler Ladestrom + + + + Maximum offline charging current + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {93654273-c4d3-4389-a81e-c0f065d9cd92}) + Maximaler Ladestrom offline + + + + PC Electric + The name of the plugin PcElectric ({aa7ff833-a8e0-45cc-a1ef-65f05871f272}) + PC Electric + + + + PC Electric GmbH + The name of the vendor ({b365937b-f1d6-46bf-9ff1-e787373b8aa6}) + PC Electric GmbH + + + + PCE EV11.X + The name of the ThingClass ({88d96940-a940-4a07-8176-5e6aba7ca832}) + PCE EV11.X + + + + Plugged in + The name of the StateType ({50164bbd-9802-4cf6-82de-626b74293a1b}) of ThingClass ev11 + Angesteckt + + + + Set maximum charging current + The name of the ActionType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + Setze maximalen Ladestrom + + + diff --git a/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-en_US.ts b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-en_US.ts new file mode 100644 index 0000000..a03f87a --- /dev/null +++ b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-en_US.ts @@ -0,0 +1,117 @@ + + + + + IntegrationPluginPcElectric + + + The network device discovery is not available. + + + + + PcElectric + + + Active phases + The name of the StateType ({bca88c23-e940-40c1-afca-eb511fd17aab}) of ThingClass ev11 + + + + + Charging + The name of the StateType ({b7972cd7-471a-46bd-ab99-f49997f12309}) of ThingClass ev11 + + + + + + Charging enabled + The name of the ParamType (ThingClass: ev11, ActionType: power, ID: {c12a7a27-fa56-450c-a1ec-717c868554f2}) +---------- +The name of the StateType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + + + + + Connected + The name of the StateType ({ca8d680c-c2f8-456a-a246-9c6cd64e25a7}) of ThingClass ev11 + + + + + Enable or disable charging + The name of the ActionType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + + + + + Firmware version + The name of the StateType ({142b4276-e2e9-4149-adc4-89d9d3e31117}) of ThingClass ev11 + + + + + Hardware version + The name of the StateType ({b6e65baf-6dcd-4db1-a3dc-962a4c33d157}) of ThingClass ev11 + + + + + LED brightness + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {3a1329a2-84cc-47b9-a6c2-e96fdfd0c454}) + + + + + MAC address + The name of the ParamType (ThingClass: ev11, Type: thing, ID: {0a3f8d12-9d33-4ae2-b763-9568f32e8da1}) + + + + + + Maximum charging current + The name of the ParamType (ThingClass: ev11, ActionType: maxChargingCurrent, ID: {b5bbf23c-06db-463b-bb5c-3aea38e18818}) +---------- +The name of the StateType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + + + + + Maximum offline charging current + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {93654273-c4d3-4389-a81e-c0f065d9cd92}) + + + + + PC Electric + The name of the plugin PcElectric ({aa7ff833-a8e0-45cc-a1ef-65f05871f272}) + + + + + PC Electric GmbH + The name of the vendor ({b365937b-f1d6-46bf-9ff1-e787373b8aa6}) + + + + + PCE EV11.X + The name of the ThingClass ({88d96940-a940-4a07-8176-5e6aba7ca832}) + + + + + Plugged in + The name of the StateType ({50164bbd-9802-4cf6-82de-626b74293a1b}) of ThingClass ev11 + + + + + Set maximum charging current + The name of the ActionType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + + + +