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
+
+
+
+