Merge PR #6: New plugin: SunSpec

This commit is contained in:
Jenkins nymea 2021-02-19 01:34:27 +01:00
commit 6ef901e8a1
21 changed files with 4903 additions and 3 deletions

18
debian/control vendored
View File

@ -50,7 +50,7 @@ Architecture: any
Section: libs
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-modbus-translations,
nymea-plugins-modbus-translations
Description: nymea.io plugin for my-pv heating rods
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
@ -61,6 +61,21 @@ Description: nymea.io plugin for my-pv heating rods
This package will install the nymea.io plugin for my-pv
Package: nymea-plugin-sunspec
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-modbus-translations
Description: nymea.io plugin for SunSpec Modbus devices.
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for SunSpec.
Package: nymea-plugin-wallbe
Architecture: any
Section: libs
@ -89,4 +104,3 @@ Description: Translation files for nymea modbus plugins - translations
in the system and create individual scenes and behaviors for your environment.
.
This package provides the translation files for all nymea modbus plugins.

View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsunspec.so

View File

@ -195,7 +195,6 @@ void IntegrationPluginMyPv::executeAction(ThingActionInfo *info)
void IntegrationPluginMyPv::onRefreshTimer()
{
foreach (Thing *thing, myThings()) {
update(thing);
}

View File

@ -4,6 +4,7 @@ PLUGIN_DIRS = \
drexelundweiss \
modbuscommander \
mypv \
sunspec \
wallbe \
message(============================================)

24
sunspec/README.md Normal file
View File

@ -0,0 +1,24 @@
# SunSpec
Connect nymea to SunSpec devices over Modbus TCP
## Supported Things
* SunSpec Inverter
* Model ID 101 & 111 Single phase inverter
* Model ID 102 & 112 Split phase inverter
* Model ID 103 & 113 Three phase inverter
* SunSpec Meter
* Model ID 201 & 211 Single phase meter
* Model ID 202 & 212 Split phase meter
* Model ID 203 & 213 Three phase meter
* SunSpec Storage
* Model ID 124
## Requirements
* The package 'nymea-plugin-sunspec' must be installed.
* The SunSpec device must support SunSpec over modbus TCP
## More
https://sunspec.org

View File

@ -0,0 +1,847 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 "plugininfo.h"
#include "integrationpluginsunspec.h"
#include <QHostAddress>
IntegrationPluginSunSpec::IntegrationPluginSunSpec()
{
}
void IntegrationPluginSunSpec::init()
{
m_connectedStateTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionConnectedStateTypeId);
m_connectedStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterConnectedStateTypeId);
m_connectedStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterConnectedStateTypeId);
m_connectedStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterConnectedStateTypeId);
m_connectedStateTypeIds.insert(sunspecStorageThingClassId, sunspecStorageConnectedStateTypeId);
m_connectedStateTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterConnectedStateTypeId);
m_connectedStateTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterConnectedStateTypeId);
m_connectedStateTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterConnectedStateTypeId);
m_modelIdParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingModelIdParamTypeId);
m_modelIdParamTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterThingModelIdParamTypeId);
m_modelIdParamTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterThingModelIdParamTypeId);
m_modelIdParamTypeIds.insert(sunspecStorageThingClassId, sunspecStorageThingModelIdParamTypeId);
m_modelIdParamTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterThingModelIdParamTypeId);
m_modelIdParamTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterThingModelIdParamTypeId);
m_modelIdParamTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterThingModelIdParamTypeId);
m_modbusAddressParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingModbusAddressParamTypeId);
m_modbusAddressParamTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterThingModbusAddressParamTypeId);
m_modbusAddressParamTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterThingModbusAddressParamTypeId);
m_modbusAddressParamTypeIds.insert(sunspecStorageThingClassId, sunspecStorageThingModbusAddressParamTypeId);
m_modbusAddressParamTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterThingModbusAddressParamTypeId);
m_modbusAddressParamTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterThingModbusAddressParamTypeId);
m_modbusAddressParamTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterThingModbusAddressParamTypeId);
m_inverterCurrentPowerStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterCurrentPowerStateTypeId);
m_inverterCurrentPowerStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterCurrentPowerStateTypeId);
m_inverterCurrentPowerStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterCurrentPowerStateTypeId);
m_inverterTotalEnergyProducedStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterTotalEnergyProducedStateTypeId);
m_inverterTotalEnergyProducedStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterTotalEnergyProducedStateTypeId);
m_inverterTotalEnergyProducedStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterTotalEnergyProducedStateTypeId);
m_inverterOperatingStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterOperatingStateStateTypeId);
m_inverterOperatingStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterOperatingStateStateTypeId);
m_inverterOperatingStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterOperatingStateStateTypeId);
m_inverterErrorStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterErrorStateTypeId);
m_inverterErrorStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterErrorStateTypeId);
m_inverterErrorStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterErrorStateTypeId);
m_inverterCabinetTemperatureStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterCabinetTemperatureStateTypeId);
m_inverterCabinetTemperatureStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterCabinetTemperatureStateTypeId);
m_inverterCabinetTemperatureStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterCabinetTemperatureStateTypeId);
m_inverterAcCurrentStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterTotalCurrentStateTypeId);
m_inverterAcCurrentStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterTotalCurrentStateTypeId);
m_inverterAcCurrentStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterTotalCurrentStateTypeId);
m_frequencyStateTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterFrequencyStateTypeId);
m_frequencyStateTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterFrequencyStateTypeId);
m_frequencyStateTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterFrequencyStateTypeId);
m_frequencyStateTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterFrequencyStateTypeId);
m_frequencyStateTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterFrequencyStateTypeId);
m_frequencyStateTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterFrequencyStateTypeId);
}
void IntegrationPluginSunSpec::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
qCDebug(dcSunSpec()) << "Setup thing" << thing->name();
if (thing->thingClassId() == sunspecConnectionThingClassId) {
QHostAddress address = QHostAddress(info->thing()->paramValue(sunspecConnectionThingIpAddressParamTypeId).toString());
int port = info->thing()->paramValue(sunspecConnectionThingPortParamTypeId).toInt();
int slaveId = info->thing()->paramValue(sunspecConnectionThingSlaveIdParamTypeId).toInt();
if (m_sunSpecConnections.contains(thing->id())) {
qCDebug(dcSunSpec()) << "Reconfigure SunSpec connection with new address" << address;
m_sunSpecConnections.take(thing->id())->deleteLater();
}
SunSpec *sunSpec;
sunSpec = new SunSpec(address, port, slaveId, this);
sunSpec->setTimeout(configValue(sunSpecPluginTimeoutParamTypeId).toUInt());
sunSpec->setNumberOfRetries(configValue(sunSpecPluginNumberOfRetriesParamTypeId).toUInt());
m_sunSpecConnections.insert(thing->id(), sunSpec);
if (!sunSpec->connectModbus()) {
qCWarning(dcSunSpec()) << "Error connecting to SunSpec device";
return info->finish(Thing::ThingErrorHardwareNotAvailable);
}
connect(sunSpec, &SunSpec::connectionStateChanged, info, [sunSpec, info] (bool status) {
qCDebug(dcSunSpec()) << "Modbus connection init finished" << status;
sunSpec->findBaseRegister();
connect(sunSpec, &SunSpec::foundBaseRegister, info, [info] (uint modbusAddress) {
qCDebug(dcSunSpec()) << "Found base register" << modbusAddress;
info->finish(Thing::ThingErrorNoError);
});
});
connect(info, &ThingSetupInfo::aborted, sunSpec, &SunSpec::deleteLater);
connect(sunSpec, &SunSpec::destroyed, [this, thing] {
m_sunSpecConnections.remove(thing->id());
});
connect(sunSpec, &SunSpec::foundSunSpecModel, this, &IntegrationPluginSunSpec::onFoundSunSpecModel);
connect(sunSpec, &SunSpec::sunspecModelSearchFinished, this, &IntegrationPluginSunSpec::onSunSpecModelSearchFinished);
connect(sunSpec, &SunSpec::commonModelReceived, thing, [thing] (const QString &manufacturer, const QString &deviceModel, const QString &serielNumber) {
thing->setStateValue(sunspecConnectionConnectedStateTypeId, true);
thing->setStateValue(sunspecConnectionManufacturerStateTypeId, manufacturer);
thing->setStateValue(sunspecConnectionDeviceModelStateTypeId, deviceModel);
thing->setStateValue(sunspecConnectionSerialNumberStateTypeId, serielNumber);
});
} else if (thing->thingClassId() == sunspecThreePhaseInverterThingClassId ||
thing->thingClassId() == sunspecSplitPhaseInverterThingClassId ||
thing->thingClassId() == sunspecSinglePhaseInverterThingClassId ) {
Thing *parent = myThings().findById(thing->parentId());
if (parent->setupStatus() == Thing::ThingSetupStatusComplete) {
setupInverter(info);
} else {
connect(parent, &Thing::setupStatusChanged, info, [this, info] {
setupInverter(info);
});
}
} else if (thing->thingClassId() == sunspecSinglePhaseMeterThingClassId ||
thing->thingClassId() == sunspecSplitPhaseMeterThingClassId ||
thing->thingClassId() == sunspecThreePhaseMeterThingClassId) {
Thing *parent = myThings().findById(thing->parentId());
if (parent->setupStatus() == Thing::ThingSetupStatusComplete) {
setupMeter(info);
} else {
connect(parent, &Thing::setupStatusChanged, info, [this, info] {
setupMeter(info);
});
}
} else if (info->thing()->thingClassId() == sunspecStorageThingClassId) {
Thing *parent = myThings().findById(thing->parentId());
if (parent->setupStatus() == Thing::ThingSetupStatusComplete) {
setupStorage(info);
} else {
connect(parent, &Thing::setupStatusChanged, info, [this, info] {
setupStorage(info);
});
}
} else {
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(info->thing()->thingClassId().toString()).toUtf8());
}
}
void IntegrationPluginSunSpec::postSetupThing(Thing *thing)
{
qCDebug(dcSunSpec()) << "Post setup thing" << thing->name();
if (!m_refreshTimer) {
qCDebug(dcSunSpec()) << "Starting refresh timer";
int refreshTime = configValue(sunSpecPluginUpdateIntervalParamTypeId).toInt();
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime);
connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSunSpec::onRefreshTimer);
}
if (thing->thingClassId() == sunspecConnectionThingClassId) {
SunSpec *connection = m_sunSpecConnections.value(thing->id());
if (!connection) {
qCDebug(dcSunSpec()) << "SunSpecConnection not found";
return;
}
connection->readCommonModel();
connection->findSunSpecModels(QList<SunSpec::ModelId>()); // Discover all models, without filter
} else if (thing->thingClassId() == sunspecSinglePhaseInverterThingClassId ||
thing->thingClassId() == sunspecSplitPhaseInverterThingClassId ||
thing->thingClassId() == sunspecThreePhaseInverterThingClassId) {
SunSpecInverter *sunSpecInverter = m_sunSpecInverters.value(thing);
if (!sunSpecInverter) {
qCDebug(dcSunSpec()) << "SunSpecInverter not found";
return;
}
sunSpecInverter->getInverterModelDataBlock();
} else if (thing->thingClassId() == sunspecSinglePhaseMeterThingClassId ||
thing->thingClassId() == sunspecSplitPhaseMeterThingClassId ||
thing->thingClassId() == sunspecThreePhaseMeterThingClassId) {
SunSpecMeter *sunSpecMeter = m_sunSpecMeters.value(thing);
if (!sunSpecMeter) {
qCDebug(dcSunSpec()) << "SunSpecMeter not found";
return;
}
sunSpecMeter->getMeterModelDataBlock();
} else if (thing->thingClassId() == sunspecStorageThingClassId) {
SunSpecStorage *sunSpecStorage = m_sunSpecStorages.value(thing);
if (!sunSpecStorage) {
qCDebug(dcSunSpec()) << "SunSpecStorage not found";
return;
}
sunSpecStorage->getStorageModelDataBlock();
} else {
Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
}
}
void IntegrationPluginSunSpec::thingRemoved(Thing *thing)
{
qCDebug(dcSunSpec()) << "Thing removed" << thing->name();
if (thing->thingClassId() == sunspecConnectionThingClassId) {
SunSpec *sunSpecConnection = m_sunSpecConnections.take(thing->id());
if (sunSpecConnection)
sunSpecConnection->deleteLater();
} else if (thing->thingClassId() == sunspecSinglePhaseInverterThingClassId ||
thing->thingClassId() == sunspecSplitPhaseInverterThingClassId ||
thing->thingClassId() == sunspecThreePhaseInverterThingClassId) {
SunSpecInverter *sunSpecInverter = m_sunSpecInverters.take(thing);
if (sunSpecInverter)
sunSpecInverter->deleteLater();
} else if (thing->thingClassId() == sunspecSinglePhaseMeterThingClassId ||
thing->thingClassId() == sunspecSplitPhaseMeterThingClassId ||
thing->thingClassId() == sunspecThreePhaseMeterThingClassId) {
SunSpecMeter *sunSpecMeter = m_sunSpecMeters.take(thing);
if (sunSpecMeter)
sunSpecMeter->deleteLater();
} else if (thing->thingClassId() == sunspecStorageThingClassId) {
SunSpecStorage *sunSpecStorage = m_sunSpecStorages.take(thing);
if (sunSpecStorage)
sunSpecStorage->deleteLater();
} else {
Q_ASSERT_X(false, "thingRemoved", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
}
if (myThings().isEmpty()) {
qCDebug(dcSunSpec()) << "Stopping refresh timer";
hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
m_refreshTimer = nullptr;
}
}
void IntegrationPluginSunSpec::executeAction(ThingActionInfo *info)
{
Thing *thing = info->thing();
Action action = info->action();
if (thing->thingClassId() == sunspecStorageThingClassId) {
SunSpecStorage *sunSpecStorage = m_sunSpecStorages.value(thing);
if (!sunSpecStorage) {
qWarning(dcSunSpec()) << "Could not find sunspec instance for thing";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
if (action.actionTypeId() == sunspecStorageGridChargingActionTypeId) {
QUuid requestId = sunSpecStorage->setGridCharging(action.param(sunspecStorageGridChargingActionGridChargingParamTypeId).value().toBool());
if (requestId.isNull()) {
info->finish(Thing::ThingErrorHardwareFailure);
} else {
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
}
} else if (action.actionTypeId() == sunspecStorageEnableChargingActionTypeId) {
bool charging = action.param(sunspecStorageEnableChargingActionEnableChargingParamTypeId).value().toBool();
bool discharging = thing->stateValue(sunspecStorageEnableDischargingStateTypeId).toBool();
QUuid requestId = sunSpecStorage->setStorageControlMode(charging, discharging);
if (requestId.isNull()) {
info->finish(Thing::ThingErrorHardwareFailure);
} else {
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
}
} else if (action.actionTypeId() == sunspecStorageChargingRateActionTypeId) {
QUuid requestId = sunSpecStorage->setChargingRate(action.param(sunspecStorageChargingRateActionChargingRateParamTypeId).value().toInt());
if (requestId.isNull()) {
info->finish(Thing::ThingErrorHardwareFailure);
} else {
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
}
} else if (action.actionTypeId() == sunspecStorageEnableDischargingActionTypeId) {
bool discharging = action.param(sunspecStorageEnableDischargingActionEnableDischargingParamTypeId).value().toBool();
bool charging = thing->stateValue(sunspecStorageEnableChargingStateTypeId).toBool();
QUuid requestId = sunSpecStorage->setStorageControlMode(charging, discharging);
if (requestId.isNull()) {
info->finish(Thing::ThingErrorHardwareFailure);
} else {
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
}
} else if (action.actionTypeId() == sunspecStorageDischargingRateActionTypeId) {
QUuid requestId = sunSpecStorage->setDischargingRate(action.param(sunspecStorageDischargingRateActionDischargingRateParamTypeId).value().toInt());
if (requestId.isNull()) {
info->finish(Thing::ThingErrorHardwareFailure);
} else {
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
}
} else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8());
}
} else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(info->thing()->thingClassId().toString()).toUtf8());
}
}
bool IntegrationPluginSunSpec::checkIfThingExists(uint modelId, uint modbusAddress)
{
Q_FOREACH(Thing *thing, myThings()) {
if (thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toUInt() == modelId &&
thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toUInt() == modbusAddress) {
return true;
}
}
return false;
}
void IntegrationPluginSunSpec::setupInverter(ThingSetupInfo *info)
{
Thing *thing = info->thing();
uint modelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toInt();
int modbusAddress = thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toInt();
SunSpec *connection = m_sunSpecConnections.value(thing->parentId());
if (!connection) {
qCWarning(dcSunSpec()) << "Could not find SunSpec connection";
return info->finish(Thing::ThingErrorHardwareNotAvailable);
}
SunSpecInverter *sunSpecInverter = new SunSpecInverter(connection, SunSpec::ModelId(modelId), modbusAddress);
sunSpecInverter->init();
connect(sunSpecInverter, &SunSpecInverter::initFinished, info, [this, sunSpecInverter, info] (bool success){
qCDebug(dcSunSpec()) << "Modbus Inverter init finished, success:" << success;
if (success) {
m_sunSpecInverters.insert(info->thing(), sunSpecInverter);
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareNotAvailable);
}
});
connect(info, &ThingSetupInfo::aborted, sunSpecInverter, &SunSpecInverter::deleteLater);
connect(sunSpecInverter, &SunSpecInverter::destroyed, thing, [thing, this] {m_sunSpecInverters.remove(thing);});
connect(sunSpecInverter, &SunSpecInverter::inverterDataReceived, this, &IntegrationPluginSunSpec::onInverterDataReceived);
}
void IntegrationPluginSunSpec::setupMeter(ThingSetupInfo *info)
{
Thing *thing = info->thing();
uint modelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toInt();
int modbusAddress = thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toInt();
SunSpec *connection = m_sunSpecConnections.value(thing->parentId());
if (!connection) {
qCWarning(dcSunSpec()) << "Could not find SunSpec connection";
return info->finish(Thing::ThingErrorHardwareNotAvailable);
}
SunSpecMeter *sunSpecMeter = new SunSpecMeter(connection, SunSpec::ModelId(modelId), modbusAddress);
sunSpecMeter->init();
connect(sunSpecMeter, &SunSpecMeter::initFinished, info, [this, sunSpecMeter, info] (bool success){
qCDebug(dcSunSpec()) << "Modbus meter init finished, success:" << success;
if (success) {
m_sunSpecMeters.insert(info->thing(), sunSpecMeter);
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareNotAvailable);
}
});
connect(info, &ThingSetupInfo::aborted, sunSpecMeter, &SunSpecMeter::deleteLater);
connect(sunSpecMeter, &SunSpecMeter::destroyed, thing, [thing, this] {m_sunSpecMeters.remove(thing);});
connect(sunSpecMeter, &SunSpecMeter::meterDataReceived, this, &IntegrationPluginSunSpec::onMeterDataReceived);
}
void IntegrationPluginSunSpec::setupStorage(ThingSetupInfo *info)
{
Thing *thing = info->thing();
uint modelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toInt();
int modbusAddress = thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toInt();
SunSpec *connection = m_sunSpecConnections.value(thing->parentId());
if (!connection) {
qCWarning(dcSunSpec()) << "Could not find SunSpec connection";
return info->finish(Thing::ThingErrorHardwareNotAvailable);
}
SunSpecStorage *sunSpecStorage = new SunSpecStorage(connection, SunSpec::ModelId(modelId), modbusAddress);
sunSpecStorage->init();
connect(sunSpecStorage, &SunSpecStorage::initFinished, info, [this, sunSpecStorage, info] (bool success){
qCDebug(dcSunSpec()) << "Modbus storage init finished, success:" << success;
if (success) {
m_sunSpecStorages.insert(info->thing(), sunSpecStorage);
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareNotAvailable);
}
});
connect(info, &ThingSetupInfo::aborted, sunSpecStorage, &SunSpecStorage::deleteLater);
connect(sunSpecStorage, &SunSpecStorage::destroyed, thing, [thing, this] {m_sunSpecStorages.remove(thing);});
connect(sunSpecStorage, &SunSpecStorage::storageDataReceived, this, &IntegrationPluginSunSpec::onStorageDataReceived);
}
void IntegrationPluginSunSpec::onRefreshTimer()
{
foreach (SunSpec *connection, m_sunSpecConnections) {
connection->readCommonModel(); //check connection
}
foreach (SunSpecInverter *inverter, m_sunSpecInverters) {
inverter->getInverterModelDataBlock();
}
foreach (SunSpecMeter *meter, m_sunSpecMeters) {
meter->getMeterModelDataBlock();
}
foreach (SunSpecStorage *storage, m_sunSpecStorages) {
storage->getStorageModelDataBlock();
}
}
void IntegrationPluginSunSpec::onPluginConfigurationChanged(const ParamTypeId &paramTypeId, const QVariant &value)
{
// Check refresh schedule
if (paramTypeId == sunSpecPluginUpdateIntervalParamTypeId) {
qCDebug(dcSunSpec()) << "Update interval has changed" << value.toInt();
if (m_refreshTimer) {
int refreshTime = value.toInt();
m_refreshTimer->stop();
m_refreshTimer->startTimer(refreshTime);
}
} else if (paramTypeId == sunSpecPluginNumberOfRetriesParamTypeId) {
qCDebug(dcSunSpec()) << "Updating number of retries" << value.toUInt();
Q_FOREACH(SunSpec *connection, m_sunSpecConnections) {
connection->setNumberOfRetries(value.toUInt());
}
} else if (paramTypeId == sunSpecPluginTimeoutParamTypeId) {
qCDebug(dcSunSpec()) << "Updating timeout" << value.toUInt() << "[ms]";
Q_FOREACH(SunSpec *connection, m_sunSpecConnections) {
connection->setTimeout(value.toUInt());
}
} else {
qCWarning(dcSunSpec()) << "Unknown plugin configuration" << paramTypeId << "Value" << value;
}
}
void IntegrationPluginSunSpec::onConnectionStateChanged(bool status)
{
SunSpec *connection = static_cast<SunSpec *>(sender());
Thing *thing = myThings().findById(m_sunSpecConnections.key(connection));
if (!thing)
return;
qCDebug(dcSunSpec()) << "Connection state changed" << status << thing->name();
if (thing->thingClassId() == sunspecConnectionConnectedStateTypeId) {
thing->setStateValue(sunspecConnectionConnectedStateTypeId, status);
}
Q_FOREACH(Thing *child, myThings().filterByParentId(thing->id())) {
child->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), status);
}
}
void IntegrationPluginSunSpec::onFoundSunSpecModel(SunSpec::ModelId modelId, int modbusStartRegister)
{
SunSpec *connection = static_cast<SunSpec *>(sender());
Thing *thing = myThings().findById(m_sunSpecConnections.key(connection));
if (!thing) {
qCWarning(dcSunSpec()) << "Thing not found for SunSpec connection" << connection->deviceModel() << connection->serialNumber();
return;
}
qCDebug(dcSunSpec()) << "On model received" << modelId << "start register" << modbusStartRegister << "Thing:" << thing->name();
if (checkIfThingExists(modelId, modbusStartRegister)) {
return;
}
QString model = thing->stateValue(sunspecConnectionDeviceModelStateTypeId).toString();
switch (modelId) {
case SunSpec::ModelId::ModelIdInverterSinglePhase:
case SunSpec::ModelId::ModelIdInverterSinglePhaseFloat: {
ThingDescriptor descriptor(sunspecSinglePhaseInverterThingClassId, model+tr(" single phase inverter"), "", thing->id());
ParamList params;
params.append(Param(sunspecSinglePhaseInverterThingModelIdParamTypeId, modelId));
params.append(Param(sunspecSinglePhaseInverterThingModbusAddressParamTypeId, modbusStartRegister));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
} break;
case SunSpec::ModelId::ModelIdInverterSplitPhase:
case SunSpec::ModelId::ModelIdInverterSplitPhaseFloat: {
ThingDescriptor descriptor(sunspecSplitPhaseInverterThingClassId, model+tr(" split phase inverter"), "", thing->id());
ParamList params;
params.append(Param(sunspecSplitPhaseInverterThingModelIdParamTypeId, modelId));
params.append(Param(sunspecSplitPhaseInverterThingModbusAddressParamTypeId, modbusStartRegister));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
} break;
case SunSpec::ModelId::ModelIdInverterThreePhase:
case SunSpec::ModelId::ModelIdInverterThreePhaseFloat: {
ThingDescriptor descriptor(sunspecThreePhaseInverterThingClassId, model+tr(" three phase inverter"), "", thing->id());
ParamList params;
params.append(Param(sunspecThreePhaseInverterThingModelIdParamTypeId, modelId));
params.append(Param(sunspecThreePhaseInverterThingModbusAddressParamTypeId, modbusStartRegister));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
} break;
case SunSpec::ModelIdSinglePhaseMeter:
case SunSpec::ModelIdSinglePhaseMeterFloat: {
ThingDescriptor descriptor(sunspecSinglePhaseMeterThingClassId, model+tr(" meter"), "", thing->id());
ParamList params;
params.append(Param(sunspecSinglePhaseMeterThingModelIdParamTypeId, modelId));
params.append(Param(sunspecSinglePhaseMeterThingModbusAddressParamTypeId, modbusStartRegister));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
} break;
case SunSpec::ModelIdSplitSinglePhaseMeter:
case SunSpec::ModelIdSplitSinglePhaseMeterFloat: {
ThingDescriptor descriptor(sunspecSplitPhaseMeterThingClassId, model+tr(" meter"), "", thing->id());
ParamList params;
params.append(Param(sunspecSplitPhaseMeterThingModelIdParamTypeId, modelId));
params.append(Param(sunspecSplitPhaseMeterThingModbusAddressParamTypeId, modbusStartRegister));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
} break;
case SunSpec::ModelIdWyeConnectThreePhaseMeter:
case SunSpec::ModelIdDeltaConnectThreePhaseMeter:
case SunSpec::ModelIdWyeConnectThreePhaseMeterFloat:
case SunSpec::ModelIdDeltaConnectThreePhaseMeterFloat: {
ThingDescriptor descriptor(sunspecThreePhaseMeterThingClassId, model+" meter", "", thing->id());
ParamList params;
params.append(Param(sunspecThreePhaseMeterThingModelIdParamTypeId, modelId));
params.append(Param(sunspecThreePhaseMeterThingModbusAddressParamTypeId, modbusStartRegister));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
} break;
case SunSpec::ModelIdStorage: {
ThingDescriptor descriptor(sunspecStorageThingClassId, model+" storage", "", thing->id());
ParamList params;
params.append(Param(sunspecStorageThingModelIdParamTypeId, modelId));
params.append(Param(sunspecStorageThingModbusAddressParamTypeId, modbusStartRegister));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
} break;
default:
qCDebug(dcSunSpec()) << "Model Id not handled";
}
}
void IntegrationPluginSunSpec::onSunSpecModelSearchFinished(const QHash<SunSpec::ModelId, int> &modelIds)
{
SunSpec *connection = static_cast<SunSpec *>(sender());
Thing *thing = myThings().findById(m_sunSpecConnections.key(connection));
if (!thing) {
qCWarning(dcSunSpec()) << "Thing not found for SunSpec connection" << connection->deviceModel() << connection->serialNumber();
return;
}
qCDebug(dcSunSpec()) << "On sunspec model search finished, models:" << modelIds.count();
}
void IntegrationPluginSunSpec::onWriteRequestExecuted(QUuid requestId, bool success)
{
qCDebug(dcSunSpec()) << "Write request executed" << requestId << success;
if (m_asyncActions.contains(requestId)) {
ThingActionInfo *info = m_asyncActions.take(requestId);
if (success) {
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareFailure);
}
}
}
void IntegrationPluginSunSpec::onWriteRequestError(QUuid requestId, const QString &error)
{
qCDebug(dcSunSpec()) << "Write request error" << requestId << error;
}
void IntegrationPluginSunSpec::onInverterDataReceived(const SunSpecInverter::InverterData &inverterData)
{
SunSpecInverter *inverter = static_cast<SunSpecInverter *>(sender());
Thing *thing = m_sunSpecInverters.key(inverter);
if(!thing) {
return;
}
qCDebug(dcSunSpec()) << "Inverter data received";
qCDebug(dcSunSpec()) << " - Total AC Current" << inverterData.acCurrent << "[A]";
qCDebug(dcSunSpec()) << " - Phase A Current" << inverterData.phaseACurrent << "[A]";
qCDebug(dcSunSpec()) << " - Phase B Current" << inverterData.phaseBCurrent << "[A]";
qCDebug(dcSunSpec()) << " - Phase C Current" << inverterData.phaseCCurrent << "[A]";
qCDebug(dcSunSpec()) << " - Phase voltage AB" << inverterData.phaseVoltageAB << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage BC" << inverterData.phaseVoltageBC << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage CA" << inverterData.phaseVoltageCA << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage AN" << inverterData.phaseVoltageAN << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage BN" << inverterData.phaseVoltageBN << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage CN" << inverterData.phaseVoltageCN << "[V]";
qCDebug(dcSunSpec()) << " - AC Power" << inverterData.acPower << "[W]";
qCDebug(dcSunSpec()) << " - Line frequency" << inverterData.lineFrequency << "[Hz]";
qCDebug(dcSunSpec()) << " - AC energy" << inverterData.acEnergy << "[Wh]";
qCDebug(dcSunSpec()) << " - Cabinet temperature" << inverterData.cabinetTemperature << "[°C]";
qCDebug(dcSunSpec()) << " - Operating state" << inverterData.operatingState;
thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), true);
thing->setStateValue(m_inverterCurrentPowerStateTypeIds.value(thing->thingClassId()), inverterData.acPower);
thing->setStateValue(m_inverterTotalEnergyProducedStateTypeIds.value(thing->thingClassId()), inverterData.acEnergy/1000.00);
thing->setStateValue(m_frequencyStateTypeIds.value(thing->thingClassId()), inverterData.lineFrequency);
thing->setStateValue(m_inverterAcCurrentStateTypeIds.value(thing->thingClassId()), inverterData.acCurrent);
thing->setStateValue(m_inverterCabinetTemperatureStateTypeIds.value(thing->thingClassId()), inverterData.cabinetTemperature);
if (thing->thingClassId() == sunspecSinglePhaseInverterThingClassId) {
thing->setStateValue(sunspecSinglePhaseInverterPhaseVoltageStateTypeId, inverterData.phaseVoltageAN);
} else if (thing->thingClassId() == sunspecSplitPhaseInverterThingClassId) {
thing->setStateValue(sunspecSplitPhaseInverterPhaseANVoltageStateTypeId, inverterData.phaseVoltageAN);
thing->setStateValue(sunspecSplitPhaseInverterPhaseBNVoltageStateTypeId, inverterData.phaseVoltageBN);
thing->setStateValue(sunspecSplitPhaseInverterPhaseACurrentStateTypeId, inverterData.phaseACurrent);
thing->setStateValue(sunspecSplitPhaseInverterPhaseBCurrentStateTypeId, inverterData.phaseBCurrent);
} else if (thing->thingClassId() == sunspecThreePhaseInverterThingClassId) {
thing->setStateValue(sunspecThreePhaseInverterPhaseANVoltageStateTypeId, inverterData.phaseVoltageAN);
thing->setStateValue(sunspecThreePhaseInverterPhaseBNVoltageStateTypeId, inverterData.phaseVoltageBN);
thing->setStateValue(sunspecThreePhaseInverterPhaseCNVoltageStateTypeId, inverterData.phaseVoltageCN);
thing->setStateValue(sunspecThreePhaseInverterPhaseACurrentStateTypeId, inverterData.phaseACurrent);
thing->setStateValue(sunspecThreePhaseInverterPhaseBCurrentStateTypeId, inverterData.phaseBCurrent);
thing->setStateValue(sunspecThreePhaseInverterPhaseCCurrentStateTypeId, inverterData.phaseCCurrent);
}
switch(inverterData.operatingState) {
case SunSpec::SunSpecOperatingState::Off:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "Off");
break;
case SunSpec::SunSpecOperatingState::MPPT:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "MPPT");
break;
case SunSpec::SunSpecOperatingState::Fault:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "Fault");
break;
case SunSpec::SunSpecOperatingState::Standby:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "Standby");
break;
case SunSpec::SunSpecOperatingState::Sleeping:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "Sleeping");
break;
case SunSpec::SunSpecOperatingState::Starting:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "Starting");
break;
case SunSpec::SunSpecOperatingState::Throttled:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "Throttled");
break;
case SunSpec::SunSpecOperatingState::ShuttingDown:
thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, "Shutting down");
break;
}
//FIXME: Event1 may have multiple states at once. Only one is stated in nymea
if (inverterData.event1.overTemperature) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Over temperature");
} else if (inverterData.event1.underTemperature) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Under temperature");
} else if (inverterData.event1.groundFault) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Ground fault");
} else if (inverterData.event1.memoryLoss) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Memory loss");
} else if (inverterData.event1.acOverVolt) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "AC voltage above limit");
} else if (inverterData.event1.cabinetOpen) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Cabinet open");
} else if (inverterData.event1.acDisconnect) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "AC disconnect open");
} else if (inverterData.event1.acUnderVolt) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "AC voltage under limit");
} else if (inverterData.event1.dcDicconnect) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "DC disconnect open");
} else if (inverterData.event1.dcOverVoltage) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "DC over voltage");
} else if (inverterData.event1.overFrequency) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Frequency above limit");
} else if (inverterData.event1.gridDisconnect) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Grid disconnect");
} else if (inverterData.event1.hwTestFailure) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Hardware test failure");
} else if (inverterData.event1.manualShutdown) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Manual shutdown");
} else if (inverterData.event1.underFrequency) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Frequency under limit");
} else if (inverterData.event1.blownStringFuse) {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "Blown string fuse on input");
} else {
thing->setStateValue(m_inverterErrorStateTypeIds.value(thing->thingClassId()), "None");
}
}
void IntegrationPluginSunSpec::onStorageDataReceived(const SunSpecStorage::StorageData &mandatory, const SunSpecStorage::StorageDataOptional &optional)
{
SunSpecStorage *storage = static_cast<SunSpecStorage *>(sender());
Thing *thing = m_sunSpecStorages.key(storage);
if(!thing) {
return;
}
qCDebug(dcSunSpec()) << "Storage data received";
qCDebug(dcSunSpec()) << " - Setpoint for maximum charge" << mandatory.WChaMax << "[W]";
qCDebug(dcSunSpec()) << " - Setpoint for maximum charging rate." << mandatory.WChaGra << "[%]";
qCDebug(dcSunSpec()) << " - Setpoint for maximum discharging rate." << mandatory.WDisChaGra << "[%]";
qCDebug(dcSunSpec()) << " - Charging enabled" << mandatory.StorCtl_Mod_ChargingEnabled;
qCDebug(dcSunSpec()) << " - Discharging enabled" << mandatory.StorCtl_Mod_DischargingEnabled;
qCDebug(dcSunSpec()) << " - Storage status" << optional.ChaSt;
qCDebug(dcSunSpec()) << " - Currently available energy" << optional.ChaState << "[%]";
qCDebug(dcSunSpec()) << " - Grid charging enabled" << optional.ChaGriSet;
thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), true);
thing->setStateValue(sunspecStorageChargingRateStateTypeId, mandatory.WChaGra);
thing->setStateValue(sunspecStorageDischargingRateStateTypeId, mandatory.WDisChaGra);
thing->setStateValue(sunspecStorageEnableChargingStateTypeId, mandatory.StorCtl_Mod_ChargingEnabled);
thing->setStateValue(sunspecStorageEnableDischargingStateTypeId, mandatory.StorCtl_Mod_DischargingEnabled);
thing->setStateValue(sunspecStorageGridChargingStateTypeId, optional.ChaGriSet);
bool charging = false;
switch (optional.ChaSt) {
case SunSpecStorage::ChargingStatusOff:
thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Off");
break;
case SunSpecStorage::ChargingStatusFull:
thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Full");
break;
case SunSpecStorage::ChargingStatusEmpty:
thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Empty");
break;
case SunSpecStorage::ChargingStatusHolding:
thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Holding");
break;
case SunSpecStorage::ChargingStatusTesting:
thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Testing");
break;
case SunSpecStorage::ChargingStatusCharging:
thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Charging");
break;
case SunSpecStorage::ChargingStatusDischarging:
thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Discharging");
break;
};
double batteryLevel = optional.ChaState;
thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, batteryLevel);
thing->setStateValue(sunspecStorageBatteryCriticalStateTypeId, (batteryLevel < 5 && !charging));
}
void IntegrationPluginSunSpec::onMeterDataReceived(const SunSpecMeter::MeterData &meterData)
{
SunSpecMeter *meter = static_cast<SunSpecMeter *>(sender());
Thing *thing = m_sunSpecMeters.key(meter);
if (!thing) {
return;
}
qCDebug(dcSunSpec()) << "Meter data received";
qCDebug(dcSunSpec()) << " - Total AC Current" << meterData.totalAcCurrent << "[A]";
qCDebug(dcSunSpec()) << " - Phase A current" << meterData.phaseACurrent << "[A]";
qCDebug(dcSunSpec()) << " - Phase B current" << meterData.phaseBCurrent << "[A]";
qCDebug(dcSunSpec()) << " - Phase C current" << meterData.phaseCCurrent << "[A]";
qCDebug(dcSunSpec()) << " - Voltage LN" << meterData.voltageLN << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage AN" << meterData.phaseVoltageAN << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage BN" << meterData.phaseVoltageBN << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage CN" << meterData.phaseVoltageCN<< "[V]";
qCDebug(dcSunSpec()) << " - Voltage LL" << meterData.voltageLL << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage AB" << meterData.phaseVoltageAB << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage BC" << meterData.phaseVoltageBC << "[V]";
qCDebug(dcSunSpec()) << " - Phase voltage CA" << meterData.phaseVoltageCA << "[V]";
qCDebug(dcSunSpec()) << " - Frequency" << meterData.frequency << "[Hz]";
qCDebug(dcSunSpec()) << " - Total real power" << meterData.totalRealPower << "[W]";
qCDebug(dcSunSpec()) << " - Total real energy exported" << meterData.totalRealEnergyExported<< "[kWH]";
qCDebug(dcSunSpec()) << " - Total real energy imported" << meterData.totalRealEnergyImported<< "[kWH]";
thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), true);
thing->setStateValue(m_frequencyStateTypeIds.value(thing->thingClassId()), meterData.frequency);
if (thing->thingClassId() == sunspecSinglePhaseMeterThingClassId) {
thing->setStateValue(sunspecSinglePhaseMeterPhaseACurrentStateTypeId, meterData.phaseACurrent);
thing->setStateValue(sunspecSinglePhaseMeterCurrentPowerEventTypeId, meterData.totalRealPower);
thing->setStateValue(sunspecSinglePhaseMeterLnACVoltageStateTypeId, meterData.voltageLN);
thing->setStateValue(sunspecSinglePhaseMeterTotalEnergyProducedStateTypeId, meterData.totalRealEnergyExported);
thing->setStateValue(sunspecSinglePhaseMeterTotalEnergyConsumedStateTypeId, meterData.totalRealEnergyImported);
} else if (thing->thingClassId() == sunspecSplitPhaseMeterThingClassId) {
thing->setStateValue(sunspecSplitPhaseMeterPhaseACurrentStateTypeId, meterData.phaseACurrent);
thing->setStateValue(sunspecSplitPhaseMeterPhaseBCurrentStateTypeId, meterData.phaseBCurrent);
thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerEventTypeId, meterData.totalRealPower);
thing->setStateValue(sunspecSplitPhaseMeterLnACVoltageStateTypeId, meterData.voltageLN);
thing->setStateValue(sunspecSplitPhaseMeterPhaseANVoltageStateTypeId, meterData.phaseVoltageAN);
thing->setStateValue(sunspecSplitPhaseMeterPhaseBNVoltageStateTypeId, meterData.phaseVoltageBN);
thing->setStateValue(sunspecSplitPhaseMeterTotalEnergyProducedStateTypeId, meterData.totalRealEnergyExported);
thing->setStateValue(sunspecSplitPhaseMeterTotalEnergyConsumedStateTypeId, meterData.totalRealEnergyImported);
} else if (thing->thingClassId() == sunspecThreePhaseMeterThingClassId) {
thing->setStateValue(sunspecThreePhaseMeterPhaseACurrentStateTypeId, meterData.phaseACurrent);
thing->setStateValue(sunspecThreePhaseMeterPhaseBCurrentStateTypeId, meterData.phaseBCurrent);
thing->setStateValue(sunspecThreePhaseMeterPhaseCCurrentStateTypeId, meterData.phaseCCurrent);
thing->setStateValue(sunspecThreePhaseMeterLnACVoltageStateTypeId, meterData.voltageLN);
thing->setStateValue(sunspecThreePhaseMeterPhaseANVoltageStateTypeId, meterData.phaseVoltageAN);
thing->setStateValue(sunspecThreePhaseMeterPhaseBNVoltageStateTypeId, meterData.phaseVoltageBN);
thing->setStateValue(sunspecThreePhaseMeterPhaseCNVoltageStateTypeId, meterData.phaseVoltageCN);
thing->setStateValue(sunspecThreePhaseMeterCurrentPowerEventTypeId, meterData.totalRealPower);
thing->setStateValue(sunspecThreePhaseMeterTotalEnergyProducedStateTypeId, meterData.totalRealEnergyExported);
thing->setStateValue(sunspecThreePhaseMeterTotalEnergyConsumedStateTypeId, meterData.totalRealEnergyImported);
}
}

