diff --git a/abbb2x/2CMC485003M0201_en_B23_B24_User_Manual.pdf b/abbb2x/2CMC485003M0201_en_B23_B24_User_Manual.pdf new file mode 100644 index 0000000..757a2c0 Binary files /dev/null and b/abbb2x/2CMC485003M0201_en_B23_B24_User_Manual.pdf differ diff --git a/abbb2x/2CMC485004M0201_en_D_B21_User_Manual.pdf.pdf b/abbb2x/2CMC485004M0201_en_D_B21_User_Manual.pdf.pdf new file mode 100644 index 0000000..f96109e Binary files /dev/null and b/abbb2x/2CMC485004M0201_en_D_B21_User_Manual.pdf.pdf differ diff --git a/abbb2x/abb.png b/abbb2x/abb.png new file mode 100644 index 0000000..7f54801 Binary files /dev/null and b/abbb2x/abb.png differ diff --git a/abbb2x/abbb2x-registers.json b/abbb2x/abbb2x-registers.json new file mode 100644 index 0000000..dbcec7d --- /dev/null +++ b/abbb2x/abbb2x-registers.json @@ -0,0 +1,164 @@ +{ + "className": "AbbB2x", + "protocol": "BOTH", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 15, + "checkReachableRegister": "voltagePhaseA", + "blocks": [ + { + "id": "instantaneousValues", + "readSchedule": "update", + "registers": [ + { + "id": "voltagePhaseA", + "address": 23296, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L1-N", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltagePhaseB", + "address": 23298, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L2-N", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltagePhaseC", + "address": 23300, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L3-N", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "currentPhaseA", + "address": 23308, + "size": 2, + "type": "uint32", + "unit": "0.01A", + "registerType": "holdingRegister", + "description": "Current L1", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "currentPhaseB", + "address": 23310, + "size": 2, + "type": "uint32", + "unit": "0.01A", + "registerType": "holdingRegister", + "description": "Current L2", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "currentPhaseC", + "address": 23312, + "size": 2, + "type": "uint32", + "unit": "0.01A", + "registerType": "holdingRegister", + "description": "Current L3", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "activePowerTotal", + "address": 23316, + "size": 2, + "type": "int32", + "unit": "0.01W", + "registerType": "holdingRegister", + "description": "Active power Total (signed: + import / - export)", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "activePowerPhaseA", + "address": 23318, + "size": 2, + "type": "int32", + "unit": "0.01W", + "registerType": "holdingRegister", + "description": "Active power L1 (signed)", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "activePowerPhaseB", + "address": 23320, + "size": 2, + "type": "int32", + "unit": "0.01W", + "registerType": "holdingRegister", + "description": "Active power L2 (signed)", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "activePowerPhaseC", + "address": 23322, + "size": 2, + "type": "int32", + "unit": "0.01W", + "registerType": "holdingRegister", + "description": "Active power L3 (signed)", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "frequency", + "address": 23340, + "size": 1, + "type": "uint16", + "unit": "0.01Hz", + "registerType": "holdingRegister", + "description": "Frequency", + "defaultValue": "0", + "access": "RO" + } + ] + }, + { + "id": "energyAccumulators", + "readSchedule": "update", + "registers": [ + { + "id": "totalEnergyConsumed", + "address": 20480, + "size": 4, + "type": "uint64", + "unit": "0.01kWh", + "registerType": "holdingRegister", + "description": "Active import (total consumed energy)", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "totalEnergyProduced", + "address": 20484, + "size": 4, + "type": "uint64", + "unit": "0.01kWh", + "registerType": "holdingRegister", + "description": "Active export (total produced energy)", + "defaultValue": "0", + "access": "RO" + } + ] + } + ] +} diff --git a/abbb2x/abbb2x.pro b/abbb2x/abbb2x.pro new file mode 100644 index 0000000..b1f5e7e --- /dev/null +++ b/abbb2x/abbb2x.pro @@ -0,0 +1,12 @@ +include(../plugins.pri) + +# Generate modbus connections +MODBUS_CONNECTIONS += abbb2x-registers.json +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + integrationpluginabbb2x.h + +SOURCES += \ + integrationpluginabbb2x.cpp diff --git a/abbb2x/integrationpluginabbb2x.cpp b/abbb2x/integrationpluginabbb2x.cpp new file mode 100644 index 0000000..17b1754 --- /dev/null +++ b/abbb2x/integrationpluginabbb2x.cpp @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2025, ETM-Schurig SARL +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginabbb2x.h" +#include "plugininfo.h" + +// Facteurs d'echelle ABB B2x (cf. registers.json / manuel B2x) : +// tension : 0,1 V -> /10 +// courant : 0,01 A -> /100 +// puissance : 0,01 W -> /100 (registre SIGNED : + import / - export) +// frequence : 0,01 Hz -> /100 +// energie cumulee : 0,01 kWh -> /100 +// NB : le generateur renvoie la valeur BRUTE du registre ; le scaling se fait ici +// (meme convention que le plugin abbterra). + +IntegrationPluginAbbB2x::IntegrationPluginAbbB2x() +{ +} + +void IntegrationPluginAbbB2x::init() +{ + connect(hardwareManager()->modbusRtuResource(), &ModbusRtuHardwareResource::modbusRtuMasterRemoved, + this, [=](const QUuid &modbusUuid) { + qCDebug(dcAbbB2x()) << "Modbus RTU master removed:" << modbusUuid.toString(); + + foreach (Thing *thing, myThings()) { + if (thing->paramValue(abbB2xThingModbusMasterUuidParamTypeId) == modbusUuid) { + thing->setStateValue(abbB2xConnectedStateTypeId, false); + if (m_connections.contains(thing)) + delete m_connections.take(thing); + } + } + }); +} + +void IntegrationPluginAbbB2x::discoverThings(ThingDiscoveryInfo *info) +{ + if (info->thingClassId() != abbB2xThingClassId) { + info->finish(Thing::ThingErrorThingClassNotFound); + return; + } + + if (hardwareManager()->modbusRtuResource()->modbusRtuMasters().isEmpty()) { + info->finish(Thing::ThingErrorHardwareNotAvailable, + QT_TR_NOOP("No Modbus RTU interface available. Please set up the Modbus RTU interface first.")); + return; + } + + uint slaveAddress = info->params().paramValue(abbB2xDiscoverySlaveAddressParamTypeId).toUInt(); + if (slaveAddress == 0 || slaveAddress > 254) { + info->finish(Thing::ThingErrorInvalidParameter, + QT_TR_NOOP("The Modbus slave address must be a value between 1 and 254.")); + return; + } + + foreach (ModbusRtuMaster *modbusMaster, hardwareManager()->modbusRtuResource()->modbusRtuMasters()) { + qCDebug(dcAbbB2x()) << "Found RTU master" << modbusMaster << "connected:" << modbusMaster->connected(); + if (!modbusMaster->connected()) + continue; + + ThingDescriptor descriptor(info->thingClassId(), "ABB B2x", + QString::number(slaveAddress) + " " + modbusMaster->serialPort()); + ParamList params; + params << Param(abbB2xThingSlaveAddressParamTypeId, slaveAddress); + params << Param(abbB2xThingModbusMasterUuidParamTypeId, modbusMaster->modbusUuid()); + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginAbbB2x::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() != abbB2xThingClassId) { + info->finish(Thing::ThingErrorThingClassNotFound); + return; + } + + uint address = thing->paramValue(abbB2xThingSlaveAddressParamTypeId).toUInt(); + if (address == 0 || address > 254) { + qCWarning(dcAbbB2x()) << "Setup failed, invalid slave address" << address; + info->finish(Thing::ThingErrorSetupFailed, + QT_TR_NOOP("The Modbus address not valid. It must be a value between 1 and 254.")); + return; + } + + QUuid uuid = thing->paramValue(abbB2xThingModbusMasterUuidParamTypeId).toUuid(); + if (!hardwareManager()->modbusRtuResource()->hasModbusRtuMaster(uuid)) { + qCWarning(dcAbbB2x()) << "Setup failed, Modbus RTU master not available"; + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus RTU interface not available.")); + return; + } + + if (m_connections.contains(thing)) + m_connections.take(thing)->deleteLater(); + + AbbB2xModbusRtuConnection *connection = new AbbB2xModbusRtuConnection( + hardwareManager()->modbusRtuResource()->getModbusRtuMaster(uuid), address, this); + + connect(connection, &AbbB2xModbusRtuConnection::reachableChanged, this, [=](bool reachable) { + thing->setStateValue(abbB2xConnectedStateTypeId, reachable); + }); + + // Tensions (0,1 V) + connect(connection, &AbbB2xModbusRtuConnection::voltagePhaseAChanged, this, [=](quint32 v) { + thing->setStateValue(abbB2xVoltagePhaseAStateTypeId, v / 10.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::voltagePhaseBChanged, this, [=](quint32 v) { + thing->setStateValue(abbB2xVoltagePhaseBStateTypeId, v / 10.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::voltagePhaseCChanged, this, [=](quint32 v) { + thing->setStateValue(abbB2xVoltagePhaseCStateTypeId, v / 10.0); + }); + + // Courants (0,01 A) + connect(connection, &AbbB2xModbusRtuConnection::currentPhaseAChanged, this, [=](quint32 v) { + thing->setStateValue(abbB2xCurrentPhaseAStateTypeId, v / 100.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::currentPhaseBChanged, this, [=](quint32 v) { + thing->setStateValue(abbB2xCurrentPhaseBStateTypeId, v / 100.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::currentPhaseCChanged, this, [=](quint32 v) { + thing->setStateValue(abbB2xCurrentPhaseCStateTypeId, v / 100.0); + }); + + // Puissances (0,01 W, SIGNED : + import / - export) + connect(connection, &AbbB2xModbusRtuConnection::activePowerTotalChanged, this, [=](qint32 v) { + thing->setStateValue(abbB2xCurrentPowerStateTypeId, v / 100.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::activePowerPhaseAChanged, this, [=](qint32 v) { + thing->setStateValue(abbB2xCurrentPowerPhaseAStateTypeId, v / 100.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::activePowerPhaseBChanged, this, [=](qint32 v) { + thing->setStateValue(abbB2xCurrentPowerPhaseBStateTypeId, v / 100.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::activePowerPhaseCChanged, this, [=](qint32 v) { + thing->setStateValue(abbB2xCurrentPowerPhaseCStateTypeId, v / 100.0); + }); + + // Frequence (0,01 Hz) + connect(connection, &AbbB2xModbusRtuConnection::frequencyChanged, this, [=](quint16 v) { + thing->setStateValue(abbB2xFrequencyStateTypeId, v / 100.0); + }); + + // Energie cumulee (0,01 kWh) + connect(connection, &AbbB2xModbusRtuConnection::totalEnergyConsumedChanged, this, [=](quint64 v) { + thing->setStateValue(abbB2xTotalEnergyConsumedStateTypeId, v / 100.0); + }); + connect(connection, &AbbB2xModbusRtuConnection::totalEnergyProducedChanged, this, [=](quint64 v) { + thing->setStateValue(abbB2xTotalEnergyProducedStateTypeId, v / 100.0); + }); + + m_connections.insert(thing, connection); + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginAbbB2x::postSetupThing(Thing *thing) +{ + qCDebug(dcAbbB2x()) << "Post setup thing" << thing->name(); + if (!m_refreshTimer) { + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_refreshTimer, &PluginTimer::timeout, this, [this] { + foreach (Thing *thing, myThings()) { + if (m_connections.contains(thing)) + m_connections.value(thing)->update(); + } + }); + qCDebug(dcAbbB2x()) << "Refresh timer started"; + m_refreshTimer->start(); + } +} + +void IntegrationPluginAbbB2x::thingRemoved(Thing *thing) +{ + qCDebug(dcAbbB2x()) << "Thing removed" << thing->name(); + + if (m_connections.contains(thing)) + m_connections.take(thing)->deleteLater(); + + if (myThings().isEmpty() && m_refreshTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + qCDebug(dcAbbB2x()) << "Refresh timer stopped"; + } +} diff --git a/abbb2x/integrationpluginabbb2x.h b/abbb2x/integrationpluginabbb2x.h new file mode 100644 index 0000000..a8ec198 --- /dev/null +++ b/abbb2x/integrationpluginabbb2x.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2025, ETM-Schurig SARL +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINABBB2X_H +#define INTEGRATIONPLUGINABBB2X_H + +#include +#include +#include + +#include "abbb2xmodbusrtuconnection.h" + +#include "extern-plugininfo.h" + +#include +#include + +class IntegrationPluginAbbB2x: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginabbb2x.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginAbbB2x(); + void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +private: + PluginTimer *m_refreshTimer = nullptr; + QHash m_connections; +}; + +#endif // INTEGRATIONPLUGINABBB2X_H diff --git a/abbb2x/integrationpluginabbb2x.json b/abbb2x/integrationpluginabbb2x.json new file mode 100644 index 0000000..a02c758 --- /dev/null +++ b/abbb2x/integrationpluginabbb2x.json @@ -0,0 +1,181 @@ +{ + "id": "0c5c64dd-52b4-4afe-8438-83997f3485f9", + "name": "AbbB2x", + "displayName": "ABB B2x", + "vendors": [ + { + "id": "c112c5ba-6680-400a-9e67-3333ba6e3bd2", + "name": "abb", + "displayName": "ABB", + "thingClasses": [ + { + "id": "bd391ff1-ce19-48ba-8080-d74b8b88dfa2", + "name": "abbB2x", + "displayName": "ABB B2x energy meter", + "createMethods": ["discovery", "user"], + "interfaces": ["energymeter", "connectable"], + "providedInterfaces": [], + "discoveryParamTypes": [ + { + "id": "4d716b9c-3dab-4827-abdf-2bd394dabe6d", + "name": "slaveAddress", + "displayName": "Modbus slave address", + "type": "uint", + "minValue": 1, + "maxValue": 254, + "defaultValue": 1 + } + ], + "paramTypes": [ + { + "id": "54faf3d6-b243-4111-ace8-18eef6192e14", + "name": "slaveAddress", + "displayName": "Modbus slave address", + "type": "uint", + "minValue": 1, + "maxValue": 254, + "defaultValue": 1, + "readOnly": true + }, + { + "id": "3c10aac2-e125-40d8-8a7e-23185d3a7667", + "name": "modbusMasterUuid", + "displayName": "Modbus RTU master", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "", + "readOnly": true + } + ], + "stateTypes": [ + { + "id": "0ef1e287-b5c0-4011-9887-eb549a9d0e19", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "01fd9397-5a66-4a67-8144-ebad166ce926", + "name": "currentPower", + "displayName": "Current power", + "displayNameEvent": "Current power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "aa41495d-f4b9-44e3-a98c-1e6b83cb57d6", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "displayNameEvent": "Total energy consumed changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "975a11da-40f0-4b7a-af2b-b96b2dba6eb4", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "displayNameEvent": "Total energy produced changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "d7f3752b-b653-42c7-89aa-cf20cb06e69c", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0 + }, + { + "id": "a1a68363-072b-497b-bbb0-6f946198de26", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "69c25429-c4cf-4eb8-ace4-b43b9bb42777", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "c15752c6-7321-4c74-a65e-18ee3eb0cd13", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "78ce9dad-6432-4c53-b9bf-0a20ffaf8203", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "13cd360e-3f25-4c73-b5fe-72aa9ec96b9a", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "542b90dd-b5c7-4b9e-ba3a-c7416386f9a1", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "ec52d2f1-af43-4268-8d9e-16bfbe089259", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "displayNameEvent": "Current power phase A changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "0e9a9a3d-4df1-4531-aff3-63b21e560f40", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "displayNameEvent": "Current power phase B changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "93c9329d-fbda-4d79-bc24-c133b3763b9b", + "name": "currentPowerPhaseC", + "displayName": "Current power phase C", + "displayNameEvent": "Current power phase C changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + } + ] + } + ] + } + ] +} diff --git a/abbb2x/meta.json b/abbb2x/meta.json new file mode 100644 index 0000000..b373dfb --- /dev/null +++ b/abbb2x/meta.json @@ -0,0 +1,13 @@ +{ + "title": "ABB B2x", + "tagline": "Connect ABB B21 / B23 / B24 energy meters via Modbus RTU.", + "icon": "abb.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "modbus" + ], + "categories": [ + "energy" + ] +}