Merge PR #6: New plugin: SunSpec
This commit is contained in:
commit
6ef901e8a1
18
debian/control
vendored
18
debian/control
vendored
@ -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.
|
||||
|
||||
|
||||
1
debian/nymea-plugin-sunspec.install.in
vendored
Normal file
1
debian/nymea-plugin-sunspec.install.in
vendored
Normal file
@ -0,0 +1 @@
|
||||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsunspec.so
|
||||
@ -195,7 +195,6 @@ void IntegrationPluginMyPv::executeAction(ThingActionInfo *info)
|
||||
|
||||
void IntegrationPluginMyPv::onRefreshTimer()
|
||||
{
|
||||
|
||||
foreach (Thing *thing, myThings()) {
|
||||
update(thing);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ PLUGIN_DIRS = \
|
||||
drexelundweiss \
|
||||
modbuscommander \
|
||||
mypv \
|
||||
sunspec \
|
||||
wallbe \
|
||||
|
||||
message(============================================)
|
||||
|
||||
24
sunspec/README.md
Normal file
24
sunspec/README.md
Normal 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
|
||||
847
sunspec/integrationpluginsunspec.cpp
Normal file
847
sunspec/integrationpluginsunspec.cpp
Normal 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 ¶mTypeId, 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);
|
||||
}
|
||||
}
|
||||
106
sunspec/integrationpluginsunspec.h
Normal file
106
sunspec/integrationpluginsunspec.h
Normal 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 ¶mTypeId, 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
|
||||
|
||||
1077
sunspec/integrationpluginsunspec.json
Normal file
1077
sunspec/integrationpluginsunspec.json
Normal file
File diff suppressed because it is too large
Load Diff
12
sunspec/meta.json
Normal file
12
sunspec/meta.json
Normal 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
427
sunspec/sunspec.cpp
Normal 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
221
sunspec/sunspec.h
Normal 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
BIN
sunspec/sunspec.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
19
sunspec/sunspec.pro
Normal file
19
sunspec/sunspec.pro
Normal 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
172
sunspec/sunspecinverter.cpp
Normal 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
187
sunspec/sunspecinverter.h
Normal 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
152
sunspec/sunspecmeter.cpp
Normal 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
167
sunspec/sunspecmeter.h
Normal 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
175
sunspec/sunspecstorage.cpp
Normal 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
154
sunspec/sunspecstorage.h
Normal 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
19
sunspec/test/sunspec_server.sh
Executable 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
|
||||
1126
sunspec/translations/cb4bdec6-cf2c-4a0f-9709-42d951ca2d8b-en_US.ts
Normal file
1126
sunspec/translations/cb4bdec6-cf2c-4a0f-9709-42d951ca2d8b-en_US.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user