78 Commits

Author SHA1 Message Date
Patrick Schurig
72965d0dbf Merge fix/fronius-meter-sign-and-battery-presence into landing-silo
Monte le support Fronius GEN24 vers la ligne testing : signe du compteur,
batterie fantôme, et ordre des mots des float32. Les trois sont liés — le
signe n'est observable qu'une fois l'ordre des mots correct (D-8) — donc ils
montent ensemble. nymea-plugin-sunspec-etm01 passe de 1.15.2+etm6 à +etm9.

Ce que cette fusion emporte, au delà des trois correctifs : la branche
descend d'experimental-silo, donc elle amène aussi toute la ligne SunSpec qui
n'était pas encore dans landing-silo (vendoring du plugin, vendor Fronius,
seconde connexion pour le Smart Meter, chaîne de consignes storage) ainsi que
trois commits abbterra (TCP, SEGV de découverte, checkReachableRegister) et
les commits de documentation du modèle de branches. Aucun binaire abbterra
n'est publié par cette montée : seul le .deb sunspec-etm01 part vers testing.

waveshare-relay-d8 reste supprimé. 045d7c6 avertissait qu'une descente
pourrait le ressusciter ; ce n'est pas le cas ici, la branche fusionnée ne
touche pas ce répertoire, donc git honore la suppression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 08:20:22 +02:00
Patrick Schurig
de55c9fb39 sunspec: rendre le verdict de read-float-order.py indépendant de la version
Le message affirmait « le plugin lit de travers », vrai avant 1.15.2+etm9 et
faux depuis. L'outil sert à mesurer un APPAREIL, pas à juger une version : il
dit maintenant ce que lirait un lecteur gardant le défaut de la bibliothèque,
et rappelle depuis quelle version le vendor Fronius force big endian.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:44:10 +02:00
Patrick Schurig
b6740cff93 debian: changelog entry 1.15.2+etm9
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:30:34 +02:00
Patrick Schurig
86fb244b9d fix(sunspec): lire les float32 Fronius en big endian, comme le matériel
byteOrder() renvoyait ByteOrderLittleEndian au motif qu'on « supposait le
défaut SunSpec ». La supposition confondait le défaut de la SPÉCIFICATION avec
celui de libnymea-sunspec, qui est son contraire : convertToUInt32 lit
registers.at(1) puis .at(0) en little endian. Garder ce défaut revient à lire
tous les float32 avec leurs deux mots permutés.

Mesuré le 2026-08-07 sur un Symo réel, en lecture seule : le compteur 213 sur
l'unité 200 sert Hz = 4248 0000, soit 50,0000 en big endian et 2,3777e-41 en
mots inversés. L'onduleur 113 concorde.

Le coût n'était pas un décalage mais une absurdité. Reproduit sur banc avec le
simulateur émettant l'ordre du matériel : un compteur important 2,7 kW rapporté
comme exportant 107 MW, et l'arbitre enclenchant toutes les charges sur un
« Surplus PV 107622496 W ». Le thing reste connected, sans erreur Modbus.

Conséquence sur l'ordre des correctifs : meterPowerSign() reste juste mais
n'est observable qu'une fois celui-ci appliqué — corriger le signe d'un
dénormal ne produit rien.

solarEdge force déjà big endian de son côté et le générique garde son paramètre
utilisateur : aucun des deux chemins n'est touché. test_AC_18 fige le
raisonnement plutôt que la seule valeur. Voir DIVERGENCES.md D-8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:30:14 +02:00
Patrick Schurig
a4bf8aace3 debian: changelog entry 1.15.2+etm8
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 20:05:25 +02:00
Patrick Schurig
abf4c168bf fix(sunspec): interroger le thing de connexion, pas une recherche par id
Le garde de présence batterie demandait isFroniusChild(thing->id()), qui
résout le parent par myThings().findById(). Pendant le setup initial la
connexion n'est pas encore enregistrée : la recherche répond faux, le garde
est contourné, et l'enfant storage est créé alors qu'InBatV et StorAval sont
nuls. Constaté en boucle fermée sur le simulateur, avec la trace nymead
"Invalid parent thing id ... Not adding thing to the system".