View File

@ -0,0 +1,106 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 INTEGRATIONPLUGINSUNSPEC_H
#define INTEGRATIONPLUGINSUNSPEC_H
#include "integrations/integrationplugin.h"
#include "plugintimer.h"
#include "../modbus/modbustcpmaster.h"
#include "sunspecinverter.h"
#include "sunspecstorage.h"
#include "sunspecmeter.h"
#include <QUuid>
class IntegrationPluginSunSpec: public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsunspec.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginSunSpec();
void init() override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
void thingRemoved(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
private:
QHash<ThingClassId, ParamTypeId> m_modelIdParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_modbusAddressParamTypeIds;
QHash<ThingClassId, StateTypeId> m_connectedStateTypeIds;
QHash<ThingClassId, StateTypeId> m_frequencyStateTypeIds;
QHash<ThingClassId, StateTypeId> m_inverterCurrentPowerStateTypeIds;
QHash<ThingClassId, StateTypeId> m_inverterTotalEnergyProducedStateTypeIds;
QHash<ThingClassId, StateTypeId> m_inverterOperatingStateTypeIds;
QHash<ThingClassId, StateTypeId> m_inverterErrorStateTypeIds;
QHash<ThingClassId, StateTypeId> m_inverterCabinetTemperatureStateTypeIds;
QHash<ThingClassId, StateTypeId> m_inverterAcCurrentStateTypeIds;
PluginTimer *m_refreshTimer = nullptr;
QHash<QUuid, ThingActionInfo *> m_asyncActions;
QHash<ThingId, SunSpec *> m_sunSpecConnections;
QHash<Thing *, SunSpecInverter *> m_sunSpecInverters;
QHash<Thing *, SunSpecStorage *> m_sunSpecStorages;
QHash<Thing *, SunSpecMeter *> m_sunSpecMeters;
bool checkIfThingExists(uint modelId, uint modbusAddress);
void setupInverter(ThingSetupInfo *info);
void setupMeter(ThingSetupInfo *info);
void setupStorage(ThingSetupInfo *info);
private slots:
void onRefreshTimer();
void onPluginConfigurationChanged(const ParamTypeId &paramTypeId, const QVariant &value);
void onConnectionStateChanged(bool status);
void onFoundSunSpecModel(SunSpec::ModelId modelId, int modbusStartRegister);
void onSunSpecModelSearchFinished(const QHash<SunSpec::ModelId, int> &modelIds);
void onWriteRequestExecuted(QUuid requestId, bool success);
void onWriteRequestError(QUuid requestId, const QString &error);
void onInverterDataReceived(const SunSpecInverter::InverterData &inverterData);
void onStorageDataReceived(const SunSpecStorage::StorageData &mandatory, const SunSpecStorage::StorageDataOptional &optional);
void onMeterDataReceived(const SunSpecMeter::MeterData &meterData);
};
#endif // INTEGRATIONPLUGINSUNSPEC_H

