Patrick Schurig 51a211e080 fix(sunspec): rendre la batterie SunSpec visible au moteur (energystorage)
sunspecStorage déclarait `battery` mais pas `energystorage`. Les trois
consommateurs qui filtrent par cette interface ne la trouvaient donc pas : le
bilan de puissance de nymea (`currentPowerStorage`, celui que le CLIENT lit sur
son tableau de bord), la réserve de stockage de la recharge VE, et celle du
waterfall. Vérifié sans authentification dans `energylogs.sqlite` de .75 : la
colonne `storage` de `powerBalance` est à 0.0 sur toutes les lignes, pendant
que la batterie affiche 50 % et « Holding ». Elle n'était pas à zéro, elle
était invisible.

Ce n'était pas une ligne de JSON. `energystorage` exige `currentPower` (W,
signé) et `capacity` (kWh), qu'aucun des quatorze états ne portait, et les
déclarer sans les câbler aurait donné une batterie éternellement inerte — pire
que son absence, puisque la réserve lirait « ni charge ni décharge » au lieu de
« je ne sais pas ».

D'où viennent les deux états. Le modèle 124 ne porte AUCUNE puissance : WChaMax
est un maximum, StorAval est en ampères-heures. La puissance instantanée vit
dans le modèle 160, réparti sur deux blocs `StCha` et `StDisCha` dont le `DCW`
est un uint16 toujours positif, l'inactif à zéro. D'où
`currentPower = DCW[StCha] - DCW[StDisCha]`, positif en charge, conformément à
libnymea/interfaces/energystorage.json (« positive is 'consumed' »). C'est
l'INVERSE de la convention constructeur du contrat §4, et
`FroniusRules::batteryPower()` est réécrite comme la négation stricte de la
nouvelle `storageCurrentPower()` pour qu'aucune des deux ne dérive seule.
`capacity` vient de WHRtg du modèle 120, et l'état est laissé tranquille plutôt
que mis à zéro quand le nameplate ne l'implémente pas.

Le signe est MESURÉ, pas déduit. Banc .211 unité 1, consigne batterie imposée
par MQTT, registres relus en lecture seule :
  +2000 W -> StCha 2000,0 W  StDisCha    0,0 W  ChaSt Charging
  -1500 W -> StCha    0,0 W  StDisCha 1500,0 W  ChaSt Discharging
Le même relevé a montré `DCW_SF` passant de -2 à -1 entre repos et charge : le
facteur d'échelle n'est jamais mis en cache d'un relevé à l'autre.

Écart assumé, D-9 dans DIVERGENCES.md : `sunspecStorage` est partagé avec le
vendor générique, et les interfaces se déclarent par thingClass. Un appareil
générique publiant le 124 sans le 160 porte désormais `energystorage` avec un
`currentPower` figé à zéro. Les trois alternatives sont pires — une thingClass
Fronius distincte détruirait le Thing existant et son historique. Le plugin
avertit donc UNE FOIS par thing quand il ne trouve pas de source de puissance,
en distinguant « pas de modèle 160 » de « modèle 160 sans bloc batterie ». Ça ne
remplace pas la mesure, mais ça retire le silence.

La cohérence des unités avec l'interface est vérifiée à la COMPILATION par
nymea-plugininfocompiler : mettre `KiloWattHour` sur `currentPower` fait échouer
le build. Onze tests `test_D9_*` figent le reste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3gzsuwENJfJtvuNDT3xGn
2026-08-26 14:02:37 +02:00

184 lines
10 KiB
Plaintext

