diff --git a/debian/control b/debian/control index 05f8045..5eec3ae 100644 --- a/debian/control +++ b/debian/control @@ -127,6 +127,17 @@ Description: nymea integration plugin for inepro Metering Modbus based energy me made by inepro Metering. +Package: nymea-plugin-inro +Architecture: any +Multi-Arch: same +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for INRO PANTABOX chargers + This package contains the nymea integration plugin for Modbus based PANTABOX + support. + + Package: nymea-plugin-huawei Architecture: any Multi-Arch: same diff --git a/debian/nymea-plugin-inro.install.in b/debian/nymea-plugin-inro.install.in new file mode 100644 index 0000000..d507c35 --- /dev/null +++ b/debian/nymea-plugin-inro.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugininro.so +inro/translations/*qm usr/share/nymea/translations/ diff --git a/inro/README.md b/inro/README.md new file mode 100644 index 0000000..df973b1 --- /dev/null +++ b/inro/README.md @@ -0,0 +1,25 @@ +# INRO PANTABOX + +Connects nymea to INRO PANTABOX wallboxes. + +## Requirements + +In order to connect your PANTABOX to the nymea system, the charger must be in the same network as the nymea system. The nymea server uses the Modbus TCP connection to connect to the wallbox. + +Modbus TCP is available since firmware version `V1.13.6` of the PANTABOX. Please make sure your PANTABOX is up to date before setting up the charger in nymea. + +Once the network connection has been established, use the original [PANTABOX app](https://www.pantabox.de/Informationen/App/) in order to enable the `Modbus TCP` profile. + +![Loading profiles](docs/inro-app-profiles.jpg) + +![Modbus TCP profile](docs/inro-app-modbus-active.jpg) + +Once the `Modbus TCP` profile has been enabled, the PANTABOX can be discovered in the network. The default port is `502` and the default slave ID is `1`. + +## Settings + +In the thing settings can be defined, which phases are connected to the wallbox. This information is important for the energy management in order to be able to have an overload protection. + +## More + +https://www.pantabox.de/ diff --git a/inro/docs/inro-app-modbus-active.jpg b/inro/docs/inro-app-modbus-active.jpg new file mode 100644 index 0000000..28de078 Binary files /dev/null and b/inro/docs/inro-app-modbus-active.jpg differ diff --git a/inro/docs/inro-app-profiles.jpg b/inro/docs/inro-app-profiles.jpg new file mode 100644 index 0000000..0ec80fd Binary files /dev/null and b/inro/docs/inro-app-profiles.jpg differ diff --git a/inro/inro.png b/inro/inro.png new file mode 100644 index 0000000..921fe7d Binary files /dev/null and b/inro/inro.png differ diff --git a/inro/inro.pro b/inro/inro.pro new file mode 100644 index 0000000..8abdb2d --- /dev/null +++ b/inro/inro.pro @@ -0,0 +1,15 @@ +include(../plugins.pri) + +MODBUS_CONNECTIONS += pantabox-registers.json + +MODBUS_TOOLS_CONFIG += VERBOSE + +include(../modbus.pri) + +HEADERS += \ + integrationplugininro.h \ + pantaboxdiscovery.h + +SOURCES += \ + integrationplugininro.cpp \ + pantaboxdiscovery.cpp diff --git a/inro/integrationplugininro.cpp b/inro/integrationplugininro.cpp new file mode 100644 index 0000000..5eda2bc --- /dev/null +++ b/inro/integrationplugininro.cpp @@ -0,0 +1,329 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "integrationplugininro.h" +#include "plugininfo.h" + +#include +#include +#include + +#include "pantaboxdiscovery.h" + +IntegrationPluginInro::IntegrationPluginInro() +{ + +} + +void IntegrationPluginInro::discoverThings(ThingDiscoveryInfo *info) +{ + + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcInro()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; + } + + PantaboxDiscovery *discovery = new PantaboxDiscovery(hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &PantaboxDiscovery::discoveryFinished, info, [this, info, discovery](){ + + foreach (const PantaboxDiscovery::Result &result, discovery->results()) { + QString title = QString("PANTABOX - %1").arg(result.serialNumber); + QString description = QString("%1 (%2)").arg(result.networkDeviceInfo.macAddress(), result.networkDeviceInfo.address().toString()); + ThingDescriptor descriptor(pantaboxThingClassId, title, description); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(pantaboxThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcInro()) << "This PANTABOX already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(pantaboxThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + params << Param(pantaboxThingSerialNumberParamTypeId, result.serialNumber); + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + + discovery->startDiscovery(); +} + +void IntegrationPluginInro::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcInro()) << "Setup thing" << thing << thing->params(); + + if (m_connections.contains(thing)) { + qCDebug(dcInro()) << "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(pantaboxThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcInro()) << "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(dcInro()) << "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(dcInro()) << "Network device" << thing->name() << "is not reachable yet. Continue with the setup once reachable."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcInro()) << "Network device" << thing->name() << "is now reachable. Continue with the setup..."; + setupConnection(info); + } + }); + } + } else { + setupConnection(info); + } +} + +void IntegrationPluginInro::postSetupThing(Thing *thing) +{ + qCDebug(dcInro()) << "Post setup thing" << thing->name(); + if (!m_refreshTimer) { + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_refreshTimer, &PluginTimer::timeout, this, [this] { + foreach (PantaboxModbusTcpConnection *connection, m_connections) { + if (connection->reachable()) { + connection->update(); + } + } + }); + + qCDebug(dcInro()) << "Starting refresh timer..."; + m_refreshTimer->start(); + } +} + +void IntegrationPluginInro::executeAction(ThingActionInfo *info) +{ + if (info->thing()->thingClassId() == pantaboxThingClassId) { + + PantaboxModbusTcpConnection *connection = m_connections.value(info->thing()); + + if (!connection->reachable()) { + qCWarning(dcInro()) << "Cannot execute action. The PANTABOX is not reachable"; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (info->action().actionTypeId() == pantaboxPowerActionTypeId) { + bool power = info->action().paramValue(pantaboxPowerActionPowerParamTypeId).toBool(); + qCDebug(dcInro()) << "PANTABOX: Set power" << (power ? 1 : 0); + + QModbusReply *reply = connection->setChargingEnabled(power ? 1 : 0); + if (!reply) { + qCWarning(dcInro()) << "Execute action failed because the reply could not be created."; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply, power](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(pantaboxPowerStateTypeId, power); + qCDebug(dcInro()) << "PANTABOX: Set power finished successfully"; + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcInro()) << "Error setting power:" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + if (info->action().actionTypeId() == pantaboxMaxChargingCurrentActionTypeId) { + quint16 chargingCurrent = info->action().paramValue(pantaboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + qCDebug(dcInro()) << "PANTABOX: Set max charging current" << chargingCurrent << "A"; + + QModbusReply *reply = connection->setMaxChargingCurrent(chargingCurrent); + if (!reply) { + qCWarning(dcInro()) << "Execute action failed because the reply could not be created."; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply, chargingCurrent](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(pantaboxMaxChargingCurrentStateTypeId, chargingCurrent); + qCDebug(dcInro()) << "PANTABOX: Set max charging current finished successfully"; + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcInro()) << "Error setting charging current:" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + } +} + +void IntegrationPluginInro::thingRemoved(Thing *thing) +{ + qCDebug(dcInro()) << "Thing removed" << thing->name(); + + if (m_connections.contains(thing)) { + PantaboxModbusTcpConnection *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(dcInro()) << "Stopping reconnect timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +void IntegrationPluginInro::setupConnection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + + PantaboxModbusTcpConnection *connection = new PantaboxModbusTcpConnection(monitor->networkDeviceInfo().address(), 502, 1, this); + connect(info, &ThingSetupInfo::aborted, connection, &PantaboxModbusTcpConnection::deleteLater); + + // Monitor reachability + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + if (!thing->setupComplete()) + return; + + qCDebug(dcInro()) << "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, &PantaboxModbusTcpConnection::reachableChanged, thing, [thing, connection](bool reachable){ + qCInfo(dcInro()) << "Reachable changed to" << reachable << "for" << thing; + thing->setStateValue("connected", reachable); + + if (!reachable) { + // Reset energy live values on disconnected + thing->setStateValue(pantaboxCurrentPowerStateTypeId, 0); + } else { + thing->setStateValue(pantaboxModbusTcpVersionStateTypeId, PantaboxDiscovery::modbusVersionToString(connection->modbusTcpVersion())); + } + }); + + connect(connection, &PantaboxModbusTcpConnection::updateFinished, thing, [thing, connection](){ + qCDebug(dcInro()) << "Update finished for" << thing; + qCDebug(dcInro()) << connection; + + QString chargingStateString; + switch(connection->chargingState()) { + case PantaboxModbusTcpConnection::ChargingStateA: + chargingStateString = "A"; + break; + case PantaboxModbusTcpConnection::ChargingStateB: + chargingStateString = "B"; + break; + case PantaboxModbusTcpConnection::ChargingStateC: + chargingStateString = "C"; + break; + case PantaboxModbusTcpConnection::ChargingStateD: + chargingStateString = "D"; + break; + case PantaboxModbusTcpConnection::ChargingStateE: + chargingStateString = "E"; + break; + case PantaboxModbusTcpConnection::ChargingStateF: + chargingStateString = "F"; + break; + } + thing->setStateValue(pantaboxChargingStateStateTypeId, chargingStateString); + + // A: not connected + // B: connected, not charging + // C: connected, charging + // D: ventilation required + // E: F: fault/error + thing->setStateValue(pantaboxPluggedInStateTypeId, connection->chargingState() >= PantaboxModbusTcpConnection::ChargingStateB); + thing->setStateValue(pantaboxChargingStateTypeId, connection->chargingState() >= PantaboxModbusTcpConnection::ChargingStateC); + thing->setStateValue(pantaboxCurrentPowerStateTypeId, connection->currentPower()); // W + thing->setStateValue(pantaboxTotalEnergyConsumedStateTypeId, connection->chargedEnergy() / 1000.0); // Wh + thing->setStateMaxValue(pantaboxMaxChargingCurrentActionTypeId, connection->maxPossibleChargingCurrent()); + + // Phase count is a setting, since we don't get the information from the device. + // Maybe we could assume the from current power and set charging current how many phases get used, + // but we could not tell which pashes are active. + + Electricity::Phases phases = Electricity::convertPhasesFromString(thing->setting(pantaboxSettingsPhasesParamTypeId).toString()); + thing->setStateValue(pantaboxPhaseCountStateTypeId, Electricity::getPhaseCount(phases)); + thing->setStateValue(pantaboxUsedPhasesStateTypeId, thing->setting(pantaboxSettingsPhasesParamTypeId).toString()); + + }); + + m_connections.insert(thing, connection); + info->finish(Thing::ThingErrorNoError); + + qCDebug(dcInro()) << "Setting up PANTABOX finished successfully" << monitor->networkDeviceInfo().address().toString(); + + // 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/inro/integrationplugininro.h b/inro/integrationplugininro.h new file mode 100644 index 0000000..3267bdb --- /dev/null +++ b/inro/integrationplugininro.h @@ -0,0 +1,65 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 INTEGRATIONPLUGININRO_H +#define INTEGRATIONPLUGININRO_H + +#include +#include +#include + +#include "extern-plugininfo.h" +#include "pantaboxmodbustcpconnection.h" + +class IntegrationPluginInro: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugininro.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginInro(); + + 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: + PluginTimer *m_refreshTimer = nullptr; + QHash m_connections; + QHash m_monitors; + + void setupConnection(ThingSetupInfo *info); +}; + +#endif // INTEGRATIONPLUGININRO_H diff --git a/inro/integrationplugininro.json b/inro/integrationplugininro.json new file mode 100644 index 0000000..d5e1962 --- /dev/null +++ b/inro/integrationplugininro.json @@ -0,0 +1,155 @@ +{ + "name": "inro", + "displayName": "INRO", + "id": "e2751951-4d53-4156-bd1a-a54c39e5c6cc", + "vendors": [ + { + "name": "inro", + "displayName": "INRO Elektrotechnik GmbH", + "id": "53669eec-4e67-444d-973f-40668aaa37a6", + "thingClasses": [ + { + "name": "pantabox", + "displayName": "PANTABOX", + "id": "6de119fb-a579-4a88-9903-f05aac167b19", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "smartmeterconsumer", "connectable"], + "paramTypes": [ + { + "id": "a3bc042c-0613-40a9-867a-3482d4d0901e", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "defaultValue": "", + "readOnly": true + }, + { + "id": "064e393b-b921-4c9c-9127-8b03852d9687", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "", + "readOnly": true + } + ], + "settingsTypes": [ + { + "id": "78225651-565c-49f7-8610-c067faf8822a", + "name": "phases", + "displayName": "Phases connected", + "type": "QString", + "allowedValues": ["A", "B", "C", "AB", "BC", "AC", "ABC"], + "defaultValue": "ABC" + } + ], + "stateTypes": [ + { + "id": "345909fc-22e2-44bb-a063-26dd8e30793b", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "bb2f4ff0-f7ca-4ffd-a2f1-73f9b230a1eb", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "5a606c6a-030c-4d55-9700-723c9c859c7f", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "969eb83e-a044-46f2-9aab-f113e2cd7d4d", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["A", "B", "C", "D", "E", "F"], + "defaultValue": "A", + "cached": false + }, + { + "id": "e23181a7-0747-4ff2-8b10-90256a8377b3", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "b6e309d4-d480-477a-88f1-79e00bca450a", + "name": "totalEnergyConsumed", + "displayName": "Total consumed energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0, + "cached": true + }, + { + "id": "d885ee23-8c50-48f1-82a9-72e0e92715ac", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "db02bf36-1ce2-40b4-bf88-8a4e3842a63b", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "minValue": 6, + "maxValue": 16, + "defaultValue": 6, + "writable": true + }, + { + "id": "95719ca2-bda2-4eb4-b76d-e54d7a98dfdb", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "5cc3aa90-c83f-4453-9f22-5ef408e847a0", + "name": "phaseCount", + "displayName": "Active phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 3 + }, + { + "id": "db8d452a-8459-429b-b8f1-d73c805bd857", + "name": "usedPhases", + "displayName": "Used phases", + "type": "QString", + "possibleValues": ["A", "B", "C", "AB", "BC", "AC", "ABC"], + "defaultValue": "ABC" + }, + { + "id": "c454b965-8b21-491e-85e9-71068575c5e1", + "name": "modbusTcpVersion", + "displayName": "Modbus TCP version", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + } + ] +} diff --git a/inro/meta.json b/inro/meta.json new file mode 100644 index 0000000..7a2f39c --- /dev/null +++ b/inro/meta.json @@ -0,0 +1,14 @@ +{ + "title": "INRO", + "tagline": "Connect INRO PANTABOX wallboxes to nymea.", + "icon": "inro.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network", + "modbus" + ], + "categories": [ + "energy" + ] +} diff --git a/inro/pantabox-registers.json b/inro/pantabox-registers.json new file mode 100644 index 0000000..05ec69f --- /dev/null +++ b/inro/pantabox-registers.json @@ -0,0 +1,144 @@ +{ + "className": "Pantabox", + "protocol": "TCP", + "endianness": "LittleEndian", + "errorLimitUntilNotReachable": 2, + "checkReachableRegister": "chargingState", + "blocks": [ ], + "enums": [ + { + "name": "ChargingState", + "values": [ + { + "key": "A", + "value": 65 + }, + { + "key": "B", + "value": 66 + }, + { + "key": "C", + "value": 67 + }, + { + "key": "D", + "value": 68 + }, + { + "key": "E", + "value": 69 + }, + { + "key": "F", + "value": 70 + } + ] + } + ], + "registers": [ + { + "id": "serialNumber", + "address": 256, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Serial number (hex)", + "readSchedule": "init", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "modbusTcpVersion", + "address": 258, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "ModbusTCP version", + "readSchedule": "init", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargingState", + "address": 512, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Charging state", + "enum": "ChargingState", + "readSchedule": "update", + "defaultValue": "ChargingStateA", + "access": "RO" + }, + { + "id": "currentPower", + "address": 513, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Current charging power", + "unit": "W", + "readSchedule": "update", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargedEnergy", + "address": 515, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Charged energy", + "unit": "Wh", + "readSchedule": "update", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "maxPossibleChargingCurrent", + "address": 517, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Maximal possible charging current (adapter)", + "unit": "A", + "readSchedule": "update", + "defaultValue": "6", + "access": "RO" + }, + { + "id": "chargingCurrent", + "address": 518, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Actual charging current", + "unit": "A", + "readSchedule": "update", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargingEnabled", + "address": 768, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Charging enabled (1) / disabled (0)", + "defaultValue": 0, + "access": "RW" + }, + { + "id": "maxChargingCurrent", + "address": 769, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Max charging current", + "access": "RW" + } + ] +} diff --git a/inro/pantaboxdiscovery.cpp b/inro/pantaboxdiscovery.cpp new file mode 100644 index 0000000..7c6ccad --- /dev/null +++ b/inro/pantaboxdiscovery.cpp @@ -0,0 +1,152 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "pantaboxdiscovery.h" +#include "extern-plugininfo.h" + +PantaboxDiscovery::PantaboxDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) + : QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery} +{ + +} + +QList PantaboxDiscovery::results() const +{ + return m_results; +} + +QString PantaboxDiscovery::modbusVersionToString(quint32 value) +{ + quint16 modbusVersionMinor = (value >> 8) & 0xffff; + quint16 modbusVersionMajor = value & 0xffff; + return QString("%1.%2").arg(modbusVersionMajor).arg(modbusVersionMinor); +} + +void PantaboxDiscovery::startDiscovery() +{ + qCInfo(dcInro()) << "Discovery: Start searching for PANTABOX wallboxes in the network..."; + m_startDateTime = QDateTime::currentDateTime(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &PantaboxDiscovery::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(dcInro()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); + }); +} + +void PantaboxDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + PantaboxModbusTcpConnection *connection = new PantaboxModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this); + m_connections.append(connection); + + connect(connection, &PantaboxModbusTcpConnection::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, &PantaboxModbusTcpConnection::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcInro()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue..."; + cleanupConnection(connection); + return; + } + + // FIXME: find a better way to discover the device besides a valid init + qCDebug(dcInro()) << "Discovery: Connection initialized successfully" << connection->serialNumber(); + + Result result; + result.serialNumber = QString::number(connection->serialNumber(), 16).toUpper(); + result.modbusTcpVersion = modbusVersionToString(connection->modbusTcpVersion()); + result.networkDeviceInfo = networkDeviceInfo; + m_results.append(result); + + qCInfo(dcInro()) << "Discovery: --> Found" + << "Serial number:" << result.serialNumber + << "(" << connection->serialNumber() << ")" + << "ModbusTCP version:" << result.modbusTcpVersion + << result.networkDeviceInfo; + + // Done with this connection + cleanupConnection(connection); + }); + + // Initializing... + if (!connection->initialize()) { + qCDebug(dcInro()) << "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(dcInro()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue..."; + cleanupConnection(connection); + } + }); + + // If check reachability failed...skip this host... + connect(connection, &PantaboxModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(dcInro()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue..."; + cleanupConnection(connection); + }); + + // Try to connect, maybe it works, maybe not... + connection->connectDevice(); +} + +void PantaboxDiscovery::cleanupConnection(PantaboxModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void PantaboxDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (PantaboxModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcInro()) << "Discovery: Finished the discovery process. Found" << m_results.count() + << "PANTABOXE wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + emit discoveryFinished(); +} diff --git a/inro/pantaboxdiscovery.h b/inro/pantaboxdiscovery.h new file mode 100644 index 0000000..c0364ce --- /dev/null +++ b/inro/pantaboxdiscovery.h @@ -0,0 +1,79 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 PANTABOXDISCOVERY_H +#define PANTABOXDISCOVERY_H + +#include + +#include + +#include "pantaboxmodbustcpconnection.h" + +class PantaboxDiscovery : public QObject +{ + Q_OBJECT +public: + explicit PantaboxDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + + typedef struct Result { + QString serialNumber; + QString modbusTcpVersion; + NetworkDeviceInfo networkDeviceInfo; + } Result; + + QList results() const; + + static QString modbusVersionToString(quint32 value); + +public slots: + void startDiscovery(); + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + quint16 m_port = 502; + quint16 m_modbusAddress = 1; + + QDateTime m_startDateTime; + + QList m_connections; + + QList m_results; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(PantaboxModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // PANTABOXDISCOVERY_H diff --git a/inro/translations/e2751951-4d53-4156-bd1a-a54c39e5c6cc-de.ts b/inro/translations/e2751951-4d53-4156-bd1a-a54c39e5c6cc-de.ts new file mode 100644 index 0000000..75afd83 --- /dev/null +++ b/inro/translations/e2751951-4d53-4156-bd1a-a54c39e5c6cc-de.ts @@ -0,0 +1,170 @@ + + + + + IntegrationPluginInro + + + The network device discovery is not available. + Die Suche nach Netzwerkgeräten ist nicht verfügbar. + + + + The MAC address is not known. Please reconfigure the thing. + Die MAC-Adresse ist nicht bekannt. Bitte richte das Gerät erneut ein. + + + + inro + + + A + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + A + + + + AB + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + AB + + + + ABC + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + ABC + + + + AC + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + AC + + + + Active phases + The name of the StateType ({5cc3aa90-c83f-4453-9f22-5ef408e847a0}) of ThingClass pantabox + Aktive Phasen + + + + Active power + The name of the StateType ({e23181a7-0747-4ff2-8b10-90256a8377b3}) of ThingClass pantabox + Leistung + + + + B + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + B + + + + BC + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + BC + + + + C + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + C + + + + Charging + The name of the StateType ({5a606c6a-030c-4d55-9700-723c9c859c7f}) of ThingClass pantabox + Lade + + + + + Charging enabled + The name of the ParamType (ThingClass: pantabox, ActionType: power, ID: {d885ee23-8c50-48f1-82a9-72e0e92715ac}) +---------- +The name of the StateType ({d885ee23-8c50-48f1-82a9-72e0e92715ac}) of ThingClass pantabox + Ladefreigabe + + + + Connected + The name of the StateType ({345909fc-22e2-44bb-a063-26dd8e30793b}) of ThingClass pantabox + Verbunden + + + + INRO + The name of the plugin inro ({e2751951-4d53-4156-bd1a-a54c39e5c6cc}) + INRO + + + + INRO Elektrotechnik GmbH + The name of the vendor ({53669eec-4e67-444d-973f-40668aaa37a6}) + INRO Elektrotechnik GmbH + + + + MAC address + The name of the ParamType (ThingClass: pantabox, Type: thing, ID: {a3bc042c-0613-40a9-867a-3482d4d0901e}) + MAC-Adresse + + + + + Maximum charging current + The name of the ParamType (ThingClass: pantabox, ActionType: maxChargingCurrent, ID: {db02bf36-1ce2-40b4-bf88-8a4e3842a63b}) +---------- +The name of the StateType ({db02bf36-1ce2-40b4-bf88-8a4e3842a63b}) of ThingClass pantabox + Maximaler Ladestrom + + + + PANTABOX + The name of the ThingClass ({6de119fb-a579-4a88-9903-f05aac167b19}) + PANTABOX + + + + Phases connected + The name of the ParamType (ThingClass: pantabox, Type: settings, ID: {78225651-565c-49f7-8610-c067faf8822a}) + Angeschlossene Phasen + + + + Plugged in + The name of the StateType ({bb2f4ff0-f7ca-4ffd-a2f1-73f9b230a1eb}) of ThingClass pantabox + Angesteckt + + + + Session energy + The name of the StateType ({95719ca2-bda2-4eb4-b76d-e54d7a98dfdb}) of ThingClass pantabox + Energie Ladevorgang + + + + Set charging enabled + The name of the ActionType ({d885ee23-8c50-48f1-82a9-72e0e92715ac}) of ThingClass pantabox + Ladefreigabe erteilen + + + + Set maximum charging current + The name of the ActionType ({db02bf36-1ce2-40b4-bf88-8a4e3842a63b}) of ThingClass pantabox + Setze Ladestrom + + + + Total consumed energy + The name of the StateType ({b6e309d4-d480-477a-88f1-79e00bca450a}) of ThingClass pantabox + Gesamte Energiemenge + + + + Used phases + The name of the StateType ({db8d452a-8459-429b-b8f1-d73c805bd857}) of ThingClass pantabox + Verwendete Phasen + + + diff --git a/inro/translations/e2751951-4d53-4156-bd1a-a54c39e5c6cc-en_US.ts b/inro/translations/e2751951-4d53-4156-bd1a-a54c39e5c6cc-en_US.ts new file mode 100644 index 0000000..31f2eeb --- /dev/null +++ b/inro/translations/e2751951-4d53-4156-bd1a-a54c39e5c6cc-en_US.ts @@ -0,0 +1,170 @@ + + + + + IntegrationPluginInro + + + The network device discovery is not available. + + + + + The MAC address is not known. Please reconfigure the thing. + + + + + inro + + + A + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + + + + + AB + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + + + + + ABC + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + + + + + AC + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + + + + + Active phases + The name of the StateType ({5cc3aa90-c83f-4453-9f22-5ef408e847a0}) of ThingClass pantabox + + + + + Active power + The name of the StateType ({e23181a7-0747-4ff2-8b10-90256a8377b3}) of ThingClass pantabox + + + + + B + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + + + + + BC + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + + + + + C + The name of a possible value of StateType {db8d452a-8459-429b-b8f1-d73c805bd857} of ThingClass pantabox + + + + + Charging + The name of the StateType ({5a606c6a-030c-4d55-9700-723c9c859c7f}) of ThingClass pantabox + + + + + + Charging enabled + The name of the ParamType (ThingClass: pantabox, ActionType: power, ID: {d885ee23-8c50-48f1-82a9-72e0e92715ac}) +---------- +The name of the StateType ({d885ee23-8c50-48f1-82a9-72e0e92715ac}) of ThingClass pantabox + + + + + Connected + The name of the StateType ({345909fc-22e2-44bb-a063-26dd8e30793b}) of ThingClass pantabox + + + + + INRO + The name of the plugin inro ({e2751951-4d53-4156-bd1a-a54c39e5c6cc}) + + + + + INRO Elektrotechnik GmbH + The name of the vendor ({53669eec-4e67-444d-973f-40668aaa37a6}) + + + + + MAC address + The name of the ParamType (ThingClass: pantabox, Type: thing, ID: {a3bc042c-0613-40a9-867a-3482d4d0901e}) + + + + + + Maximum charging current + The name of the ParamType (ThingClass: pantabox, ActionType: maxChargingCurrent, ID: {db02bf36-1ce2-40b4-bf88-8a4e3842a63b}) +---------- +The name of the StateType ({db02bf36-1ce2-40b4-bf88-8a4e3842a63b}) of ThingClass pantabox + + + + + PANTABOX + The name of the ThingClass ({6de119fb-a579-4a88-9903-f05aac167b19}) + + + + + Phases connected + The name of the ParamType (ThingClass: pantabox, Type: settings, ID: {78225651-565c-49f7-8610-c067faf8822a}) + + + + + Plugged in + The name of the StateType ({bb2f4ff0-f7ca-4ffd-a2f1-73f9b230a1eb}) of ThingClass pantabox + + + + + Session energy + The name of the StateType ({95719ca2-bda2-4eb4-b76d-e54d7a98dfdb}) of ThingClass pantabox + + + + + Set charging enabled + The name of the ActionType ({d885ee23-8c50-48f1-82a9-72e0e92715ac}) of ThingClass pantabox + + + + + Set maximum charging current + The name of the ActionType ({db02bf36-1ce2-40b4-bf88-8a4e3842a63b}) of ThingClass pantabox + + + + + Total consumed energy + The name of the StateType ({b6e309d4-d480-477a-88f1-79e00bca450a}) of ThingClass pantabox + + + + + Used phases + The name of the StateType ({db8d452a-8459-429b-b8f1-d73c805bd857}) of ThingClass pantabox + + + + diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index bc1a5c8..d2e0e0c 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -12,6 +12,7 @@ PLUGIN_DIRS = \ huawei \ idm \ inepro \ + inro \ kostal \ mennekes \ modbuscommander \