File diff suppressed because it is too large Load Diff

12
sunspec/meta.json Normal file
View File

@ -0,0 +1,12 @@
{
"title": "SunSpec",
"tagline": "Connect to SunSpec devices.",
"icon": "sunspec.png",
"stability": "consumer",
"offline": true,
"technologies": [
"network"
],
"categories": [
]
}

427
sunspec/sunspec.cpp Normal file
View File

@ -0,0 +1,427 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 "sunspec.h"
#include "extern-plugininfo.h"
#include <QtEndian>
SunSpec::SunSpec(const QHostAddress &hostAddress, uint port, uint slaveId, QObject *parent) :
QObject(parent),
m_hostAddress(hostAddress),
m_port(port),
m_slaveId(slaveId)
{
qCDebug(dcSunSpec()) << "SunSpec: Creating SunSpec connection";
m_modbusTcpClient = new QModbusTcpClient(this);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString());
m_modbusTcpClient->setTimeout(2000);
m_modbusTcpClient->setNumberOfRetries(3);
connect(m_modbusTcpClient, &QModbusTcpClient::stateChanged, this, &SunSpec::onModbusStateChanged);
}
SunSpec::~SunSpec()
{
qCDebug(dcSunSpec()) << "SunSpec: Deleting SunSpec connection";
}
bool SunSpec::connectModbus()
{
qCDebug(dcSunSpec()) << "SunSpec: Connect modbus";
return m_modbusTcpClient->connectDevice();
}
void SunSpec::setHostAddress(const QHostAddress &hostAddress)
{
if (m_hostAddress != hostAddress) {
qCDebug(dcSunSpec()) << "SunSpec: Set host address" << hostAddress.toString();
m_hostAddress = hostAddress;
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString());
}
}
void SunSpec::setPort(uint port)
{
if (port != m_port) {
qCDebug(dcSunSpec()) << "SunSpec: Set Port" << port;
m_port = port;
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, port);
}
}
void SunSpec::setSlaveId(uint slaveId)
{
qCDebug(dcSunSpec()) << "SunSpec: Set slave id" << slaveId;
m_slaveId = slaveId;
}
void SunSpec::setTimeout(uint milliSeconds)
{
qCDebug(dcSunSpec()) << "SunSpec: Set timeout" << milliSeconds << "[ms]";
m_modbusTcpClient->setTimeout(milliSeconds);
}
void SunSpec::setNumberOfRetries(uint retries)
{
qCDebug(dcSunSpec()) << "SunSpec: Set number of retries" << retries;
m_modbusTcpClient->setNumberOfRetries(retries);
}
QHostAddress SunSpec::hostAddress() const
{
return m_hostAddress;
}
uint SunSpec::port()
{
return m_port;
}
uint SunSpec::slaveId()
{
return m_slaveId;
}
QString SunSpec::manufacturer()
{
return m_manufacturer;
}
QString SunSpec::deviceModel()
{
return m_deviceModel;
}
QString SunSpec::serialNumber()
{
return m_serialNumber;
}
void SunSpec::findBaseRegister()
{
qCDebug(dcSunSpec()) << "SunSpec: Find base register";
QList<int> validBaseRegisters;
validBaseRegisters.append(0);
validBaseRegisters.append(40000);
validBaseRegisters.append(50000);
Q_FOREACH (int baseRegister, validBaseRegisters) {
qCDebug(dcSunSpec()) << " - Searching address" << baseRegister;
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, baseRegister, 2);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, m_slaveId)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, baseRegister, this] {
if (reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit unit = reply->result();
if ((unit.value(0) << 16 | unit.value(1)) == 0x53756e53) {
//Well-known value. Uniquely identifies this as a SunSpec Modbus model
qCDebug(dcSunSpec()) << "SunSpec: Found start of modbus model" << baseRegister;
m_baseRegister = baseRegister;
emit foundBaseRegister(baseRegister);
} else {
qCWarning(dcSunSpec()) << "SunSpec: Got reply on base register" << baseRegister << ", but value didn't mach 0x53756e53";
}
} else {
qCDebug(dcSunSpec()) << "SunSpec: Find base register not found at:" << baseRegister;
}
});
} else {
delete reply; // broadcast replies return immediately
return;
}
}
}
}
void SunSpec::findSunSpecModels(const QList<ModelId> &ids, uint modbusAddressOffset)
{
qCDebug(dcSunSpec()) << "SunSpec: Find modbus model. Start register" << m_baseRegister+modbusAddressOffset;
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, m_baseRegister+modbusAddressOffset, 2);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, m_slaveId)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [ids, reply, this] {
if (reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit unit = reply->result();
uint modbusAddress = unit.startAddress();
ModelId modelId = ModelId(unit.value(0));
int modelLength = unit.value(1);
if (modelId == ModelIdEnd) {
qCDebug(dcSunSpec()) << "SunSpec: Model Id End";
sunspecModelSearchFinished(m_modelList);
return;
}
if (ids.isEmpty() || ids.contains(modelId)) {
// If ids is empty then emit all models
qCDebug(dcSunSpec()) << "SunSpec: Found model" << ModelId(modelId) << "with length" << modelLength;
m_modelList.insert(ModelId(modelId), modbusAddress);
foundSunSpecModel(ModelId(modelId), modbusAddress);
}
findSunSpecModels(ids, modbusAddress+2+modelLength-m_baseRegister); //read next model
} else {
qCWarning(dcSunSpec()) << "SunSpec: Find modbus model, read response error:" << reply->error();
}
});
} else {
delete reply; // broadcast replies return immediately
return;
}
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read error: " << m_modbusTcpClient->errorString();
return;
}
}
void SunSpec::readModelHeader(uint modbusAddress)
{
qCDebug(dcSunSpec()) << "SunSpec: Read model header, modbus address:" << modbusAddress << "Slave ID" << m_slaveId;
if (modbusAddress == 0 || modbusAddress == 40000 || modbusAddress == 50000)
modbusAddress += 2;
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, modbusAddress, 2);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, m_slaveId)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, this] {
if (reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit unit = reply->result();
uint modbusAddress = unit.startAddress();
ModelId modelId = ModelId(unit.value(0));
int length = unit.value(1);
qCDebug(dcSunSpec()) << "SunSpec: Received model header response. Model ID:" << modelId << "length" << length;
modelHeaderReceived(modbusAddress, modelId, length);
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read model header response error:" << reply->error();
}
});
connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error) {
qCWarning(dcSunSpec()) << "SunSpec: Read model header, modbus reply error:" << error;
reply->finished(); // To make sure it will be deleted
});
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read model header error: " << m_modbusTcpClient->errorString();
delete reply; // broadcast replies return immediately
return;
}
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read model header error: " << m_modbusTcpClient->errorString();
return;
}
}
void SunSpec::readModelDataBlock(uint modbusAddress, uint length)
{
qCDebug(dcSunSpec()) << "SunSpec: Read model, modbus address" << modbusAddress << "length" << length << ", Slave ID" << m_slaveId;
if (length > 125) { //Modbus register limit is 125
qCWarning(dcSunSpec()) << "SunSpec: Data block length is too long, max 125 register";
return;
}
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, modbusAddress, length+2);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, m_slaveId)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, this] {
if (reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit unit = reply->result();
uint modbusAddress = unit.startAddress();
ModelId modelId = ModelId(unit.value(0));
uint length = unit.value(1);
qCDebug(dcSunSpec()) << "SunSpec: Received model. Modbus address" << modbusAddress << "model ID" << modelId << "length" << length;
emit modelDataBlockReceived(modelId, length, unit.values().mid(2));
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read response error:" << reply->error();
}
});
connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error) {
qCWarning(dcSunSpec()) << "SunSpec: Modbus reply error:" << error;
reply->finished(); // To make sure it will be deleted
});
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read error: " << m_modbusTcpClient->errorString();
delete reply; // broadcast replies return immediately
return;
}
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read error: " << m_modbusTcpClient->errorString();
return;
}
}
void SunSpec::readCommonModel()
{
qCDebug(dcSunSpec()) << "SunSpec: Read common model header. Modbus Address" << m_baseRegister+2 << ", Slave ID" << m_slaveId;
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, m_baseRegister+2, 66);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, m_slaveId)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, this] {
if (reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit unit = reply->result();
m_manufacturer = convertModbusRegisters(unit.values(), MandatoryRegistersModel1::Manufacturer, 16);
m_manufacturer.remove('\x00');
m_deviceModel = convertModbusRegisters(unit.values(), MandatoryRegistersModel1::Model, 16);
m_deviceModel.remove('\x00');
m_serialNumber = convertModbusRegisters(unit.values(), MandatoryRegistersModel1::SerialNumber, 16);
m_serialNumber.remove('\x00');
qCDebug(dcSunSpec()) << "SunSpec: Received common block response. Manufacturer" << m_manufacturer << "Model" << m_deviceModel << "Serial number" << m_serialNumber;
commonModelReceived(m_manufacturer, m_deviceModel, m_serialNumber);
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read common model, read response error:" << reply->error();
}
});
} else {
qCWarning(dcSunSpec()) << "Sunspec: Read common model read error: " << m_modbusTcpClient->errorString();
delete reply; // broadcast replies return immediately
return;
}
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read error: " << m_modbusTcpClient->errorString();
return;
}
}
QByteArray SunSpec::convertModbusRegister(const uint16_t &modbusData)
{
uint8_t data[2];
data[0] = modbusData >> 8;
data[1] = modbusData & 0xFF;
//qCDebug(dcSunSpec()) << (char)data[0] << (char)data[1];
return QByteArray().append((char)data[0]).append((char)data[1]);
}
QBitArray SunSpec::convertModbusRegisterBits(const uint16_t &modbusData)
{
QByteArray data = convertModbusRegister(modbusData);
QBitArray bits(data.count() * 8);
// Convert from QByteArray to QBitArray
for(int i = 0; i < data.count(); ++i) {
for(int b = 0; b < 8; b++) {
bits.setBit(i * 8 + b, data.at(i) & (1 << ( 7 - b)));
}
}
return bits;
}
QByteArray SunSpec::convertModbusRegisters(const QVector<quint16> &modbusData, int offset, int size)
{
QByteArray bytes;
for (int i = offset; i < offset + size; i++)
bytes.append(convertModbusRegister(modbusData[i]));
return bytes;
}
float SunSpec::convertToFloatWithSSF(quint32 rawValue, quint16 sunssf)
{
float value;
value = rawValue * pow(10, static_cast<qint16>(sunssf));
return value;
}
quint32 SunSpec::convertFromFloatWithSSF(float value, quint16 sunssf)
{
quint32 rawValue;
rawValue = value / pow(10, static_cast<qint16>(sunssf));
return rawValue;
}
float SunSpec::convertFloatValues(quint16 rawValue0, quint16 rawValue1)
{
suns_modbus_v32_t value;
value.u = (static_cast<uint32_t>(rawValue0) << 16) + rawValue1;
return value.f;
}
void SunSpec::onModbusStateChanged(QModbusDevice::State state)
{
bool connected = (state == QModbusDevice::ConnectedState);
if (!connected) {
//try to reconnect in 10 seconds
QTimer::singleShot(10000, m_modbusTcpClient, [this] {
if (m_modbusTcpClient->connectDevice()) {
qCDebug(dcSunSpec()) << "SunSpec: Could not reconnect";
}
});
}
emit connectionStateChanged(connected);
}
QUuid SunSpec::writeHoldingRegister(uint registerAddress, quint16 value)
{
return writeHoldingRegisters(registerAddress, QVector<quint16>() << value);
}
QUuid SunSpec::writeHoldingRegisters(uint registerAddress, const QVector<quint16> &values)
{
if (!m_modbusTcpClient) {
return "";
}
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, values.length());
request.setValues(values);
if (QModbusReply *reply = m_modbusTcpClient->sendWriteRequest(request, m_slaveId)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, requestId, this] {
if (reply->error() != QModbusDevice::NoError) {
qCWarning(dcSunSpec()) << "SunSpec: Read response error:" << reply->error();
emit requestExecuted(requestId, false);
return;
}
emit requestExecuted(requestId, true);
});
} else {
delete reply; // broadcast replies return immediately
return "";
}
} else {
qCWarning(dcSunSpec()) << "SunSpec: Read error: " << m_modbusTcpClient->errorString();
return "";
}
return requestId;
}