etm-powersync-plugins-modbus (1.15.2+etm10) trixie; urgency=medium
* sunspec: make the SunSpec storage visible to the energy engine. The
sunspecStorage thing class declared "battery" but not "energystorage", so
the three consumers that filter by that interface never found it: nymea's
power balance (currentPowerStorage, the figure the customer reads on their
dashboard), the storage reserve of the EV surplus charging, and the reserve
of the whole waterfall. Observed on the bench: GetPowerBalance answering
currentPowerStorage 0 while the battery reported 50 % and "Holding". It was
not at zero, it was invisible.
* sunspec: publish currentPower and capacity on the storage, wired to
registers rather than declared empty. Model 124 carries no power at all -
WChaMax is a maximum, StorAval is in amp hours - so the instantaneous power
is read from model 160, whose StCha and StDisCha blocks each carry an
unsigned DCW with the inactive one at zero. currentPower is therefore
DCW[StCha] - DCW[StDisCha], positive when charging, per the convention of
libnymea/interfaces/energystorage.json. capacity comes from WHRtg of the
nameplate model 120, and is left alone rather than zeroed when the
nameplate does not implement it.
* sunspec: the sign is measured, not deduced. Read only on the bench with the
battery power driven over MQTT: a +2000 W charge order gives StCha 2000 W,
StDisCha 0 W and ChaSt Charging; a -1500 W discharge order gives StCha 0 W,
StDisCha 1500 W and ChaSt Discharging. The same run showed DCW_SF moving
from -2 to -1 between idle and load, which is why the scale factor is never
cached across a poll.
* sunspec: warn once per thing when a storage has no power source - no model
160 on its connection, or a model 160 with no StCha/StDisCha block. Such a
thing now claims energystorage while reporting a flat zero, and a silent
zero is what made the original defect expensive. See DIVERGENCES.md D-9.
-- Patrick Schurig <patrick@etm-schurig.eu> Wed, 26 Aug 2026 14:00:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm9) trixie; urgency=medium
* sunspec: read Fronius float32 points big endian, the way the hardware
emits them. byteOrder() returned ByteOrderLittleEndian on the grounds that
"the SunSpec default is assumed"; that conflated the default of the
specification with libnymea-sunspec's own, which is the opposite. Every
float32 of the Fronius personality was therefore read with its two
registers swapped. Measured on a Symo, read only: the model 213 meter on
unit 200 serves Hz = 4248 0000, which is 50.0000 big endian and 2.3777e-41
swapped. Reproduced on the bench: a meter importing 2.7 kW reported as
exporting 107 MW, with the arbiter switching every load on, and the thing
still reporting itself connected with no Modbus error. The meter sign fix
only becomes observable once this is right. solarEdge already forces big
endian and the generic vendor keeps its user parameter; neither changes.
* sunspec: add tools/read-float-order.py, a read-only probe that decodes Hz
and PhVphA both ways so the word order can be settled by measurement
rather than assumption.
-- Patrick Schurig <patrick@etm-schurig.eu> Fri, 07 Aug 2026 07:00:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm8) trixie; urgency=medium
* sunspec: ask the connection thing directly instead of resolving it by id.
The battery presence gate went through isFroniusChild(), which looks the
parent up in myThings(). During the initial setup the parent is not
registered yet, the lookup answers false, the Fronius gate is bypassed and
the phantom storage child is created after all. Caught on the bench, not
by the unit suite, which covers the decision and not the wiring. See
sunspec/DIVERGENCES.md D-7.
-- Patrick Schurig <patrick@etm-schurig.eu> Wed, 05 Aug 2026 19:20:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm7) trixie; urgency=medium
* sunspec: correct the grid meter sign on Fronius. onMeterBlockUpdated()
negated the power of every meter model regardless of vendor, but the
Fronius Smart Meter already reports in the nymea convention, where a
negative value is feed-in. The negation is now undone for meters sitting
under a froniusConnection and only for those; generic and solarEdge keep
their previous behaviour. Established on a Primo GEN24 3.0, whose single
phase inverter gives the direction reference: the plugin reported the
inverter's own phase as importing while the two phases carrying no source
read as exporting. The arbiter concluded "insufficient surplus" and left
the water heater and the heat pump off while the site was exporting.
The energy counters were already correct and are left alone.
* sunspec: stop creating a phantom battery on Fronius. A hybrid GEN24
publishes model 124 whether or not a battery is wired to it, so a storage
child was created unconditionally and sat on batteryCritical forever. The
child is now created only when a physical quantity says a battery is
there. The state of charge is deliberately not used: 0% cannot be told
apart from a real but empty battery. See sunspec/DIVERGENCES.md D-6, the
battery-present side is still inferred.
-- Patrick Schurig <patrick@etm-schurig.eu> Wed, 05 Aug 2026 15:58:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm6) trixie; urgency=medium
* sunspec: package the vendored SunSpec plugin as nymea-plugin-sunspec-etm01.
The plugin was added to the build by the vendoring commit but never
packaged, so dh_missing found its .so in debian/tmp with nowhere to install
it and aborted the whole source package: no binary package at all came out
of this branch, not even for the four unrelated plugins.
* sunspec: add the Fronius vendor. A GEN24 answers for the inverter on one
unit ID and for its Smart Meter on another, behind the same host and port,
so one froniusConnection thing now carries two SunSpecConnection objects.
The meter connection is brought up only once the primary has finished
discovering, and the parent connected state follows the primary alone.
* sunspec: correct the storage setpoint chain - charge and discharge rates
read back from InWRte/OutWRte instead of the read-only gradients, the rate
range widened to -100..100 so forced charging can be expressed at all,
enableDischarging reconciled from StorCtl_Mod, and the revert timer armed
with every setpoint so one left behind by a stopped HEMS expires.
* debian: add libnymea-sunspec-dev to Build-Depends. Upstream does not need
it because it builds libnymea-sunspec from the same source tree; this
repository vendors only the plugin and links against the installed library.
* The new binary package conflicts with, replaces and provides
nymea-plugin-sunspec: it ships the same plugin file and the same pluginId
as the upstream package this repository already mirrors.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Sun, 02 Aug 2026 12:05:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm5) trixie; urgency=medium
* abbterra: decode the serial-number and firmware register fields as BCD, as
specified by the ABB Modbus spec §5.1 (e.g. 0x49 = week 49, 0x22 = 22 kW),
instead of as plain binary. The binary reading made deviceInfoFromValues()
reject every real charger whose BCD production-week byte exceeded 0x35
(e.g. week 46 = 0x46 = 70 > 53), failing setup with "The device does not
match the ABB Terra AC Modbus register map", and mis-decoded the 11 kW/22 kW
rated-power codes. Manual TCP add of the charger now completes.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 13:15:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm4) trixie; urgency=medium
* abbterra: fix TCP manual setup never establishing the connection.
setupTcpThing() wired all the connection signals but never called
connectDevice(), so reachableChanged()/initializationFinished() never fired
and the setup hung until timeout ("Thing setup failed"). Now the connection
is started when the NetworkDeviceMonitor reports the device online.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 12:55:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm3) trixie; urgency=medium
* abbterra: disable TCP auto-discovery (createMethods = "user" only) for the
Terra AC Charger (TCP) thing class. Discovery blind-scans the LAN and blows
up nymead with a SIGSEGV as soon as any Modbus device answers :502 with an
exception response — the crash is in the generated reachability handler and
cannot be fixed from the discovery code alone (two attempts failed). For the
Waveshare RS485-gateway use case discovery can't determine the slave id
anyway; the charger must be added manually (address + port + slaveId). This
unblocks adding the charger and stops the crash loop. The underlying
generated-code crash is tracked separately.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 12:40:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm2) trixie; urgency=medium
* abbterra: fix nymead SIGSEGV during ABB Terra AC TCP discovery. On a
reachability-check exception (e.g. any Modbus device on the LAN answering
:502 with "illegal data address"), AbbTerraTcpDiscovery::cleanupConnection()
tore the connection down synchronously from inside the connection's own
QModbusReply "finished" handler, freeing the in-flight reply that the
generated code still dereferences at abbterramodbustcpconnection.cpp:2062.
The teardown is now deferred (Qt::QueuedConnection) and made idempotent.
* abbterra: restore checkReachableRegister ("serialNumber"), required by the
libnymea-modbus 1.15 connection-class generator.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 12:00:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm1) trixie; urgency=medium
* Build 1.15,2
-- ETM-Schurig SARL <contact@etm-schurig.eu> Tue, 24 Jun 2026 11:00:00 +0200
etm-powersync-plugins-modbus (1.15.0+etm5) trixie; urgency=medium
* abbterra: fix RTU setup on slow radio links — trigger initialize() on
master connectedChanged, not only on reachableChanged (which never fires
within setup timeout over 433 MHz).
-- ETM-Schurig SARL <contact@etm-schurig.eu> Tue, 24 Jun 2026 10:00:00 +0200
etm-powersync-plugins-modbus (1.15.0+etm4) trixie; urgency=medium
* Correction modbus registers.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 13 Jun 2026 10:00:00 +0200