# Relevé machine — 3g-1 sur le banc `.75`, 2026-08-26 **Paquets** : `1.15.2+etm20` → `+etm21` → `+etm22` (le dernier corrige ce que le passage machine a révélé). **Batterie** : Fronius Storage, SoC 50 %, `Holding`, `capacity` 10 kWh. **Bornes** : Terra AC réelle (192.168.1.132:502) + wallbox simulée. > **Méthode.** Ce qui suit distingue ce qui a été **observé sur machine**, ce qui vient d'un > **test unitaire**, et ce qui n'est qu'une **lecture de code**. Les trois ne valent pas pareil, > et mélanger les trois est exactement ce qui a envoyé un agent dans le vide le 2026-08-25. --- ## 1. La bascule `batteryLevelConsideration` — FAITE, dans les deux sens Première exécution possible depuis que `nymea-plugin-sunspec-etm01 1.15.2+etm11` déclare `energystorage` : la batterie est enfin **vue** (`Having 1 batteries. Overall battery level is 50 %`, journal 17:16:00). | | seuil | surplus | motif publié | |---|---|---|---| | **etm20**, 17:12:00 | 0,9 | **négatif** | `BATTERY_RESERVE`, *« **0 W** de surplus retenus »* | | **etm21**, 17:17:00 et 17:18:00 | 0,9 | **négatif** | `SURPLUS_INSUFFICIENT` | | **etm21**, 17:19:00 | 0,4 | négatif | `SURPLUS_INSUFFICIENT` | **La correction `withheldW: 0` est vérifiée sur machine.** L'A/B est propre et les deux côtés sont au journal : dans des conditions identiques — même batterie, même seuil, même surplus négatif — etm20 disait « une règle vous bloque » et etm22 dit « il n'y a rien ». C'était la confusion que ce motif existe pour supprimer, retournée contre elle-même. ⚠️ **La moitié qui manque, et il faut la dire** : la réserve n'a **jamais été observée en train de retenir un surplus réel** (`withheldW > 0`). Il y aurait fallu un surplus positif **et** un SoC sous le seuil, et l'observation s'est faite en soirée. La courbe du jour montre que c'est atteignable : **surplus maximum 8 733 W à 12:46Z**. À refaire vers midi. **Défaut de rendu corrigé au passage** : le journal affichait *« batterie à 50 %% »*. `%%` est une échappée `printf` ; `QString::arg` ne l'interprète pas. --- ## 2. LM-1204-b — NON franchi, et pour deux raisons distinctes ### 2.1 Ce qui a été démontré Le discriminant **fonctionne** et il a été observé sur une borne réelle du contexte (cycle 17:34:00, `+etm22`) : ``` {09e520fd…} 0 W funding=surplus BELOW_MIN_POWER {budgetW: 1096, minPowerW: 2070} ``` journal : *« Budget 1096 W sous le premier palier de Simulated wallbox (2070 W) — 0 W, le budget passe à la charge suivante »*. **C'est l'arbitre qui a refusé, et il le dit.** La cascade se réconcilie : `chauffe-eau 3500 + pac-terrain 3000 = 6500 = budget.allocatedW`. La borne indisponible sort en `LOAD_UNAVAILABLE` + `faultCode: THING_MISSING`. ### 2.2 Pourquoi le critère n'est pas franchi **Il faut DEUX bornes arbitrées. Il n'y en a qu'une.** La Terra AC est injoignable — `ThingErrorTimeout` à chaque tentative de setup, `connected: false` — donc elle n'est pas une concurrente dans la cascade, elle est un défaut. Le scénario « 4 kW, deux planchers de 1,4 kW, une seule démarre » n'a pas d'objet tant que la seconde borne ne répond pas. **Et le test de rang est impossible tel qu'il est écrit** — voir §3.2 : le rang d'une borne n'est pas configurable. --- ## 3. Ce que le passage machine a révélé, et qui n'était visible d'aucune autre façon ### 3.1 CORRIGÉ (`+etm22`) — les bornes étaient arbitrées sans franchir la frontière RPC `buildTelemetry()` n'interrogeait que la table des charges de configuration et sautait l'EV avec un commentaire devenu faux. Le journal montrait les décisions des deux bornes ; `GetLoadTelemetry` n'en montrait **aucune**. Le changement de contrat annoncé par `+etm21` n'était donc pas livré — et LM-1204-b était **inéprouvable**, puisque son discriminant *est* le motif publié. Nouveau champ **`loads[].funding`** (`"surplus"` / `"grid"`), rendu nécessaire par l'entrée des bornes : une borne servie par la cascade est comptée dans `budget.allocatedW`, une borne servie par l'échéance ou le tarif dynamique soutire au réseau et vit dans `budget.evReservedW`. ### 3.2 NON CORRIGÉ — le rang d'une borne n'est pas configurable `EvAdapter::descriptor()` fixe `priority = 100` **en dur, pour toutes les bornes**. Deux bornes ont donc la même clé de tri, et `std::sort` n'est pas stable. La cascade reste correcte — une seule démarre, l'autre reçoit son motif — mais **laquelle** n'est ni réglable ni garanti reproductible d'un cycle à l'autre. À ne pas présenter comme un réglage dans l'app avant qu'un lot dédié le traite. ### 3.3 NON CORRIGÉ, ET C'EST LE PLUS GRAVE — la double commande Journal 17:34:00, deux lignes consécutives : ``` [Arbitre] "{09e520fd…}" → "Budget 1096 W sous le premier palier … — 0 W" ---------------- Adjusting chargers ---------------- Executing action Simulated wallbox to power: ON, Charging current: 6A, Issuer: Surplus ``` **`adjustEvChargers()` re-décide le surplus après le waterfall et passe outre le refus de l'arbitre.** 3g-1 a retiré la *planification* du surplus EV (la branche `else` de `planSurplusCharging` est devenue `continue`) mais a laissé tourner le chemin d'*exécution* du proxy, qui dérive sa propre consigne. La même ligne part vers la **Terra AC réelle** — elle n'a rien fait cette fois **uniquement parce que la borne est hors ligne**. > **Conséquence à énoncer sans détour : tant que ceci n'est pas traité, la transplantation ne > commande rien.** L'arbitre décide, publie un motif juste, et le proxy commande autre chose une > milliseconde plus tard. Tout ce qui précède dans ce relevé décrit une décision qui n'atteint > pas le matériel. **Ce n'est pas corrigé ici volontairement** : la correction porte sur le chemin d'exécution d'une borne réelle, et le choix — `adjustEvChargers()` ne doit plus agir que sur les bornes ayant reçu une action financée au **réseau** — est un arbitrage de lot (3g-2), pas une retouche. L'improviser un soir sur un banc qui porte une Terra AC serait exactement le genre de décision que le workflow interdit. ### 3.4 NON CORRIGÉ — le plancher EV est sous le seuil de démarrage réel `evFloorW()` = `max(borne, voiture) × acquisitionTolerance × 230 × phases`. Avec 6 A, une tolérance de 0,5 et 3 phases : **2 070 W**. Or démarrer exige réellement `6 A × 230 × 3`, soit **4 140 W**. Observé au cycle 17:33:00 : le waterfall alloue **2 828 W** à la wallbox (au-dessus du « plancher » de 2 070), puis l'adaptateur convertit — `2828 / (230 × 3) = 4,1 A`, sous les 6 A minimum — et refuse d'enclencher : `activé: false | courant: 0 A`. **2 828 W ont été retirés du budget et donnés à personne.** `acquisitionTolerance` était, chez le proxy, un seuil de **déclenchement** — la part de réseau qu'on accepte de soutirer pour démarrer. Ce n'est pas la même grandeur qu'un **plancher** au sens du §12, qui doit être la puissance que la charge exige réellement. Les deux ont été confondus en écrivant `evFloorW()`. --- ## 4. Autres constats, hors 3g - **`Energy.GetPowerBalanceLogs` à `SampleRate15Mins` rend 50 échantillons TOUS À ZÉRO**, alors que `SampleRate1Min` (681 échantillons) et `SampleRate1Hour` (12) sont peuplés sur la même fenêtre. C'est le cœur nymea, pas notre plugin — mais c'est la table que l'écran d'historique de l'app voudra. **À vérifier avant de promettre les 90 jours.** - **`SetChargingInfo` substitue `targetPercentage = 80` à un champ absent.** Constaté en restaurant le banc : envoyer l'objet sans ce champ l'a mis à 80 alors qu'il valait 0. Un champ omis y acquiert donc une valeur — l'app doit **toujours** l'envoyer explicitement. - **Les Things Fronius portent des octets nuls de bourrage dans leur nom** (`"Fronius"` suivi de 25 `\0` avant `" Storage"`). Cosmétique, mais cela traversera jusqu'à l'écran. - **`currentPowerStorage` vaut 0** au bilan de puissance. Cohérent avec une batterie `Holding` / `idle`, donc **non concluant** : on ne peut pas encore distinguer « au repos » de « non câblé ». À reprendre quand la batterie charge ou décharge vraiment. --- ## 5. État où le banc a été rendu | | valeur | pourquoi | |---|---|---| | Paquet | **`1.15.2+etm22`** | contient la correction du §3.1 | | `batteryLevelConsideration` | **0,4 — PAS remis à 0,9** | voir ci-dessous | | `acquisitionTolerance` | 0,5 | inchangé | | `phasePowerLimit` | 0 | inchangé — désactive la protection de surcharge | | `LoadConfig` | **identique à l'octet près** au relevé d'avant | vérifié par comparaison | | `ChargingInfos` | **identiques à l'origine** | les deux bornes en `ChargingModeNormal`, sans voiture | | Voitures simulées de test | **retirées** | `LM1204-CAR-A/B-A-SUPPRIMER`, ajoutées puis supprimées | **Le seuil n'est délibérément PAS remis à 0,9.** À 0,9 avec une batterie à 50 %, la règle gèle toute l'installation — c'est exactement ce que le journal de 17:12:00 montre, et c'est la raison pour laquelle le défaut d'usine est passé à 0,2 en `+etm20`. Y revenir rendrait le banc inutilisable pour la reprise de LM-1204-b, sans rien démontrer de plus. **0,9 n'était pas un réglage voulu du banc : c'était l'ancien défaut d'usine, hérité.** ### Pour reprendre l'essai (les deux voitures sont à recréer) ```bash dart tools/rpc/probe.dart 192.168.1.75 Integrations.AddThing \ '{"thingClassId":"4afcafb1-d341-4532-8062-738cae529cb5","name":"LM1204-CAR-A-A-SUPPRIMER"}' dart tools/rpc/probe.dart 192.168.1.75 NymeaEnergy.SetChargingInfo \ '{"chargingInfo":{"evChargerId":"","chargingMode":"ChargingModeEco","assignedCarId":""}}' ``` --- ## 6. Ce qui manque au banc — la question posée, la réponse **La Terra AC ne répond pas** (`192.168.1.132:502`, `ThingErrorTimeout` à chaque cycle). C'est le seul obstacle matériel, et il bloque à lui seul LM-1204-b, qui exige deux bornes arbitrées. **Et une wallbox simulée ne remplace pas la seconde** : elle ne prouve ni le délai d'établissement, ni la négociation de courant, ni ce qu'une borne fait d'une consigne qu'elle n'accepte pas. Elle a un défaut de plus, visible ce soir : elle annonce `desiredPhaseCount: 3` en permanence, ce qui a suffi à faire viser 3 phases et à porter le plancher publié à 2 070 W alors qu'aucun véhicule n'est branché. Sur ce point précis, **le simulateur a fabriqué la condition du défaut §3.4** — utile pour le trouver, sans valeur pour le valider.