221
sunspec/sunspec.h Normal file
View File

@ -0,0 +1,221 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 SUNSPEC_H
#define SUNSPEC_H
#include <QObject>
#include <QUuid>
#include <QHostAddress>
#include <QtSerialBus>
class SunSpec : public QObject
{
Q_OBJECT
public:
typedef union {
int32_t s;
uint32_t u;
float f;
} suns_modbus_v32_t;
enum MandatoryRegistersModel1 {
Manufacturer = 2,
Model = 18,
SerialNumber = 50
};
enum OptionalRegistersModel1 {
Options = 34,
Version = 40,
DeviceAddress = 64,
ForceAlignment = 65
};
enum SunSpecOperatingState {
Off = 1,
Sleeping,
Starting,
MPPT,
Throttled,
ShuttingDown,
Fault,
Standby
};
Q_ENUM(SunSpecOperatingState)
enum ModelId {
ModelIdCommon = 1,
ModelIdBasicAggregator = 2,
ModelIdSecureDatasetReadRequest = 3,
ModelIdSecureDatasetReadResponse = 4,
ModelIdSecureWriteRequest = 5,
ModelIdSecureWriteSequentialRequest = 6,
ModelIdSecureWriteResponseModel = 7,
ModelIdGetDeviceSecurityCertificate = 8,
ModelIdSetOperatorSecurityCertificate = 9,
ModelIdCommunicationInterfaceHeader = 10,
ModelIdEthernetLinkLayer = 11,
ModelIdIPv4 = 12,
ModelIdIPv6 = 13,
ModelIdProxyServer = 14,
ModelIdInterfaceCountersModel = 15,
ModelIdSimpleIpNetwork = 16,
ModelIdSerialInterface = 17,
ModelIdCellularLink = 18,
ModelIdPPPLink = 19,
ModelIdInverterSinglePhase = 101,
ModelIdInverterSplitPhase = 102,
ModelIdInverterThreePhase = 103,
ModelIdInverterSinglePhaseFloat = 111,
ModelIdInverterSplitPhaseFloat = 112,
ModelIdInverterThreePhaseFloat = 113,
ModelIdNameplate = 120,
ModelIdBasicSettings = 121,
ModelIdMeasurementsStatus = 122,
ModelIdImmediateControls = 123,
ModelIdStorage = 124,
ModelIdPricing = 125,
ModelIdStaticVoltVAR = 126,
ModelIdFreqWattParam = 127,
ModelIdDynamicReactiveCurrent = 128,
ModelIdLVRTD = 129,
ModelIdHVRTD = 130,
ModelIdWattPF = 131,
ModelIdVoltWatt = 132,
ModelIdBasicScheduling = 133,
ModelIdFreqWattCrv = 134,
ModelIdLFRT = 135,
ModelIdHFRT = 136,
ModelIdLVRTC = 137,
ModelIdHVRTC = 138,
ModelIdMultipleMPPTInverterExtensionModel = 160,
ModelIdSinglePhaseMeter = 201,
ModelIdSplitSinglePhaseMeter = 202,
ModelIdWyeConnectThreePhaseMeter = 203,
ModelIdDeltaConnectThreePhaseMeter = 204,
ModelIdSinglePhaseMeterFloat = 211,
ModelIdSplitSinglePhaseMeterFloat = 212,
ModelIdWyeConnectThreePhaseMeterFloat = 213,
ModelIdDeltaConnectThreePhaseMeterFloat = 214,
ModelIdSecureACMeterSelectedReadings = 220,
ModelIdIrradianceModel = 302,
ModelIdBackOfModuleTemperatureModel = 303,
ModelIdInclinometerModel = 304,
ModelIdGPS = 305,
ModelIdReferencePointModel = 306,
ModelIdBaseMet = 307,
ModelIdMiniMetModel = 308,
ModelIdStringCombiner = 401,
ModelIdStringCombinerAdvanced = 402,
ModelIdStringCombinerCurrent = 403,
ModelIdStringCombinerCurrentAdvanced = 404,
ModelIdSolarModuleFloat = 501,
ModelIdSolarModule = 502,
ModelIdTrackerController = 601,
ModelIdEnergyStorageBaseModel = 801,
ModelIdBatteryBaseModel = 802,
ModelIdLithiumIonBatteryModel = 803,
ModelIdVerisStatusConfiguration = 64001,
ModelIdMersenGreenString = 64020,
ModelIdEltekInverterExtension = 64101,
ModelIdOutBackAXSDevice = 64110,
ModelIdBasicChargeController = 64111,
ModelIdOutBackFMChargeController = 64112,
ModelIdEnd = 65535
};
Q_ENUM(ModelId)
explicit SunSpec(const QHostAddress &hostAddress, uint port = 502, uint slaveId = 1, QObject *parent = 0);
~SunSpec();
bool connectModbus();
void setHostAddress(const QHostAddress &hostAddress);
void setPort(uint port);
void setSlaveId(uint slaveId);
void setTimeout(uint milliSeconds);
void setNumberOfRetries(uint retries);
QHostAddress hostAddress() const;
uint port();
uint slaveId();
QString manufacturer();
QString deviceModel();
QString serialNumber();
QHostAddress m_hostAddress;
uint m_port;
QModbusTcpClient *m_modbusTcpClient = nullptr;
int m_slaveId = 1;
int m_baseRegister = 40000;
bool m_floatingPointRepresentation = false;
QString m_manufacturer = "Unknown";
QString m_deviceModel = "Unknown";
QString m_serialNumber = "Unknown";
QHash<ModelId, int> m_modelList;
void findBaseRegister();
void findSunSpecModels(const QList<ModelId> &modelIds, uint modbusAddressOffset = 2);
void readCommonModel();
void readModelHeader(uint modbusAddress);
void readModelDataBlock(uint modbusAddress, uint modelLength); //modbusAddress = model start address, model length is without header
float convertToFloatWithSSF(quint32 rawValue, quint16 sunssf);
quint32 convertFromFloatWithSSF(float value, quint16 sunssf);
float convertFloatValues(quint16 rawValue0, quint16 rawValue1);
QByteArray convertModbusRegister(const uint16_t &modbusData);
QBitArray convertModbusRegisterBits(const uint16_t &modbusData);
QByteArray convertModbusRegisters(const QVector<quint16> &modbusData, int offset, int size);
QUuid writeHoldingRegister(uint registerAddress, quint16 value);
QUuid writeHoldingRegisters(uint registerAddress, const QVector<quint16> &values);
signals:
void connectionStateChanged(bool status);
void requestExecuted(const QUuid &requestId, bool success);
void foundBaseRegister(int modbusAddress);
void commonModelReceived(const QString &manufacturer, const QString &deviceModel, const QString &serialNumber);
void foundSunSpecModel(ModelId modelId, int modbusStartRegister);
void sunspecModelSearchFinished(const QHash<ModelId, int> &mapIds);
void modelHeaderReceived(uint modbusAddress, ModelId mapId, uint mapLength);
void modelDataBlockReceived(ModelId id, uint ength, QVector<quint16> data);
private slots:
void onModbusStateChanged(QModbusDevice::State state);
};
#endif // SUNSPEC_H