Le garde interroge maintenant directement le thing de connexion que la
fonction tient déjà. Le signe compteur passe de même par le parentThing déjà
résolu, dont la non-nullité est garantie quelques lignes plus haut.

La suite logique ne pouvait pas attraper ce défaut : elle couvre la décision
pure, pas le câblage. Consigné en D-7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 20:05:13 +02:00
Patrick Schurig
6c4c5c2bfe sunspec: antidote triphasé et outil de relevé du modèle 124
L'antidote AC-16 triphasé verrouille le chemin réellement emprunté par le
matériel terrain, reproduit sur le simulateur en profil Symo (onduleur
modèle 103, compteur modèle 203) : compteur à -500/-100/-100 W, que le
plugin etm6 rapportait en +500/+100/+100.

tools/read-model124.py relève en lecture seule le bloc storage d'un
onduleur, sans aucune dépendance à installer. Il sert à lever D-6 : le côté
« batterie présente » du discriminateur reste inféré faute d'un GEN24 équipé.
Sur le simulateur il montre pourquoi ni le SoC ni les consignes ne peuvent
servir de discriminateur : le bloc annonce un SoC de 50 %, un statut Holding
et WChaMax à 10 kW alors qu'InBatV et StorAval sont nuls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:06:08 +02:00
Patrick Schurig
935dcb07a1 debian: changelog entry 1.15.2+etm7 for the SunSpec meter sign and battery presence fixes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:57:36 +02:00
Patrick Schurig
115c42764c fix(sunspec): signe du compteur Fronius et batterie fantôme
Deux défauts constatés sur le GEN24 des beaux-parents (Primo GEN24 3.0,
Smart Meter TS 65A-3, sans batterie).

Signe du compteur. onMeterBlockUpdated() négociait le signe de tous les
modèles compteur sans distinction de fabricant. Sur Fronius la négation est
fausse : le Smart Meter émet déjà dans la convention nymea. L'onduleur étant
monophasé, il sert de référence de direction : le plugin rapportait la phase
portant l'onduleur en soutirage et les deux autres en injection, ce qui est
impossible sans source. L'arbitre en concluait « Surplus insuffisant
(-842 W) » et laissait chauffe-eau et PAC éteints en plein export.

La décision vit dans FroniusRules::meterPowerSign() et n'est annulée que sous
une froniusConnection. Les vendors generic et solarEdge sont inchangés, ce
qu'un antidote verrouille. Les compteurs d'énergie sont corrects et ne sont
pas permutés.

Batterie fantôme. Un GEN24 hybride publie le modèle 124 sans batterie câblée,
d'où un enfant storage bloqué sur batteryCritical.
FroniusRules::hasBattery() exige une grandeur physique non nulle (InBatV ou
StorAval), le SoC étant écarté car 0 % ne se distingue pas d'une batterie
vide. Le côté « batterie présente » reste INFÉRÉ : voir D-6, à valider sur un
GEN24 équipé avant tout déploiement.

Suite logique : 50 tests verts, dont six antidotes AC-16 et AC-17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 08:29:16 +02:00
Patrick Schurig
1594242ab3 debian: changelog entry 1.15.2+etm6 for the SunSpec plugin
Records the new nymea-plugin-sunspec-etm01 binary package, the Fronius
vendor it carries, the storage setpoint corrections and the added
Build-Depends. The previous entry described an unrelated abbterra fix,
so the package would have shipped under a version that never mentioned
it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 12:10:20 +02:00
Patrick Schurig
1bafa999d0 debian: package the SunSpec plugin as nymea-plugin-sunspec-etm01
The plugin was added to PLUGIN_DIRS by the vendoring commit but never
packaged, so dh_missing found its .so in debian/tmp with nowhere to go
and aborted the whole source package: no .deb at all came out of
experimental-silo, not even for the four unrelated plugins.

