Patrick Schurig 4f2076f504 sunspec: decide whether a Fronius meter connection is wanted
A GEN24 answers for the inverter on one unit ID and for the Smart Meter
on another, behind the same host and port. Isolate the decision to open
that second connection as a pure function, so it can be pinned down
without nymea, and leave the logging of the refusal to the caller.

Refuses a meter slave ID out of the 0..247 range, treats 0 as "no meter
on this connection" as documented, and refuses a meter slave ID equal to
the inverter one: a second socket on the same unit ID would rediscover
the very same models and duplicate every child thing. That last case is
reachable, since the discovery writes the scanned slave ID into the
thing parameter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 08:50:25 +02:00

143 lines
5.3 KiB
C++

// 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;
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 &registerName);
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