BIN
sunspec/sunspec.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

19
sunspec/sunspec.pro Normal file
View File

@ -0,0 +1,19 @@
include(../plugins.pri)
QT += \
network \
serialbus \
SOURCES += \
integrationpluginsunspec.cpp \
sunspec.cpp \
sunspecinverter.cpp \
sunspecmeter.cpp \
sunspecstorage.cpp
HEADERS += \
integrationpluginsunspec.h \
sunspec.h \
sunspecinverter.h \
sunspecmeter.h \
sunspecstorage.h

172
sunspec/sunspecinverter.cpp Normal file
View File

@ -0,0 +1,172 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 "sunspecinverter.h"
#include "extern-plugininfo.h"
#include <QTimer>
SunSpecInverter::SunSpecInverter(SunSpec *sunspec, SunSpec::ModelId modelId, int modbusAddress) :
QObject(sunspec),
m_connection(sunspec),
m_id(modelId),
m_modelModbusStartRegister(modbusAddress)
{
qCDebug(dcSunSpec()) << "SunSpecInverter: Setting up inverter";
connect(m_connection, &SunSpec::modelDataBlockReceived, this, &SunSpecInverter::onModelDataBlockReceived);
}
SunSpec::ModelId SunSpecInverter::modelId()
{
return m_id;
}
void SunSpecInverter::init()
{
qCDebug(dcSunSpec()) << "SunSpecInverter: Init";
m_connection->readModelHeader(m_modelModbusStartRegister);
connect(m_connection, &SunSpec::modelHeaderReceived, this, [this] (uint modbusAddress, SunSpec::ModelId modelId, uint length) {
if (modelId == m_id) {
qCDebug(dcSunSpec()) << "SunSpecInverter: Model Header received, modbus address:" << modbusAddress << "model Id:" << modelId << "length:" << length;
m_modelLength = length;
emit initFinished(true);
m_initFinishedSuccess = true;
}
});
QTimer::singleShot(10000, this,[this] {
if (!m_initFinishedSuccess) {
emit initFinished(false);
}
});
}
void SunSpecInverter::getInverterModelDataBlock()
{
qCDebug(dcSunSpec()) << "SunSpecInverter: get inverter model data block, modbus register" << m_modelModbusStartRegister << "length" << m_modelLength;
m_connection->readModelDataBlock(m_modelModbusStartRegister, m_modelLength);
}
SunSpecInverter::SunSpecEvent1 SunSpecInverter::bitfieldToSunSpecEvent1(quint16 register0, quint16 register1)
{
SunSpecEvent1 event1;
quint32 value = (static_cast<quint32>(register0)<<16 | register1);
//qCDebug(dcSunSpec()) << "Event1" << QString::number(value, 16);
event1.groundFault = ((value & (0x01 << 0)) != 0);
event1.dcOverVoltage = ((value & (0x01 << 1)) != 0);
event1.acDisconnect = ((value & (0x01 << 2)) != 0);
event1.dcDicconnect = ((value & (0x01 << 3)) != 0);
event1.gridDisconnect = ((value & (0x01 << 4)) != 0);
event1.cabinetOpen = ((value & (0x01 << 5)) != 0);
event1.manualShutdown = ((value & (0x01 << 6)) != 0);
event1.overTemperature = ((value & (0x01 << 7)) != 0);
event1.overFrequency = ((value & (0x01 << 8)) != 0);
event1.underFrequency = ((value & (0x01 << 9)) != 0);
event1.acOverVolt = ((value & (0x01 << 10)) != 0);
event1.acUnderVolt = ((value & (0x01 << 11)) != 0);
event1.blownStringFuse = ((value & (0x01 << 12)) != 0);
event1.underTemperature = ((value & (0x01 << 13)) != 0);
event1.memoryLoss = ((value & (0x01 << 14)) != 0);
event1.hwTestFailure = ((value & (0x01 << 15)) != 0);
return event1;
}
void SunSpecInverter::getInverterModelHeader()
{
qCDebug(dcSunSpec()) << "SunSpecInverter: get inverter model header, modbus register" << m_modelModbusStartRegister;
m_connection->readModelHeader(m_modelModbusStartRegister);
}
void SunSpecInverter::onModelDataBlockReceived(SunSpec::ModelId modelId, uint length, QVector<quint16> data)
{
Q_UNUSED(length)
if (modelId != m_id) {
return;
}
if (length < m_modelLength) {
qCDebug(dcSunSpec()) << "SunSpecInverter: on model data block received, model length is too short" << length;
return;
}
InverterData inverterData;
qCDebug(dcSunSpec()) << "SunSpecInverter: Received" << modelId;
switch (modelId) {
case SunSpec::ModelIdInverterSinglePhase:
case SunSpec::ModelIdInverterSplitPhase:
case SunSpec::ModelIdInverterThreePhase: {
inverterData.acCurrent= m_connection->convertToFloatWithSSF(data[Model10X::Model10XAcCurrent], data[Model10X::Model10XAmpereScaleFactor]);
inverterData.acPower = m_connection->convertToFloatWithSSF(data[Model10X::Model10XACPower], data[Model10X::Model10XWattScaleFactor]);
inverterData.lineFrequency = m_connection->convertToFloatWithSSF(data[Model10X::Model10XLineFrequency], data[Model10X::Model10XHerzScaleFactor]);
quint16 ampereScaleFactor = data[Model10X::Model10XAmpereScaleFactor];
inverterData.phaseACurrent = m_connection->convertToFloatWithSSF(data[Model10X::Model10XPhaseACurrent], ampereScaleFactor);
inverterData.phaseBCurrent = m_connection->convertToFloatWithSSF(data[Model10X::Model10XPhaseBCurrent], ampereScaleFactor);
inverterData.phaseCCurrent = m_connection->convertToFloatWithSSF(data[Model10X::Model10XPhaseCCurrent], ampereScaleFactor);
quint16 voltageScaleFactor = data[Model10X::Model10XVoltageScaleFactor];
inverterData.phaseVoltageAN = m_connection->convertToFloatWithSSF(data[Model10X::Model10XPhaseVoltageAN], voltageScaleFactor);
inverterData.phaseVoltageBN = m_connection->convertToFloatWithSSF(data[Model10X::Model10XPhaseVoltageBN], voltageScaleFactor);
inverterData.phaseVoltageCN = m_connection->convertToFloatWithSSF(data[Model10X::Model10XPhaseVoltageCN], voltageScaleFactor);
quint32 acEnergy = ((static_cast<quint32>(data.value(Model10X::Model10XAcEnergy))<<16)|static_cast<quint32>(data.value(Model10X::Model10XAcEnergy+1)));
inverterData.acEnergy = m_connection->convertToFloatWithSSF(acEnergy, data[Model10X::Model10XWattHoursScaleFactor]);
inverterData.cabinetTemperature = m_connection->convertToFloatWithSSF(data[Model10X::Model10XCabinetTemperature], data[Model10X::Model10XTemperatureScaleFactor]);
inverterData.event1 = bitfieldToSunSpecEvent1(data[Model10X::Model10XEvent1], data[Model10X::Model10XEvent1+1]);
inverterData.operatingState = SunSpec::SunSpecOperatingState(data[Model10X::Model10XOperatingState]);
emit inverterDataReceived(inverterData);
} break;
case SunSpec::ModelIdInverterThreePhaseFloat:
case SunSpec::ModelIdInverterSplitPhaseFloat:
case SunSpec::ModelIdInverterSinglePhaseFloat: {
inverterData.acCurrent = m_connection->convertFloatValues(data[Model11X::Model11XAcCurrent], data[Model11X::Model11XAcCurrent+1]);
inverterData.phaseVoltageAN = m_connection->convertFloatValues(data[Model11X::Model11XPhaseVoltageAN], data[Model11X::Model11XPhaseVoltageAN+1]);
inverterData.phaseVoltageBN = m_connection->convertFloatValues(data[Model11X::Model11XPhaseVoltageBN], data[Model11X::Model11XPhaseVoltageBN+1]);
inverterData.phaseVoltageCN = m_connection->convertFloatValues(data[Model11X::Model11XPhaseVoltageCN], data[Model11X::Model11XPhaseVoltageCN+1]);
inverterData.phaseACurrent = m_connection->convertFloatValues(data[Model11X::Model11XPhaseACurrent], data[Model11X::Model11XPhaseACurrent+1]);
inverterData.phaseBCurrent = m_connection->convertFloatValues(data[Model11X::Model11XPhaseBCurrent], data[Model11X::Model11XPhaseBCurrent+1]);
inverterData.phaseCCurrent = m_connection->convertFloatValues(data[Model11X::Model11XPhaseCCurrent], data[Model11X::Model11XPhaseCCurrent+1]);
inverterData.acPower = m_connection->convertFloatValues(data[Model11X::Model11XACPower], data[Model11X::Model11XACPower+1]);
inverterData.lineFrequency = m_connection->convertFloatValues(data[Model11X::Model11XLineFrequency], data[Model11X::Model11XLineFrequency+1]);
inverterData.acEnergy = m_connection->convertFloatValues(data[Model11X::Model11XAcEnergy], data[Model11X::Model11XAcEnergy+1]);
inverterData.cabinetTemperature = m_connection->convertFloatValues(data[Model11X::Model11XCabinetTemperature], data[Model11X::Model11XCabinetTemperature+1]);
inverterData.event1 = bitfieldToSunSpecEvent1(data[Model11X::Model11XEvent1], data[Model11X::Model11XEvent1+1]);
inverterData.operatingState = SunSpec::SunSpecOperatingState(data[Model11X::Model11XOperatingState]);
emit inverterDataReceived(inverterData);
} break;
default:
//ignore
break;
}
}