Declares the binary package and adds libnymea-sunspec-dev to
Build-Depends. Upstream does not need that dependency because it builds
libnymea-sunspec from the same source tree; this repository vendors only
the plugin and links against the installed library.

Named with the -etm01 fork suffix rather than the upstream name, which
our own repository already publishes in stable and testing as a mirror
of 1.15.2. Same file path and same pluginId, hence Conflicts, Replaces
and Provides against nymea-plugin-sunspec so the two can never be
installed side by side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:52:10 +02:00
Patrick Schurig
304cf7b279 Merge fix/drop-waveshare-from-stable into landing-silo
Keeps the nightly candidate free of a plugin that nothing builds and no
package ships. It remains in experimental-silo.

main is deliberately left untouched for now: it will lose the plugin
through the normal promotion, when that promotion is decided.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:08:32 +02:00
Patrick Schurig
045d7c6df1 Drop waveshare-relay-d8 from the stable line
The plugin is not ready to sit next to stable. It has been in the tree
since the initial commit but is built by nothing: it is absent from
PLUGIN_DIRS in the root project file and from debian/control, so no
package has ever shipped it.

It stays in experimental-silo, which is where unproven work belongs.
This branch is merged into landing-silo only, never down into
experimental-silo.

Note for whoever promotes next: a hotfix descending from main or
landing-silo into experimental-silo would carry this deletion with it and
remove the plugin there too. Re-add it from this commit's parent if that
happens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:08:22 +02:00
Patrick Schurig
739946cbfb build: add QMAKE_RPATHDIR for the sysroot cross-build
Building against a local sysroot rather than an installed
libnymea-sunspec-dev left the resulting binaries unable to find
libnymea-sunspec1 at run time: the sysroot is not in the dynamic
linker's cache, so -L alone is not enough and every test had to be
launched through LD_LIBRARY_PATH.

Only the NYMEA_SUNSPEC_PATH branch is touched. The package build resolves
the library through pkg-config and gets no rpath, as before.

Verified both ways: with NYMEA_SUNSPEC_PATH the binary carries a RUNPATH
to the sysroot, resolves libnymea-sunspec1 there and runs its 44 tests
with LD_LIBRARY_PATH unset; without it, no sunspec rpath is generated at
all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:01:19 +02:00
Patrick Schurig
05f1286efa Merge the Fronius work into experimental-silo
Where it should have landed in the first place. A change climbs the
chain: experimental-silo first, landing-silo once a nightly has proven
itself, main last. The previous merge put this straight into
landing-silo, one rung below stable, for work only ever verified against
the bench simulator and never on hardware.

Carries what landing-silo had wrongly received: the vendored SunSpec
plugin and the Fronius multi unit ID support, the three abbterra commits
the branch dragged in from the abbterra TCP line, and the two AGENTS.md
commits.

landing-silo goes back to main in the next step; nothing is lost, it all
lives here now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:48:06 +02:00
Patrick Schurig
e78eb09970 Merge feature/fronius-sunspec into landing-silo
First content of the promotion chain. Brings the vendored SunSpec plugin,
the Fronius vendor and its multi unit ID support: one froniusConnection
thing now carries two SunSpecConnection objects, inverter and Smart Meter
on separate unit IDs behind the same host and port.

Verified before merging: 60 offline tests green, plus 8 green against the
bench SunSpec simulator in froniusgen24-sunspec mode.

Note: the branch was cut from the abbterra TCP line, so this merge also
brings three abbterra commits that were never in main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:19:55 +02:00
Patrick Schurig
1165d0ea71 sunspec: fix a dangling capture in the device suite and widen it
The suite captured a stack local of initTestCase() by reference in two
lambdas that outlive it. Once the function returned they wrote into a
stack slot reused by later frames, corrupting the running test: a
QSignalSpy reported a phantom emission, reading its recorded arguments
segfaulted, and the count changed when unrelated locals were added. Both
observed failures came from that, not from the plugin. The two flags are
members now.

