sunspec: arm the storage revert timer after every setpoint
setInOutWRteRvrtTms() existed but was never called. InOutWRte_RvrtTms is the delay after which the inverter drops the setpoint and resumes its native behaviour. Left at 0, a setpoint stays active indefinitely, HEMS stopped, network lost or process crashed included - the battery stays where the last command put it. Add a per-thing revertTime parameter, 0..28800 s, defaulting to 0 so the generic vendor keeps its current behaviour, and route every setpoint through SunSpecStorageLogic::planSetpoint(). The plan orders the writes - ChaGriSet, StorCtl_Mod, then the rates - and appends the revert timer last, so the timer never starts counting against a setpoint that has not been written yet. Writes are issued sequentially rather than in parallel: the order is part of the contract with the hardware.
This commit is contained in:
parent
4ec7aa50e8
commit
c5c3f46f57
@ -469,97 +469,101 @@ void IntegrationPluginSunSpec::executeAction(ThingActionInfo *info)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Every setpoint goes through a plan rather than a lone write. The plan
|
||||
* puts the registers in the order the hardware expects and closes the
|
||||
* sequence with the revert timer, so that a setpoint left behind by a
|
||||
* stopped HEMS expires instead of holding the battery forever. */
|
||||
QVector<SunSpecStorageLogic::Write> setpoints;
|
||||
|
||||
if (action.actionTypeId() == sunspecStorageGridChargingActionTypeId) {
|
||||
bool gridCharging = action.param(sunspecStorageGridChargingActionGridChargingParamTypeId).value().toBool();
|
||||
QModbusReply *reply = storage->setChaGriSet(gridCharging ? SunSpecStorageModel::ChagrisetGrid : SunSpecStorageModel::ChagrisetPv);
|
||||
if (!reply) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
|
||||
connect(reply, &QModbusReply::finished, info, [info, reply]{
|
||||
if (reply->error() != QModbusDevice::NoError) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
setpoints.append({SunSpecStorageLogic::RegisterChaGriSet,
|
||||
gridCharging ? double(SunSpecStorageModel::ChagrisetGrid) : double(SunSpecStorageModel::ChagrisetPv)});
|
||||
} else if (action.actionTypeId() == sunspecStorageEnableChargingActionTypeId) {
|
||||
SunSpecStorageModel::Storctl_modFlags controlModeFlags = storage->storCtlMod();
|
||||
bool enabled = action.param(sunspecStorageEnableChargingActionEnableChargingParamTypeId).value().toBool();
|
||||
controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modCharge, enabled);
|
||||
QModbusReply *reply = storage->setStorCtlMod(controlModeFlags);
|
||||
if (!reply) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
|
||||
connect(reply, &QModbusReply::finished, info, [info, reply, enabled]{
|
||||
if (reply->error() != QModbusDevice::NoError) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
info->thing()->setStateValue(sunspecStorageEnableChargingStateTypeId, enabled);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
setpoints.append({SunSpecStorageLogic::RegisterStorCtlMod, double(int(controlModeFlags))});
|
||||
} else if (action.actionTypeId() == sunspecStorageEnableDischargingActionTypeId) {
|
||||
SunSpecStorageModel::Storctl_modFlags controlModeFlags = storage->storCtlMod();
|
||||
bool enabled = action.param(sunspecStorageEnableDischargingActionEnableDischargingParamTypeId).value().toBool();
|
||||
controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modDiScharge, enabled);
|
||||
QModbusReply *reply = storage->setStorCtlMod(controlModeFlags);
|
||||
if (!reply) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
|
||||
connect(reply, &QModbusReply::finished, info, [info, reply, enabled]{
|
||||
if (reply->error() != QModbusDevice::NoError) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
info->thing()->setStateValue(sunspecStorageEnableDischargingStateTypeId, enabled);
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
setpoints.append({SunSpecStorageLogic::RegisterStorCtlMod, double(int(controlModeFlags))});
|
||||
} else if (action.actionTypeId() == sunspecStorageChargingRateActionTypeId) {
|
||||
QModbusReply *reply = storage->setInWRte(action.param(sunspecStorageChargingRateActionChargingRateParamTypeId).value().toInt());
|
||||
if (!reply) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
|
||||
connect(reply, &QModbusReply::finished, info, [info, reply]{
|
||||
if (reply->error() != QModbusDevice::NoError) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
setpoints.append({SunSpecStorageLogic::RegisterInWRte,
|
||||
action.param(sunspecStorageChargingRateActionChargingRateParamTypeId).value().toDouble()});
|
||||
} else if (action.actionTypeId() == sunspecStorageDischargingRateActionTypeId) {
|
||||
QModbusReply *reply = storage->setOutWRte(action.param(sunspecStorageDischargingRateActionDischargingRateParamTypeId).value().toInt());
|
||||
if (!reply) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
|
||||
connect(reply, &QModbusReply::finished, info, [info, reply]{
|
||||
if (reply->error() != QModbusDevice::NoError) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
setpoints.append({SunSpecStorageLogic::RegisterOutWRte,
|
||||
action.param(sunspecStorageDischargingRateActionDischargingRateParamTypeId).value().toDouble()});
|
||||
} else {
|
||||
Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8());
|
||||
info->finish(Thing::ThingErrorActionTypeNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint revertTime = thing->paramValue(sunspecStorageThingRevertTimeParamTypeId).toUInt();
|
||||
executeSetpointPlan(info, storage, SunSpecStorageLogic::planSetpoint(setpoints, revertTime));
|
||||
|
||||
} else {
|
||||
Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(info->thing()->thingClassId().toString()).toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
QModbusReply *IntegrationPluginSunSpec::writeSetpointRegister(SunSpecStorageModel *storage, const SunSpecStorageLogic::Write &write)
|
||||
{
|
||||
switch (write.reg) {
|
||||
case SunSpecStorageLogic::RegisterChaGriSet:
|
||||
return storage->setChaGriSet(write.value != 0 ? SunSpecStorageModel::ChagrisetGrid
|
||||
: SunSpecStorageModel::ChagrisetPv);
|
||||
case SunSpecStorageLogic::RegisterStorCtlMod:
|
||||
return storage->setStorCtlMod(SunSpecStorageModel::Storctl_modFlags(int(write.value)));
|
||||
case SunSpecStorageLogic::RegisterInWRte:
|
||||
return storage->setInWRte(write.value);
|
||||
case SunSpecStorageLogic::RegisterOutWRte:
|
||||
return storage->setOutWRte(write.value);
|
||||
case SunSpecStorageLogic::RegisterRvrtTms:
|
||||
return storage->setInOutWRteRvrtTms(static_cast<quint16>(write.value));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* The writes are issued one after another rather than all at once: the order is
|
||||
* part of the contract with the hardware, and several inverters ask for
|
||||
* sequential requests. */
|
||||
void IntegrationPluginSunSpec::executeSetpointWrite(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan, int index)
|
||||
{
|
||||
if (index >= plan.writes.count()) {
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
return;
|
||||
}
|
||||
|
||||
const SunSpecStorageLogic::Write write = plan.writes.at(index);
|
||||
|
||||
QModbusReply *reply = writeSetpointRegister(storage, write);
|
||||
if (!reply) {
|
||||
qCWarning(dcSunSpec()) << "Could not write" << SunSpecStorageLogic::registerName(write.reg) << "on" << info->thing();
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
|
||||
connect(reply, &QModbusReply::finished, info, [this, info, storage, plan, index, write, reply] {
|
||||
if (reply->error() != QModbusDevice::NoError) {
|
||||
qCWarning(dcSunSpec()) << "Error writing" << SunSpecStorageLogic::registerName(write.reg)
|
||||
<< "on" << info->thing() << reply->errorString();
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
executeSetpointWrite(info, storage, plan, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
void IntegrationPluginSunSpec::executeSetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan)
|
||||
{
|
||||
executeSetpointWrite(info, storage, plan, 0);
|
||||
}
|
||||
|
||||
Thing *IntegrationPluginSunSpec::getThingForSunSpecModel(uint modelId, uint modbusAddress, const ThingId &parentId)
|
||||
{
|
||||
foreach (Thing *thing, myThings()) {
|
||||
|
||||
@ -109,6 +109,11 @@ private:
|
||||
bool hasManufacturer(const QStringList &manufacturers, const QString &manufacturer);
|
||||
void markThingStatesDisconnected(Thing *thing);
|
||||
|
||||
// Storage setpoints
|
||||
void executeSetpointPlan(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan);
|
||||
void executeSetpointWrite(ThingActionInfo *info, SunSpecStorageModel *storage, const SunSpecStorageLogic::SetpointPlan &plan, int index);
|
||||
QModbusReply *writeSetpointRegister(SunSpecStorageModel *storage, const SunSpecStorageLogic::Write &write);
|
||||
|
||||
private slots:
|
||||
void onRefreshTimer();
|
||||
void onPluginConfigurationChanged(const ParamTypeId ¶mTypeId, const QVariant &value);
|
||||
|
||||
@ -1349,6 +1349,16 @@
|
||||
"displayName": "Serial number",
|
||||
"type": "QString",
|
||||
"defaultValue": "Unkown"
|
||||
},
|
||||
{
|
||||
"id": "74d530d1-6fd3-4afa-8acd-1e91041f88b2",
|
||||
"name":"revertTime",
|
||||
"displayName": "Setpoint revert time",
|
||||
"type": "uint",
|
||||
"unit": "Seconds",
|
||||
"minValue": 0,
|
||||
"maxValue": 28800,
|
||||
"defaultValue": 0
|
||||
}
|
||||
],
|
||||
"stateTypes":[
|
||||
|
||||
@ -56,4 +56,71 @@ StorageStates readStates(SunSpecStorageModel *storage)
|
||||
return states;
|
||||
}
|
||||
|
||||
bool isScaledRate(Register reg)
|
||||
{
|
||||
return reg == RegisterInWRte || reg == RegisterOutWRte;
|
||||
}
|
||||
|
||||
QString registerName(Register reg)
|
||||
{
|
||||
switch (reg) {
|
||||
case RegisterChaGriSet: return QStringLiteral("ChaGriSet");
|
||||
case RegisterStorCtlMod: return QStringLiteral("StorCtl_Mod");
|
||||
case RegisterInWRte: return QStringLiteral("InWRte");
|
||||
case RegisterOutWRte: return QStringLiteral("OutWRte");
|
||||
case RegisterRvrtTms: return QStringLiteral("InOutWRte_RvrtTms");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
/* Rank within the write order: ChaGriSet, then StorCtl_Mod, then the rates,
|
||||
* then the revert timer. Arming the timer first would start it counting against
|
||||
* a setpoint that has not been written yet. */
|
||||
static int writeOrderRank(Register reg)
|
||||
{
|
||||
switch (reg) {
|
||||
case RegisterChaGriSet: return 0;
|
||||
case RegisterStorCtlMod: return 1;
|
||||
case RegisterInWRte:
|
||||
case RegisterOutWRte: return 2;
|
||||
case RegisterRvrtTms: return 3;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
SetpointPlan planSetpoint(const QVector<Write> &setpoints, quint16 revertTime)
|
||||
{
|
||||
SetpointPlan plan;
|
||||
|
||||
QVector<Write> writes;
|
||||
foreach (const Write &write, setpoints) {
|
||||
/* The revert time is appended below, once, in last position. A caller
|
||||
* passing it explicitly must not end up with two of them. */
|
||||
if (write.reg == RegisterRvrtTms)
|
||||
continue;
|
||||
|
||||
if (isScaledRate(write.reg))
|
||||
plan.rereadScaleFactor = true;
|
||||
|
||||
writes.append(write);
|
||||
}
|
||||
|
||||
std::stable_sort(writes.begin(), writes.end(), [](const Write &left, const Write &right) {
|
||||
return writeOrderRank(left.reg) < writeOrderRank(right.reg);
|
||||
});
|
||||
|
||||
/* Every register that carries the intent is read back afterwards. The
|
||||
* revert timer is excluded: it is a countdown, so its read-back value is
|
||||
* not expected to match what was written. */
|
||||
foreach (const Write &write, writes) {
|
||||
if (!plan.verifyReadback.contains(write.reg))
|
||||
plan.verifyReadback.append(write.reg);
|
||||
}
|
||||
|
||||
plan.writes = writes;
|
||||
plan.writes.append({RegisterRvrtTms, static_cast<double>(revertTime)});
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -31,12 +31,47 @@
|
||||
/* Decision logic of the storage (model 124) handling, kept free of the
|
||||
* integration framework so it can be exercised without a running nymea.
|
||||
*
|
||||
* readStates() maps a storage block update onto the state values the plugin
|
||||
* publishes. Having it in one place is what makes it possible to check that a
|
||||
* state is fed from the very register the matching action writes to.
|
||||
* Two things live here:
|
||||
*
|
||||
* - readStates() maps a storage block update onto the state values the plugin
|
||||
* publishes. Having it in one place is what makes it possible to check that
|
||||
* a state is fed from the very register the matching action writes to;
|
||||
* - planSetpoint() turns a set of setpoints into an ordered sequence of
|
||||
* register writes. Order matters on real hardware, and the revert timer
|
||||
* must close every sequence.
|
||||
*/
|
||||
namespace SunSpecStorageLogic {
|
||||
|
||||
/* The registers of model 124 this plugin ever writes. */
|
||||
enum Register {
|
||||
RegisterChaGriSet,
|
||||
RegisterStorCtlMod,
|
||||
RegisterInWRte,
|
||||
RegisterOutWRte,
|
||||
RegisterRvrtTms
|
||||
};
|
||||
|
||||
struct Write {
|
||||
Register reg;
|
||||
double value;
|
||||
};
|
||||
|
||||
struct SetpointPlan {
|
||||
/* Scale factors are not necessarily static: they may change with firmware
|
||||
* and at runtime. Whenever a scaled quantity is written, its scale factor
|
||||
* should be read again immediately before the write rather than reused
|
||||
* from an earlier poll. */
|
||||
bool rereadScaleFactor = false;
|
||||
|
||||
/* Ordered: ChaGriSet, StorCtl_Mod, InWRte/OutWRte, RvrtTms last. */
|
||||
QVector<Write> writes;
|
||||
|
||||
/* Registers to read back and compare after the sequence. Several inverters
|
||||
* accept a write and then ignore it without raising a Modbus exception, so
|
||||
* an unverified setpoint is an unapplied setpoint. */
|
||||
QVector<Register> verifyReadback;
|
||||
};
|
||||
|
||||
struct StorageStates {
|
||||
/* Percent of WChaMax, from InWRte / OutWRte - the very registers the
|
||||
* chargingRate / dischargingRate actions write to. */
|
||||
@ -63,8 +98,19 @@ struct StorageStates {
|
||||
const double MinimumRatePercent = -100.0;
|
||||
const double MaximumRatePercent = 100.0;
|
||||
|
||||
/* Range of InOutWRte_RvrtTms. */
|
||||
const int MaximumRevertTimeSeconds = 28800;
|
||||
|
||||
StorageStates readStates(SunSpecStorageModel *storage);
|
||||
|
||||
/* True for the registers carried by InOutWRte_SF, i.e. those whose write should
|
||||
* be preceded by a fresh read of that scale factor. */
|
||||
bool isScaledRate(Register reg);
|
||||
|
||||
QString registerName(Register reg);
|
||||
|
||||
SetpointPlan planSetpoint(const QVector<Write> &setpoints, quint16 revertTime);
|
||||
|
||||
}
|
||||
|
||||
#endif // SUNSPECSTORAGELOGIC_H
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user