// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* 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 .
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "sunspecstoragelogic.h"
#include
namespace SunSpecStorageLogic {
StorageStates readStates(SunSpecStorageModel *storage)
{
StorageStates states;
if (!storage)
return states;
/* InWRte / OutWRte are what the chargingRate / dischargingRate actions
* write. Reading the gradients here instead would mean the user writes one
* quantity and reads back another. */
states.chargingRate = storage->inWRte();
states.dischargingRate = storage->outWRte();
/* The gradients stay available, under names that cannot be mistaken for the
* setpoints above. */
states.chargeGradient = storage->wChaGra();
states.dischargeGradient = storage->wDisChaGra();
const SunSpecStorageModel::Storctl_modFlags controlMode = storage->storCtlMod();
states.enableCharging = controlMode.testFlag(SunSpecStorageModel::Storctl_modCharge);
states.enableDischarging = controlMode.testFlag(SunSpecStorageModel::Storctl_modDiScharge);
states.gridCharging = (storage->chaGriSet() == SunSpecStorageModel::ChagrisetGrid);
states.batteryLevel = qRound(storage->chaState());
states.batteryCritical = storage->chaState() < 5;
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 &setpoints, quint16 revertTime)
{
SetpointPlan plan;
QVector 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(revertTime)});
return plan;
}
}