Covers the two remaining claims the offline suites cannot reach: a meter
slave ID of 0 opens no second connection against a real device, and
losing the meter connection leaves the parent connected and the inverter
data flowing, marking only the meter child disconnected.

That last test drives one explicit block read first. This bench replaces
the plugin timer with a null object, on purpose, so nothing would
otherwise ever mark a child connected and the cut would prove nothing.

Run against the bench simulator: 8 tests, 8 green. Each of the two
corrections was confirmed able to fail by reintroducing its pre-F3 form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:51:10 +02:00
Patrick Schurig
7baa7f94b7 sunspec: keep Qt Creator session files out of the repository
They carry absolute local paths and differ per workstation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:11:34 +02:00
Patrick Schurig
29d1aaed21 sunspec: pin the single connection vendors against regressions
The whole point of keeping the meter connection in a hash of its own was
that not a line of the generic and solarEdge vendors would change.
Checks it rather than claiming it: a stored solarEdge connection, whose
behaviour no suite covered until now, must still end up with exactly one
connection, as must a generic one.

Also records the connection a discovery came from in
processDiscoveryResult() rather than in the discoveryFinished handler.
The initial setup calls it from a second handler on the same signal, so
recording it there made the orphan sweep depend on the order Qt happens
to invoke the two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:10:48 +02:00
Patrick Schurig
3a8086f71a sunspec: add a device suite for the two connection behaviour
The two most certain defects of the multi unit ID work are invisible to
the offline suites, because they only appear once a discovery has
actually run: the orphan sweep deleting the children of the other
connection, and the aggregation of the connected state.

This suite mounts the plugin behind a real ThingManager against a real
device. It is deliberately not a testcase, so "make check" never runs
it, but it is still built so it cannot rot unnoticed. Without
SUNSPEC_TEST_HOST every test skips, and no address is written into the
repository: the address of a real installation is customer data.

NOT YET RUN against hardware. It needs the address of a GEN24 and, given
the polling rate guidance, a deliberate decision to point it at a
customer inverter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:07:06 +02:00
Patrick Schurig
68a8e40b0c sunspec: sweep orphan children against every connection at once
processDiscoveryResult() makes child things disappear when their model
is no longer announced. It used to confront them with the models of the
one connection that had just finished discovering, which is only half of
the picture on a thing carrying two: the inverter discovery would delete
the meter child, and the meter discovery the inverter and storage ones,
on every single reconnect.

The sweep now waits for every connection of the parent to have reported
a successful discovery, then confronts the children with the union of
their models. A grace period releases it if a connection never reports,
so a hung discovery cannot freeze the cleanup for good.

That grace period is inferred, not measured: it is set generously
because sweeping too late leaves a stale child showing as disconnected,
while sweeping too early deletes a thing of the user. Recorded as D-2 in
DIVERGENCES.md with what to measure to replace it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:02:35 +02:00
Patrick Schurig
8e87ef5f10 sunspec: open a second connection for the Fronius Smart Meter
A GEN24 answers for the inverter on unit ID 1 and for its Smart Meter on
unit ID 200 by default, behind the same host and port. One thing now
carries two SunSpecConnection objects, the second one driven by the
meterSlaveId parameter that until now no line of C++ ever read.

The meter connection is created during setup but left idle: it is only
brought up once the primary connection has finished discovering, so the
two never handshake at the same time towards the same inverter. An
absent meter never fails the pairing, since a GEN24 without a Smart
Meter is a legitimate installation and the parameter has a non zero
default the user did not choose.

thingRemoved() and the reconfigure path now dispose of both connections
through a single entry point. Their else if chain used to stop at the
primary one, which left the meter connection outliving its thing with an
open socket and signals pointing at a destroyed Thing.

