diff --git a/debian/control b/debian/control index 71da841..cadd11d 100644 --- a/debian/control +++ b/debian/control @@ -258,6 +258,15 @@ Description: nymea integration plugin for PhoenixConnect wallboxes This package contains the nymea integration plugin for wallboxes made by PhonenixConnect and rebranded as Wallbe, Compleo and Scapo. +Package: nymea-plugin-wattsonic +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for Wattsonic hybrid inverters + This package contains the nymea integration plugin for hybrid inverters made + by Wattsonic. + Package: nymea-plugin-webasto Architecture: any Section: libs diff --git a/debian/nymea-plugin-wattsonic.install.in b/debian/nymea-plugin-wattsonic.install.in new file mode 100644 index 0000000..c860a58 --- /dev/null +++ b/debian/nymea-plugin-wattsonic.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginwattsonic.so +wattsonic/translations/*qm usr/share/nymea/translations/ diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index 771cf06..135ef91 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -25,6 +25,7 @@ PLUGIN_DIRS = \ stiebeleltron \ sunspec \ unipi \ + wattsonic \ webasto \ gcc { diff --git a/wattsonic/README.md b/wattsonic/README.md new file mode 100644 index 0000000..b0a1386 --- /dev/null +++ b/wattsonic/README.md @@ -0,0 +1,4 @@ +# Wattsonic + +This plugin allows to connect wattsonic hybrid inverters to nymea + diff --git a/wattsonic/integrationpluginwattsonic.cpp b/wattsonic/integrationpluginwattsonic.cpp new file mode 100644 index 0000000..f9b5c9c --- /dev/null +++ b/wattsonic/integrationpluginwattsonic.cpp @@ -0,0 +1,262 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "integrationpluginwattsonic.h" +#include "plugininfo.h" +#include "wattsonicdiscovery.h" + +#include +#include + +IntegrationPluginWattsonic::IntegrationPluginWattsonic() +{ + +} + +void IntegrationPluginWattsonic::discoverThings(ThingDiscoveryInfo *info) +{ + + if (info->thingClassId() == inverterThingClassId) { + + WattsonicDiscovery *discovery = new WattsonicDiscovery(hardwareManager()->modbusRtuResource(), info); + connect(discovery, &WattsonicDiscovery::discoveryFinished, info, [=](bool modbusRtuMasterAvailable){ + if (!modbusRtuMasterAvailable) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No suitable Modbus RTU connection available. Please set up a Modbus RTU master with a baudrate of 9600, 8 data bits, 1 stop bit and no parity.")); + return; + } + + foreach (const WattsonicDiscovery::Result &result, discovery->discoveryResults()) { + + QString name = "Wattsonic hybrid inverter"; + ThingDescriptor descriptor(inverterThingClassId, name, result.serialNumber); + qCDebug(dcWattsonic()) << "Discovered:" << descriptor.title() << descriptor.description(); + + ParamList params { + {inverterThingModbusMasterUuidParamTypeId, result.modbusRtuMasterId}, + {inverterThingSlaveAddressParamTypeId, result.slaveId} + }; + descriptor.setParams(params); + + // Check if we already have set up this device + Thing *existingThing = myThings().findByParams(params); + if (existingThing) { + qCDebug(dcWattsonic()) << "This inverter already exists in the system:" << result.serialNumber; + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + discovery->startDiscovery(); + } +} + +void IntegrationPluginWattsonic::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcWattsonic()) << "Setup" << thing << thing->params(); + + if (info->thing()->thingClassId() == inverterThingClassId) { + if (m_connections.contains(thing)) { + qCDebug(dcWattsonic()) << "Reconfiguring existing thing" << thing->name(); + m_connections.take(thing)->deleteLater(); + } + + setupWattsonicConnection(info); + return; + } + + if (info->thing()->thingClassId() == meterThingClassId) { + info->finish(Thing::ThingErrorNoError); + return; + } + + if (info->thing()->thingClassId() == batteryThingClassId) { + info->finish(Thing::ThingErrorNoError); + return; + } + +} + +void IntegrationPluginWattsonic::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == inverterThingClassId) { + Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId); + if (meters.isEmpty()) { + qCInfo(dcWattsonic()) << "No energy meter set up yet. Creating thing..."; + ThingDescriptor descriptor(meterThingClassId, "Wattsonic energy meter", QString(), thing->id()); + emit autoThingsAppeared({descriptor}); + } + Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId); + if (batteries.isEmpty()) { + qCInfo(dcWattsonic()) << "No battery set up yet. Creating thing..."; + ThingDescriptor descriptor(batteryThingClassId, "Wattsonic energy storage", QString(), thing->id()); + emit autoThingsAppeared({descriptor}); + } + } + + + if (!m_pluginTimer) { + qCDebug(dcWattsonic()) << "Starting plugin timer..."; + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { + foreach(WattsonicModbusRtuConnection *connection, m_connections) { + qCDebug(dcWattsonic()) << "Updating connection" << connection->modbusRtuMaster()->serialPort() << connection->slaveId(); + connection->update(); + } + }); + + m_pluginTimer->start(); + } +} + +void IntegrationPluginWattsonic::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == inverterThingClassId && m_connections.contains(thing)) { + delete m_connections.take(thing); + } + + if (myThings().isEmpty() && m_pluginTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } +} + +void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + uint slaveId = thing->paramValue(inverterThingSlaveAddressParamTypeId).toUInt(); + if (slaveId > 247 || slaveId == 0) { + qCWarning(dcWattsonic()) << "Setup failed, slave ID is not valid" << slaveId; + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus address not valid. It must be a value between 1 and 247.")); + return; + } + + QUuid uuid = thing->paramValue(inverterThingModbusMasterUuidParamTypeId).toUuid(); + if (!hardwareManager()->modbusRtuResource()->hasModbusRtuMaster(uuid)) { + qCWarning(dcWattsonic()) << "Setup failed, hardware manager not available"; + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus RTU resource is not available.")); + return; + } + + WattsonicModbusRtuConnection *connection = new WattsonicModbusRtuConnection(hardwareManager()->modbusRtuResource()->getModbusRtuMaster(uuid), slaveId, this); + connect(info, &ThingSetupInfo::aborted, connection, &ModbusRtuMaster::deleteLater); + + m_connections.insert(thing, connection); + connect(info, &ThingSetupInfo::aborted, this, [=](){ + m_connections.take(info->thing())->deleteLater(); + }); + + connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [connection, thing, this](bool reachable){ + qCDebug(dcWattsonic()) << "Reachable state changed" << reachable; + if (reachable) { + connection->initialize(); + } else { + thing->setStateValue("connected", false); + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + child->setStateValue("connected", true); + } + } + }); + + connect(connection, &WattsonicModbusRtuConnection::initializationFinished, info, [=](bool success){ + qCDebug(dcWattsonic()) << "Initialisation finished" << success; + if (info->isInitialSetup() && !success) { + m_connections.take(info->thing())->deleteLater(); + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + info->finish(Thing::ThingErrorNoError); + + if (success) { + qCDebug(dcWattsonic) << "Firmware version:" << connection->firmwareVersion(); +// info->thing()->setStateValue(inverterCurrentVersionStateTypeId, compact20Connection->firmwareVersion()); + } + }); + + connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [=](bool reachable){ + thing->setStateValue(inverterConnectedStateTypeId, reachable); + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + child->setStateValue("connected", reachable); + } + }); + + + connect(connection, &WattsonicModbusRtuConnection::updateFinished, thing, [this, connection, thing](){ + qCDebug(dcWattsonic()) << "Update finished:" << thing->name() << connection; + + Thing *inverter = thing; + + inverter->setStateValue(inverterCurrentPowerStateTypeId, connection->pAC() * -1.0); + inverter->setStateValue(inverterTotalEnergyProducedStateTypeId, connection->totalPVGenerationFromInstallation() * 0.1); + qCInfo(dcWattsonic()) << "Updating inverter:" << inverter->stateValue(inverterCurrentPowerStateTypeId).toDouble() << "W" << inverter->stateValue(inverterTotalEnergyProducedStateTypeId).toDouble() << "kWh"; + + Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId); + if (!meters.isEmpty()) { + Thing *meter = meters.first(); + meter->setStateValue(meterCurrentPowerStateTypeId, connection->totalPowerOnMeter() * -1.0); + meter->setStateValue(meterTotalEnergyConsumedStateTypeId, connection->totalEnergyPurchasedFromGrid() / 10.0); + meter->setStateValue(meterTotalEnergyProducedStateTypeId, connection->totalEnergyInjectedToGrid() / 10.0); + meter->setStateValue(meterCurrentPowerPhaseAStateTypeId, connection->phaseAPower() * -1.0); + meter->setStateValue(meterCurrentPowerPhaseBStateTypeId, connection->phaseBPower() * -1.0); + meter->setStateValue(meterCurrentPowerPhaseCStateTypeId, connection->phaseCPower() * -1.0); + meter->setStateValue(meterVoltagePhaseAStateTypeId, connection->gridPhaseAVoltage() / 10.0); + meter->setStateValue(meterVoltagePhaseBStateTypeId, connection->gridPhaseBVoltage() / 10.0); + meter->setStateValue(meterVoltagePhaseCStateTypeId, connection->gridPhaseCVoltage() / 10.0); + // The phase current registers don't seem to contain proper values. Calculating ourselves instead +// meter->setStateValue(meterCurrentPhaseAStateTypeId, connection->gridPhaseACurrent() / 10.0); +// meter->setStateValue(meterCurrentPhaseBStateTypeId, connection->gridPhaseBCurrent() / 10.0); +// meter->setStateValue(meterCurrentPhaseCStateTypeId, connection->gridPhaseCCurrent() / 10.0); + meter->setStateValue(meterCurrentPhaseAStateTypeId, (connection->phaseAPower() * -1.0) / (connection->gridPhaseAVoltage() / 10.0)); + meter->setStateValue(meterCurrentPhaseBStateTypeId, (connection->phaseBPower() * -1.0) / (connection->gridPhaseBVoltage() / 10.0)); + meter->setStateValue(meterCurrentPhaseCStateTypeId, (connection->phaseCPower() * -1.0) / (connection->gridPhaseCVoltage() / 10.0)); + qCInfo(dcWattsonic()) << "Updating meter:" << meter->stateValue(meterCurrentPowerStateTypeId).toDouble() << "W" << meter->stateValue(meterTotalEnergyProducedStateTypeId).toDouble() << "kWh"; + } + Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId); + if (!batteries.isEmpty() && connection->SOC() > 0) { + Thing *battery = batteries.first(); + QHash map { + {WattsonicModbusRtuConnection::BatteryModeDischarge, "discharging"}, + {WattsonicModbusRtuConnection::BatteryModeCharge, "charging"} + }; + battery->setStateValue(batteryChargingStateStateTypeId, map.value(connection->batteryMode())); + battery->setStateValue(batteryCurrentPowerStateTypeId, connection->batteryPower() * -1.0); + battery->setStateValue(batteryBatteryLevelStateTypeId, connection->SOC() / 100.0); + battery->setStateValue(batteryBatteryCriticalStateTypeId, connection->SOC() < 500); + } + + }); + + +} diff --git a/wattsonic/integrationpluginwattsonic.h b/wattsonic/integrationpluginwattsonic.h new file mode 100644 index 0000000..7ab7271 --- /dev/null +++ b/wattsonic/integrationpluginwattsonic.h @@ -0,0 +1,66 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 INTEGRATIONPLUGINWATTSONIC_H +#define INTEGRATIONPLUGINWATTSONIC_H + +#include +#include +#include + +#include "extern-plugininfo.h" + +#include "wattsonicmodbusrtuconnection.h" + +class IntegrationPluginWattsonic: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginwattsonic.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginWattsonic(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +private: + void setupWattsonicConnection(ThingSetupInfo *info); + + PluginTimer *m_pluginTimer = nullptr; + QHash m_connections; +}; + +#endif // INTEGRATIONPLUGINWATTSONIC_H + + diff --git a/wattsonic/integrationpluginwattsonic.json b/wattsonic/integrationpluginwattsonic.json new file mode 100644 index 0000000..ee3a1d5 --- /dev/null +++ b/wattsonic/integrationpluginwattsonic.json @@ -0,0 +1,246 @@ +{ + "name": "Wattsonic", + "displayName": "Wattsonic", + "id": "d0eaf684-001e-4b1c-9e37-122955958de3", + "vendors": [ + { + "name": "wattsonic", + "displayName": "Wattsonic", + "id": "c335f9bc-3bc9-46bf-8c50-800cd93e827a", + "thingClasses": [ + { + "name": "inverter", + "displayName": "Wattsonic hybrid inverter", + "id": "688bef8d-2ba8-4eb3-b30e-16193eba02fb", + "createMethods": ["discovery", "user"], + "interfaces": ["solarinverter", "connectable"], + "paramTypes": [ + { + "id": "55a7d9ed-5f4f-41a2-8dc1-c6a5a79512d2", + "name": "slaveAddress", + "displayName": "Modbus slave address", + "type": "uint", + "defaultValue": 1 + }, + { + "id": "4f1238b5-07e0-4516-b84a-71670141ef81", + "name": "modbusMasterUuid", + "displayName": "Modbus RTU master", + "type": "QUuid", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "c0f77c00-b82a-478e-826b-bc3204d66100", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "2cbb25e6-c1bd-4216-b354-6ad6fa957e29", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "3107958e-07a2-4ebd-83a1-96fb5998cfb9", + "name": "currentPower", + "displayName": "Current power consumption", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + } + ] + }, + { + "id": "e27a9590-0d13-4d7f-b66b-946dad86b8c8", + "name": "meter", + "displayName": "Wattsonic energy meter", + "createMethods": ["auto"], + "interfaces": ["energymeter", "connectable"], + "stateTypes": [ + { + "id": "d3bf44be-00d0-4119-ad16-45648bc1532a", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "d5ce5ba0-bc1d-4af3-9afa-cbfb6c0d01ec", + "name": "currentPower", + "displayName": "Current power consumption", + "type": "double", + "unit": "Watt", + "cached": false, + "defaultValue": 0 + }, + { + "id": "60d486a6-337d-4977-9624-78d99657aea9", + "name": "totalEnergyConsumed", + "displayName": "Total consumed energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "3fd2e110-8851-4530-bb53-e8e5b20cd4cb", + "name": "totalEnergyProduced", + "displayName": "Total returned energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "1def2f00-8d1c-4d22-b662-a31a552c8a82", + "name": "currentPowerPhaseA", + "displayName": "Power consumption phase A", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "404549a1-de1e-42e2-8d5d-f581b8674b7d", + "name": "currentPowerPhaseB", + "displayName": "Power consumption phase B", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "6fccc09c-7c89-45b7-908e-9cbe5bda1c2c", + "name": "currentPowerPhaseC", + "displayName": "Power consumption phase C", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "12798e83-3093-411e-ab8e-5955956717da", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "eef67976-2c39-4d79-8b6d-053e369346a8", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "1e237634-d23c-4c20-b710-b3586bbb988f", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "80c12358-c2d9-4c1b-9150-b7dc3da8a7ae", + "name": "currentPhaseA", + "displayName": "Current phase A", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "d0914688-59ae-4bd9-97ee-1668e7948ab0", + "name": "currentPhaseB", + "displayName": "Current phase B", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "ecb8072f-dcb0-4e5e-8cc9-95947d3b7185", + "name": "currentPhaseC", + "displayName": "Current phase C", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + } + ] + }, + { + "id": "04c4e5fd-7b64-444f-8905-75651833224e", + "name": "battery", + "displayName": "Wattsonic energy storage", + "createMethods": ["auto"], + "interfaces": ["energystorage", "connectable"], + "stateTypes": [ + { + "id": "a8673429-043e-4149-8775-c89cab0b63ca", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "76c70da1-484d-4bb2-8070-22e5e141cadd", + "name": "batteryCritical", + "displayName": "Battery critical", + "type": "bool", + "defaultValue": false + }, + { + "id": "113cac91-24cf-45eb-93bf-e81145ba6c4e", + "name": "batteryLevel", + "displayName": "Battery level", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": 0 + }, + { + "id": "dfaa3890-e3d2-4262-be3d-ece18b520ed9", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "608da87c-e45f-471d-975d-936807a94c9a", + "name": "currentPower", + "displayName": "Current power consumption", + "type": "double", + "unit": "Watt", + "cached": false, + "defaultValue": 0 + }, + { + "id": "5c652ecd-1302-4796-92fa-672a3d9442c8", + "name": "capacity", + "displayName": "Capacity", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] + } + ] + } + ] +} diff --git a/wattsonic/meta.json b/wattsonic/meta.json new file mode 100644 index 0000000..3fd2ac8 --- /dev/null +++ b/wattsonic/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Wattsonic", + "tagline": "Connect Wattsonic devices to nymea.", + "icon": "wattsonic.svg", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/wattsonic/translations/d0eaf684-001e-4b1c-9e37-122955958de3-en_US.ts b/wattsonic/translations/d0eaf684-001e-4b1c-9e37-122955958de3-en_US.ts new file mode 100644 index 0000000..edb17f7 --- /dev/null +++ b/wattsonic/translations/d0eaf684-001e-4b1c-9e37-122955958de3-en_US.ts @@ -0,0 +1,65 @@ + + + + + IntegrationPluginWattsonic + + + The Modbus address not valid. It must be a value between 1 and 247. + + + + + The Modbus RTU resource is not available. + + + + + Wattsonic + + + Connected + The name of the StateType ({c0f77c00-b82a-478e-826b-bc3204d66100}) of ThingClass inverter + + + + + Current power consumption + The name of the StateType ({3107958e-07a2-4ebd-83a1-96fb5998cfb9}) of ThingClass inverter + + + + + Modbus RTU master + The name of the ParamType (ThingClass: inverter, Type: thing, ID: {4f1238b5-07e0-4516-b84a-71670141ef81}) + + + + + Modbus slave address + The name of the ParamType (ThingClass: inverter, Type: thing, ID: {55a7d9ed-5f4f-41a2-8dc1-c6a5a79512d2}) + + + + + Total energy produced + The name of the StateType ({2cbb25e6-c1bd-4216-b354-6ad6fa957e29}) of ThingClass inverter + + + + + + Wattsonic + The name of the vendor ({c335f9bc-3bc9-46bf-8c50-800cd93e827a}) +---------- +The name of the plugin Wattsonic ({d0eaf684-001e-4b1c-9e37-122955958de3}) + + + + + Wattsonic hybrid inverter + The name of the ThingClass ({688bef8d-2ba8-4eb3-b30e-16193eba02fb}) + + + + diff --git a/wattsonic/wattsonic-registers.json b/wattsonic/wattsonic-registers.json new file mode 100644 index 0000000..383cfba --- /dev/null +++ b/wattsonic/wattsonic-registers.json @@ -0,0 +1,362 @@ +{ + "className": "Wattsonic", + "protocol": "RTU", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 20, + "checkReachableRegister": "serialNumber", + "enums": [ + { + "name": "InverterStatus", + "values": [ + { + "key": "Wait", + "value": 0 + }, + { + "key": "Check", + "value": 1 + }, + { + "key": "OnGrid", + "value": 2 + }, + { + "key": "Fault", + "value": 3 + }, + { + "key": "Flash", + "value": 4 + }, + { + "key": "OffGrid", + "value": 5 + } + ] + }, + { + "name": "BatteryMode", + "values": [ + { + "key": "Discharge", + "value": 0 + }, + { + "key": "Charge", + "value": 1 + } + ] + } + ], + "blocks": [ + ], + "registers": [ + { + "id": "serialNumber", + "address": 10000, + "size": 8, + "type": "string", + "readSchedule": "init", + "registerType": "holdingRegister", + "description": "Serial number", + "access": "RO" + }, + { + "id": "firmwareVersion", + "address": 10011, + "size": 2, + "type": "uint32", + "readSchedule": "init", + "registerType": "holdingRegister", + "description": "Firmware version", + "access": "RO" + }, + { + "id": "inverterStatus", + "address": 10105, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Inverter status", + "enum": "InverterStatus", + "defaultValue": "InverterStatusWait", + "access": "RO" + }, + { + "id": "phaseAPower", + "address": 10994, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Phase A power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "phaseBPower", + "address": 10996, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Phase B power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "phaseCPower", + "address": 10998, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Phase C power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalPowerOnMeter", + "address": 11000, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total power on meter", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalGridInjectionEnergy", + "address": 11002, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total grid injection energy on meter", + "defaultValue": 0, + "unit": "1/100 kWh", + "access": "RO" + }, + { + "id": "totalPurchasingEnergyFromGrid", + "address": 11004, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total purchasing energy from grid on meter", + "defaultValue": 0, + "unit": "1/100 kWh", + "access": "RO" + }, + { + "id": "gridPhaseAVoltage", + "address": 11009, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase A Voltage", + "defaultValue": 0, + "unit": "1/10 V", + "access": "RO" + }, + { + "id": "gridPhaseACurrent", + "address": 11010, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase A Current", + "defaultValue": 0, + "unit": "1/10 A", + "access": "RO" + }, + { + "id": "gridPhaseBVoltage", + "address": 11011, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase B Voltage", + "defaultValue": 0, + "unit": "1/10 V", + "access": "RO" + }, + { + "id": "gridPhaseBCurrent", + "address": 11012, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase B Current", + "defaultValue": 0, + "unit": "1/10 A", + "access": "RO" + }, + { + "id": "gridPhaseCVoltage", + "address": 11013, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase C Voltage", + "defaultValue": 0, + "unit": "1/10 V", + "access": "RO" + }, + { + "id": "gridPhaseCCurrent", + "address": 11014, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase C Current", + "defaultValue": 0, + "unit": "1/10 A", + "access": "RO" + }, + { + "id": "pAC", + "address": 11016, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "P_AC", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalPVGenerationFromInstallation", + "address": 11020, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total PV Generation from installation", + "defaultValue": 0, + "unit": "1/10 kWh", + "access": "RO" + }, + { + "id": "pvInputTotalPower", + "address": 11028, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "PV Total Input Power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalBackupP", + "address": 30230, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total_Backup_P/AC Active Power", + "unit": "1/1000 kW", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "batteryMode", + "address": 30256, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery mode", + "enum": "BatteryMode", + "defaultValue": "BatteryModeDischarge", + "access": "RO" + }, + { + "id": "batteryPower", + "address": 30258, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalEnergyInjectedToGrid", + "address": 31102, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total energy injected to grid", + "defaultValue": 0, + "unit": "1/10 kWh", + "access": "RO" + }, + { + "id": "totalEnergyPurchasedFromGrid", + "address": 31104, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total energy purchased from grid", + "defaultValue": 0, + "unit": "1/10 kWh", + "access": "RO" + }, + { + "id": "batteryStrings", + "address": 32001, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery strings", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "SOC", + "address": 33000, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "SOC", + "defaultValue": 0, + "unit": "% * 100", + "access": "RO" + }, + { + "id": "SOH", + "address": 33001, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "SOH", + "defaultValue": 0, + "unit": "% * 100", + "access": "RO" + } + ] +} diff --git a/wattsonic/wattsonic.pro b/wattsonic/wattsonic.pro new file mode 100644 index 0000000..9875f8e --- /dev/null +++ b/wattsonic/wattsonic.pro @@ -0,0 +1,15 @@ +include(../plugins.pri) + +# Generate modbus connection +MODBUS_CONNECTIONS += wattsonic-registers.json + +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + wattsonicdiscovery.h \ + integrationpluginwattsonic.h + +SOURCES += \ + wattsonicdiscovery.cpp \ + integrationpluginwattsonic.cpp diff --git a/wattsonic/wattsonicdiscovery.cpp b/wattsonic/wattsonicdiscovery.cpp new file mode 100644 index 0000000..5e599d1 --- /dev/null +++ b/wattsonic/wattsonicdiscovery.cpp @@ -0,0 +1,100 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "wattsonicdiscovery.h" +#include "extern-plugininfo.h" + +#include + +QList slaveIdCandidates = {247}; + +WattsonicDiscovery::WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent): + QObject{parent}, + m_modbusRtuResource(modbusRtuResource) +{ + +} + +void WattsonicDiscovery::startDiscovery() +{ + qCInfo(dcWattsonic()) << "Discovery: Searching for Wattsonic device on modbus RTU..."; + + QList candidateMasters; + foreach (ModbusRtuMaster *master, m_modbusRtuResource->modbusRtuMasters()) { + if (master->baudrate() == 9600 && master->dataBits() == 8 && master->stopBits() == 1 && master->parity() == QSerialPort::NoParity) { + candidateMasters.append(master); + } + } + + if (candidateMasters.isEmpty()) { + qCWarning(dcWattsonic()) << "No usable modbus RTU master found."; + emit discoveryFinished(false); + return; + } + + foreach (ModbusRtuMaster *master, candidateMasters) { + if (master->connected()) { + tryConnect(master, 0); + } else { + qCWarning(dcWattsonic()) << "Modbus RTU master" << master->modbusUuid().toString() << "is not connected."; + } + } +} + +QList WattsonicDiscovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void WattsonicDiscovery::tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex) +{ + quint8 slaveId = slaveIdCandidates.at(slaveIdIndex); + qCDebug(dcWattsonic()) << "Scanning modbus RTU master" << master->modbusUuid() << "Slave ID:" << slaveId; + + ModbusRtuReply *reply = master->readHoldingRegister(slaveId, 10000, 8); + connect(reply, &ModbusRtuReply::finished, this, [=](){ + + if (reply->error() == ModbusRtuReply::NoError) { + + QString serialNumber = ModbusDataUtils::convertToString(reply->result(), ModbusDataUtils::ByteOrderBigEndian); + qCDebug(dcWattsonic()) << "Test reply finished!" << reply->error() << reply->result() << serialNumber; + + Result result {master->modbusUuid(), serialNumber, slaveId}; + m_discoveryResults.append(result); + + } + + if (slaveIdIndex < slaveIdCandidates.count() - 1) { + tryConnect(master, slaveIdIndex+1); + } else { + emit discoveryFinished(true); + } + }); +} diff --git a/wattsonic/wattsonicdiscovery.h b/wattsonic/wattsonicdiscovery.h new file mode 100644 index 0000000..1001b52 --- /dev/null +++ b/wattsonic/wattsonicdiscovery.h @@ -0,0 +1,65 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 WATTSONICDISCOVERY_H +#define WATTSONICDISCOVERY_H + +#include +#include + +class WattsonicDiscovery : public QObject +{ + Q_OBJECT +public: + explicit WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent = nullptr); + + struct Result { + QUuid modbusRtuMasterId; + QString serialNumber; + quint16 slaveId; + }; + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(bool modbusRtuMasterAvailable); + +private slots: + void tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex); + +private: + ModbusRtuHardwareResource *m_modbusRtuResource = nullptr; + + QList m_discoveryResults; +}; + +#endif // WATTSONICDISCOVERY_H