187
sunspec/sunspecinverter.h Normal file
View File

@ -0,0 +1,187 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 SUNSPECINVERTER_H
#define SUNSPECINVERTER_H
#include <QObject>
#include "sunspec.h"
class SunSpecInverter : public QObject
{
Q_OBJECT
public:
enum Model10X { // Mandatory register
Model10XAcCurrent = 0,
Model10XPhaseACurrent = 1,
Model10XPhaseBCurrent = 2,
Model10XPhaseCCurrent = 3,
Model10XAmpereScaleFactor = 4,
Model10XPhaseVoltageAN = 8,
Model10XPhaseVoltageBN = 9,
Model10XPhaseVoltageCN = 10,
Model10XVoltageScaleFactor = 11,
Model10XACPower = 12,
Model10XWattScaleFactor = 13,
Model10XLineFrequency = 14,
Model10XHerzScaleFactor = 15,
Model10XAcEnergy = 22,
Model10XWattHoursScaleFactor = 24,
Model10XCabinetTemperature = 31,
Model10XTemperatureScaleFactor = 35,
Model10XOperatingState = 36,
Model10XEvent1 = 38
};
enum Model11X { // Mandatory register
Model11XAcCurrent = 0,
Model11XPhaseACurrent = 2,
Model11XPhaseBCurrent = 4,
Model11XPhaseCCurrent = 6,
Model11XPhaseVoltageAN = 14,
Model11XPhaseVoltageBN = 16,
Model11XPhaseVoltageCN = 18,
Model11XACPower = 20,
Model11XLineFrequency = 22,
Model11XAcEnergy = 30,
Model11XCabinetTemperature = 38,
Model11XOperatingState = 46,
Model11XEvent1 = 48
};
enum Model10XOptional { // Optional register
Model10XPhaseVoltageAB = 5,
Model10XPhaseVoltageBC = 6,
Model10XPhaseVoltageCA = 7,
Model10XACApparentPower = 16,
Model10XACApparentPowerSF = 17,
Model10XACReactivePower = 18,
Model10XACReactivePowerSF = 19,
Model10XACPowerFactor = 20,
Model10XACPowerFactorSF = 21,
Model10XDCCurrent = 25,
Model10XDCCurrentSF = 26,
Model10XDCVoltage = 27,
Model10XDCVoltageSF = 28,
Model10XDCPower = 29,
Model10XDCPowerSF = 30,
Model10XHeatSinkTemperature = 32,
Model10XTransformerTemperature = 33,
Model10XOtherTemperature = 34,
Model10XVendorOperatingState = 37,
Model10XVendorEventBitfield1 = 42,
Model10XVendorEventBitfield2 = 44,
Model10XVendorEventBitfield3 = 46,
Model10XVendorEventBitfield4 = 48
};
enum Model11XOptional { // Optinal registers
Model11XPhaseVoltageAB = 8,
Model11XPhaseVoltageBC = 10,
Model11XPhaseVoltageCA = 12,
Model11XACApparentPower = 24,
Model11XACReactivePower = 26,
Model11XACPowerFactor = 28,
Model11XDCCurrent = 32,
Model11XDCVoltage = 34,
Model11XDCPower = 36,
Model11XHeatSinkTemperature = 40,
Model11XTransformerTemperature = 42,
Model11XOtherTemperature = 44,
Model11XVendorOperatingState = 47,
Model11XVendorEventBitfield1 = 52,
Model11XVendorEventBitfield2 = 54,
Model11XVendorEventBitfield3 = 56,
Model11XVendorEventBitfield4 = 58
};
struct SunSpecEvent1 {
bool groundFault;
bool dcOverVoltage;
bool acDisconnect;
bool dcDicconnect;
bool gridDisconnect;
bool cabinetOpen;
bool manualShutdown;
bool overTemperature;
bool overFrequency;
bool underFrequency;
bool acOverVolt;
bool acUnderVolt;
bool blownStringFuse;
bool underTemperature;
bool memoryLoss;
bool hwTestFailure;
};
struct InverterData {
float acCurrent; //in ampere
float phaseACurrent;
float phaseBCurrent;
float phaseCCurrent;
float phaseVoltageAB;
float phaseVoltageBC;
float phaseVoltageCA;
float phaseVoltageAN;
float phaseVoltageBN;
float phaseVoltageCN;
float acPower;
float lineFrequency;
float acEnergy;
float cabinetTemperature; // in degree Celsius
SunSpecEvent1 event1;
SunSpec::SunSpecOperatingState operatingState;
};
SunSpecInverter(SunSpec *sunspec, SunSpec::ModelId modelId, int modbusAddress);
SunSpec::ModelId modelId();
void init();
void getInverterModelDataBlock();
private:
SunSpec *m_connection = nullptr;
SunSpec::ModelId m_id;
uint m_modelLength = 0;
uint m_modelModbusStartRegister = 40000;
bool m_initFinishedSuccess = false;
SunSpecEvent1 bitfieldToSunSpecEvent1(quint16 register0, quint16 register1);
void getInverterModelHeader();
private slots:
void onModelDataBlockReceived(SunSpec::ModelId mapId, uint mapLength, QVector<quint16> data);
signals:
void initFinished(bool success);
void inverterDataReceived(InverterData data);
};
#endif // SUNSPECINVERTER_H