Child things look their model up across every connection of their
parent. The two connections of a GEN24 never announce the same model ID,
so no discriminator has to be persisted.

Adds a test suite instantiating the plugin behind a real ThingManager
with two connections. It lives in its own binary because the F1 suite
checks global call counters that extra configured things would falsify.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:00:27 +02:00
Patrick Schurig
c70ea6aaf7 sunspec: let createConnection() serve a secondary connection
Takes the slave ID as an argument instead of reading it from the thing,
and takes whether it is the primary connection of that thing. Only the
primary one follows the network monitor, writes the connected state of
the thing and lands in m_sunSpecConnections; a secondary one lands in a
hash of its own.

Child things are updated per connection rather than per parent, resolved
from the model each child is bound to. With a single connection that is
every child of the parent, so nothing changes here yet.

No caller creates a secondary connection at this point: behaviour is
unchanged for all three vendors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 08:53:28 +02:00
Patrick Schurig
be0817f436 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
Patrick Schurig
5b0dc2731e sunspec: record why meterSlaveId cannot be marked advanced
AC-7 asks for the meterSlaveId parameter to be advanced. That property
does not exist on nymea 1.15.2 param types: the metadata verifier
rejects unknown fields, so adding it makes the whole plugin fail to
load rather than merely hiding a parameter.

Measured by loading the plugin JSON, with and without the flag, into a
PluginMetadata linked against the installed libnymea, rather than read
from an upstream tree that may differ from the package.

Also records the three other divergences the multi unit ID work runs
into, two of which predate it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 08:50:18 +02:00
Patrick Schurig
31f44c62ea sunspec: dispatch froniusConnection in setupThing()
setupThing() only knew about sunspecConnection and solarEdgeConnection, so a
froniusConnection thing fell into the final Q_ASSERT_X. In a release build the
assert is compiled out, setupThing() returns without calling info->finish(),
and the pairing hangs until the setup times out - with no message at all. In a
debug build nymead dies on the assert instead.

Take the same branch as the two vendors that already work: createConnection()
already reads the slave ID and picks the byte order for froniusConnection, so
no new logic is needed.

postSetupThing() and thingRemoved() are unchanged on purpose. The former only
special cases solarEdgeConnection, for the battery search a Fronius must not
run, and registers the refresh timer for every thing. The latter is keyed on
m_sunSpecConnections, which createConnection() now populates for Fronius as
well.

Makes the four cases of tst_sunspecplugin pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:20:09 +02:00
Patrick Schurig
d10128e6b6 sunspec: add a failing test that sets up a Fronius connection
The existing suite only exercises free functions and the plugin JSON, so it
cannot see whether a thing class is dispatched at all. A thing class missing
from the dispatch in setupThing() falls into the final Q_ASSERT_X: in a debug
build the process dies, in a release build the assert is compiled out and
setupThing() returns without ever calling info->finish(), leaving the pairing
stuck with no message.

Split the tests into two suites. The logic one keeps its former scope and its
single dependency on libnymea-sunspec. The new one instantiates the plugin
behind the real ThingManager of libnymea-core and a faked HardwareManager, so
the dispatch of setupThing(), postSetupThing() and thingRemoved() is covered.
No network access takes place: the faked monitor is never reachable, so the
plugin never attempts a Modbus connection.

The four new cases fail: froniusConnection is not dispatched yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:18:41 +02:00
Patrick Schurig
ceb358c89a specs: record acceptance criteria status and remaining gaps 2026-07-31 10:33:49 +02:00
Patrick Schurig
6290a6cf7b 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.
2026-07-31 10:33:12 +02:00
Patrick Schurig
17e5751b5e 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.
2026-07-31 10:26:01 +02:00
Patrick Schurig
2a0ab1cda6 sunspec: reconcile storage states from the registers actions write
onStorageBlockUpdated() fed chargingRate from wChaGra() and dischargingRate
from wDisChaGra(). Those are the gradients, in percent of WChaMax per
second, and read only on several inverters. The matching actions write
InWRte and OutWRte, in percent of WChaMax. The user wrote one quantity and
read back another.

