diff --git a/abbterra/README.md b/abbterra/README.md new file mode 100644 index 0000000..5b236e1 --- /dev/null +++ b/abbterra/README.md @@ -0,0 +1,19 @@ +# ABB Terra AC + +This plugin integrates ABB Terra AC chargers via Modbus TCP and Modbus RTU. + +Implemented features: + +- network discovery for Modbus TCP chargers +- Modbus RTU discovery using nymea's managed RTU hardware resource +- connection state +- plugged-in and charging state detection +- charging enable/disable via current-limit control +- writable maximum charging current +- active power and session energy +- firmware version, serial number, and error code +- communication timeout setting + +The register model is based on: + +- `ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf` diff --git a/abbterra/abbterra-registers.json b/abbterra/abbterra-registers.json new file mode 100644 index 0000000..0bf1e80 --- /dev/null +++ b/abbterra/abbterra-registers.json @@ -0,0 +1,243 @@ +{ + "className": "AbbTerra", + "protocol": "BOTH", + "endianness": "BigEndian", + "stringEndianness": "BigEndian", + "errorLimitUntilNotReachable": 3, + "checkReachableRegister": "serialNumber", + "queuedRequests": true, + "queuedRequestsDelay": 50, + "blocks": [ + { + "id": "deviceInfo", + "readSchedule": "init", + "registers": [ + { + "id": "serialNumber", + "address": 16384, + "size": 4, + "type": "uint64", + "registerType": "holdingRegister", + "description": "Product serial number", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "firmwareVersionRaw", + "address": 16388, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Firmware version", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "userSettableMaxCurrent", + "address": 16390, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Maximum user settable charging current", + "defaultValue": "32000", + "access": "RO" + } + ] + }, + { + "id": "status", + "readSchedule": "update", + "registers": [ + { + "id": "errorCode", + "address": 16392, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Last error code", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "socketLockState", + "address": 16394, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Socket and cable lock state", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargingStateRaw", + "address": 16396, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Charging state", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargingCurrentLimit", + "address": 16398, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current charging current limit", + "defaultValue": "6000", + "access": "RO" + }, + { + "id": "currentL1", + "address": 16400, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current L1", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "currentL2", + "address": 16402, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current L2", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "currentL3", + "address": 16404, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current L3", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltageL1", + "address": 16406, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L1", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltageL2", + "address": 16408, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L2", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltageL3", + "address": 16410, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L3", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "activePower", + "address": 16412, + "size": 2, + "type": "uint32", + "unit": "W", + "registerType": "holdingRegister", + "description": "Measured active power", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "sessionEnergy", + "address": 16414, + "size": 2, + "type": "uint32", + "unit": "Wh", + "registerType": "holdingRegister", + "description": "Delivered energy of the current session", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "communicationTimeoutReadback", + "address": 16416, + "size": 1, + "type": "uint16", + "unit": "s", + "registerType": "holdingRegister", + "description": "Communication timeout", + "defaultValue": "60", + "access": "RO" + } + ] + } + ], + "registers": [ + { + "id": "chargingCurrentLimitCommand", + "address": 16640, + "size": 2, + "type": "uint32", + "unit": "mA", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Set charging current limit", + "defaultValue": "6000", + "access": "WO" + }, + { + "id": "socketLockCommand", + "address": 16642, + "size": 1, + "type": "uint16", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Socket lock control", + "defaultValue": "0", + "access": "WO" + }, + { + "id": "startStopChargingSession", + "address": 16645, + "size": 1, + "type": "uint16", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Start or stop charging session", + "defaultValue": "0", + "access": "WO" + }, + { + "id": "communicationTimeoutCommand", + "address": 16646, + "size": 1, + "type": "uint16", + "unit": "s", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Set communication timeout", + "defaultValue": "60", + "access": "WO" + } + ] +} diff --git a/abbterra/abbterra.pro b/abbterra/abbterra.pro new file mode 100644 index 0000000..e2cbeca --- /dev/null +++ b/abbterra/abbterra.pro @@ -0,0 +1,20 @@ +include(../plugins.pri) + +MODBUS_CONNECTIONS += abbterra-registers.json +MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +TARGET = $$qtLibraryTarget(nymea_integrationpluginabbterra) + +OTHER_FILES += abbterra-registers.json + +SOURCES += \ + abbterrartudiscovery.cpp \ + abbterratcpdiscovery.cpp \ + integrationpluginabbterra.cpp + +HEADERS += \ + abbterrartudiscovery.h \ + abbterratcpdiscovery.h \ + abbterrautils.h \ + integrationpluginabbterra.h diff --git a/abbterra/abbterrartudiscovery.cpp b/abbterra/abbterrartudiscovery.cpp new file mode 100644 index 0000000..33078ea --- /dev/null +++ b/abbterra/abbterrartudiscovery.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "abbterrartudiscovery.h" +#include "abbterrautils.h" +#include "extern-plugininfo.h" + +AbbTerraRtuDiscovery::AbbTerraRtuDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent) + : QObject(parent), + m_modbusRtuResource(modbusRtuResource) +{ +} + +void AbbTerraRtuDiscovery::startDiscovery() +{ + qCInfo(dcAbbTerra()) << "Discovery: Searching for ABB Terra AC chargers on Modbus RTU..."; + + m_candidateMasters.clear(); + m_results.clear(); + m_masterIndex = 0; + m_slaveId = 1; + + foreach (ModbusRtuMaster *master, m_modbusRtuResource->modbusRtuMasters()) { + if (master->connected()) { + m_candidateMasters.append(master); + } + } + + if (m_candidateMasters.isEmpty()) { + qCWarning(dcAbbTerra()) << "No connected Modbus RTU master available for ABB Terra AC discovery."; + emit discoveryFinished(false); + return; + } + + scanNext(); +} + +QList AbbTerraRtuDiscovery::results() const +{ + return m_results; +} + +void AbbTerraRtuDiscovery::scanNext() +{ + if (m_masterIndex >= m_candidateMasters.count()) { + emit discoveryFinished(true); + return; + } + + if (m_slaveId > 247) { + m_masterIndex++; + m_slaveId = 1; + scanNext(); + return; + } + + ModbusRtuMaster *master = m_candidateMasters.at(m_masterIndex); + const quint16 currentSlaveId = m_slaveId++; + + ModbusRtuReply *reply = master->readHoldingRegister(currentSlaveId, 0x4000, 8); + connect(reply, &ModbusRtuReply::finished, this, [this, master, currentSlaveId, reply]() { + if (reply->error() == ModbusRtuReply::NoError) { + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromRegisters(reply->result()); + if (deviceInfo.valid) { + Result result; + result.modbusRtuMasterId = master->modbusUuid(); + result.slaveId = currentSlaveId; + result.serialNumber = deviceInfo.serialNumber; + result.productName = deviceInfo.productName; + result.firmwareVersion = deviceInfo.firmwareVersion; + m_results.append(result); + } + } + + scanNext(); + }); +} diff --git a/abbterra/abbterrartudiscovery.h b/abbterra/abbterrartudiscovery.h new file mode 100644 index 0000000..ddeac2f --- /dev/null +++ b/abbterra/abbterrartudiscovery.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ABBTERRARTUDISCOVERY_H +#define ABBTERRARTUDISCOVERY_H + +#include + +#include + +class AbbTerraRtuDiscovery : public QObject +{ + Q_OBJECT +public: + struct Result { + QUuid modbusRtuMasterId; + quint16 slaveId; + QString serialNumber; + QString productName; + QString firmwareVersion; + }; + + explicit AbbTerraRtuDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent = nullptr); + + void startDiscovery(); + QList results() const; + +signals: + void discoveryFinished(bool modbusRtuMasterAvailable); + +private: + void scanNext(); + +private: + ModbusRtuHardwareResource *m_modbusRtuResource = nullptr; + QList m_candidateMasters; + QList m_results; + int m_masterIndex = 0; + quint16 m_slaveId = 1; +}; + +#endif // ABBTERRARTUDISCOVERY_H diff --git a/abbterra/abbterratcpdiscovery.cpp b/abbterra/abbterratcpdiscovery.cpp new file mode 100644 index 0000000..e0077f1 --- /dev/null +++ b/abbterra/abbterratcpdiscovery.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "abbterratcpdiscovery.h" +#include "abbterrautils.h" +#include "extern-plugininfo.h" + +#include +#include +#include + +AbbTerraTcpDiscovery::AbbTerraTcpDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) + : QObject(parent), + m_networkDeviceDiscovery(networkDeviceDiscovery) +{ +} + +void AbbTerraTcpDiscovery::startDiscovery() +{ + qCInfo(dcAbbTerra()) << "Discovery: Starting to search for ABB Terra AC chargers on the network..."; + + m_startDateTime = QDateTime::currentDateTime(); + m_networkDeviceInfos.clear(); + m_temporaryResults.clear(); + m_results.clear(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &AbbTerraTcpDiscovery::checkNetworkDevice); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [this, discoveryReply]() { + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + QTimer::singleShot(3000, this, &AbbTerraTcpDiscovery::finishDiscovery); + }); +} + +QList AbbTerraTcpDiscovery::results() const +{ + return m_results; +} + +void AbbTerraTcpDiscovery::checkNetworkDevice(const QHostAddress &address) +{ + AbbTerraModbusTcpConnection *connection = new AbbTerraModbusTcpConnection(address, 502, 1, this); + m_connections.append(connection); + + connect(connection, &AbbTerraModbusTcpConnection::reachableChanged, this, [this, connection](bool reachable) { + if (!reachable) { + cleanupConnection(connection); + return; + } + + connection->initialize(); + }); + + connect(connection, &AbbTerraModbusTcpConnection::initializationFinished, this, [this, connection, address](bool success) { + if (!success) { + cleanupConnection(connection); + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (deviceInfo.valid) { + Result result; + result.serialNumber = deviceInfo.serialNumber; + result.productName = deviceInfo.productName; + result.firmwareVersion = deviceInfo.firmwareVersion; + result.networkDeviceInfo = m_networkDeviceInfos.get(address); + if (result.networkDeviceInfo.address().isNull()) { + NetworkDeviceInfo info; + info.setAddress(address); + result.networkDeviceInfo = info; + } + m_temporaryResults.append(result); + } + + cleanupConnection(connection); + }); + + connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [this, connection](QModbusDevice::Error error) { + if (error != QModbusDevice::NoError) { + cleanupConnection(connection); + } + }); + + connect(connection, &AbbTerraModbusTcpConnection::checkReachabilityFailed, this, [this, connection]() { + cleanupConnection(connection); + }); + + connection->connectDevice(); +} + +void AbbTerraTcpDiscovery::cleanupConnection(AbbTerraModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void AbbTerraTcpDiscovery::finishDiscovery() +{ + foreach (const Result &result, m_temporaryResults) { + bool known = false; + foreach (const Result &existing, m_results) { + if (existing.serialNumber == result.serialNumber || existing.networkDeviceInfo.address() == result.networkDeviceInfo.address()) { + known = true; + break; + } + } + + if (!known) { + qCDebug(dcAbbTerra()) << "Discovery: Found" << result.productName << result.networkDeviceInfo; + m_results.append(result); + } + } + + const QList leftoverConnections = m_connections; + foreach (AbbTerraModbusTcpConnection *connection, leftoverConnections) { + cleanupConnection(connection); + } + + const qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + qCInfo(dcAbbTerra()) << "Discovery: Finished ABB Terra AC network discovery in" + << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz") + << "with" << m_results.count() << "result(s)."; + emit discoveryFinished(); +} diff --git a/abbterra/abbterratcpdiscovery.h b/abbterra/abbterratcpdiscovery.h new file mode 100644 index 0000000..48dcda3 --- /dev/null +++ b/abbterra/abbterratcpdiscovery.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ABBTERRATCPDISCOVERY_H +#define ABBTERRATCPDISCOVERY_H + +#include +#include + +#include + +#include "abbterramodbustcpconnection.h" + +class AbbTerraTcpDiscovery : public QObject +{ + Q_OBJECT +public: + struct Result { + NetworkDeviceInfo networkDeviceInfo; + QString serialNumber; + QString productName; + QString firmwareVersion; + }; + + explicit AbbTerraTcpDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + + void startDiscovery(); + QList results() const; + +signals: + void discoveryFinished(); + +private: + void checkNetworkDevice(const QHostAddress &address); + void cleanupConnection(AbbTerraModbusTcpConnection *connection); + void finishDiscovery(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + QDateTime m_startDateTime; + NetworkDeviceInfos m_networkDeviceInfos; + QList m_connections; + QList m_results; + QList m_temporaryResults; +}; + +#endif // ABBTERRATCPDISCOVERY_H diff --git a/abbterra/abbterrautils.h b/abbterra/abbterrautils.h new file mode 100644 index 0000000..07580ad --- /dev/null +++ b/abbterra/abbterrautils.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ABBTERRAUTILS_H +#define ABBTERRAUTILS_H + +#include +#include + +#include + +namespace AbbTerraUtils { + +struct DeviceInfo { + bool valid = false; + QString serialNumber; + QString productName; + QString firmwareVersion; + double maxChargingCurrent = 32.0; +}; + +inline QString connectorTypeName(quint8 connectorType) +{ + switch (connectorType) { + case 'G': + return QStringLiteral("Cable"); + case 'P': + return QStringLiteral("Outlet"); + case 'S': + return QStringLiteral("Socket"); + case 'T': + return QStringLiteral("Socket"); + default: + return QString(); + } +} + +inline int ratedPowerFromCode(quint8 ratedPowerCode) +{ + switch (ratedPowerCode) { + case 7: + return 7; + case 11: + return 11; + case 22: + return 22; + default: + return -1; + } +} + +inline DeviceInfo deviceInfoFromValues(quint64 serialNumberRaw, quint32 firmwareVersionRaw, quint32 maxCurrentRaw) +{ + DeviceInfo deviceInfo; + + const quint8 connectorType = static_cast((serialNumberRaw >> 56) & 0xff); + const quint8 ratedPowerCode = static_cast((serialNumberRaw >> 48) & 0xff); + const quint8 plantId = static_cast((serialNumberRaw >> 40) & 0xff); + const quint8 productionWeek = static_cast((serialNumberRaw >> 24) & 0xff); + const quint8 productionYear = static_cast((serialNumberRaw >> 16) & 0xff); + const quint8 uniqueHigh = static_cast((serialNumberRaw >> 8) & 0xff); + const quint8 uniqueLow = static_cast(serialNumberRaw & 0xff); + const int ratedPower = ratedPowerFromCode(ratedPowerCode); + const QString connectorName = connectorTypeName(connectorType); + + if (ratedPower <= 0 || connectorName.isEmpty() || productionWeek == 0 || productionWeek > 53) { + return deviceInfo; + } + + deviceInfo.valid = true; + deviceInfo.serialNumber = QStringLiteral("TACW%1-%2-%3%4-%5%6%7") + .arg(ratedPower) + .arg(plantId) + .arg(productionWeek, 2, 10, QLatin1Char('0')) + .arg(productionYear, 2, 10, QLatin1Char('0')) + .arg(QChar(static_cast(connectorType))) + .arg(uniqueHigh, 2, 10, QLatin1Char('0')) + .arg(uniqueLow, 2, 10, QLatin1Char('0')); + deviceInfo.productName = QStringLiteral("ABB Terra AC %1 kW %2").arg(ratedPower).arg(connectorName); + deviceInfo.firmwareVersion = QStringLiteral("%1.%2.%3") + .arg((firmwareVersionRaw >> 24) & 0xff) + .arg((firmwareVersionRaw >> 16) & 0xff) + .arg((firmwareVersionRaw >> 8) & 0xff); + if (maxCurrentRaw >= 6000 && maxCurrentRaw <= 32000) { + deviceInfo.maxChargingCurrent = maxCurrentRaw / 1000.0; + } + + return deviceInfo; +} + +inline DeviceInfo deviceInfoFromRegisters(const QVector ®isters) +{ + if (registers.count() < 8) { + return DeviceInfo(); + } + + return deviceInfoFromValues( + ModbusDataUtils::convertToUInt64(registers.mid(0, 4), ModbusDataUtils::ByteOrderBigEndian), + ModbusDataUtils::convertToUInt32(registers.mid(4, 2), ModbusDataUtils::ByteOrderBigEndian), + ModbusDataUtils::convertToUInt32(registers.mid(6, 2), ModbusDataUtils::ByteOrderBigEndian) + ); +} + +inline quint8 chargingStateCode(quint32 chargingStateRaw) +{ + return static_cast((chargingStateRaw >> 8) & 0x7f); +} + +inline bool chargingLimitedByCar(quint32 chargingStateRaw) +{ + return ((chargingStateRaw >> 15) & 0x1) == 0x1; +} + +inline bool isVehiclePluggedIn(quint32 chargingStateRaw, quint32 socketLockState) +{ + const quint8 chargingState = chargingStateCode(chargingStateRaw); + if (chargingState >= 3 && chargingState <= 6) { + return true; + } + + return socketLockState == 0x0101 || socketLockState == 0x0111; +} + +inline bool isCharging(quint32 chargingStateRaw, quint32 activePower) +{ + return chargingStateCode(chargingStateRaw) == 6 || activePower > 100; +} + +inline uint phaseCount(quint32 voltageL1, quint32 voltageL2, quint32 voltageL3) +{ + uint phases = 0; + if (voltageL1 > 1000) { + phases++; + } + if (voltageL2 > 1000) { + phases++; + } + if (voltageL3 > 1000) { + phases++; + } + return qMax(phases, 1u); +} + +} // namespace AbbTerraUtils + +#endif // ABBTERRAUTILS_H diff --git a/abbterra/docs/ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf b/abbterra/docs/ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf new file mode 100644 index 0000000..4dd264a Binary files /dev/null and b/abbterra/docs/ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf differ diff --git a/abbterra/integrationpluginabbterra.cpp b/abbterra/integrationpluginabbterra.cpp new file mode 100644 index 0000000..762181a --- /dev/null +++ b/abbterra/integrationpluginabbterra.cpp @@ -0,0 +1,483 @@ +#include "integrationpluginabbterra.h" +#include "abbterrartudiscovery.h" +#include "abbterratcpdiscovery.h" +#include "abbterrautils.h" + +#include "integrations/thing.h" +#include "integrations/thingactioninfo.h" +#include "integrations/thingdescriptor.h" +#include "integrations/thingdiscoveryinfo.h" +#include "integrations/thingsetupinfo.h" +#include "plugininfo.h" + +#include +#include +#include + +#include +#include +#include + +IntegrationPluginAbbterra::IntegrationPluginAbbterra() +{ +} + +void IntegrationPluginAbbterra::discoverThings(ThingDiscoveryInfo *info) +{ + if (info->thingClassId() == terraAcTcpThingClassId) { + AbbTerraTcpDiscovery *discovery = new AbbTerraTcpDiscovery(hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &AbbTerraTcpDiscovery::discoveryFinished, info, [this, info, discovery]() { + foreach (const AbbTerraTcpDiscovery::Result &result, discovery->results()) { + ThingDescriptor descriptor(terraAcTcpThingClassId, result.productName, result.serialNumber); + ParamList params; + params.append(Param(terraAcTcpThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress())); + params.append(Param(terraAcTcpThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName())); + params.append(Param(terraAcTcpThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress())); + params.append(Param(terraAcTcpThingPortParamTypeId, 502)); + params.append(Param(terraAcTcpThingSlaveIdParamTypeId, 1)); + descriptor.setParams(params); + + if (Thing *existingThing = myThings().findByParams(params)) { + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + discovery->startDiscovery(); + return; + } + + if (info->thingClassId() == terraAcRtuThingClassId) { + AbbTerraRtuDiscovery *discovery = new AbbTerraRtuDiscovery(hardwareManager()->modbusRtuResource(), info); + connect(discovery, &AbbTerraRtuDiscovery::discoveryFinished, info, [this, info, discovery](bool modbusRtuMasterAvailable) { + if (!modbusRtuMasterAvailable) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No connected Modbus RTU master available.")); + return; + } + + foreach (const AbbTerraRtuDiscovery::Result &result, discovery->results()) { + ThingDescriptor descriptor(terraAcRtuThingClassId, result.productName, QStringLiteral("Slave ID: %1").arg(result.slaveId)); + ParamList params{ + Param(terraAcRtuThingRtuMasterParamTypeId, result.modbusRtuMasterId), + Param(terraAcRtuThingSlaveIdParamTypeId, result.slaveId) + }; + descriptor.setParams(params); + + if (Thing *existingThing = myThings().findByParams(params)) { + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + discovery->startDiscovery(); + return; + } + + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginAbbterra::setupThing(ThingSetupInfo *info) +{ + if (info->thing()->thingClassId() == terraAcTcpThingClassId) { + setupTcpThing(info); + return; + } + + if (info->thing()->thingClassId() == terraAcRtuThingClassId) { + setupRtuThing(info); + return; + } + + info->finish(Thing::ThingErrorUnsupportedFeature); +} + +void IntegrationPluginAbbterra::postSetupThing(Thing *thing) +{ + Q_UNUSED(thing) + + if (m_pluginTimer) { + return; + } + + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(30); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { + foreach (AbbTerraModbusTcpConnection *connection, m_tcpConnections) { + connection->update(); + } + foreach (AbbTerraModbusRtuConnection *connection, m_rtuConnections) { + connection->update(); + } + }); + m_pluginTimer->start(); +} + +void IntegrationPluginAbbterra::thingRemoved(Thing *thing) +{ + delete m_tcpConnections.take(thing); + delete m_rtuConnections.take(thing); + 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 IntegrationPluginAbbterra::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == terraAcTcpThingClassId) { + AbbTerraModbusTcpConnection *connection = m_tcpConnections.value(thing); + if (!connection || !connection->reachable()) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The charging station is not reachable.")); + return; + } + + if (info->action().actionTypeId() == terraAcTcpPowerActionTypeId) { + const bool power = info->action().paramValue(terraAcTcpPowerActionPowerParamTypeId).toBool(); + const quint32 currentMilliAmps = power ? static_cast(qRound(thing->stateValue(terraAcTcpMaxChargingCurrentStateTypeId).toDouble() * 1000.0)) : 0; + QModbusReply *reply = connection->setChargingCurrentLimitCommand(currentMilliAmps); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, thing, connection, reply, power]() { + if (reply->error() == QModbusDevice::NoError) { + thing->setStateValue(terraAcTcpPowerStateTypeId, power); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + if (info->action().actionTypeId() == terraAcTcpMaxChargingCurrentActionTypeId) { + const double current = info->action().paramValue(terraAcTcpMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble(); + QModbusReply *reply = connection->setChargingCurrentLimitCommand(static_cast(qRound(current * 1000.0))); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, thing, connection, reply, current]() { + if (reply->error() == QModbusDevice::NoError) { + thing->setStateValue(terraAcTcpMaxChargingCurrentStateTypeId, current); + thing->setStateValue(terraAcTcpPowerStateTypeId, current >= 6.0); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + info->finish(Thing::ThingErrorUnsupportedFeature); + return; + } + + if (thing->thingClassId() == terraAcRtuThingClassId) { + AbbTerraModbusRtuConnection *connection = m_rtuConnections.value(thing); + if (!connection || !connection->reachable()) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The charging station is not reachable.")); + return; + } + + if (info->action().actionTypeId() == terraAcRtuPowerActionTypeId) { + const bool power = info->action().paramValue(terraAcRtuPowerActionPowerParamTypeId).toBool(); + const quint32 currentMilliAmps = power ? static_cast(qRound(thing->stateValue(terraAcRtuMaxChargingCurrentStateTypeId).toDouble() * 1000.0)) : 0; + ModbusRtuReply *reply = connection->setChargingCurrentLimitCommand(currentMilliAmps); + connect(reply, &ModbusRtuReply::finished, info, [info, thing, connection, reply, power]() { + if (reply->error() == ModbusRtuReply::NoError) { + thing->setStateValue(terraAcRtuPowerStateTypeId, power); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + if (info->action().actionTypeId() == terraAcRtuMaxChargingCurrentActionTypeId) { + const double current = info->action().paramValue(terraAcRtuMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble(); + ModbusRtuReply *reply = connection->setChargingCurrentLimitCommand(static_cast(qRound(current * 1000.0))); + connect(reply, &ModbusRtuReply::finished, info, [info, thing, connection, reply, current]() { + if (reply->error() == ModbusRtuReply::NoError) { + thing->setStateValue(terraAcRtuMaxChargingCurrentStateTypeId, current); + thing->setStateValue(terraAcRtuPowerStateTypeId, current >= 6.0); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + } + + info->finish(Thing::ThingErrorUnsupportedFeature); +} + +void IntegrationPluginAbbterra::setupTcpThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (m_tcpConnections.contains(thing)) { + m_tcpConnections.take(thing)->deleteLater(); + } + + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + if (!monitor) { + monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing); + if (!monitor) { + info->finish(Thing::ThingErrorInvalidParameter); + return; + } + m_monitors.insert(thing, monitor); + } + + const quint16 port = static_cast(thing->paramValue(terraAcTcpThingPortParamTypeId).toUInt()); + const quint16 slaveId = static_cast(thing->paramValue(terraAcTcpThingSlaveIdParamTypeId).toUInt()); + AbbTerraModbusTcpConnection *connection = new AbbTerraModbusTcpConnection(monitor->networkDeviceInfo().address(), port, slaveId, thing); + + connect(info, &ThingSetupInfo::aborted, connection, &AbbTerraModbusTcpConnection::deleteLater); + connect(info, &ThingSetupInfo::aborted, monitor, [this, thing]() { + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + connect(monitor, &NetworkDeviceMonitor::networkDeviceInfoChanged, connection, [connection](const NetworkDeviceInfo &networkDeviceInfo) { + connection->modbusTcpMaster()->setHostAddress(networkDeviceInfo.address()); + }); + + connect(connection, &AbbTerraModbusTcpConnection::reachableChanged, thing, [this, thing, connection](bool reachable) { + if (reachable) { + connection->initialize(); + } else { + setDisconnectedState(thing); + } + }); + + connect(connection, &AbbTerraModbusTcpConnection::initializationFinished, thing, [this, thing, connection](bool success) { + if (!success) { + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + return; + } + + thing->setStateValue(terraAcTcpConnectedStateTypeId, true); + thing->setStateValue(terraAcTcpFirmwareVersionStateTypeId, deviceInfo.firmwareVersion); + thing->setStateValue(terraAcTcpSerialNumberStateTypeId, deviceInfo.serialNumber); + thing->setStateMinMaxValues(terraAcTcpMaxChargingCurrentStateTypeId, 6.0, deviceInfo.maxChargingCurrent); + applyTimeoutSetting(thing, connection); + }); + + connect(connection, &AbbTerraModbusTcpConnection::initializationFinished, info, [this, info, thing, connection](bool success) { + if (!success) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Could not initialize the communication with the charger.")); + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The device does not match the ABB Terra AC Modbus register map.")); + return; + } + + m_tcpConnections.insert(thing, connection); + connection->update(); + info->finish(Thing::ThingErrorNoError); + }); + + connect(connection, &AbbTerraModbusTcpConnection::updateFinished, thing, [this, thing, connection]() { + updateThing(thing, connection); + }); + + connect(thing, &Thing::settingChanged, connection, [this, thing, connection](const ParamTypeId ¶mTypeId, const QVariant &) { + if (paramTypeId == terraAcTcpSettingsCommunicationTimeoutParamTypeId) { + applyTimeoutSetting(thing, connection); + } + }); +} + +void IntegrationPluginAbbterra::setupRtuThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (m_rtuConnections.contains(thing)) { + m_rtuConnections.take(thing)->deleteLater(); + } + + ModbusRtuMaster *master = hardwareManager()->modbusRtuResource()->getModbusRtuMaster(thing->paramValue(terraAcRtuThingRtuMasterParamTypeId).toUuid()); + if (!master) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The Modbus RTU connection is not available.")); + return; + } + + const quint16 slaveId = static_cast(thing->paramValue(terraAcRtuThingSlaveIdParamTypeId).toUInt()); + AbbTerraModbusRtuConnection *connection = new AbbTerraModbusRtuConnection(master, slaveId, thing); + + connect(connection, &AbbTerraModbusRtuConnection::reachableChanged, thing, [this, thing, connection](bool reachable) { + if (reachable) { + connection->initialize(); + } else { + setDisconnectedState(thing); + } + }); + + connect(connection, &AbbTerraModbusRtuConnection::initializationFinished, thing, [this, thing, connection](bool success) { + if (!success) { + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + return; + } + + thing->setStateValue(terraAcRtuConnectedStateTypeId, true); + thing->setStateValue(terraAcRtuFirmwareVersionStateTypeId, deviceInfo.firmwareVersion); + thing->setStateValue(terraAcRtuSerialNumberStateTypeId, deviceInfo.serialNumber); + thing->setStateMinMaxValues(terraAcRtuMaxChargingCurrentStateTypeId, 6.0, deviceInfo.maxChargingCurrent); + applyTimeoutSetting(thing, connection); + }); + + connect(connection, &AbbTerraModbusRtuConnection::initializationFinished, info, [this, info, thing, connection](bool success) { + if (!success) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Could not initialize the communication with the charger.")); + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The device does not match the ABB Terra AC Modbus register map.")); + return; + } + + m_rtuConnections.insert(thing, connection); + connection->update(); + info->finish(Thing::ThingErrorNoError); + }); + + connect(connection, &AbbTerraModbusRtuConnection::updateFinished, thing, [this, thing, connection]() { + updateThing(thing, connection); + }); + + connect(thing, &Thing::settingChanged, connection, [this, thing, connection](const ParamTypeId ¶mTypeId, const QVariant &) { + if (paramTypeId == terraAcRtuSettingsCommunicationTimeoutParamTypeId) { + applyTimeoutSetting(thing, connection); + } + }); +} + +void IntegrationPluginAbbterra::applyTimeoutSetting(Thing *thing, AbbTerraModbusTcpConnection *connection) +{ + QModbusReply *reply = connection->setCommunicationTimeoutCommand(static_cast(thing->setting(terraAcTcpSettingsCommunicationTimeoutParamTypeId).toUInt())); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, connection, [connection, reply]() { + if (reply->error() != QModbusDevice::NoError && connection->reachable()) { + connection->updateCommunicationTimeoutReadback(); + } + }); +} + +void IntegrationPluginAbbterra::applyTimeoutSetting(Thing *thing, AbbTerraModbusRtuConnection *connection) +{ + ModbusRtuReply *reply = connection->setCommunicationTimeoutCommand(static_cast(thing->setting(terraAcRtuSettingsCommunicationTimeoutParamTypeId).toUInt())); + connect(reply, &ModbusRtuReply::finished, connection, [connection, reply]() { + if (reply->error() != ModbusRtuReply::NoError && connection->reachable()) { + connection->updateCommunicationTimeoutReadback(); + } + }); +} + +void IntegrationPluginAbbterra::updateThing(Thing *thing, AbbTerraModbusTcpConnection *connection) +{ + thing->setStateValue(terraAcTcpConnectedStateTypeId, connection->reachable()); + thing->setStateValue(terraAcTcpPluggedInStateTypeId, AbbTerraUtils::isVehiclePluggedIn(connection->chargingStateRaw(), connection->socketLockState())); + thing->setStateValue(terraAcTcpChargingStateTypeId, AbbTerraUtils::isCharging(connection->chargingStateRaw(), connection->activePower())); + thing->setStateValue(terraAcTcpPowerStateTypeId, connection->chargingCurrentLimit() >= 6000); + thing->setStateValue(terraAcTcpMaxChargingCurrentStateTypeId, connection->chargingCurrentLimit() / 1000.0); + thing->setStateValue(terraAcTcpPhaseCountStateTypeId, AbbTerraUtils::phaseCount(connection->voltageL1(), connection->voltageL2(), connection->voltageL3())); + thing->setStateValue(terraAcTcpCurrentPowerStateTypeId, static_cast(connection->activePower())); + thing->setStateValue("currentPhase1", connection->currentL1() / 1000.0); + thing->setStateValue("currentPhase2", connection->currentL2() / 1000.0); + thing->setStateValue("currentPhase3", connection->currentL3() / 1000.0); + thing->setStateValue("voltagePhase1", connection->voltageL1() / 10.0); + thing->setStateValue("voltagePhase2", connection->voltageL2() / 10.0); + thing->setStateValue("voltagePhase3", connection->voltageL3() / 10.0); + thing->setStateValue(terraAcTcpSessionEnergyStateTypeId, connection->sessionEnergy() / 1000.0); + thing->setStateValue(terraAcTcpErrorCodeStateTypeId, connection->errorCode()); + thing->setSettingValue(terraAcTcpSettingsCommunicationTimeoutParamTypeId, connection->communicationTimeoutReadback()); +} + +void IntegrationPluginAbbterra::updateThing(Thing *thing, AbbTerraModbusRtuConnection *connection) +{ + thing->setStateValue(terraAcRtuConnectedStateTypeId, connection->reachable()); + thing->setStateValue(terraAcRtuPluggedInStateTypeId, AbbTerraUtils::isVehiclePluggedIn(connection->chargingStateRaw(), connection->socketLockState())); + thing->setStateValue(terraAcRtuChargingStateTypeId, AbbTerraUtils::isCharging(connection->chargingStateRaw(), connection->activePower())); + thing->setStateValue(terraAcRtuPowerStateTypeId, connection->chargingCurrentLimit() >= 6000); + thing->setStateValue(terraAcRtuMaxChargingCurrentStateTypeId, connection->chargingCurrentLimit() / 1000.0); + thing->setStateValue(terraAcRtuPhaseCountStateTypeId, AbbTerraUtils::phaseCount(connection->voltageL1(), connection->voltageL2(), connection->voltageL3())); + thing->setStateValue(terraAcRtuCurrentPowerStateTypeId, static_cast(connection->activePower())); + thing->setStateValue("currentPhase1", connection->currentL1() / 1000.0); + thing->setStateValue("currentPhase2", connection->currentL2() / 1000.0); + thing->setStateValue("currentPhase3", connection->currentL3() / 1000.0); + thing->setStateValue("voltagePhase1", connection->voltageL1() / 10.0); + thing->setStateValue("voltagePhase2", connection->voltageL2() / 10.0); + thing->setStateValue("voltagePhase3", connection->voltageL3() / 10.0); + thing->setStateValue(terraAcRtuSessionEnergyStateTypeId, connection->sessionEnergy() / 1000.0); + thing->setStateValue(terraAcRtuErrorCodeStateTypeId, connection->errorCode()); + thing->setSettingValue(terraAcRtuSettingsCommunicationTimeoutParamTypeId, connection->communicationTimeoutReadback()); +} + +void IntegrationPluginAbbterra::setDisconnectedState(Thing *thing) +{ + if (thing->thingClassId() == terraAcTcpThingClassId) { + thing->setStateValue(terraAcTcpConnectedStateTypeId, false); + thing->setStateValue(terraAcTcpChargingStateTypeId, false); + thing->setStateValue(terraAcTcpPluggedInStateTypeId, false); + thing->setStateValue(terraAcTcpCurrentPowerStateTypeId, 0); + thing->setStateValue("currentPhase1", 0); + thing->setStateValue("currentPhase2", 0); + thing->setStateValue("currentPhase3", 0); + thing->setStateValue("voltagePhase1", 0); + thing->setStateValue("voltagePhase2", 0); + thing->setStateValue("voltagePhase3", 0); + return; + } + + if (thing->thingClassId() == terraAcRtuThingClassId) { + thing->setStateValue(terraAcRtuConnectedStateTypeId, false); + thing->setStateValue(terraAcRtuChargingStateTypeId, false); + thing->setStateValue(terraAcRtuPluggedInStateTypeId, false); + thing->setStateValue(terraAcRtuCurrentPowerStateTypeId, 0); + thing->setStateValue("currentPhase1", 0); + thing->setStateValue("currentPhase2", 0); + thing->setStateValue("currentPhase3", 0); + thing->setStateValue("voltagePhase1", 0); + thing->setStateValue("voltagePhase2", 0); + thing->setStateValue("voltagePhase3", 0); + } +} diff --git a/abbterra/integrationpluginabbterra.h b/abbterra/integrationpluginabbterra.h new file mode 100644 index 0000000..84eb1e6 --- /dev/null +++ b/abbterra/integrationpluginabbterra.h @@ -0,0 +1,46 @@ +#ifndef INTEGRATIONPLUGINABBTERRA_H +#define INTEGRATIONPLUGINABBTERRA_H + +#include +#include "integrations/integrationplugin.h" +#include + +#include "extern-plugininfo.h" +#include "abbterramodbusrtuconnection.h" +#include "abbterramodbustcpconnection.h" + +class IntegrationPluginAbbterra : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginabbterra.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginAbbterra(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +public slots: + void executeAction(ThingActionInfo *info) override; + +private: + void setupTcpThing(ThingSetupInfo *info); + void setupRtuThing(ThingSetupInfo *info); + void applyTimeoutSetting(Thing *thing, AbbTerraModbusTcpConnection *connection); + void applyTimeoutSetting(Thing *thing, AbbTerraModbusRtuConnection *connection); + void updateThing(Thing *thing, AbbTerraModbusTcpConnection *connection); + void updateThing(Thing *thing, AbbTerraModbusRtuConnection *connection); + void setDisconnectedState(Thing *thing); + +private: + PluginTimer *m_pluginTimer = nullptr; + QHash m_tcpConnections; + QHash m_rtuConnections; + QHash m_monitors; +}; + +#endif // INTEGRATIONPLUGINABBTERRA_H diff --git a/abbterra/integrationpluginabbterra.json b/abbterra/integrationpluginabbterra.json new file mode 100644 index 0000000..392c218 --- /dev/null +++ b/abbterra/integrationpluginabbterra.json @@ -0,0 +1,416 @@ +{ + "name": "AbbTerra", + "displayName": "ABB Terra AC", + "id": "d7f1cb28-b18b-449e-8cd2-1d99b9d8f681", + "paramTypes": [], + "vendors": [ + { + "id": "0369ebad-5186-437d-b520-041a0b9b7582", + "name": "abb", + "displayName": "ABB", + "thingClasses": [ + { + "id": "93ad828a-9a7a-4fca-be3f-88641317845f", + "name": "terraAcTcp", + "displayName": "Terra AC Charger (TCP)", + "interfaces": [ + "evcharger", + "connectable", + "networkdevice" + ], + "createMethods": [ + "discovery", + "user" + ], + "discoveryType": "weak", + "paramTypes": [ + { + "id": "9b3332aa-6c26-4399-b3dc-d7fdbe3f4420", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "", + "readOnly": true + }, + { + "id": "962ef9d9-30a2-4636-8ff2-2a28742277f4", + "name": "address", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "f9e6fed4-2a18-4f23-b70e-78919d4b01f8", + "name": "hostName", + "displayName": "Host name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "153258dd-e81b-48e5-9ba7-46d59e854d85", + "name": "port", + "displayName": "Port", + "type": "uint", + "defaultValue": 502 + }, + { + "id": "cc4c66c1-185f-4c97-88a0-2bc66319d157", + "name": "slaveId", + "displayName": "Slave ID", + "type": "uint", + "defaultValue": 1, + "minValue": 1, + "maxValue": 255 + } + ], + "settingsTypes": [ + { + "id": "5a0e678a-ffc8-443a-9ae7-bb2243b0ec4b", + "name": "communicationTimeout", + "displayName": "Communication timeout", + "type": "uint", + "unit": "Seconds", + "defaultValue": 60, + "minValue": 10, + "maxValue": 65535 + } + ], + "stateTypes": [ + { + "id": "21e1a2bb-0e05-462c-9909-4f9a0eada438", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "2fb30a0b-dc5a-4831-b5c6-de653c0c9ee1", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "0c685ced-27fa-46ba-8c0b-c49af82054f0", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "207e2074-0147-4617-9a8b-3f326dcd6a0b", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": true, + "writable": true + }, + { + "id": "e3d27f8a-73d0-493a-b99a-29e7dc184485", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "double", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "stepSize": 0.1, + "defaultValue": 6, + "writable": true + }, + { + "id": "0764bce9-fd26-4da8-8d92-f6a5ce73e81e", + "name": "phaseCount", + "displayName": "Phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "59b486a3-aa44-4a94-8478-1077af124441", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "953ce672-a9a2-4cd6-bf87-ccfe720c7595", + "name": "currentPhase1", + "displayName": "Current phase 1", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "e4f97cde-191c-4c84-9668-169c5e648338", + "name": "currentPhase2", + "displayName": "Current phase 2", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "c1cead50-9894-4b12-9211-c5bc84109b1d", + "name": "currentPhase3", + "displayName": "Current phase 3", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "bcff4c2d-5567-40ff-adef-5b47dc5ace76", + "name": "voltagePhase1", + "displayName": "Voltage phase 1", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "db7af25a-cb7b-4640-9515-a7af345bd770", + "name": "voltagePhase2", + "displayName": "Voltage phase 2", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "fe67e1bf-6871-45a6-a253-819cb70c5ec4", + "name": "voltagePhase3", + "displayName": "Voltage phase 3", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "be2cc28e-79c2-4eaf-91ad-8131ac1f87e2", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "6d9b38a7-c6eb-47bc-8929-5a875d0fd5d0", + "name": "firmwareVersion", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" + }, + { + "id": "69c9f4e2-ec8c-4d65-9cf4-ce8b7a3f8c3b", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + }, + { + "id": "ce98bfda-b27a-4f69-bd4e-49eb3e722245", + "name": "errorCode", + "displayName": "Error code", + "type": "uint", + "defaultValue": 0 + } + ], + "actionTypes": [] + }, + { + "id": "a385801e-600c-4e27-ad73-5184a7516860", + "name": "terraAcRtu", + "displayName": "Terra AC Charger (RTU)", + "interfaces": [ + "evcharger", + "connectable" + ], + "createMethods": [ + "discovery", + "user" + ], + "paramTypes": [ + { + "id": "deb19a6b-33b3-417e-abf1-a1fb18585fe4", + "name": "rtuMaster", + "displayName": "Modbus RTU master", + "type": "QString", + "defaultValue": "" + }, + { + "id": "23dbb93d-6605-435c-9a41-b8a8e8242ea0", + "name": "slaveId", + "displayName": "Modbus slave ID", + "type": "uint", + "defaultValue": 1, + "minValue": 1, + "maxValue": 247 + } + ], + "settingsTypes": [ + { + "id": "c4e0a515-270e-403e-8453-02002859938e", + "name": "communicationTimeout", + "displayName": "Communication timeout", + "type": "uint", + "unit": "Seconds", + "defaultValue": 60, + "minValue": 10, + "maxValue": 65535 + } + ], + "stateTypes": [ + { + "id": "c43068ca-dddc-4c43-9149-68927afae5e7", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "14fcf045-52d7-4f47-b64e-2bf3f87c242f", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "64708d42-bbfe-44a5-82ae-c8e66e25c5a5", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": true, + "writable": true + }, + { + "id": "ea933a77-a098-4303-bbdb-15c72dfd3634", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "double", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "stepSize": 0.1, + "defaultValue": 6, + "writable": true + }, + { + "id": "cd1add95-18d9-46b5-a3d5-f0f29d5160c9", + "name": "phaseCount", + "displayName": "Phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "152a58cb-0bf1-4d4a-aac2-233d3c68f81b", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "a8e2e2cf-b2ca-48c5-a702-9226062c43e3", + "name": "currentPhase1", + "displayName": "Current phase 1", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "210ff017-ea75-4705-9f23-f73e324601ef", + "name": "currentPhase2", + "displayName": "Current phase 2", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "3a5d6ce5-d3ed-4ba5-996f-15a2b3124faf", + "name": "currentPhase3", + "displayName": "Current phase 3", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "52ebc781-a527-4641-b768-3a0bd0209f96", + "name": "voltagePhase1", + "displayName": "Voltage phase 1", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "bb513653-70ca-4ee1-a913-aab3d2881708", + "name": "voltagePhase2", + "displayName": "Voltage phase 2", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "c84a5e75-121d-4af8-a203-23f586613cf2", + "name": "voltagePhase3", + "displayName": "Voltage phase 3", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "7a9a6ec3-7572-42d8-833d-5b434ca95765", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "071d7408-fa96-49da-b56b-f5564bee9b5d", + "name": "firmwareVersion", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" + }, + { + "id": "c95ea476-4ce3-4c04-bc23-238bdee496ee", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + }, + { + "id": "1f7d2791-c23e-47fe-8512-850ea25e09b3", + "name": "errorCode", + "displayName": "Error code", + "type": "uint", + "defaultValue": 0 + } + ], + "actionTypes": [] + } + ] + } + ] +} diff --git a/abbterra/meta.json b/abbterra/meta.json new file mode 100644 index 0000000..6ebe49a --- /dev/null +++ b/abbterra/meta.json @@ -0,0 +1,13 @@ +{ + "title": "ABB Terra AC", + "tagline": "Connect ABB Terra AC chargers over Modbus TCP or Modbus RTU.", + "stability": "consumer", + "icon": "", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/abbterra/translations/d7f1cb28-b18b-449e-8cd2-1d99b9d8f681-en_US.ts b/abbterra/translations/d7f1cb28-b18b-449e-8cd2-1d99b9d8f681-en_US.ts new file mode 100644 index 0000000..7365756 --- /dev/null +++ b/abbterra/translations/d7f1cb28-b18b-449e-8cd2-1d99b9d8f681-en_US.ts @@ -0,0 +1,296 @@ + + + + + AbbTerra + + + ABB + The name of the vendor ({0369ebad-5186-437d-b520-041a0b9b7582}) + + + + + ABB Terra AC + The name of the plugin AbbTerra ({d7f1cb28-b18b-449e-8cd2-1d99b9d8f681}) + + + + + + Active power + The name of the StateType ({152a58cb-0bf1-4d4a-aac2-233d3c68f81b}) of ThingClass terraAcRtu +---------- +The name of the StateType ({59b486a3-aa44-4a94-8478-1077af124441}) of ThingClass terraAcTcp + + + + + + Charging + The name of the StateType ({64708d42-bbfe-44a5-82ae-c8e66e25c5a5}) of ThingClass terraAcRtu +---------- +The name of the StateType ({0c685ced-27fa-46ba-8c0b-c49af82054f0}) of ThingClass terraAcTcp + + + + + + + + Charging enabled + The name of the ParamType (ThingClass: terraAcRtu, ActionType: power, ID: {e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6}) +---------- +The name of the StateType ({e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6}) of ThingClass terraAcRtu +---------- +The name of the ParamType (ThingClass: terraAcTcp, ActionType: power, ID: {207e2074-0147-4617-9a8b-3f326dcd6a0b}) +---------- +The name of the StateType ({207e2074-0147-4617-9a8b-3f326dcd6a0b}) of ThingClass terraAcTcp + + + + + + Communication timeout + The name of the ParamType (ThingClass: terraAcRtu, Type: settings, ID: {c4e0a515-270e-403e-8453-02002859938e}) +---------- +The name of the ParamType (ThingClass: terraAcTcp, Type: settings, ID: {5a0e678a-ffc8-443a-9ae7-bb2243b0ec4b}) + + + + + + Connected + The name of the StateType ({c43068ca-dddc-4c43-9149-68927afae5e7}) of ThingClass terraAcRtu +---------- +The name of the StateType ({21e1a2bb-0e05-462c-9909-4f9a0eada438}) of ThingClass terraAcTcp + + + + + + Current phase 1 + The name of the StateType ({a8e2e2cf-b2ca-48c5-a702-9226062c43e3}) of ThingClass terraAcRtu +---------- +The name of the StateType ({953ce672-a9a2-4cd6-bf87-ccfe720c7595}) of ThingClass terraAcTcp + + + + + + Current phase 2 + The name of the StateType ({210ff017-ea75-4705-9f23-f73e324601ef}) of ThingClass terraAcRtu +---------- +The name of the StateType ({e4f97cde-191c-4c84-9668-169c5e648338}) of ThingClass terraAcTcp + + + + + + Current phase 3 + The name of the StateType ({3a5d6ce5-d3ed-4ba5-996f-15a2b3124faf}) of ThingClass terraAcRtu +---------- +The name of the StateType ({c1cead50-9894-4b12-9211-c5bc84109b1d}) of ThingClass terraAcTcp + + + + + + Error code + The name of the StateType ({1f7d2791-c23e-47fe-8512-850ea25e09b3}) of ThingClass terraAcRtu +---------- +The name of the StateType ({ce98bfda-b27a-4f69-bd4e-49eb3e722245}) of ThingClass terraAcTcp + + + + + + Firmware version + The name of the StateType ({071d7408-fa96-49da-b56b-f5564bee9b5d}) of ThingClass terraAcRtu +---------- +The name of the StateType ({6d9b38a7-c6eb-47bc-8929-5a875d0fd5d0}) of ThingClass terraAcTcp + + + + + Host address + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {962ef9d9-30a2-4636-8ff2-2a28742277f4}) + + + + + Host name + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {f9e6fed4-2a18-4f23-b70e-78919d4b01f8}) + + + + + MAC address + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {9b3332aa-6c26-4399-b3dc-d7fdbe3f4420}) + + + + + + + + Maximum charging current + The name of the ParamType (ThingClass: terraAcRtu, ActionType: maxChargingCurrent, ID: {ea933a77-a098-4303-bbdb-15c72dfd3634}) +---------- +The name of the StateType ({ea933a77-a098-4303-bbdb-15c72dfd3634}) of ThingClass terraAcRtu +---------- +The name of the ParamType (ThingClass: terraAcTcp, ActionType: maxChargingCurrent, ID: {e3d27f8a-73d0-493a-b99a-29e7dc184485}) +---------- +The name of the StateType ({e3d27f8a-73d0-493a-b99a-29e7dc184485}) of ThingClass terraAcTcp + + + + + Modbus RTU master + The name of the ParamType (ThingClass: terraAcRtu, Type: thing, ID: {deb19a6b-33b3-417e-abf1-a1fb18585fe4}) + + + + + Modbus slave ID + The name of the ParamType (ThingClass: terraAcRtu, Type: thing, ID: {23dbb93d-6605-435c-9a41-b8a8e8242ea0}) + + + + + + Phase count + The name of the StateType ({cd1add95-18d9-46b5-a3d5-f0f29d5160c9}) of ThingClass terraAcRtu +---------- +The name of the StateType ({0764bce9-fd26-4da8-8d92-f6a5ce73e81e}) of ThingClass terraAcTcp + + + + + + Plugged in + The name of the StateType ({14fcf045-52d7-4f47-b64e-2bf3f87c242f}) of ThingClass terraAcRtu +---------- +The name of the StateType ({2fb30a0b-dc5a-4831-b5c6-de653c0c9ee1}) of ThingClass terraAcTcp + + + + + Port + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {153258dd-e81b-48e5-9ba7-46d59e854d85}) + + + + + + Serial number + The name of the StateType ({c95ea476-4ce3-4c04-bc23-238bdee496ee}) of ThingClass terraAcRtu +---------- +The name of the StateType ({69c9f4e2-ec8c-4d65-9cf4-ce8b7a3f8c3b}) of ThingClass terraAcTcp + + + + + + Session energy + The name of the StateType ({7a9a6ec3-7572-42d8-833d-5b434ca95765}) of ThingClass terraAcRtu +---------- +The name of the StateType ({be2cc28e-79c2-4eaf-91ad-8131ac1f87e2}) of ThingClass terraAcTcp + + + + + + Set charging enabled + The name of the ActionType ({e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6}) of ThingClass terraAcRtu +---------- +The name of the ActionType ({207e2074-0147-4617-9a8b-3f326dcd6a0b}) of ThingClass terraAcTcp + + + + + + Set maximum charging current + The name of the ActionType ({ea933a77-a098-4303-bbdb-15c72dfd3634}) of ThingClass terraAcRtu +---------- +The name of the ActionType ({e3d27f8a-73d0-493a-b99a-29e7dc184485}) of ThingClass terraAcTcp + + + + + Slave ID + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {cc4c66c1-185f-4c97-88a0-2bc66319d157}) + + + + + Terra AC Charger (RTU) + The name of the ThingClass ({a385801e-600c-4e27-ad73-5184a7516860}) + + + + + Terra AC Charger (TCP) + The name of the ThingClass ({93ad828a-9a7a-4fca-be3f-88641317845f}) + + + + + + Voltage phase 1 + The name of the StateType ({52ebc781-a527-4641-b768-3a0bd0209f96}) of ThingClass terraAcRtu +---------- +The name of the StateType ({bcff4c2d-5567-40ff-adef-5b47dc5ace76}) of ThingClass terraAcTcp + + + + + + Voltage phase 2 + The name of the StateType ({bb513653-70ca-4ee1-a913-aab3d2881708}) of ThingClass terraAcRtu +---------- +The name of the StateType ({db7af25a-cb7b-4640-9515-a7af345bd770}) of ThingClass terraAcTcp + + + + + + Voltage phase 3 + The name of the StateType ({c84a5e75-121d-4af8-a203-23f586613cf2}) of ThingClass terraAcRtu +---------- +The name of the StateType ({fe67e1bf-6871-45a6-a253-819cb70c5ec4}) of ThingClass terraAcTcp + + + + + IntegrationPluginAbbterra + + + No connected Modbus RTU master available. + + + + + + The charging station is not reachable. + + + + + + Could not initialize the communication with the charger. + + + + + + The device does not match the ABB Terra AC Modbus register map. + + + + + The Modbus RTU connection is not available. + + + +