152
sunspec/sunspecmeter.cpp Normal file
View File

@ -0,0 +1,152 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 "sunspecmeter.h"
#include "extern-plugininfo.h"
SunSpecMeter::SunSpecMeter(SunSpec *sunspec, SunSpec::ModelId modelId, int modbusAddress) :
QObject(sunspec),
m_connection(sunspec),
m_id(modelId),
m_modelModbusStartRegister(modbusAddress)
{
qCDebug(dcSunSpec()) << "SunSpecMeter: Setting up meter";
connect(m_connection, &SunSpec::modelDataBlockReceived, this, &SunSpecMeter::onModelDataBlockReceived);
}
SunSpec::ModelId SunSpecMeter::modelId()
{
return m_id;
}
void SunSpecMeter::init()
{
qCDebug(dcSunSpec()) << "SunSpecMeter: Init";
m_connection->readModelHeader(m_modelModbusStartRegister);
connect(m_connection, &SunSpec::modelHeaderReceived, this, [this] (uint modbusAddress, SunSpec::ModelId modelId, uint length) {
if (modelId == m_id) {
qCDebug(dcSunSpec()) << "SunSpecMeter: Model Header received, modbus address:" << modbusAddress << "model Id:" << modelId << "length:" << length;
m_modelLength = length;
emit initFinished(true);
m_initFinishedSuccess = true;
}
});
QTimer::singleShot(10000, this,[this] {
if (!m_initFinishedSuccess) {
emit initFinished(false);
}
});
}
void SunSpecMeter::getMeterModelDataBlock()
{
qCDebug(dcSunSpec()) << "SunSpecMeter: get meter model data block, modbus register" << m_modelModbusStartRegister << "length" << m_modelLength;
m_connection->readModelDataBlock(m_modelModbusStartRegister, m_modelLength);
}
void SunSpecMeter::getMeterModelHeader()
{
qCDebug(dcSunSpec()) << "SunSpecMeter: get meter model header, modbus register" << m_modelModbusStartRegister << "length" << m_modelLength;
m_connection->readModelHeader(m_modelModbusStartRegister);
}
void SunSpecMeter::onModelDataBlockReceived(SunSpec::ModelId modelId, uint length, QVector<quint16> data)
{
if (modelId != m_id) {
return;
}
if (length < m_modelLength) {
qCDebug(dcSunSpec()) << "SunSpecMeter: on model data block received, model length is too short" << length;
return;
}
qCDebug(dcSunSpec()) << "SunSpecMeter: Received" << modelId;
switch (modelId) {
case SunSpec::ModelIdSinglePhaseMeter:
case SunSpec::ModelIdSplitSinglePhaseMeter:
case SunSpec::ModelIdDeltaConnectThreePhaseMeter:
case SunSpec::ModelIdWyeConnectThreePhaseMeter: {
MeterData meterData;
quint16 currentScaleFactor = data[Model20XCurrentScaleFactor];
meterData.totalAcCurrent = m_connection->convertToFloatWithSSF(data[Model20XTotalAcCurrent], currentScaleFactor);
meterData.phaseACurrent = m_connection->convertToFloatWithSSF(data[Model20XPhaseACurrent], currentScaleFactor);
meterData.phaseBCurrent = m_connection->convertToFloatWithSSF(data[Model20XPhaseBCurrent], currentScaleFactor);
meterData.phaseCCurrent = m_connection->convertToFloatWithSSF(data[Model20XPhaseCCurrent], currentScaleFactor);
quint16 voltageScaleFactor = data[Model20XVoltageScaleFactor];
meterData.voltageLN = m_connection->convertToFloatWithSSF(data[Model20XVoltageLN], voltageScaleFactor);
meterData.phaseVoltageAN = m_connection->convertToFloatWithSSF(data[Model20XPhaseVoltageAN], voltageScaleFactor);
meterData.phaseVoltageBN = m_connection->convertToFloatWithSSF(data[Model20XPhaseVoltageBN], voltageScaleFactor);
meterData.phaseVoltageCN = m_connection->convertToFloatWithSSF(data[Model20XPhaseVoltageCN], voltageScaleFactor);
meterData.voltageLL = m_connection->convertToFloatWithSSF(data[Model20XVoltageLL], voltageScaleFactor);
meterData.phaseVoltageAB = m_connection->convertToFloatWithSSF(data[Model20XPhaseVoltageAB], voltageScaleFactor);
meterData.phaseVoltageBC = m_connection->convertToFloatWithSSF(data[Model20XPhaseVoltageBC], voltageScaleFactor);
meterData.phaseVoltageCA = m_connection->convertToFloatWithSSF(data[Model20XPhaseVoltageCA], voltageScaleFactor);
meterData.frequency = m_connection->convertToFloatWithSSF(data[Model20XFrequency], data[Model20XFrequencyScaleFactor]);
meterData.totalRealPower = m_connection->convertToFloatWithSSF(data[Model20XTotalRealPower], data[Model20XRealPowerScaleFactor]);
quint16 energyScaleFactor = data[Model20XRealEnergyScaleFactor];
meterData.totalRealEnergyExported = m_connection->convertToFloatWithSSF(data[Model20XTotalRealEnergyExported], energyScaleFactor);
meterData.totalRealEnergyImported = m_connection->convertToFloatWithSSF(data[Model20XTotalRealEnergyImported], energyScaleFactor);;
meterData.meterEventFlags = (static_cast<quint32>(data[Model20XMeterEventFlags]) << 16) | data[Model20XMeterEventFlags+1];
emit meterDataReceived(meterData);
} break;
case SunSpec::ModelIdSinglePhaseMeterFloat:
case SunSpec::ModelIdSplitSinglePhaseMeterFloat:
case SunSpec::ModelIdDeltaConnectThreePhaseMeterFloat:
case SunSpec::ModelIdWyeConnectThreePhaseMeterFloat: {
MeterData meterData;
meterData.totalAcCurrent = m_connection->convertFloatValues(data[Model21XTotalAcCurrent], data[Model21XTotalAcCurrent+1]);
meterData.phaseACurrent = m_connection->convertFloatValues(data[Model21XPhaseACurrent], data[Model21XPhaseACurrent+1]);
meterData.phaseBCurrent = m_connection->convertFloatValues(data[Model21XPhaseBCurrent], data[Model21XPhaseBCurrent+1]);
meterData.phaseCCurrent = m_connection->convertFloatValues(data[Model21XPhaseCCurrent], data[Model21XPhaseCCurrent+1]);
meterData.voltageLN = m_connection->convertFloatValues(data[Model21XVoltageLN], data[Model21XVoltageLN+1]);
meterData.phaseVoltageAN = m_connection->convertFloatValues(data[Model21XPhaseVoltageAN], data[Model21XPhaseVoltageAN+1]);
meterData.phaseVoltageBN = m_connection->convertFloatValues(data[Model21XPhaseVoltageBN], data[Model21XPhaseVoltageBN+1]);
meterData.phaseVoltageCN = m_connection->convertFloatValues(data[Model21XPhaseVoltageCN], data[Model21XPhaseVoltageCN+1]);
meterData.voltageLL = m_connection->convertFloatValues(data[Model21XVoltageLL], data[Model21XVoltageLL+1]);
meterData.phaseVoltageAB = m_connection->convertFloatValues(data[Model21XPhaseVoltageAB], data[Model21XPhaseVoltageAB+1]);
meterData.phaseVoltageBC = m_connection->convertFloatValues(data[Model21XPhaseVoltageBC], data[Model21XPhaseVoltageBC+1]);
meterData.phaseVoltageCA = m_connection->convertFloatValues(data[Model21XPhaseVoltageCA], data[Model21XPhaseVoltageCA+1]);
meterData.frequency = m_connection->convertFloatValues(data[Model21XFrequency], data[Model21XFrequency+1]);
meterData.totalRealPower = m_connection->convertFloatValues(data[Model21XTotalRealPower], data[Model21XTotalRealPower+1]);
meterData.totalRealEnergyExported = m_connection->convertFloatValues(data[Model21XTotalRealEnergyExported], data[Model21XTotalRealEnergyExported+1]);
meterData.totalRealEnergyImported = m_connection->convertFloatValues(data[Model21XTotalRealEnergyImported], data[Model21XTotalRealEnergyImported+1]);
meterData.meterEventFlags = ((static_cast<quint32>(data[Model21XMeterEventFlags]) << 16) | data[Model21XMeterEventFlags+1]);
emit meterDataReceived(meterData);
} break;
default:
break;
}
}

167
sunspec/sunspecmeter.h Normal file
View File

@ -0,0 +1,167 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 SUNSPECMETER_H
#define SUNSPECMETER_H
#include <QObject>
#include "sunspec.h"
class SunSpecMeter : public QObject
{
Q_OBJECT
public:
enum MeterEventFlags {
MeterEventPowerFailure = 2,
MeterEventUnderVoltage,
MeterEventLowPF,
MeterEventOverCurrent,
MeterEventOverVoltage,
MeterEventMissing_Sensor,
MeterEventReserved1,
MeterEventReserved2,
MeterEventReserved3,
MeterEventReserved4,
MeterEventReserved5,
MeterEventReserved6,
MeterEventReserved7,
MeterEventReserved8,
MeterEventOEM01,
MeterEventOEM02,
MeterEventOEM03,
MeterEventOEM04,
MeterEventOEM05,
MeterEventOEM06,
MeterEventOEM07,
MeterEventOEM08,
MeterEventOEM09,
MeterEventOEM10,
MeterEventOEM11,
MeterEventOEM12,
MeterEventOEM13,
MeterEventOEM14,
MeterEventOEM15
};
//Model 201 = Single phase meter SF
//Model 202 = Split phase meter SF
//Model 203 = Three phase meter SF
//Note: For example single phase inverters, Phase B current is optional then.
enum Model20X {
Model20XTotalAcCurrent = 0,
Model20XPhaseACurrent = 1,
Model20XPhaseBCurrent = 2,
Model20XPhaseCCurrent = 3,
Model20XCurrentScaleFactor = 4,
Model20XVoltageLN = 5,
Model20XPhaseVoltageAN = 6,
Model20XPhaseVoltageBN = 7,
Model20XPhaseVoltageCN = 8,
Model20XVoltageLL = 9,
Model20XPhaseVoltageAB = 10,
Model20XPhaseVoltageBC = 11,
Model20XPhaseVoltageCA = 12,
Model20XVoltageScaleFactor = 13,
Model20XFrequency = 14,
Model20XFrequencyScaleFactor = 15,
Model20XTotalRealPower = 16,
Model20XRealPowerScaleFactor = 20,
Model20XTotalRealEnergyExported = 36,
Model20XTotalRealEnergyImported = 44,
Model20XRealEnergyScaleFactor = 52,
Model20XMeterEventFlags = 103
};
//Model 211 = Single phase meter float
//Model 212 = Split phase meter float
//Model 213 = Three phase meter float
enum Model21X {
Model21XTotalAcCurrent = 0,
Model21XPhaseACurrent = 2,
Model21XPhaseBCurrent = 4,
Model21XPhaseCCurrent = 6,
Model21XVoltageLN = 8,
Model21XPhaseVoltageAN = 10,
Model21XPhaseVoltageBN = 12,
Model21XPhaseVoltageCN = 14,
Model21XVoltageLL = 16,
Model21XPhaseVoltageAB = 18,
Model21XPhaseVoltageBC = 20,
Model21XPhaseVoltageCA = 22,
Model21XFrequency = 24,
Model21XTotalRealPower = 26,
Model21XTotalRealEnergyExported = 58,
Model21XTotalRealEnergyImported = 66,
Model21XMeterEventFlags = 122
};
struct MeterData {
double totalAcCurrent; // [A]
double phaseACurrent; // [A]
double phaseBCurrent; // [A]
double phaseCCurrent; // [A]
double voltageLN; // [V]
double phaseVoltageAN; // [V]
double phaseVoltageBN; // [V]
double phaseVoltageCN; // [V]
double voltageLL; // [V]
double phaseVoltageAB; // [V]
double phaseVoltageBC; // [V]
double phaseVoltageCA; // [V]
double frequency; // [Hz]
double totalRealPower; // [W]
double totalRealEnergyExported; // [kWh]
double totalRealEnergyImported; // [kWh]
quint32 meterEventFlags; // MeterEventFlags
};
SunSpecMeter(SunSpec *sunspec, SunSpec::ModelId modelId, int modbusAddress);
SunSpec::ModelId modelId();
void init();
void getMeterModelHeader();
void getMeterModelDataBlock();
private:
SunSpec *m_connection = nullptr;
SunSpec::ModelId m_id = SunSpec::ModelIdDeltaConnectThreePhaseMeter;
uint m_modelLength = 0;
uint m_modelModbusStartRegister = 40000;
bool m_initFinishedSuccess = false;
private slots:
void onModelDataBlockReceived(SunSpec::ModelId modelId, uint length, QVector<quint16> data);
signals:
void initFinished(bool success);
void meterDataReceived(const MeterData &data);
};
#endif // SUNSPECMETER_H