Feed the two rate states from inWRte() and outWRte(), and keep the
gradients under names that cannot be mistaken for setpoints:
chargeGradient and dischargeGradient, read only.

enableDischarging was never reconciled either, while enableCharging was.
It stayed frozen on the last written value and drifted silently as soon as
the inverter changed StorCtl_Mod on its own. Reconcile both flags.

The mapping moves into SunSpecStorageLogic so it can be exercised on a
register dump without a running nymea.
2026-07-31 10:22:44 +02:00
Patrick Schurig
2ab5f176d7 sunspec: widen storage charge and discharge rate range
chargingRate and dischargingRate map onto InWRte and OutWRte of model 124,
which are int16 setpoints ranging from -100.00 to +100.00 percent of
WChaMax. Declaring them as 0..100 makes a forced charge structurally
inexpressible: nymea rejects the negative action parameter before it ever
reaches the plugin.

Also fixes the default value, which was the boolean false on an int state.
2026-07-31 10:20:05 +02:00
Patrick Schurig
96e5401633 sunspec: add failing tests for every acceptance criterion
One test per AC of specs/fronius-sunspec.md, named test_AC_<n>_<what>.

Definition criteria (value ranges, thing classes, defaults) are checked by
re-reading integrationpluginsunspec.json. Behaviour criteria run either on
a model 124 register dump fed to the real libnymea-sunspec model, or on the
pure logic units the plugin is about to grow.

All red at this point: sunspecstoragelogic and froniusrules do not exist
yet, so the suite does not compile. That is the expected starting state.
2026-07-31 10:16:12 +02:00
Patrick Schurig
56c17603cf specs: add acceptance criteria for Fronius SunSpec support
AC-1..AC-4 are the four upstream anomalies of the generic sunspec vendor
(setpoint range, read/write mismatch on the rate states, enableDischarging
never reconciled, revert timer never written).

AC-5..AC-15 are the Fronius-specific behaviour: manufacturer filter, byte
order, connection parameters, polling floor, forbidden writes, the HEMS
intent to register table, scale factor reread, readback verification and
the model 160 block classification.

Points the design contract leaves open are recorded as explicit working
hypotheses (H-1..H-6) rather than blocking on them.
2026-07-31 10:16:06 +02:00
Patrick Schurig
4ed29e535e sunspec: vendor plugin sources from nymea-plugins-modbus 1.15.2
Vendor the sunspec plugin into this repo so the Fronius work and the
upstream storage fixes can live here. Sources only: build artefacts and
generated plugininfo headers are excluded.

libnymea-sunspec is deliberately NOT vendored - the plugin must stay
rebasable on upstream nymea, so it links against libnymea-sunspec-dev.
sunspec.pri resolves it via pkg-config, with a NYMEA_SUNSPEC_PATH
override for a local sysroot, mirroring modbus.pri.

See sunspec/VENDORED.md.
2026-07-31 10:15:59 +02:00
Patrick Schurig
453ad809e3 abbterra: make ABB Terra AC addable over Modbus TCP (etm2..etm5)
Follow-up to the etm1 fixes, driven by live debugging against a real ABB
Terra AC 7 kW behind a Waveshare RS485->TCP gateway (<ip-lan>, slave 2):

- etm3: disable TCP auto-discovery (createMethods = "user" only). Discovery
  blind-scans the LAN and SIGSEGVs nymead as soon as any Modbus device answers
  :502 with an exception response. The crash is in the generated reachability
  handler (abbterramodbustcpconnection.cpp:2062 dereferences the check-reachable
  reply after it is freed); it could not be fixed from the discovery code alone.
  A gateway's slave id can't be discovered anyway, so manual add is required.
- etm2: still defer/guard AbbTerraTcpDiscovery::cleanupConnection() teardown
  (kept for correctness even though discovery is now off by default).
