diff --git a/debian/control b/debian/control index eefc9d4..7eab6a1 100644 --- a/debian/control +++ b/debian/control @@ -136,7 +136,17 @@ Section: libs Depends: ${shlibs:Depends}, ${misc:Depends} Description: nymea.io plugin for Kostal Solar inverters - This package will install the nymea.io plugin for Kostal Solar inverters + This package contains the nymea.io plugin for Kostal Solar inverters + + +Package: nymea-plugin-mennekes +Architecture: any +Multi-Arch: same +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends} +Description: nymea.io plugin for Mennekes wallboxes + This package contains the nymea.io plugin for Mennekes wallboxes Package: nymea-plugin-modbuscommander diff --git a/debian/nymea-plugin-mennekes.install.in b/debian/nymea-plugin-mennekes.install.in new file mode 100644 index 0000000..af89e83 --- /dev/null +++ b/debian/nymea-plugin-mennekes.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginmennekes.so +mennekes/translations/*qm usr/share/nymea/translations/ diff --git a/libnymea-modbus/tools/connectiontool/modbustcp.py b/libnymea-modbus/tools/connectiontool/modbustcp.py index 6513107..ae3f11e 100644 --- a/libnymea-modbus/tools/connectiontool/modbustcp.py +++ b/libnymea-modbus/tools/connectiontool/modbustcp.py @@ -79,7 +79,7 @@ def writePropertyGetSetMethodImplementationsTcp(fileDescriptor, className, regis def writePropertyUpdateMethodImplementationsTcp(fileDescriptor, className, registerDefinitions): for registerDefinition in registerDefinitions: - if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': + if not 'readSchedule' in registerDefinition or registerDefinition['readSchedule'] == 'init': continue propertyName = registerDefinition['id'] diff --git a/mennekes/Modbus_AMTRON HCC3_v01_2021-06-25_en.pdf b/mennekes/Modbus_AMTRON HCC3_v01_2021-06-25_en.pdf new file mode 100644 index 0000000..fef1a07 Binary files /dev/null and b/mennekes/Modbus_AMTRON HCC3_v01_2021-06-25_en.pdf differ diff --git a/mennekes/Modbus_AMTRON_ACU_modbus_tcp_server_spec_rev_1.07.pdf b/mennekes/Modbus_AMTRON_ACU_modbus_tcp_server_spec_rev_1.07.pdf new file mode 100644 index 0000000..c5f9d9c Binary files /dev/null and b/mennekes/Modbus_AMTRON_ACU_modbus_tcp_server_spec_rev_1.07.pdf differ diff --git a/mennekes/README.md b/mennekes/README.md new file mode 100644 index 0000000..091497c --- /dev/null +++ b/mennekes/README.md @@ -0,0 +1,23 @@ +# MENNEKES + +Connects nymea to a MENNEKES wallboxes. Currently supported models are: + +* Amtron Xtra +* Amtron Premium +* Amtron Professional +* Amtron Charge Control +* Amedio Professional + +# Requirements + +nymea uses the Modbus TCP connection to connect to the wallbox. + +> The Modbus TCP connection needs to be enabled manually on the Wallbox. + +For the Amtron Charge Control and Premium models, log in to the wallbox's web interface as operator. The login credentials can be obtained +from the user manual of the wallbox. Once logged in, navigate to the Load Management tab and set the Modbus TCP Server to On. + +## More + +It is highly recommended to update the wallbox to the latest firmware which can be downloaded from [here](https://www.chargeupyourday.de/services/software-updates/). + diff --git a/mennekes/amtron-ecu-registers.json b/mennekes/amtron-ecu-registers.json new file mode 100644 index 0000000..3f95959 --- /dev/null +++ b/mennekes/amtron-ecu-registers.json @@ -0,0 +1,302 @@ +{ + "className": "AmtronECU", + "protocol": "TCP", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 20, + "checkReachableRegister": "cpSignalState", + "enums": [ + { + "name": "CPSignalState", + "values": [ + { + "key": "A", + "value": 1 + }, + { + "key": "B", + "value": 2 + }, + { + "key": "C", + "value": 3 + }, + { + "key": "D", + "value": 4 + }, + { + "key": "E", + "value": 5 + } + ] + } + ], + "blocks": [ + { + "id": "consumptions", + "readSchedule": "update", + "registers": [ + { + "id": "meterEnergyL1", + "address": 200, + "size": 2, + "type": "uint32", + "unit": "Wh", + "registerType": "holdingRegister", + "description": "Meter energy L1", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterEnergyL2", + "address": 202, + "size": 2, + "type": "uint32", + "unit": "Wh", + "registerType": "holdingRegister", + "description": "Meter energy L2", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterEnergyL3", + "address": 204, + "size": 2, + "type": "uint32", + "unit": "Wh", + "registerType": "holdingRegister", + "description": "Meter energy L3", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterPowerL1", + "address": 206, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter power L1", + "unit": "W", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "meterPowerL2", + "address": 208, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter power L2", + "unit": "W", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "meterPowerL3", + "address": 210, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter power L3", + "unit": "W", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "meterCurrentL1", + "address": 212, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter current L1", + "unit": "mA", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterCurrentL2", + "address": 214, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter current L2", + "unit": "mA", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterCurrentL3", + "address": 216, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter current L3", + "unit": "mA", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterTotoalEnergy", + "address": 218, + "size": 2, + "type": "uint32", + "unit": "Wh", + "registerType": "holdingRegister", + "description": "Meter total energy", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterTotalPower", + "address": 220, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter totoal power", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterVoltageL1", + "address": 222, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter voltage L1", + "unit": "V", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterVoltageL2", + "address": 224, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter voltage L2", + "unit": "V", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterVoltageL3", + "address": 226, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Meter voltage L3", + "unit": "V", + "defaultValue": "0", + "access": "RO" + } + ] + } + ], + "registers": [ + { + "id": "firmwareVersion", + "address": 100, + "size": 2, + "type": "uint32", + "readSchedule": "init", + "registerType": "holdingRegister", + "description": "Firmware version", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "cpSignalState", + "address": 122, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "CP signal state", + "enum": "CPSignalState", + "defaultValue": "CPSignalStateA", + "access": "RO" + }, + { + "id": "cpAvailability", + "address": 124, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Charge Point availability", + "defaultValue": 0, + "access": "RW" + }, + { + "id": "model", + "address": 142, + "size": 10, + "type": "string", + "readSchedule": "init", + "registerType": "holdingRegister", + "description": "Device model", + "access": "RO" + }, + { + "id": "signalledCurrent", + "address": 706, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Signalled current to EV", + "unit": "A", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "minCurrentLimit", + "address": 712, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Minimum current limit", + "unit": "A", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "maxCurrentLimit", + "address": 715, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Maximum current limit", + "unit": "A", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargedEnergy", + "address": 716, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Charged energy for current session", + "unit": "Wh", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "hemsCurrentLimit", + "address": 1000, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "HEMS current limit", + "unit": "A", + "defaultValue": "0", + "access": "RW" + } + ] +} diff --git a/mennekes/amtron-hcc3-registers.json b/mennekes/amtron-hcc3-registers.json new file mode 100644 index 0000000..c17a910 --- /dev/null +++ b/mennekes/amtron-hcc3-registers.json @@ -0,0 +1,368 @@ +{ + "className": "AmtronHCC3", + "protocol": "TCP", + "endianness": "LittleEndian", + "errorLimitUntilNotReachable": 20, + "checkReachableRegister": "customerCurrentLimitation", + "enums": [ + { + "name": "CPSignalState", + "values": [ + { + "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": "D1", + "value": 7 + }, + { + "key": "D2", + "value": 8 + } + ] + }, + { + "name": "PPState", + "values": [ + { + "key": "Illegal", + "value": 0 + }, + { + "key": "Open", + "value": 1 + }, + { + "key": "13A", + "value": 2 + }, + { + "key": "20A", + "value": 3 + }, + { + "key": "32A", + "value": 4 + } + ] + }, + { + "name": "ChargeState", + "values": [ + { + "key": "Pause", + "value": 1 + }, + { + "key": "Continue", + "value": 2 + }, + { + "key": "Terminate", + "value": 3 + }, + { + "key": "Start", + "value": 4 + } + ] + }, + { + "name": "HCC3ErrorCode", + "values": [ + { + "key": "NoError", + "value": 0 + }, + { + "key": "InstallationFault", + "value": 10 + }, + { + "key": "ControllerFault", + "value": 11 + }, + { + "key": "Misconfiguration", + "value": 12 + }, + { + "key": "Overtemperature", + "value": 13 + }, + { + "key": "MirrorContactorError", + "value": 14 + }, + { + "key": "InvalidDeviceTime", + "value": 15 + }, + { + "key": "EnergyManagerConnectionError", + "value": 16 + }, + { + "key": "DeviceStartup", + "value": 30 + }, + { + "key": "InternalTestNotPassed", + "value": 31 + }, + { + "key": "HMINoConnection", + "value": 32 + }, + { + "key": "BadlyPluggedCable", + "value": 50 + }, + { + "key": "WrongCable", + "value": 51 + }, + { + "key": "DefectCable", + "value": 52 + }, + { + "key": "ACUCommunicationError", + "value": 100 + }, + { + "key": "NotPolledByACU", + "value": 101 + }, + { + "key": "Maintenance", + "value": 102 + }, + { + "key": "Disabled", + "value": 103 + }, + { + "key": "UnknownError", + "value": 255 + } + ] + }, + { + "name": "AmtronState", + "values": [ + { + "key": "Idle", + "value": 0 + }, + { + "key": "StandByAuthorize", + "value": 1 + }, + { + "key": "StandbyConnect", + "value": 2 + }, + { + "key": "Charging", + "value": 3 + }, + { + "key": "Paused", + "value": 4 + }, + { + "key": "Terminated", + "value": 5 + }, + { + "key": "Error", + "value": 6 + } + ] + } + ], + "blocks": [ + { + "id": "states", + "readSchedule": "update", + "registers": [ + { + "id": "cpSignalState", + "address": 770, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "CP signal state", + "enum": "CPSignalState", + "defaultValue": "CPSignalStateA1", + "access": "RO" + }, + { + "id": "ppState", + "address": 771, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "PP state", + "enum": "PPState", + "defaultValue": "PPStateIllegal", + "access": "RO" + }, + { + "id": "hcc3ErrorCode", + "address": 772, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "HCC3 Error Code", + "enum": "HCC3ErrorCode", + "defaultValue": "HCC3ErrorCodeNoError", + "access": "RO" + }, + { + "id": "amtronState", + "address": 773, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "AMTRON state", + "enum": "AmtronState", + "defaultValue": "AmtronStateIdle", + "access": "RO" + } + ] + }, + { + "id": "maxValues", + "readSchedule": "update", + "registers": [ + { + "id": "phaseCount", + "address": 776, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Phase count", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "ratedCurrent", + "address": 777, + "size": 1, + "type": "uint16", + "unit": "A", + "registerType": "inputRegister", + "description": "Rated Current", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "installationCurrent", + "address": 778, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Installation current", + "access": "RO" + } + ] + }, + { + "id": "consumptions", + "readSchedule": "update", + "registers": [ + { + "id": "chargingSessionMeter", + "address": 781, + "size": 2, + "type": "uint32", + "unut": "Wh", + "registerType": "inputRegister", + "description": "Charging session meter count", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "actualPowerConsumption", + "address": 783, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Actual power consumption", + "unit": "W", + "defaultValue": "0", + "access": "RO" + } + ] + } + ], + "registers": [ + { + "id": "serialNumber", + "address": 779, + "size": 2, + "type": "uint32", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Serial number", + "unit": "", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "name", + "address": 785, + "size": 11, + "type": "string", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Wallbox name", + "access": "RO" + }, + { + "id": "customerCurrentLimitation", + "address": 1024, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Customer Current Limitation", + "unit": "A", + "defaultValue": "0", + "access": "RW" + }, + { + "id": "changeChargeState", + "address": 1025, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Change charge state", + "enum": "ChargeState", + "access": "WO" + } + ] +} diff --git a/mennekes/amtronecudiscovery.cpp b/mennekes/amtronecudiscovery.cpp new file mode 100644 index 0000000..e97d17a --- /dev/null +++ b/mennekes/amtronecudiscovery.cpp @@ -0,0 +1,135 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 "amtronecudiscovery.h" +#include "extern-plugininfo.h" + +AmtronECUDiscovery::AmtronECUDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : + QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery} +{ + m_gracePeriodTimer.setSingleShot(true); + m_gracePeriodTimer.setInterval(3000); + connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ + qCDebug(dcMennekes()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); +} + +void AmtronECUDiscovery::startDiscovery() +{ + qCInfo(dcMennekes()) << "Discovery: Searching for AMTRON wallboxes in the network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &AmtronECUDiscovery::checkNetworkDevice); + + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcMennekes()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_gracePeriodTimer.start(); + discoveryReply->deleteLater(); + }); +} + +QList AmtronECUDiscovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void AmtronECUDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + if (networkDeviceInfo.macAddressManufacturer() != "GIGA-BYTE TECHNOLOGY CO.,LTD.") { + return; + } + + int port = 502; + int slaveId = 0xff; + qCDebug(dcMennekes()) << "Checking network device:" << networkDeviceInfo << "Port:" << port << "Slave ID:" << slaveId; + + AmtronECUModbusTcpConnection *connection = new AmtronECUModbusTcpConnection(networkDeviceInfo.address(), port, slaveId, this); + m_connections.append(connection); + + connect(connection, &AmtronECUModbusTcpConnection::reachableChanged, this, [=](bool reachable){ + if (!reachable) { + cleanupConnection(connection); + return; + } + + connect(connection, &AmtronECUModbusTcpConnection::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcMennekes()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + return; + } + Result result; + result.firmwareVersion = connection->firmwareVersion(); + result.model = connection->model(); + result.networkDeviceInfo = networkDeviceInfo; + m_discoveryResults.append(result); + + qCDebug(dcMennekes()) << "Discovery: Found wallbox with firmware version:" << result.firmwareVersion << result.networkDeviceInfo; + + cleanupConnection(connection); + }); + + if (!connection->initialize()) { + qCDebug(dcMennekes()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + } + }); + + connect(connection, &AmtronECUModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(dcMennekes()) << "Discovery: Checking reachability failed on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + }); + + connection->connectDevice(); +} + +void AmtronECUDiscovery::cleanupConnection(AmtronECUModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void AmtronECUDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (AmtronECUModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcMennekes()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() + << "AMTRON wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + + emit discoveryFinished(); +} diff --git a/mennekes/amtronecudiscovery.h b/mennekes/amtronecudiscovery.h new file mode 100644 index 0000000..5a997ad --- /dev/null +++ b/mennekes/amtronecudiscovery.h @@ -0,0 +1,75 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 AMTRONECUDISCOVERY_H +#define AMTRONECUDISCOVERY_H + +#include +#include + +#include + +#include "amtronecumodbustcpconnection.h" + +class AmtronECUDiscovery : public QObject +{ + Q_OBJECT +public: + explicit AmtronECUDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + struct Result { + QString firmwareVersion; + QString model; + NetworkDeviceInfo networkDeviceInfo; + }; + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + + QTimer m_gracePeriodTimer; + QDateTime m_startDateTime; + + QList m_connections; + + QList m_discoveryResults; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(AmtronECUModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // AMTRONECUDISCOVERY_H diff --git a/mennekes/amtronhcc3discovery.cpp b/mennekes/amtronhcc3discovery.cpp new file mode 100644 index 0000000..81104d6 --- /dev/null +++ b/mennekes/amtronhcc3discovery.cpp @@ -0,0 +1,143 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 "amtronhcc3discovery.h" +#include "extern-plugininfo.h" + +AmtronHCC3Discovery::AmtronHCC3Discovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : + QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery} +{ + m_gracePeriodTimer.setSingleShot(true); + m_gracePeriodTimer.setInterval(3000); + connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ + qCDebug(dcMennekes()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); +} + +void AmtronHCC3Discovery::startDiscovery() +{ + qCInfo(dcMennekes()) << "Discovery: Searching for AMTRON wallboxes in the network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &AmtronHCC3Discovery::checkNetworkDevice); + + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcMennekes()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_gracePeriodTimer.start(); + discoveryReply->deleteLater(); + }); +} + +QList AmtronHCC3Discovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void AmtronHCC3Discovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + if (networkDeviceInfo.macAddressManufacturer() != "GIGA-BYTE TECHNOLOGY CO.,LTD.") { + return; + } + + int port = 502; + int slaveId = 0xff; + qCDebug(dcMennekes()) << "Checking network device:" << networkDeviceInfo << "Port:" << port << "Slave ID:" << slaveId; + + AmtronHCC3ModbusTcpConnection *connection = new AmtronHCC3ModbusTcpConnection(networkDeviceInfo.address(), port, slaveId, this); + m_connections.append(connection); + + connect(connection, &AmtronHCC3ModbusTcpConnection::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, &AmtronHCC3ModbusTcpConnection::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcMennekes()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + return; + } + AmtronDiscoveryResult result; + result.wallboxName = connection->name(); + result.serialNumber = connection->serialNumber(); + result.networkDeviceInfo = networkDeviceInfo; + m_discoveryResults.append(result); + + qCDebug(dcMennekes()) << "Discovery: --> Found" << result.wallboxName + << "Serial number:" << result.serialNumber + << result.networkDeviceInfo; + + + // Done with this connection + cleanupConnection(connection); + }); + + if (!connection->initialize()) { + qCDebug(dcMennekes()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + } + }); + + // If check reachability failed...skip this host... + connect(connection, &AmtronHCC3ModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(dcMennekes()) << "Discovery: Checking reachability failed on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + }); + + // Try to connect, maybe it works, maybe not... + connection->connectDevice(); +} + +void AmtronHCC3Discovery::cleanupConnection(AmtronHCC3ModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void AmtronHCC3Discovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (AmtronHCC3ModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcMennekes()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() + << "AMTRON wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + + emit discoveryFinished(); +} diff --git a/mennekes/amtronhcc3discovery.h b/mennekes/amtronhcc3discovery.h new file mode 100644 index 0000000..2d5b8d4 --- /dev/null +++ b/mennekes/amtronhcc3discovery.h @@ -0,0 +1,75 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 AMTRONHCC3DISCOVERY_H +#define AMTRONHCC3DISCOVERY_H + +#include +#include + +#include + +#include "amtronhcc3modbustcpconnection.h" + +class AmtronHCC3Discovery : public QObject +{ + Q_OBJECT +public: + explicit AmtronHCC3Discovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + typedef struct AmtronDiscoveryResult { + QString wallboxName; + QString serialNumber; + NetworkDeviceInfo networkDeviceInfo; + } AmtronDiscoveryResult; + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + + QTimer m_gracePeriodTimer; + QDateTime m_startDateTime; + + QList m_connections; + + QList m_discoveryResults; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(AmtronHCC3ModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // AMTRONHCC3DISCOVERY_H diff --git a/mennekes/integrationpluginmennekes.cpp b/mennekes/integrationpluginmennekes.cpp new file mode 100644 index 0000000..cfa4545 --- /dev/null +++ b/mennekes/integrationpluginmennekes.cpp @@ -0,0 +1,520 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 "integrationpluginmennekes.h" +#include "plugininfo.h" +#include "amtronecudiscovery.h" +#include "amtronhcc3discovery.h" + +#include +#include + +IntegrationPluginMennekes::IntegrationPluginMennekes() +{ + +} + +void IntegrationPluginMennekes::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcMennekes()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; + } + + if (info->thingClassId() == amtronECUThingClassId) { + AmtronECUDiscovery *discovery = new AmtronECUDiscovery(hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &AmtronECUDiscovery::discoveryFinished, info, [=](){ + foreach (const AmtronECUDiscovery::Result &result, discovery->discoveryResults()) { + + QString name = "AMTRON Charge Control/Professional"; + QString description = result.model.isEmpty() ? result.networkDeviceInfo.address().toString() : + result.model + " (" + result.networkDeviceInfo.address().toString() + ")"; + if (result.model.startsWith("CC")) { + name = "AMTRON Charge Control"; + } else if (result.model.startsWith("P")) { + name = "AMTRON Professional"; + } + ThingDescriptor descriptor(amtronECUThingClassId, name, description); + qCDebug(dcMennekes()) << "Discovered:" << descriptor.title() << descriptor.description(); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(amtronECUThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcMennekes()) << "This wallbox already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(amtronECUThingMacAddressParamTypeId, 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); + }); + discovery->startDiscovery(); + + } else if (info->thingClassId() == amtronHCC3ThingClassId) { + AmtronHCC3Discovery *discovery = new AmtronHCC3Discovery(hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &AmtronHCC3Discovery::discoveryFinished, info, [=](){ + foreach (const AmtronHCC3Discovery::AmtronDiscoveryResult &result, discovery->discoveryResults()) { + + ThingDescriptor descriptor(amtronHCC3ThingClassId, result.wallboxName, "Serial: " + result.serialNumber + " - " + result.networkDeviceInfo.address().toString()); + qCDebug(dcMennekes()) << "Discovered:" << descriptor.title() << descriptor.description(); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(amtronHCC3ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcMennekes()) << "This wallbox already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(amtronHCC3ThingMacAddressParamTypeId, 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); + }); + discovery->startDiscovery(); + } +} + +void IntegrationPluginMennekes::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcMennekes()) << "Setup" << thing << thing->params(); + + if (thing->thingClassId() == amtronECUThingClassId) { + + if (m_amtronECUConnections.contains(thing)) { + qCDebug(dcMennekes()) << "Reconfiguring existing thing" << thing->name(); + m_amtronECUConnections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + MacAddress macAddress = MacAddress(thing->paramValue(amtronECUThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcMennekes()) << "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); + + QHostAddress address = monitor->networkDeviceInfo().address(); + if (address.isNull()) { + qCWarning(dcMennekes()) << "Cannot set up thing. The host address is not known yet..."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again.")); + return; + } + + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + if (m_monitors.contains(thing)) { + qCDebug(dcMennekes()) << "Unregistering monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + if (monitor->reachable()) { + setupAmtronECUConnection(info); + } else { + qCDebug(dcMennekes()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "..."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcMennekes()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup..."; + setupAmtronECUConnection(info); + } + }); + } + + return; + } + + if (info->thing()->thingClassId() == amtronHCC3ThingClassId) { + if (m_amtronHCC3Connections.contains(thing)) { + qCDebug(dcMennekes()) << "Reconfiguring existing thing" << thing->name(); + m_amtronHCC3Connections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + MacAddress macAddress = MacAddress(thing->paramValue(amtronHCC3ThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcMennekes()) << "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); + + QHostAddress address = monitor->networkDeviceInfo().address(); + if (address.isNull()) { + qCWarning(dcMennekes()) << "Cannot set up thing. The host address is not known yet..."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again.")); + return; + } + + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + if (m_monitors.contains(thing)) { + qCDebug(dcMennekes()) << "Unregistering monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + if (monitor->reachable()) { + setupAmtronHCC3Connection(info); + } else { + qCDebug(dcMennekes()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "..."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcMennekes()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup..."; + setupAmtronHCC3Connection(info); + } + }); + } + + return; + + } +} + +void IntegrationPluginMennekes::postSetupThing(Thing *thing) +{ + Q_UNUSED(thing) + if (!m_pluginTimer) { + qCDebug(dcMennekes()) << "Starting plugin timer..."; + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { + foreach(AmtronECUModbusTcpConnection *connection, m_amtronECUConnections) { + qCDebug(dcMennekes()) << "Updating connection" << connection->hostAddress().toString(); + connection->update(); + } + foreach(AmtronHCC3ModbusTcpConnection *connection, m_amtronHCC3Connections) { + qCDebug(dcMennekes()) << "Updating connection" << connection->hostAddress().toString(); + connection->update(); + } + }); + + m_pluginTimer->start(); + } +} + +void IntegrationPluginMennekes::executeAction(ThingActionInfo *info) +{ + if (info->thing()->thingClassId() == amtronECUThingClassId) { + AmtronECUModbusTcpConnection *amtronECUConnection = m_amtronECUConnections.value(info->thing()); + + if (info->action().actionTypeId() == amtronECUPowerActionTypeId) { + bool power = info->action().paramValue(amtronECUPowerActionPowerParamTypeId).toBool(); + QModbusReply *reply = amtronECUConnection->setHemsCurrentLimit(power ? info->thing()->stateValue(amtronECUMaxChargingCurrentStateTypeId).toUInt() : 0); + connect(reply, &QModbusReply::finished, info, [info, reply, power](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(amtronECUPowerStateTypeId, power); + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcMennekes()) << "Error setting cp availability:" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + } + if (info->action().actionTypeId() == amtronECUMaxChargingCurrentActionTypeId) { + int maxChargingCurrent = info->action().paramValue(amtronECUMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt(); + QModbusReply *reply = amtronECUConnection->setHemsCurrentLimit(maxChargingCurrent); + connect(reply, &QModbusReply::finished, info, [info, reply, maxChargingCurrent](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(amtronECUMaxChargingCurrentStateTypeId, maxChargingCurrent); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + } + } +} + +void IntegrationPluginMennekes::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == amtronECUThingClassId && m_amtronECUConnections.contains(thing)) { + AmtronECUModbusTcpConnection *connection = m_amtronECUConnections.take(thing); + delete connection; + } + + if (thing->thingClassId() == amtronHCC3ThingClassId && m_amtronHCC3Connections.contains(thing)) { + AmtronHCC3ModbusTcpConnection *connection = m_amtronHCC3Connections.take(thing); + delete connection; + } + + // Unregister related hardware resources + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + + if (myThings().isEmpty() && m_pluginTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } +} + +void IntegrationPluginMennekes::updateECUPhaseCount(Thing *thing) +{ + AmtronECUModbusTcpConnection *amtronECUConnection = m_amtronECUConnections.value(thing); + int phaseCount = 0; + qCDebug(dcMennekes()) << "Phases: L1" << amtronECUConnection->meterCurrentL1() << "L2" << amtronECUConnection->meterCurrentL2() << "L3" << amtronECUConnection->meterCurrentL3(); + // the current idles on some 5 - 10 mA when not charging... + // We want to detect the phases we're actually charging on. Checking the current flow for that if it's > 500mA + // If no phase is charging, let's count all phases that are not 0 instead (to determine how many phases are connected at the wallbox) + + if (amtronECUConnection->meterCurrentL1() > 500) { + phaseCount++; + } + if (amtronECUConnection->meterCurrentL2() > 500) { + phaseCount++; + } + if (amtronECUConnection->meterCurrentL3() > 500) { + phaseCount++; + } + qCDebug(dcMennekes()) << "Actively charging phases:" << phaseCount; + if (phaseCount == 0) { + if (amtronECUConnection->meterCurrentL1() > 0) { + phaseCount++; + } + if (amtronECUConnection->meterCurrentL2() > 0) { + phaseCount++; + } + if (amtronECUConnection->meterCurrentL3() > 0) { + phaseCount++; + } + qCDebug(dcMennekes()) << "Connected phases:" << phaseCount; + } + + thing->setStateValue(amtronECUPhaseCountStateTypeId, phaseCount); +} + +void IntegrationPluginMennekes::setupAmtronECUConnection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); + + qCDebug(dcMennekes()) << "Setting up amtron wallbox on" << address.toString(); + AmtronECUModbusTcpConnection *amtronECUConnection = new AmtronECUModbusTcpConnection(address, 502, 0xff, this); + connect(info, &ThingSetupInfo::aborted, amtronECUConnection, &ModbusTCPMaster::deleteLater); + + // Reconnect on monitor reachable changed + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + qCDebug(dcMennekes()) << "Network device monitor reachable changed for" << thing->name() << reachable; + if (!thing->setupComplete()) + return; + + if (reachable && !thing->stateValue("connected").toBool()) { + amtronECUConnection->setHostAddress(monitor->networkDeviceInfo().address()); + amtronECUConnection->connectDevice(); + } else if (!reachable) { + // Note: We disable autoreconnect explicitly and we will + // connect the device once the monitor says it is reachable again + amtronECUConnection->disconnectDevice(); + } + }); + + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::reachableChanged, thing, [thing, amtronECUConnection](bool reachable){ + qCDebug(dcMennekes()) << "Reachable changed to" << reachable << "for" << thing; + if (reachable) { + amtronECUConnection->initialize(); + } else { + thing->setStateValue(amtronECUConnectedStateTypeId, false); + } + }); + + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::initializationFinished, thing, [=](bool success){ + if (!thing->setupComplete()) + return; + + if (success) { + thing->setStateValue(amtronECUConnectedStateTypeId, true); + } else { + thing->setStateValue(amtronECUConnectedStateTypeId, false); + // Try once to reconnect the device + amtronECUConnection->reconnectDevice(); + } + }); + + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::initializationFinished, info, [=](bool success){ + if (!success) { + qCWarning(dcMennekes()) << "Connection init finished with errors" << thing->name() << amtronECUConnection->hostAddress().toString(); + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); + amtronECUConnection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error communicating with the wallbox.")); + return; + } + + qCDebug(dcMennekes()) << "Connection init finished successfully" << amtronECUConnection; + m_amtronECUConnections.insert(thing, amtronECUConnection); + info->finish(Thing::ThingErrorNoError); + + thing->setStateValue(amtronECUConnectedStateTypeId, true); + + amtronECUConnection->update(); + }); + + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::updateFinished, thing, [this, amtronECUConnection, thing](){ + qCDebug(dcMennekes()) << "Amtron ECU update finished:" << thing->name() << amtronECUConnection; + updateECUPhaseCount(thing); + }); + + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::cpSignalStateChanged, thing, [thing](AmtronECUModbusTcpConnection::CPSignalState cpSignalState) { + qCDebug(dcMennekes()) << "CP signal state changed" << cpSignalState; + thing->setStateValue(amtronECUPluggedInStateTypeId, cpSignalState >= AmtronECUModbusTcpConnection::CPSignalStateB); + }); + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::signalledCurrentChanged, thing, [](quint16 signalledCurrent) { + qCDebug(dcMennekes()) << "Signalled current changed:" << signalledCurrent; + }); + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::minCurrentLimitChanged, thing, [thing](quint16 minCurrentLimit) { + qCDebug(dcMennekes()) << "min current limit changed:" << minCurrentLimit; + thing->setStateMinValue(amtronECUMaxChargingCurrentStateTypeId, minCurrentLimit); + }); + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::maxCurrentLimitChanged, thing, [thing](quint16 maxCurrentLimit) { + qCDebug(dcMennekes()) << "max current limit changed:" << maxCurrentLimit; + thing->setStateMaxValue(amtronECUMaxChargingCurrentStateTypeId, maxCurrentLimit); + }); + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::hemsCurrentLimitChanged, thing, [thing](quint16 hemsCurrentLimit) { + qCDebug(dcMennekes()) << "HEMS current limit changed:" << hemsCurrentLimit; + if (hemsCurrentLimit == 0) { + thing->setStateValue(amtronECUPowerStateTypeId, false); + } else { + thing->setStateValue(amtronECUPowerStateTypeId, true); + thing->setStateValue(amtronECUMaxChargingCurrentStateTypeId, hemsCurrentLimit); + } + }); + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::meterTotoalEnergyChanged, thing, [thing](quint32 meterTotalEnergy) { + qCDebug(dcMennekes()) << "meter total energy changed:" << meterTotalEnergy; + thing->setStateValue(amtronECUTotalEnergyConsumedStateTypeId, qRound(meterTotalEnergy / 10.0) / 100.0); // rounded to 2 as it changes on every update + }); + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::meterTotalPowerChanged, thing, [thing](quint32 meterTotalPower) { + qCDebug(dcMennekes()) << "meter total power changed:" << meterTotalPower; + thing->setStateValue(amtronECUCurrentPowerStateTypeId, meterTotalPower); + thing->setStateValue(amtronECUChargingStateTypeId, meterTotalPower > 0); + }); + connect(amtronECUConnection, &AmtronECUModbusTcpConnection::chargedEnergyChanged, thing, [thing](quint32 chargedEnergy) { + qCDebug(dcMennekes()) << "charged energy changed:" << chargedEnergy; + thing->setStateValue(amtronECUSessionEnergyStateTypeId, qRound(chargedEnergy / 10.0) / 100.0); // rounded to 2 as it changes on every update + }); + + amtronECUConnection->connectDevice(); +} + +void IntegrationPluginMennekes::setupAmtronHCC3Connection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); + + qCDebug(dcMennekes()) << "Setting up amtron wallbox on" << address.toString(); + AmtronHCC3ModbusTcpConnection *amtronHCC3Connection = new AmtronHCC3ModbusTcpConnection(address, 502, 0xff, this); + connect(info, &ThingSetupInfo::aborted, amtronHCC3Connection, &ModbusTCPMaster::deleteLater); + + // Reconnect on monitor reachable changed + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + qCDebug(dcMennekes()) << "Network device monitor reachable changed for" << thing->name() << reachable; + if (!thing->setupComplete()) + return; + + if (reachable && !thing->stateValue("connected").toBool()) { + amtronHCC3Connection->setHostAddress(monitor->networkDeviceInfo().address()); + amtronHCC3Connection->connectDevice(); + } else if (!reachable) { + // Note: We disable autoreconnect explicitly and we will + // connect the device once the monitor says it is reachable again + amtronHCC3Connection->disconnectDevice(); + } + }); + + connect(amtronHCC3Connection, &AmtronHCC3ModbusTcpConnection::reachableChanged, thing, [thing, amtronHCC3Connection](bool reachable){ + qCDebug(dcMennekes()) << "Reachable changed to" << reachable << "for" << thing; + if (reachable) { + amtronHCC3Connection->initialize(); + } else { + thing->setStateValue(amtronHCC3ConnectedStateTypeId, false); + } + }); + + connect(amtronHCC3Connection, &AmtronHCC3ModbusTcpConnection::initializationFinished, thing, [=](bool success){ + if (!thing->setupComplete()) + return; + + if (success) { + thing->setStateValue(amtronHCC3ConnectedStateTypeId, true); + } else { + thing->setStateValue(amtronHCC3ConnectedStateTypeId, false); + // Try once to reconnect the device + amtronHCC3Connection->reconnectDevice(); + } + }); + + connect(amtronHCC3Connection, &AmtronHCC3ModbusTcpConnection::initializationFinished, info, [=](bool success){ + if (!success) { + qCWarning(dcMennekes()) << "Connection init finished with errors" << thing->name() << amtronHCC3Connection->hostAddress().toString(); + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); + amtronHCC3Connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error communicating with the wallbox.")); + return; + } + + qCDebug(dcMennekes()) << "Connection init finished successfully" << amtronHCC3Connection; + m_amtronHCC3Connections.insert(thing, amtronHCC3Connection); + info->finish(Thing::ThingErrorNoError); + + thing->setStateValue(amtronHCC3ConnectedStateTypeId, true); + + amtronHCC3Connection->update(); + }); + + amtronHCC3Connection->connectDevice(); +} + +bool IntegrationPluginMennekes::ensureAmtronECUVersion(AmtronECUModbusTcpConnection *connection, const QString &version) +{ + QByteArray deviceVersion = QByteArray::fromHex(QByteArray::number(connection->firmwareVersion(), 16)); + return deviceVersion >= version; +} diff --git a/mennekes/integrationpluginmennekes.h b/mennekes/integrationpluginmennekes.h new file mode 100644 index 0000000..caaa1b3 --- /dev/null +++ b/mennekes/integrationpluginmennekes.h @@ -0,0 +1,77 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 INTEGRATIONPLUGINMENNEKES_H +#define INTEGRATIONPLUGINMENNEKES_H + +#include +#include +#include + +#include "extern-plugininfo.h" + +#include "amtronecumodbustcpconnection.h" +#include "amtronhcc3modbustcpconnection.h" + +class IntegrationPluginMennekes: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginmennekes.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginMennekes(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private slots: + void updateECUPhaseCount(Thing *thing); + +private: + void setupAmtronECUConnection(ThingSetupInfo *info); + void setupAmtronHCC3Connection(ThingSetupInfo *info); + + bool ensureAmtronECUVersion(AmtronECUModbusTcpConnection *connection, const QString &version); + + PluginTimer *m_pluginTimer = nullptr; + QHash m_amtronECUConnections; + QHash m_amtronHCC3Connections; + QHash m_monitors; + +}; + +#endif // INTEGRATIONPLUGINMENNEKES_H + + diff --git a/mennekes/integrationpluginmennekes.json b/mennekes/integrationpluginmennekes.json new file mode 100644 index 0000000..1092280 --- /dev/null +++ b/mennekes/integrationpluginmennekes.json @@ -0,0 +1,184 @@ +{ + "name": "Mennekes", + "displayName": "Mennekes", + "id": "c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38", + "vendors": [ + { + "name": "mennekes", + "displayName": "MENNEKES", + "id": "7c585571-e3a3-458c-a598-e11f510cbc10", + "thingClasses": [ + { + "name": "amtronECU", + "displayName": "AMTRON Charge Control/Professional", + "id": "fb48e067-2237-4eaf-8d2c-681d406395fc", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "smartmeterconsumer", "connectable"], + "paramTypes": [ + { + "id": "0b9c1466-5eb9-4b25-9450-513e2484a3b4", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "352be84a-f5c6-434d-8a92-a4065a24ff0a", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "c93d7377-8a4a-4c35-9876-1032d8b309e3", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "c8c812c6-dd56-425c-8dd1-8dd621bd3a11", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "a51d0beb-f4fd-4279-85b5-b436b283a86e", + "name": "phaseCount", + "displayName": "Connected phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "8b2eb039-b4e3-49ae-94fc-a8b825fd8d9b", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "5b8bfdf0-eaa6-41d0-8f2f-119b06aed181", + "name": "totalEnergyConsumed", + "displayName": "Total consumed energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0, + "cached": true + }, + { + "id": "53dc845a-a397-4c90-890b-fd50512666f4", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "fb12ff61-f88a-4bfc-930f-a4a55b342d1b", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "defaultValue": 6, + "writable": true + }, + { + "id": "2ce6b363-5b8d-4703-8376-611a0e573f71", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] + }, + { + "name": "amtronHCC3", + "displayName": "AMTRON XTRA/Premium", + "id": "01995c4f-a7b5-4bdd-9333-486390ff75fd", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "smartmeterconsumer", "connectable"], + "paramTypes": [ + { + "id": "6112045e-e472-4475-bc11-f373e9c39cdd", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "899e270f-7666-44b3-8509-0dad43ac9c4c", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "96b9c121-3caf-44fe-8380-483b9b40dbd9", + "name": "currentPower", + "displayName": "Active power", + "displayNameEvent": "Active power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "3d1384fc-8b46-42b0-b043-23279d8c7665", + "name": "totalEnergyConsumed", + "displayName": "Total consumed energy", + "displayNameEvent": "Total consumed energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0, + "cached": true + }, + { + "id": "130585ca-14e9-45b7-87d7-c0d935228104", + "name": "power", + "displayName": "Charging enabled", + "displayNameEvent": "Charging enabled changed", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "87b4e9cb-e57b-461a-90f9-207bb5dab44c", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameEvent": "Maximum charging current changed", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "defaultValue": 6, + "writable": true + } + ] + } + ] + } + ] +} diff --git a/mennekes/mennekes.pro b/mennekes/mennekes.pro new file mode 100644 index 0000000..6f0dc22 --- /dev/null +++ b/mennekes/mennekes.pro @@ -0,0 +1,17 @@ +include(../plugins.pri) + +# Generate modbus connection +MODBUS_CONNECTIONS += amtron-ecu-registers.json \ + amtron-hcc3-registers.json +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + amtronecudiscovery.h \ + amtronhcc3discovery.h \ + integrationpluginmennekes.h + +SOURCES += \ + amtronecudiscovery.cpp \ + amtronhcc3discovery.cpp \ + integrationpluginmennekes.cpp diff --git a/mennekes/mennekes.svg b/mennekes/mennekes.svg new file mode 100644 index 0000000..2b3778b --- /dev/null +++ b/mennekes/mennekes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mennekes/meta.json b/mennekes/meta.json new file mode 100644 index 0000000..e78d48d --- /dev/null +++ b/mennekes/meta.json @@ -0,0 +1,13 @@ +{ + "title": "MENNEKES", + "tagline": "Connect MENNEKES wallboxes to nymea.", + "icon": "mennekes.svg", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/mennekes/translations/c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38-en_US.ts b/mennekes/translations/c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38-en_US.ts new file mode 100644 index 0000000..1ae03de --- /dev/null +++ b/mennekes/translations/c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38-en_US.ts @@ -0,0 +1,165 @@ + + + + + IntegrationPluginMennekes + + + The network device discovery is not available. + + + + + + The MAC address is not known. Please reconfigure the thing. + + + + + + The host address is not known yet. Trying later again. + + + + + + Error communicating with the wallbox. + + + + + Mennekes + + + AMTRON Charge Control/Professional + The name of the ThingClass ({fb48e067-2237-4eaf-8d2c-681d406395fc}) + + + + + AMTRON XTRA/Premium + The name of the ThingClass ({01995c4f-a7b5-4bdd-9333-486390ff75fd}) + + + + + + Active power + The name of the StateType ({96b9c121-3caf-44fe-8380-483b9b40dbd9}) of ThingClass amtronHCC3 +---------- +The name of the StateType ({8b2eb039-b4e3-49ae-94fc-a8b825fd8d9b}) of ThingClass amtronECU + + + + + Charging + The name of the StateType ({c8c812c6-dd56-425c-8dd1-8dd621bd3a11}) of ThingClass amtronECU + + + + + + + + Charging enabled + The name of the ParamType (ThingClass: amtronHCC3, ActionType: power, ID: {130585ca-14e9-45b7-87d7-c0d935228104}) +---------- +The name of the StateType ({130585ca-14e9-45b7-87d7-c0d935228104}) of ThingClass amtronHCC3 +---------- +The name of the ParamType (ThingClass: amtronECU, ActionType: power, ID: {53dc845a-a397-4c90-890b-fd50512666f4}) +---------- +The name of the StateType ({53dc845a-a397-4c90-890b-fd50512666f4}) of ThingClass amtronECU + + + + + + Connected + The name of the StateType ({899e270f-7666-44b3-8509-0dad43ac9c4c}) of ThingClass amtronHCC3 +---------- +The name of the StateType ({352be84a-f5c6-434d-8a92-a4065a24ff0a}) of ThingClass amtronECU + + + + + Connected phases + The name of the StateType ({a51d0beb-f4fd-4279-85b5-b436b283a86e}) of ThingClass amtronECU + + + + + + MAC address + The name of the ParamType (ThingClass: amtronHCC3, Type: thing, ID: {6112045e-e472-4475-bc11-f373e9c39cdd}) +---------- +The name of the ParamType (ThingClass: amtronECU, Type: thing, ID: {0b9c1466-5eb9-4b25-9450-513e2484a3b4}) + + + + + MENNEKES + The name of the vendor ({7c585571-e3a3-458c-a598-e11f510cbc10}) + + + + + + + + Maximum charging current + The name of the ParamType (ThingClass: amtronHCC3, ActionType: maxChargingCurrent, ID: {87b4e9cb-e57b-461a-90f9-207bb5dab44c}) +---------- +The name of the StateType ({87b4e9cb-e57b-461a-90f9-207bb5dab44c}) of ThingClass amtronHCC3 +---------- +The name of the ParamType (ThingClass: amtronECU, ActionType: maxChargingCurrent, ID: {fb12ff61-f88a-4bfc-930f-a4a55b342d1b}) +---------- +The name of the StateType ({fb12ff61-f88a-4bfc-930f-a4a55b342d1b}) of ThingClass amtronECU + + + + + Mennekes + The name of the plugin Mennekes ({c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38}) + + + + + Plugged in + The name of the StateType ({c93d7377-8a4a-4c35-9876-1032d8b309e3}) of ThingClass amtronECU + + + + + Session energy + The name of the StateType ({2ce6b363-5b8d-4703-8376-611a0e573f71}) of ThingClass amtronECU + + + + + + Set charging enabled + The name of the ActionType ({130585ca-14e9-45b7-87d7-c0d935228104}) of ThingClass amtronHCC3 +---------- +The name of the ActionType ({53dc845a-a397-4c90-890b-fd50512666f4}) of ThingClass amtronECU + + + + + + Set maximum charging current + The name of the ActionType ({87b4e9cb-e57b-461a-90f9-207bb5dab44c}) of ThingClass amtronHCC3 +---------- +The name of the ActionType ({fb12ff61-f88a-4bfc-930f-a4a55b342d1b}) of ThingClass amtronECU + + + + + + Total consumed energy + The name of the StateType ({3d1384fc-8b46-42b0-b043-23279d8c7665}) of ThingClass amtronHCC3 +---------- +The name of the StateType ({5b8bfdf0-eaa6-41d0-8f2f-119b06aed181}) of ThingClass amtronECU + + + + diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index a01d925..1176e20 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -12,6 +12,7 @@ PLUGIN_DIRS = \ idm \ inepro \ kostal \ + mennekes \ modbuscommander \ mtec \ mypv \