175
sunspec/sunspecstorage.cpp Normal file
View File

@ -0,0 +1,175 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 "sunspecstorage.h"
#include "extern-plugininfo.h"
SunSpecStorage::SunSpecStorage(SunSpec *sunspec, SunSpec::ModelId modelId, int modbusAddress) :
QObject(sunspec),
m_connection(sunspec),
m_id(modelId),
m_modelModbusStartRegister(modbusAddress)
{
qCDebug(dcSunSpec()) << "SunSpecStorage: Setting up storage";
connect(m_connection, &SunSpec::modelDataBlockReceived, this, &SunSpecStorage::onModelDataBlockReceived);
}
SunSpec::ModelId SunSpecStorage::modelId()
{
return m_id;
}
void SunSpecStorage::init()
{
qCDebug(dcSunSpec()) << "SunSpecStorage: Init";
getStorageModelHeader();
connect(m_connection, &SunSpec::modelHeaderReceived, this, [this] (uint modbusAddress, SunSpec::ModelId modelId, uint length) {
if (modelId == m_id) {
qCDebug(dcSunSpec()) << "SunSpecStorager: Model header received, modbus address:" << modbusAddress << "model Id:" << modelId << "length:" << length;
m_modelLength = length;
emit initFinished(true);
m_initFinishedSuccess = true;
}
});
QTimer::singleShot(10000, this, [this] {
if (!m_initFinishedSuccess) {
emit initFinished(false);
}
});
}
void SunSpecStorage::getStorageModelDataBlock()
{
qCDebug(dcSunSpec()) << "SunSpecStorage: get storage model data block, modbus register" << m_modelModbusStartRegister << "length" << m_modelLength;
m_connection->readModelDataBlock(m_modelModbusStartRegister, m_modelLength);
}
void SunSpecStorage::getStorageModelHeader()
{
qCDebug(dcSunSpec()) << "SunSpecStorage: get storage model header, modbus register" << m_modelModbusStartRegister << "length" << m_modelLength;
m_connection->readModelHeader(m_modelModbusStartRegister);
}
QUuid SunSpecStorage::setGridCharging(bool enabled)
{
// Name ChaGriSet
/* Setpoint to enable/dis-
able charging from grid
PV (charging from grid 0 disabled)
GRID (charging from 1 grid enabled*/
uint registerAddress = m_modelModbusStartRegister + Model124Optional::Model124ChaGriSet;
quint16 value = enabled;
return m_connection->writeHoldingRegister(registerAddress, value);
}
QUuid SunSpecStorage::setStorageControlMode(bool chargingEnabled, bool dischargingEnabled)
{
// Set charge bit to enable charge limit, set discharge bit to enable discharge limit, set both bits to enable both limits
quint16 value = ((static_cast<quint16>(chargingEnabled)) |
(static_cast<quint16>(dischargingEnabled) << 1)) ;
uint modbusRegister = m_modelModbusStartRegister + Model124::Model124StorCtl_Mod;
return m_connection->writeHoldingRegister(modbusRegister, value);
}
QUuid SunSpecStorage::setChargingRate(float rate)
{
if (!m_scaleFactorsSet) {
qCWarning(dcSunSpec()) << "SunSpecStorage: Set charging rate, scale factors are not set";
return "";
}
if (rate < 0.00 || rate > 100.00) {
qCWarning(dcSunSpec()) << "SunSpecStorage: Set charging rate, rate out of boundaries [0, 100]";
return "";
}
uint modbusRegister = m_modelModbusStartRegister + Model124::Model124WChaGra;
quint16 value = m_connection->convertFromFloatWithSSF(rate, m_WChaDisChaGra_SF);
return m_connection->writeHoldingRegister(modbusRegister, value);
}
QUuid SunSpecStorage::setDischargingRate(float rate)
{
if (!m_scaleFactorsSet) {
qCWarning(dcSunSpec()) << "SunSpecStorage: Set discharging rate, scale factors are not set";
}
if (rate < 0.00 || rate > 100.00) {
qCWarning(dcSunSpec()) << "SunSpecStorage: Set doscharging rate, rate out of boundaries [0, 100]";
return "";
}
uint modbusRegister = m_modelModbusStartRegister + Model124::Model124WDisChaGra;
quint16 value = m_connection->convertFromFloatWithSSF(rate, m_WChaDisChaGra_SF);
return m_connection->writeHoldingRegister(modbusRegister, value);
}
void SunSpecStorage::onModelDataBlockReceived(SunSpec::ModelId modelId, uint length, const QVector<quint16> &data)
{
if (modelId != m_id) {
return;
}
if (length < m_modelLength) {
qCDebug(dcSunSpec()) << "SunSpecMeter: on model data block received, model length is too short" << length;
return;
}
qCDebug(dcSunSpec()) << "SunSpecStorage: Received" << modelId;
switch (modelId) {
case SunSpec::ModelIdStorage: {
StorageData mandatory;
mandatory.WChaMax = m_connection->convertToFloatWithSSF(data[Model124WChaMax], data[Model124WChaMax_SF]);
mandatory.WChaGra = m_connection->convertToFloatWithSSF(data[Model124WChaGra], data[Model124WChaDisChaGra_SF]);
mandatory.WDisChaGra = m_connection->convertToFloatWithSSF(data[Model124WDisChaGra], data[Model124WChaDisChaGra_SF]);
mandatory.StorCtl_Mod_ChargingEnabled = data[Model124StorCtl_Mod]&0x01;
mandatory.StorCtl_Mod_DischargingEnabled = data[Model124StorCtl_Mod]&0x02;
StorageDataOptional optional;
optional.VAChaMax = m_connection->convertToFloatWithSSF(data[Model124VAChaMax], data[Model124VAChaMax_SF]);
optional.MinRsvPct = m_connection->convertToFloatWithSSF(data[Model124MinRsvPct], data[Model124MinRsvPct_SF]);
optional.ChaState = m_connection->convertToFloatWithSSF(data[Model124ChaState], data[Model124ChaState_SF]);
optional.StorAval = m_connection->convertToFloatWithSSF(data[Model124StorAval], data[Model124StorAval_SF]);
optional.InBatV = m_connection->convertToFloatWithSSF(data[Model124InBatV], data[Model124InBatV_SF]);
optional.ChaSt = ChargingStatus(data[Model124ChaSt]);
optional.OutWRte = m_connection->convertToFloatWithSSF(data[Model124OutWRte], data[Model124InOutWRte_SF]);
optional.InWRte = m_connection->convertToFloatWithSSF(data[Model124InWRte], data[Model124InOutWRte_SF]);
optional.InOutWRte_WinTms = data[Model124InOutWRte_WinTms];
optional.InOutWRte_RvrtTms = data[Model124InOutWRte_RvrtTms];
optional.InOutWRte_RmpTms = data[Model124InOutWRte_RmpTms];
optional.ChaGriSet = GridCharge(data[Model124ChaGriSet]);
emit storageDataReceived(mandatory, optional);
} break;
case SunSpec::ModelIdBatteryBaseModel:
case SunSpec::ModelIdLithiumIonBatteryModel: {
qCDebug(dcSunSpec()) << "Model not yet supported";
}
default:
break;
}
}

154
sunspec/sunspecstorage.h Normal file
View File

@ -0,0 +1,154 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
*
* 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 SUNSPECSTORAGE_H
#define SUNSPECSTORAGE_H
#include <QObject>
#include "sunspec.h"
class SunSpecStorage : public QObject
{
Q_OBJECT
public:
SunSpecStorage(SunSpec *sunspec, SunSpec::ModelId modelId, int modbusAddress);
SunSpec::ModelId modelId();
void init();
void getStorageModelHeader();
void getStorageModelDataBlock();
QUuid setGridCharging(bool enabled);
QUuid setDischargingRate(float rate);
QUuid setChargingRate(float rate);
QUuid setStorageControlMode(bool chargingEnabled, bool dischargingEnabled);
enum StorageControl {
StorageControlHold = 0,
StorageControlCharge = 1,
StorageControlDischarge = 2,
};
Q_ENUM(StorageControl)
enum GridCharge {
PV = 0,
Grid = 1
};
Q_ENUM(GridCharge)
enum ChargingStatus {
ChargingStatusOff = 1,
ChargingStatusEmpty,
ChargingStatusDischarging,
ChargingStatusCharging,
ChargingStatusFull,
ChargingStatusHolding,
ChargingStatusTesting
};
Q_ENUM(ChargingStatus)
enum Model124 { // Mandatory registers
Model124WChaMax = 0,
Model124WChaGra = 1,
Model124WDisChaGra = 2,
Model124StorCtl_Mod = 3,
Model124WChaMax_SF = 16,
Model124WChaDisChaGra_SF = 17,
};
enum Model124Optional { // Optional registers
Model124VAChaMax = 4,
Model124MinRsvPct = 5,
Model124ChaState = 6,
Model124StorAval = 7,
Model124InBatV = 8,
Model124ChaSt = 9,
Model124OutWRte = 10,
Model124InWRte = 11,
Model124InOutWRte_WinTms = 12,
Model124InOutWRte_RvrtTms = 13,
Model124InOutWRte_RmpTms = 14,
Model124ChaGriSet = 15,
Model124VAChaMax_SF = 18,
Model124MinRsvPct_SF = 19,
Model124ChaState_SF = 20,
Model124StorAval_SF = 21,
Model124InBatV_SF = 22,
Model124InOutWRte_SF = 23
};
Q_ENUM(Model124Optional)
struct StorageData {
double WChaMax; // [W] Setpoint for maximum charge.
double WChaGra; // [%] Setpoint for maximum charging rate. Default is MaxChaRte.
double WDisChaGra; // [%] Setpoint for maximum discharge rate. Default is MaxDisChaRte.
bool StorCtl_Mod_ChargingEnabled;
bool StorCtl_Mod_DischargingEnabled;
};
struct StorageDataOptional {
double VAChaMax; // [VA] Setpoint for maximum charging VA.
double MinRsvPct; // [%]Setpoint for minimum reserve for storage as a percentage of the nominal maximum storage.
double ChaState; // [%] Currently available energy as a percent of the capacity rating.
double StorAval; // [Ah] State of charge (ChaState) minus storage reserve (MinRsvPct) times capacity rating (AhrRtg).
double InBatV; // [V] Internal battery voltage.
ChargingStatus ChaSt; // Charge status of storage device. Enumerated value.
double OutWRte; // [%] Percent of max discharge rate.
double InWRte; // [%] Percent of max charging rate.
uint InOutWRte_WinTms; // [s] Time window for charge/discharge rate change.
uint InOutWRte_RvrtTms; // [s] Timeout period for charge/discharge rate.
uint InOutWRte_RmpTms; // [s] Ramp time for moving from current setpoint to new setpoint.
GridCharge ChaGriSet; // 0 = PV, 1 = Grid
};
private:
SunSpec *m_connection = nullptr;
SunSpec::ModelId m_id = SunSpec::ModelIdEnergyStorageBaseModel;
uint m_modelLength = 0;
uint m_modelModbusStartRegister = 40000;
bool m_initFinishedSuccess = false;
// Scale factors needed to perform write requests
bool m_scaleFactorsSet = false;
quint16 m_WChaMax_SF = 0;
quint16 m_WChaDisChaGra_SF = 0;
quint16 m_VAChaMax_SF = 0;
quint16 m_MinRsvPct_SF = 0;
quint16 m_InOutWRte_SF = 0;
private slots:
void onModelDataBlockReceived(SunSpec::ModelId modelId, uint length, const QVector<quint16> &data);
signals:
void initFinished(bool success);
void storageDataReceived(const StorageData &mandatory, const StorageDataOptional &optional);
};
#endif // SUNSPECSTORAGE_H

19
sunspec/test/sunspec_server.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
if ! command -v suns &> /dev/null
then
echo "suns could not be found"
echo "... installing"
sudo apt update
sudo apt install libmodbus-dev flex bison git
if [ ! -d "sunspec"]; then
git clone https://github.com/Boernsman/sunspec.git
fi
cd ./sunspec/src
make
sudo make install
fi
echo "Startin sunspec test server"
sudo suns -s -vvvv -m models/test/composite_superdevice.model

File diff suppressed because it is too large Load Diff