- etm4: setupTcpThing() never called connectDevice(), so manual setup hung until
  timeout. Start the connection when the network monitor reports the device.
- etm5: decode the serial-number and firmware register fields as BCD per ABB
  spec §5.1 (0x46 = week 46, not 70; 0x22 = 22 kW). The binary reading rejected
  real chargers ("does not match the ABB Terra AC Modbus register map").

Verified end to end: charger adds cleanly (TACW7-4-4624-S0034), states poll and
actions execute, no SIGSEGV. The underlying discovery/generated-code crash is
tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:14:44 +02:00
Patrick Schurig
28c348669c abbterra: fix TCP discovery SEGV + restore checkReachableRegister
Two coupled fixes so the ABB Terra AC can be added over Modbus TCP:

- Restore checkReachableRegister ("serialNumber") in the register JSON.
  The libnymea-modbus 1.15 code generator *requires* it — without it the
  connection classes (abbterramodbustcpconnection.h) are never generated and
  the plugin fails to build. Reverts the false lead in 4fa06df. The register
  is valid: mbpoll confirms holding reg 16384 (uint64) responds at slave 2.

- Fix a use-after-free in AbbTerraTcpDiscovery::cleanupConnection() that
  SIGSEGV'd nymead during network discovery whenever any Modbus device on the
  LAN answered on :502 with an exception (e.g. "illegal data address"). The
  method is invoked from several of the connection's own signals; it now
  bails out on the second (re-entrant) call and disconnects the connection's
  signals before disconnectDevice(), so the teardown can no longer re-enter
  while the QModbusReply "finished" handler is still dereferencing the reply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:48:47 +02:00
Patrick Schurig
4fa06df2d0 abbterra: remove checkReachableRegister - not supported in libnymea-modbus 1.15 2026-07-03 09:55:59 +02:00
Patrick Schurig
8e96891ed5 bump to 1.15.2+etm1 - build against nymea 1.15.2 2026-06-25 08:33:10 +02:00
Patrick Schurig
200ee44a99 abbterra: revert to etm6 - upstream blocks not compatible with libnymea-modbus 1.15 2026-06-25 02:19:09 +02:00
Patrick Schurig
378660e264 abbterra: adopt upstream block structure - init/update/WO registers 2026-06-25 02:09:49 +02:00
Patrick Schurig
d90165ab23 abbterra: split status block to respect 31-byte RT38B01 frame limit
The status block (12 registers × 2 words = 24 words) was read as a
single Modbus request, producing a 53-byte response that overflows the
RT38B01 433 MHz radio module's 31-byte frame limit.

Dissolve the status block into individual registers with
readSchedule:update so queuedRequests sends them one at a time
(max 9 bytes per response). The deviceInfo init block (8 words → 21
bytes) is left untouched.

Bump to 1.15.0+etm6.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 23:33:15 +02:00
Patrick Schurig
70c80a1da8 abbterra: fix RTU setup on slow radio links (433 MHz)
reachableChanged(true) never fires within the nymea setup timeout when
the RTU link is slow (e.g. 433 MHz radio relay). Add a fallback:
trigger initialize() immediately if master is already connected
(QTimer::singleShot 500 ms), and also on master::connectedChanged.
The existing reachableChanged path is kept for TCP-style RTU adapters.

Bump to 1.15.0+etm5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 23:08:36 +02:00
Patrick Schurig
8239747a96 abbterra: bump to etm4 - fix readSchedule=no on write-only registers 2026-06-24 22:32:43 +02:00
Patrick Schurig
3ce87fe499 abbterra: set readSchedule=no on write-only holding registers 2026-06-24 22:23:39 +02:00
Patrick Schurig
109cc64554 abbterra: fix readSchedule init on write-only holding registers 2026-06-24 22:14:44 +02:00
Patrick Schurig
aa37e17715 eastron: displayName par fonction (Compteur réseau / Onduleur PV / Consommateur) — convention ETM, AGENTS.md §3.4 2026-06-13 14:07:37 +02:00
Patrick Schurig
ee3eaaa41e v2c: connexion TCP persistante — fin des rafales SYN
Diagnostic tcpdump : le plugin ouvrait/fermait la socket Modbus TCP à
chaque cycle de poll, générant 6-7 paquets SYN consécutifs qui saturaient
l'ESP32 WiFi de la borne (timeouts en boucle). mbpoll avec une connexion
persistante = 0 % d'échec.

