// 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 .
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef FRONIUSRULES_H
#define FRONIUSRULES_H
#include
#include
#include
#include
#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;
const int MaximumSlaveId = 247;
/* Whether a second SunSpecConnection should be opened for the Smart Meter, and
* why not when it should not. The caller logs the reason; keeping it out of
* this function is what makes the decision testable without nymea. */
enum MeterConnectionDecision {
MeterConnectionCreate,
/* 0 is the documented way of saying "no meter behind this inverter". */
MeterConnectionDisabled,
/* A second socket on the unit ID already served by the inverter would
* rediscover the very same models and duplicate every child thing. */
MeterConnectionCollidesWithInverter,
MeterConnectionInvalidSlaveId
};
MeterConnectionDecision decideMeterConnection(int meterSlaveId, int inverterSlaveId);
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 &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 &readings);
}
#endif // FRONIUSRULES_H