sunspec: add Fronius vendor with its own control rules
Adds a fronius vendor and a froniusConnection thing class alongside the
generic sunspec and solarEdge ones, following the SolarEdge mechanism but
none of its workarounds - no proprietary battery block and no PV
production computed by difference, since the GEN24 exposes model 160.
FroniusRules holds the vendor decisions as pure functions:
- discovery only keeps devices announcing Fronius as manufacturer;
- the byte order is the SunSpec default, not the forced big endian that
is specific to SolarEdge, and no endianness parameter is exposed;
- meterSlaveId defaults to 200, 0 meaning no meter on this connection;
- a hard 7 s floor on the poll interval. Polling faster can push the
inverter offline towards the vendor portal, and recovery needs an
AC+DC power cycle on site, so this is a guard in the code rather than
a configurable default;
- the registers the inverter accepts and then silently ignores are
refused before anything reaches the wire;
- the six HEMS battery modes map onto ChaGriSet, StorCtl_Mod, InWRte and
OutWRte, negative meaning charge;
- model 160 blocks are classified by IDStr, never by index: on a three
or four MPPT inverter the battery blocks are not in position 3 and 4.
Storages under a Fronius connection default revertTime to 300 s.
Setpoints now reread the scale factor before writing and read the written
registers back afterwards, reporting a mismatch through a new
controlHealthy state - the only defence against writes that are accepted
and then ignored without a Modbus exception.
Byte order and the revert timer semantics on model 124 are inferences, not
measurements; see specs/fronius-sunspec.md, H-2 and H-3.
This commit is contained in:
parent
17e5751b5e
commit
6290a6cf7b
172
sunspec/froniusrules.cpp
Normal file
172
sunspec/froniusrules.cpp
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026, ETM-PowerSync
|
||||||
|
*
|
||||||
|
* This file is part of nymea.
|
||||||
|
*
|
||||||
|
* nymea is free software: you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU General Public License as published by the Free Software
|
||||||
|
* Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* nymea is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* nymea. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#include "froniusrules.h"
|
||||||
|
|
||||||
|
using namespace SunSpecStorageLogic;
|
||||||
|
|
||||||
|
namespace FroniusRules {
|
||||||
|
|
||||||
|
bool matchesManufacturer(const QStringList &manufacturers)
|
||||||
|
{
|
||||||
|
foreach (const QString &name, manufacturers) {
|
||||||
|
if (name.contains(QStringLiteral("fronius"), Qt::CaseInsensitive))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SunSpecDataPoint::ByteOrder byteOrder()
|
||||||
|
{
|
||||||
|
return SunSpecDataPoint::ByteOrderLittleEndian;
|
||||||
|
}
|
||||||
|
|
||||||
|
int clampUpdateInterval(int requestedSeconds)
|
||||||
|
{
|
||||||
|
return qMax(requestedSeconds, MinimumUpdateIntervalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isWriteForbidden(const QString ®isterName)
|
||||||
|
{
|
||||||
|
static const QStringList forbidden = {
|
||||||
|
QStringLiteral("WChaMax"),
|
||||||
|
QStringLiteral("WChaGra"),
|
||||||
|
QStringLiteral("WDisChaGra"),
|
||||||
|
QStringLiteral("InOutWRte_WinTms"),
|
||||||
|
QStringLiteral("InOutWRte_RmpTms"),
|
||||||
|
/* VAChaMax is doubly unusable: the write is ignored and its scale
|
||||||
|
* factor is not supported either. */
|
||||||
|
QStringLiteral("VAChaMax")
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (const QString &name, forbidden) {
|
||||||
|
if (registerName.compare(name, Qt::CaseInsensitive) == 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetpointPlan planBatteryMode(BatteryMode mode, double chargeLimit, double dischargeLimit, quint16 revertTime)
|
||||||
|
{
|
||||||
|
/* StorCtl_Mod is a bitfield: bit 0 CHARGE, bit 1 DISCHARGE. ChaGriSet is
|
||||||
|
* 0 PV, 1 GRID. A register absent from the vector below is deliberately not
|
||||||
|
* written for that mode.
|
||||||
|
*
|
||||||
|
* planSetpoint() puts these in the mandated order and appends the revert
|
||||||
|
* timer, so the order they are listed in here does not matter. */
|
||||||
|
QVector<Write> setpoints;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case BatteryModeNormal:
|
||||||
|
setpoints.append({RegisterStorCtlMod, 0});
|
||||||
|
setpoints.append({RegisterOutWRte, 100});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BatteryModeNoDischarge:
|
||||||
|
setpoints.append({RegisterStorCtlMod, 2});
|
||||||
|
setpoints.append({RegisterOutWRte, 0});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BatteryModeNoCharge:
|
||||||
|
setpoints.append({RegisterStorCtlMod, 1});
|
||||||
|
setpoints.append({RegisterInWRte, 0});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BatteryModeFrozen:
|
||||||
|
setpoints.append({RegisterStorCtlMod, 3});
|
||||||
|
setpoints.append({RegisterInWRte, 0});
|
||||||
|
setpoints.append({RegisterOutWRte, 0});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BatteryModeForceCharge:
|
||||||
|
/* Charging from the grid has to be allowed on the inverter as well;
|
||||||
|
* ChaGriSet alone is ANDed with a setting of the component
|
||||||
|
* configuration and fails silently if that one is off. */
|
||||||
|
setpoints.append({RegisterChaGriSet, 1});
|
||||||
|
setpoints.append({RegisterStorCtlMod, 2});
|
||||||
|
/* Negative is charging. */
|
||||||
|
setpoints.append({RegisterOutWRte, -chargeLimit});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BatteryModeLimit:
|
||||||
|
setpoints.append({RegisterStorCtlMod, 3});
|
||||||
|
setpoints.append({RegisterInWRte, chargeLimit});
|
||||||
|
setpoints.append({RegisterOutWRte, dischargeLimit});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return planSetpoint(setpoints, revertTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockRole classifyBlock(const QString &inputIdString)
|
||||||
|
{
|
||||||
|
const QString id = inputIdString.trimmed();
|
||||||
|
if (id.isEmpty())
|
||||||
|
return BlockRoleUnknown;
|
||||||
|
|
||||||
|
/* Prefix match, not contains: the role is decided by the string, never by
|
||||||
|
* the block index. StDisCha is checked first so that no cascade can ever
|
||||||
|
* swallow it into the charge case. */
|
||||||
|
if (id.startsWith(QStringLiteral("StDisCha"), Qt::CaseInsensitive))
|
||||||
|
return BlockRoleBatteryDischarge;
|
||||||
|
|
||||||
|
if (id.startsWith(QStringLiteral("StCha"), Qt::CaseInsensitive))
|
||||||
|
return BlockRoleBatteryCharge;
|
||||||
|
|
||||||
|
if (id.startsWith(QStringLiteral("MPPT"), Qt::CaseInsensitive))
|
||||||
|
return BlockRolePvString;
|
||||||
|
|
||||||
|
return BlockRoleUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
double pvPower(const QList<ModuleReading> &readings)
|
||||||
|
{
|
||||||
|
double total = 0;
|
||||||
|
foreach (const ModuleReading &reading, readings) {
|
||||||
|
if (classifyBlock(reading.inputIdString) == BlockRolePvString)
|
||||||
|
total += reading.dcPower;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
double batteryPower(const QList<ModuleReading> &readings)
|
||||||
|
{
|
||||||
|
double charging = 0;
|
||||||
|
double discharging = 0;
|
||||||
|
|
||||||
|
foreach (const ModuleReading &reading, readings) {
|
||||||
|
switch (classifyBlock(reading.inputIdString)) {
|
||||||
|
case BlockRoleBatteryCharge:
|
||||||
|
charging += reading.dcPower;
|
||||||
|
break;
|
||||||
|
case BlockRoleBatteryDischarge:
|
||||||
|
discharging += reading.dcPower;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return discharging - charging;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
126
sunspec/froniusrules.h
Normal file
126
sunspec/froniusrules.h
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026, ETM-PowerSync
|
||||||
|
*
|
||||||
|
* This file is part of nymea.
|
||||||
|
*
|
||||||
|
* nymea is free software: you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU General Public License as published by the Free Software
|
||||||
|
* Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* nymea is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* nymea. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#ifndef FRONIUSRULES_H
|
||||||
|
#define FRONIUSRULES_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
#include <sunspecdatapoint.h>
|
||||||
|
|
||||||
|
#include "sunspecstoragelogic.h"
|
||||||
|
|
||||||
|
/* Vendor specific rules for Fronius GEN24 inverters.
|
||||||
|
*
|
||||||
|
* Everything here is a pure decision: no Modbus, no thing, no timer. The
|
||||||
|
* plugin asks these functions what to do; the answers are what the unit tests
|
||||||
|
* pin down. See specs/fronius-sunspec.md.
|
||||||
|
*/
|
||||||
|
namespace FroniusRules {
|
||||||
|
|
||||||
|
/* Polling. The manufacturer's support recommends staying above seven seconds
|
||||||
|
* for third party applications. Below that, communication load can push the
|
||||||
|
* inverter offline towards the manufacturer's portal, and recovery needs an
|
||||||
|
* AC+DC power cycle on site. This floor is a guard in the code, not a
|
||||||
|
* configurable default. */
|
||||||
|
const int MinimumUpdateIntervalSeconds = 7;
|
||||||
|
const int DefaultUpdateIntervalSeconds = 10;
|
||||||
|
const int DefaultTimeoutMilliSeconds = 2000;
|
||||||
|
const int DefaultNumberOfRetries = 3;
|
||||||
|
|
||||||
|
/* Revert timer armed with every setpoint, so that a setpoint left behind by a
|
||||||
|
* stopped or crashed HEMS expires instead of holding the battery forever.
|
||||||
|
*
|
||||||
|
* WORKING HYPOTHESIS: the timer semantics are documented in the manual's
|
||||||
|
* general chapter and illustrated on model 123, not on model 124. This must be
|
||||||
|
* verified experimentally on model 124 before being frozen. If it does not
|
||||||
|
* hold, fall back to 0 plus an application level watchdog in the HEMS. */
|
||||||
|
const int DefaultRevertTimeSeconds = 300;
|
||||||
|
|
||||||
|
/* Default unit IDs. The inverter is fixed at 1 over TCP; the meter defaults to
|
||||||
|
* 200, with 201, 202... for secondary meters. 0 means "no meter on this
|
||||||
|
* connection". */
|
||||||
|
const int InverterSlaveId = 1;
|
||||||
|
const int DefaultMeterSlaveId = 200;
|
||||||
|
|
||||||
|
bool matchesManufacturer(const QStringList &manufacturers);
|
||||||
|
|
||||||
|
/* INFERRED: the byte order of the GEN24 is not documented by the manufacturer,
|
||||||
|
* so the SunSpec default is assumed. The forced big endian of the SolarEdge
|
||||||
|
* vendor is an anomaly specific to SolarEdge and must not be extended here.
|
||||||
|
* To be confirmed on the bench, then on hardware. */
|
||||||
|
SunSpecDataPoint::ByteOrder byteOrder();
|
||||||
|
|
||||||
|
int clampUpdateInterval(int requestedSeconds);
|
||||||
|
|
||||||
|
/* Registers the GEN24 accepts and then ignores without raising a Modbus
|
||||||
|
* exception. Writing them fails invisibly, so they are refused before being
|
||||||
|
* put on the wire. */
|
||||||
|
bool isWriteForbidden(const QString ®isterName);
|
||||||
|
|
||||||
|
enum BatteryMode {
|
||||||
|
BatteryModeNormal,
|
||||||
|
BatteryModeNoDischarge,
|
||||||
|
BatteryModeNoCharge,
|
||||||
|
BatteryModeFrozen,
|
||||||
|
BatteryModeForceCharge,
|
||||||
|
BatteryModeLimit
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Sign convention of the manufacturer: negative is charging, positive is
|
||||||
|
* discharging. chargeLimit and dischargeLimit are percentages of WChaMax and
|
||||||
|
* are only used by the modes that need them. */
|
||||||
|
SunSpecStorageLogic::SetpointPlan planBatteryMode(BatteryMode mode,
|
||||||
|
double chargeLimit,
|
||||||
|
double dischargeLimit,
|
||||||
|
quint16 revertTime);
|
||||||
|
|
||||||
|
/* Model 160 repeating blocks are identified by their IDStr, never by their
|
||||||
|
* index: on a three or four MPPT inverter the battery blocks are not in
|
||||||
|
* position 3 and 4. */
|
||||||
|
enum BlockRole {
|
||||||
|
BlockRolePvString,
|
||||||
|
BlockRoleBatteryCharge,
|
||||||
|
BlockRoleBatteryDischarge,
|
||||||
|
BlockRoleUnknown
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockRole classifyBlock(const QString &inputIdString);
|
||||||
|
|
||||||
|
struct ModuleReading {
|
||||||
|
QString inputIdString;
|
||||||
|
double dcPower;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Sum of the DC power of the PV string blocks. */
|
||||||
|
double pvPower(const QList<ModuleReading> &readings);
|
||||||
|
|
||||||
|
/* Discharge minus charge, so positive means discharging. DCW is a uint16 and
|
||||||
|
* therefore always positive: the sign is carried by whichever block is active,
|
||||||
|
* the inactive one reading zero. */
|
||||||
|
double batteryPower(const QList<ModuleReading> &readings);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // FRONIUSRULES_H
|
||||||
@ -66,21 +66,27 @@ void IntegrationPluginSunSpec::init()
|
|||||||
// SunSpec connection params
|
// SunSpec connection params
|
||||||
m_connectionDiscoverySlaveIdParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionDiscoverySlaveIdParamTypeId);
|
m_connectionDiscoverySlaveIdParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionDiscoverySlaveIdParamTypeId);
|
||||||
m_connectionDiscoverySlaveIdParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionDiscoverySlaveIdParamTypeId);
|
m_connectionDiscoverySlaveIdParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionDiscoverySlaveIdParamTypeId);
|
||||||
|
m_connectionDiscoverySlaveIdParamTypeIds.insert(froniusConnectionThingClassId, froniusConnectionDiscoverySlaveIdParamTypeId);
|
||||||
|
|
||||||
m_connectionMacAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingMacAddressParamTypeId);
|
m_connectionMacAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingMacAddressParamTypeId);
|
||||||
m_connectionMacAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingMacAddressParamTypeId);
|
m_connectionMacAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingMacAddressParamTypeId);
|
||||||
|
m_connectionMacAddressParamTypeIds.insert(froniusConnectionThingClassId, froniusConnectionThingMacAddressParamTypeId);
|
||||||
|
|
||||||
m_connectionHostNameParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingHostNameParamTypeId);
|
m_connectionHostNameParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingHostNameParamTypeId);
|
||||||
m_connectionHostNameParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingHostNameParamTypeId);
|
m_connectionHostNameParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingHostNameParamTypeId);
|
||||||
|
m_connectionHostNameParamTypeIds.insert(froniusConnectionThingClassId, froniusConnectionThingHostNameParamTypeId);
|
||||||
|
|
||||||
m_connectionAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingAddressParamTypeId);
|
m_connectionAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingAddressParamTypeId);
|
||||||
m_connectionAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingAddressParamTypeId);
|
m_connectionAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingAddressParamTypeId);
|
||||||
|
m_connectionAddressParamTypeIds.insert(froniusConnectionThingClassId, froniusConnectionThingAddressParamTypeId);
|
||||||
|
|
||||||
m_connectionPortParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingPortParamTypeId);
|
m_connectionPortParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingPortParamTypeId);
|
||||||
m_connectionPortParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingPortParamTypeId);
|
m_connectionPortParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingPortParamTypeId);
|
||||||
|
m_connectionPortParamTypeIds.insert(froniusConnectionThingClassId, froniusConnectionThingPortParamTypeId);
|
||||||
|
|
||||||
m_connectionSlaveIdParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingSlaveIdParamTypeId);
|
m_connectionSlaveIdParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingSlaveIdParamTypeId);
|
||||||
m_connectionSlaveIdParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingSlaveIdParamTypeId);
|
m_connectionSlaveIdParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingSlaveIdParamTypeId);
|
||||||
|
m_connectionSlaveIdParamTypeIds.insert(froniusConnectionThingClassId, froniusConnectionThingSlaveIdParamTypeId);
|
||||||
|
|
||||||
// Params for sunspec things
|
// Params for sunspec things
|
||||||
m_modelIdParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingModelIdParamTypeId);
|
m_modelIdParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingModelIdParamTypeId);
|
||||||
@ -140,6 +146,8 @@ void IntegrationPluginSunSpec::discoverThings(ThingDiscoveryInfo *info)
|
|||||||
SunSpecDataPoint::ByteOrder byteOrder = SunSpecDataPoint::ByteOrderLittleEndian;
|
SunSpecDataPoint::ByteOrder byteOrder = SunSpecDataPoint::ByteOrderLittleEndian;
|
||||||
if (info->thingClassId() == solarEdgeConnectionThingClassId)
|
if (info->thingClassId() == solarEdgeConnectionThingClassId)
|
||||||
byteOrder = SunSpecDataPoint::ByteOrderBigEndian;
|
byteOrder = SunSpecDataPoint::ByteOrderBigEndian;
|
||||||
|
else if (info->thingClassId() == froniusConnectionThingClassId)
|
||||||
|
byteOrder = FroniusRules::byteOrder();
|
||||||
|
|
||||||
quint16 discoverySlaveId = info->params().paramValue(m_connectionDiscoverySlaveIdParamTypeIds.value(info->thingClassId())).toUInt();
|
quint16 discoverySlaveId = info->params().paramValue(m_connectionDiscoverySlaveIdParamTypeIds.value(info->thingClassId())).toUInt();
|
||||||
|
|
||||||
@ -165,6 +173,12 @@ void IntegrationPluginSunSpec::discoverThings(ThingDiscoveryInfo *info)
|
|||||||
} else {
|
} else {
|
||||||
manufacturer = "SolarEdge";
|
manufacturer = "SolarEdge";
|
||||||
}
|
}
|
||||||
|
} else if (info->thingClassId() == froniusConnectionThingClassId) {
|
||||||
|
if (!FroniusRules::matchesManufacturer(result.modelManufacturers)) {
|
||||||
|
// Only offer devices that actually announce themselves as Fronius.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
manufacturer = "Fronius";
|
||||||
} else if (info->thingClassId() == sunspecConnectionThingClassId) {
|
} else if (info->thingClassId() == sunspecConnectionThingClassId) {
|
||||||
// There are some issues regarding the sunspec implementation of kostal.
|
// There are some issues regarding the sunspec implementation of kostal.
|
||||||
// Full support of meter, inverter and storage will be provided in the kostal plugin which makes
|
// Full support of meter, inverter and storage will be provided in the kostal plugin which makes
|
||||||
@ -416,7 +430,7 @@ void IntegrationPluginSunSpec::postSetupThing(Thing *thing)
|
|||||||
// Create the refresh timer if not already set up
|
// Create the refresh timer if not already set up
|
||||||
if (!m_refreshTimer) {
|
if (!m_refreshTimer) {
|
||||||
qCDebug(dcSunSpec()) << "Starting refresh timer";
|
qCDebug(dcSunSpec()) << "Starting refresh timer";
|
||||||
int refreshTime = configValue(sunSpecPluginUpdateIntervalParamTypeId).toInt();
|
int refreshTime = effectiveUpdateInterval();
|
||||||
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime);
|
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime);
|
||||||
connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSunSpec::onRefreshTimer);
|
connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSunSpec::onRefreshTimer);
|
||||||
}
|
}
|
||||||
@ -509,6 +523,36 @@ void IntegrationPluginSunSpec::executeAction(ThingActionInfo *info)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The refresh timer is plugin wide, not per connection, so the Fronius polling
|
||||||
|
* floor has to be applied to it: as soon as one Fronius connection is
|
||||||
|
* configured, the whole plugin stops polling faster than the floor.
|
||||||
|
*
|
||||||
|
* Going below it can push the inverter offline towards the manufacturer's
|
||||||
|
* portal, and recovery then needs an AC+DC power cycle on site. This is a guard
|
||||||
|
* in the code on purpose, not a configurable default - including in debug. */
|
||||||
|
int IntegrationPluginSunSpec::effectiveUpdateInterval()
|
||||||
|
{
|
||||||
|
const int configured = configValue(sunSpecPluginUpdateIntervalParamTypeId).toInt();
|
||||||
|
|
||||||
|
if (myThings().filterByThingClassId(froniusConnectionThingClassId).isEmpty())
|
||||||
|
return configured;
|
||||||
|
|
||||||
|
const int clamped = FroniusRules::clampUpdateInterval(configured);
|
||||||
|
if (clamped != configured) {
|
||||||
|
qCWarning(dcSunSpec()) << "Update interval of" << configured << "s is below the"
|
||||||
|
<< FroniusRules::MinimumUpdateIntervalSeconds
|
||||||
|
<< "s floor required while a Fronius connection is configured. Using"
|
||||||
|
<< clamped << "s instead.";
|
||||||
|
}
|
||||||
|
return clamped;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IntegrationPluginSunSpec::isFroniusChild(const ThingId &parentId)
|
||||||
|
{
|
||||||
|
Thing *parent = myThings().findById(parentId);
|
||||||
|
return parent && parent->thingClassId() == froniusConnectionThingClassId;
|
||||||
|
}
|
||||||
|
|
||||||
QModbusReply *IntegrationPluginSunSpec::writeSetpointRegister(SunSpecStorageModel *storage, const SunSpecStorageLogic::Write &write)
|
QModbusReply *IntegrationPluginSunSpec::writeSetpointRegister(SunSpecStorageModel *storage, const SunSpecStorageLogic::Write &write)
|
||||||
{
|
{
|
||||||
switch (write.reg) {
|
switch (write.reg) {
|
||||||
@ -533,12 +577,32 @@ QModbusReply *IntegrationPluginSunSpec::writeSetpointRegister(SunSpecStorageMode
|
|||||||
void IntegrationPluginSunSpec::executeSetpointWrite(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan, int index)
|
void IntegrationPluginSunSpec::executeSetpointWrite(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan, int index)
|
||||||
{
|
{
|
||||||
if (index >= plan.writes.count()) {
|
if (index >= plan.writes.count()) {
|
||||||
info->finish(Thing::ThingErrorNoError);
|
if (plan.verifyReadback.isEmpty()) {
|
||||||
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshStorageBlock(info, storage, [this, info, storage, plan]() {
|
||||||
|
verifySetpointPlan(info, storage, plan);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SunSpecStorageLogic::Write write = plan.writes.at(index);
|
const SunSpecStorageLogic::Write write = plan.writes.at(index);
|
||||||
|
|
||||||
|
/* Defence in depth: a Fronius inverter accepts some writes and then ignores
|
||||||
|
* them without raising a Modbus exception, so the failure is invisible.
|
||||||
|
* Those registers are refused before anything goes on the wire rather than
|
||||||
|
* silently pretending to have worked. */
|
||||||
|
if (isFroniusChild(info->thing()->parentId())
|
||||||
|
&& FroniusRules::isWriteForbidden(SunSpecStorageLogic::registerName(write.reg))) {
|
||||||
|
qCWarning(dcSunSpec()) << "Refusing to write" << SunSpecStorageLogic::registerName(write.reg)
|
||||||
|
<< "on" << info->thing()
|
||||||
|
<< "- this register is ignored by the inverter without raising an exception.";
|
||||||
|
info->finish(Thing::ThingErrorHardwareFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QModbusReply *reply = writeSetpointRegister(storage, write);
|
QModbusReply *reply = writeSetpointRegister(storage, write);
|
||||||
if (!reply) {
|
if (!reply) {
|
||||||
qCWarning(dcSunSpec()) << "Could not write" << SunSpecStorageLogic::registerName(write.reg) << "on" << info->thing();
|
qCWarning(dcSunSpec()) << "Could not write" << SunSpecStorageLogic::registerName(write.reg) << "on" << info->thing();
|
||||||
@ -559,9 +623,95 @@ void IntegrationPluginSunSpec::executeSetpointWrite(ThingActionInfo *info, SunSp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double IntegrationPluginSunSpec::readSetpointRegister(SunSpecStorageModel *storage, SunSpecStorageLogic::Register reg)
|
||||||
|
{
|
||||||
|
switch (reg) {
|
||||||
|
case SunSpecStorageLogic::RegisterChaGriSet:
|
||||||
|
return storage->chaGriSet();
|
||||||
|
case SunSpecStorageLogic::RegisterStorCtlMod:
|
||||||
|
return int(storage->storCtlMod());
|
||||||
|
case SunSpecStorageLogic::RegisterInWRte:
|
||||||
|
return storage->inWRte();
|
||||||
|
case SunSpecStorageLogic::RegisterOutWRte:
|
||||||
|
return storage->outWRte();
|
||||||
|
case SunSpecStorageLogic::RegisterRvrtTms:
|
||||||
|
return storage->inOutWRteRvrtTms();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reads the model block again and calls the continuation once the fresh values
|
||||||
|
* are in. The guard timer keeps a lost or unanswered read from stranding the
|
||||||
|
* action forever; the continuation then runs on stale values, which is why it
|
||||||
|
* warns. */
|
||||||
|
void IntegrationPluginSunSpec::refreshStorageBlock(ThingActionInfo *info, SunSpecStorageModel *storage, const std::function<void()> &continuation)
|
||||||
|
{
|
||||||
|
QObject *context = new QObject(info);
|
||||||
|
QSharedPointer<bool> fired = QSharedPointer<bool>::create(false);
|
||||||
|
|
||||||
|
auto proceed = [context, fired, continuation](bool timedOut) {
|
||||||
|
if (*fired)
|
||||||
|
return;
|
||||||
|
*fired = true;
|
||||||
|
if (timedOut)
|
||||||
|
qCWarning(dcSunSpec()) << "Timed out waiting for a fresh storage block; continuing on the previous values.";
|
||||||
|
context->deleteLater();
|
||||||
|
continuation();
|
||||||
|
};
|
||||||
|
|
||||||
|
connect(storage, &SunSpecModel::blockUpdated, context, [proceed]() { proceed(false); });
|
||||||
|
|
||||||
|
QTimer *guard = new QTimer(context);
|
||||||
|
guard->setSingleShot(true);
|
||||||
|
guard->setInterval(FroniusRules::DefaultTimeoutMilliSeconds * (FroniusRules::DefaultNumberOfRetries + 1));
|
||||||
|
connect(guard, &QTimer::timeout, context, [proceed]() { proceed(true); });
|
||||||
|
guard->start();
|
||||||
|
|
||||||
|
storage->readBlockData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write, read back, compare. An inverter may accept a write and then ignore it
|
||||||
|
* without raising a Modbus exception - control not enabled on the device, grid
|
||||||
|
* charging not allowed, an IP restriction, or another controller overwriting
|
||||||
|
* the setpoints. An unverified setpoint is an unapplied setpoint. */
|
||||||
|
void IntegrationPluginSunSpec::verifySetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan)
|
||||||
|
{
|
||||||
|
bool healthy = true;
|
||||||
|
|
||||||
|
foreach (const SunSpecStorageLogic::Write &write, plan.writes) {
|
||||||
|
if (!plan.verifyReadback.contains(write.reg))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const double readBack = readSetpointRegister(storage, write.reg);
|
||||||
|
|
||||||
|
/* The rates carry a scale factor of -2, so anything within a hundredth
|
||||||
|
* is the same value. The enums and the bitfield compare exactly. */
|
||||||
|
const double tolerance = SunSpecStorageLogic::isScaledRate(write.reg) ? 0.011 : 0.0001;
|
||||||
|
if (qAbs(readBack - write.value) > tolerance) {
|
||||||
|
qCWarning(dcSunSpec()) << "Setpoint not applied on" << info->thing() << ":"
|
||||||
|
<< SunSpecStorageLogic::registerName(write.reg)
|
||||||
|
<< "was written as" << write.value << "but reads back as" << readBack;
|
||||||
|
healthy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info->thing()->setStateValue(sunspecStorageControlHealthyStateTypeId, healthy);
|
||||||
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
}
|
||||||
|
|
||||||
void IntegrationPluginSunSpec::executeSetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan)
|
void IntegrationPluginSunSpec::executeSetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan)
|
||||||
{
|
{
|
||||||
executeSetpointWrite(info, storage, plan, 0);
|
if (!plan.rereadScaleFactor) {
|
||||||
|
executeSetpointWrite(info, storage, plan, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scale factors are not guaranteed static: they may change with firmware
|
||||||
|
* and at runtime. A factor cached during an earlier poll can put the
|
||||||
|
* setpoint an order of magnitude off, so refresh it right before writing. */
|
||||||
|
refreshStorageBlock(info, storage, [this, info, storage, plan]() {
|
||||||
|
executeSetpointWrite(info, storage, plan, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Thing *IntegrationPluginSunSpec::getThingForSunSpecModel(uint modelId, uint modbusAddress, const ThingId &parentId)
|
Thing *IntegrationPluginSunSpec::getThingForSunSpecModel(uint modelId, uint modbusAddress, const ThingId &parentId)
|
||||||
@ -716,6 +866,9 @@ SunSpecConnection *IntegrationPluginSunSpec::createConnection(Thing *thing)
|
|||||||
// Note: for some reason, solar edge is using big endian register order instead
|
// Note: for some reason, solar edge is using big endian register order instead
|
||||||
// of little endian as specified in sunspec and even solar edge documentation
|
// of little endian as specified in sunspec and even solar edge documentation
|
||||||
connection = new SunSpecConnection(address, port, slaveId, SunSpecDataPoint::ByteOrderBigEndian, this);
|
connection = new SunSpecConnection(address, port, slaveId, SunSpecDataPoint::ByteOrderBigEndian, this);
|
||||||
|
} else if (thing->thingClassId() == froniusConnectionThingClassId) {
|
||||||
|
// No endianness parameter here: the byte order is imposed, not chosen.
|
||||||
|
connection = new SunSpecConnection(address, port, slaveId, FroniusRules::byteOrder(), this);
|
||||||
} else {
|
} else {
|
||||||
SunSpecDataPoint::ByteOrder endianness = SunSpecDataPoint::ByteOrderLittleEndian;
|
SunSpecDataPoint::ByteOrder endianness = SunSpecDataPoint::ByteOrderLittleEndian;
|
||||||
QString endiannessParam = thing->paramValue("endianness").toString();
|
QString endiannessParam = thing->paramValue("endianness").toString();
|
||||||
@ -724,8 +877,14 @@ SunSpecConnection *IntegrationPluginSunSpec::createConnection(Thing *thing)
|
|||||||
|
|
||||||
connection = new SunSpecConnection(address, port, slaveId, endianness, this);
|
connection = new SunSpecConnection(address, port, slaveId, endianness, this);
|
||||||
}
|
}
|
||||||
connection->setTimeout(configValue(sunSpecPluginTimeoutParamTypeId).toUInt());
|
|
||||||
connection->setNumberOfRetries(configValue(sunSpecPluginNumberOfRetriesParamTypeId).toUInt());
|
if (thing->thingClassId() == froniusConnectionThingClassId) {
|
||||||
|
connection->setTimeout(FroniusRules::DefaultTimeoutMilliSeconds);
|
||||||
|
connection->setNumberOfRetries(FroniusRules::DefaultNumberOfRetries);
|
||||||
|
} else {
|
||||||
|
connection->setTimeout(configValue(sunSpecPluginTimeoutParamTypeId).toUInt());
|
||||||
|
connection->setNumberOfRetries(configValue(sunSpecPluginNumberOfRetriesParamTypeId).toUInt());
|
||||||
|
}
|
||||||
|
|
||||||
// Reconnect on monitor reachable changed
|
// Reconnect on monitor reachable changed
|
||||||
NetworkDeviceMonitor *monitor = m_monitors.value(thing);
|
NetworkDeviceMonitor *monitor = m_monitors.value(thing);
|
||||||
@ -979,6 +1138,15 @@ void IntegrationPluginSunSpec::autocreateSunSpecModelThing(const ThingClassId &t
|
|||||||
params.append(Param(m_manufacturerParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().manufacturerName));
|
params.append(Param(m_manufacturerParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().manufacturerName));
|
||||||
params.append(Param(m_deviceModelParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().modelName));
|
params.append(Param(m_deviceModelParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().modelName));
|
||||||
params.append(Param(m_serialNumberParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().serialNumber));
|
params.append(Param(m_serialNumberParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().serialNumber));
|
||||||
|
|
||||||
|
/* A storage created under a Fronius connection arms the revert timer by
|
||||||
|
* default, where the generic vendor leaves it disabled. A setpoint left
|
||||||
|
* behind by a stopped HEMS would otherwise hold the customer's battery
|
||||||
|
* indefinitely. */
|
||||||
|
if (thingClassId == sunspecStorageThingClassId && isFroniusChild(parentId)) {
|
||||||
|
params.append(Param(sunspecStorageThingRevertTimeParamTypeId, FroniusRules::DefaultRevertTimeSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
descriptor.setParams(params);
|
descriptor.setParams(params);
|
||||||
emit autoThingsAppeared({descriptor});
|
emit autoThingsAppeared({descriptor});
|
||||||
}
|
}
|
||||||
@ -1236,7 +1404,7 @@ void IntegrationPluginSunSpec::onPluginConfigurationChanged(const ParamTypeId &p
|
|||||||
if (paramTypeId == sunSpecPluginUpdateIntervalParamTypeId) {
|
if (paramTypeId == sunSpecPluginUpdateIntervalParamTypeId) {
|
||||||
qCDebug(dcSunSpec()) << "Update interval has changed" << value.toInt();
|
qCDebug(dcSunSpec()) << "Update interval has changed" << value.toInt();
|
||||||
if (m_refreshTimer) {
|
if (m_refreshTimer) {
|
||||||
int refreshTime = value.toInt();
|
int refreshTime = effectiveUpdateInterval();
|
||||||
m_refreshTimer->stop();
|
m_refreshTimer->stop();
|
||||||
hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
|
hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
|
||||||
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime);
|
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime);
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
#include "sunspecthing.h"
|
#include "sunspecthing.h"
|
||||||
#include "sunspecstoragelogic.h"
|
#include "sunspecstoragelogic.h"
|
||||||
|
#include "froniusrules.h"
|
||||||
#include "extern-plugininfo.h"
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
@ -109,10 +110,17 @@ private:
|
|||||||
bool hasManufacturer(const QStringList &manufacturers, const QString &manufacturer);
|
bool hasManufacturer(const QStringList &manufacturers, const QString &manufacturer);
|
||||||
void markThingStatesDisconnected(Thing *thing);
|
void markThingStatesDisconnected(Thing *thing);
|
||||||
|
|
||||||
|
// Fronius
|
||||||
|
bool isFroniusChild(const ThingId &parentId);
|
||||||
|
int effectiveUpdateInterval();
|
||||||
|
|
||||||
// Storage setpoints
|
// Storage setpoints
|
||||||
void executeSetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan);
|
void executeSetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan);
|
||||||
void executeSetpointWrite(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan, int index);
|
void executeSetpointWrite(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan, int index);
|
||||||
QModbusReply *writeSetpointRegister(SunSpecStorageModel *storage, const SunSpecStorageLogic::Write &write);
|
QModbusReply *writeSetpointRegister(SunSpecStorageModel *storage, const SunSpecStorageLogic::Write &write);
|
||||||
|
double readSetpointRegister(SunSpecStorageModel *storage, SunSpecStorageLogic::Register reg);
|
||||||
|
void refreshStorageBlock(ThingActionInfo *info, SunSpecStorageModel *storage, const std::function<void()> &continuation);
|
||||||
|
void verifySetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onRefreshTimer();
|
void onRefreshTimer();
|
||||||
|
|||||||
@ -1472,6 +1472,14 @@
|
|||||||
"writable": true,
|
"writable": true,
|
||||||
"displayNameAction": "Set discharging rate"
|
"displayNameAction": "Set discharging rate"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "ed738d23-4bea-40e9-a41e-ce6f37f1a3e2",
|
||||||
|
"name": "controlHealthy",
|
||||||
|
"displayName": "Control healthy",
|
||||||
|
"displayNameEvent": "Control healthy changed",
|
||||||
|
"type": "bool",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "47035ac0-d054-491a-969d-b2aaa9168b0a",
|
"id": "47035ac0-d054-491a-969d-b2aaa9168b0a",
|
||||||
"name": "chargeGradient",
|
"name": "chargeGradient",
|
||||||
@ -1751,6 +1759,91 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fronius",
|
||||||
|
"displayName": "Fronius",
|
||||||
|
"id": "8504de60-2625-4ad4-8889-623a8d0453ff",
|
||||||
|
"thingClasses": [
|
||||||
|
{
|
||||||
|
"name": "froniusConnection",
|
||||||
|
"displayName": "Fronius",
|
||||||
|
"id": "a4312ee8-ed15-4378-85e2-0372cef7de03",
|
||||||
|
"createMethods": [ "discovery", "user" ],
|
||||||
|
"interfaces": [ "gateway", "networkdevice" ],
|
||||||
|
"providedInterfaces": [ "solarinverter", "energymeter", "energystorage" ],
|
||||||
|
"discoveryParamTypes": [
|
||||||
|
{
|
||||||
|
"id": "c1d4df93-cc65-4766-a1b3-7c26edf986c0",
|
||||||
|
"name":"slaveId",
|
||||||
|
"displayName": "Slave ID",
|
||||||
|
"type": "int",
|
||||||
|
"defaultValue": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paramTypes": [
|
||||||
|
{
|
||||||
|
"id": "bc8c1825-000b-401f-90c8-e70547041fca",
|
||||||
|
"name": "address",
|
||||||
|
"displayName": "Host address",
|
||||||
|
"type": "QString",
|
||||||
|
"inputType": "IPv4Address",
|
||||||
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5882b8a7-2c83-4601-9dd4-e4cc5b5baed7",
|
||||||
|
"name": "hostName",
|
||||||
|
"displayName": "Host name",
|
||||||
|
"type": "QString",
|
||||||
|
"inputType": "TextLine",
|
||||||
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9f16596f-1695-4112-bdca-df7207b4e30b",
|
||||||
|
"name":"macAddress",
|
||||||
|
"displayName": "MAC address",
|
||||||
|
"type": "QString",
|
||||||
|
"inputType": "MacAddress",
|
||||||
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "70ea61e2-18c3-4f82-a66c-6ed6876d3567",
|
||||||
|
"name":"port",
|
||||||
|
"displayName": "Port",
|
||||||
|
"type": "int",
|
||||||
|
"defaultValue": 502
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e8876609-cfad-4ebd-b998-d0b1b2dc5434",
|
||||||
|
"name":"slaveId",
|
||||||
|
"displayName": "Slave ID",
|
||||||
|
"type": "int",
|
||||||
|
"readOnly": true,
|
||||||
|
"defaultValue": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ad2ad84f-8513-4337-b7cf-fa45a9914a15",
|
||||||
|
"name":"meterSlaveId",
|
||||||
|
"displayName": "Smart Meter slave ID",
|
||||||
|
"type": "int",
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 247,
|
||||||
|
"defaultValue": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateTypes":[
|
||||||
|
{
|
||||||
|
"id": "d4cf07e1-6b53-47b6-9cab-7c7764023453",
|
||||||
|
"name": "connected",
|
||||||
|
"displayName": "Connected",
|
||||||
|
"displayNameEvent": "Connected changed",
|
||||||
|
"type": "bool",
|
||||||
|
"defaultValue": false,
|
||||||
|
"cached": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ include(../plugins.pri)
|
|||||||
include(../sunspec.pri)
|
include(../sunspec.pri)
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
|
froniusrules.cpp \
|
||||||
integrationpluginsunspec.cpp \
|
integrationpluginsunspec.cpp \
|
||||||
solaredgebattery.cpp \
|
solaredgebattery.cpp \
|
||||||
sunspecdiscovery.cpp \
|
sunspecdiscovery.cpp \
|
||||||
@ -9,6 +10,7 @@ SOURCES += \
|
|||||||
sunspecthing.cpp
|
sunspecthing.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
froniusrules.h \
|
||||||
integrationpluginsunspec.h \
|
integrationpluginsunspec.h \
|
||||||
solaredgebattery.h \
|
solaredgebattery.h \
|
||||||
sunspecdiscovery.h \
|
sunspecdiscovery.h \
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user