Corrections :
- connectDevice() : guard "si déjà connecté → no-op" (élimine le warning
  "already in ConnectedState" et les FIN/RESET parasites).
- onConnectionStateChanged(true) : appelle initialize() directement au lieu
  de setReachable(true) — évite la ré-entrée initialize() via reachableChanged.
- initialize() : setReachable(true) uniquement au succès, après lecture
  MinIntensity/MaxIntensity ; guard || m_updating ajouté.
- update() : guard || m_initializing ajouté (pas de poll concurrent avec init).
- Timer : if (!master->update()) master->connectDevice() — le socket reste
  ouvert tant que la connexion TCP est active ; connectDevice() n'est appelé
  que quand le TCP est réellement coupé.
- initializationFinished(thing) : master->update() immédiat après reconnect.
- reachableChanged handler : suppression de l'appel à initialize() (déplacé).

Résultat attendu tcpdump : 1 seul SYN à l'établissement, puis uniquement
des échanges Modbus (P. length 12/13) toutes les 30 s, aucun FIN/RESET.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 10:37:09 +02:00
Patrick Schurig
87facfcc0a v2c: hw-validated improvements (Trydan fw2.4.6)
- Decode SlaveError (reg 0x0BC5) with full table (codes 0-10) into new
  slaveErrorMessage state; code 04 (WiFi reconnect) also surfaces in
  statusMessage for immediate SAV visibility.
- Add L1/L2/L3 phase powers (regs 0x0BD9-0x0BDB, confirmed hw) as
  diagnostic states polled every 30 s.
- Add RegChargeTime (0x0BC6) to enum for completeness (not yet exposed).
- Collapse debian/changelog to single 1.15.0+etm1 entry (new plugin).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 09:26:53 +02:00
Patrick Schurig
9f0743358a feat(v2c): remove NetworkDeviceMonitor, add poll-based connectivity + statusMessage
The Trydan ESP32 WiFi stack saturates under nymea's periodic ICMP pings
(observed: latency > 14 s, Modbus timeouts, charger drops off).

Changes:
- NetworkDeviceMonitor removed entirely from setupThing() and thingRemoved().
  The charger is expected to have a DHCP-reserved IP (documented in code).
  Address comes from trydanThingAddressParamTypeId param — already the correct
  path after the previous setup fix.
- Connectivity now derives from Modbus poll success/failure only (reachable()
  from TrydanModbusTcpMaster), NOT from network ping.  A ping response does
  not imply Modbus usability on this hardware.
- postSetupThing() timer (30 s): if not reachable, calls connectDevice() for
  reconnect; if reachable, calls update().  The 30 s period IS the backoff —
  no aggressive retry loop.
- Modbus timeout: 2 s → 5 s (tolerates residual WiFi latency spikes).
- Modbus retries per read: 1 → 0 (abort fast on first timeout; full poll
  sequence already aborts at first error via doNextRead).
- k_errorLimit: 5 → 3 (3 × 5 s = 15 s before marking unreachable).
- New state "statusMessage" (QString): set on disconnect with timestamp +
  cause ("timeout Modbus — vérifier signal WiFi de la borne"); cleared on
  successful poll.  Visible in nymea-app; helps SAV without SSH access.
- "networkdevice" removed from JSON interfaces (contract broken without monitor).

Invariants preserved: Big/Big float32 decode, no block read, PauseState+Lock
mirror, PauseDynamic conflict management, writeCompleted address filtering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 18:15:30 +02:00