fix(recharge): lire-patcher-réécrire — SetChargingInfo n'est pas une écriture partielle
La box promet le contraire de ce qu'elle fait, et par écrit : sa propre description dans JSONRPC.Introspect dit « Only given properties will be set, others will be untouched ». ChargingInfo est en réalité reconstruit entier à chaque appel, et un champ absent retombe sur son défaut C++ — 80 pour targetPercentage. Reproduit sur .75 le 2026-08-27 (+etm22), borne 88160e45 : le champ valait 0, un SetChargingInfo portant le seul chargingMode l'a laissé à 80, réponse EnergyErrorNoError. Banc restauré. L'app envoyait targetPercentage uniquement avec une échéance. Changer de mode de charge reposait donc une cible de 80 % à l'insu de l'utilisateur, sans que rien ne le signale. Désormais : GetChargingInfos, patch, réécriture de l'objet complet — sans chargingState, qui est r: au schéma et ferait rejeter l'appel. Sans échéance, on repose explicitement la valeur que la box portait déjà ; si elle n'en portait aucune, le neutre est 0, pas 80. Et la cible n'est plus affichée hors échéance : 80 peut être l'empreinte d'une écriture qui ne parlait pas de cible du tout, la montrer en ferait une intention. Deux notes de contrat au passage : - fetchPowerBalanceLogs porte enfin le piège qui a fait croire à un défaut de la box côté plugin — les entrées de log ne portent PAS le préfixe currentPower. Réutiliser les clés de GetPowerBalance ne produit aucune erreur, tout tombe sur 0. Vérifié ici avec les clés de l'app : 7 960 échantillons sur 90 jours en SampleRate15Mins, dont 4 975 de production non nulle. Rien ne bloque l'historique 90 jours. - le défaut d'usine de batteryLevelConsideration est passé de 0,9 à 0,2. La carte reste nécessaire : le nouveau défaut ne rattrape pas une box qui a déjà persisté 0,9, et le seuil reste réglable jusqu'à 1,0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
This commit is contained in:
parent
a1455290c9
commit
a92e9b2f62
@ -909,6 +909,19 @@ class NymeaService extends ChangeNotifier {
|
||||
/// [sampleRate] : "SampleRate15Mins" | "SampleRate1Hour" |
|
||||
/// "SampleRate3Hours" | "SampleRate1Day" | "SampleRate1Week"
|
||||
/// Timestamps envoyés en **secondes** (convention nymea Energy API).
|
||||
///
|
||||
/// ## ⚠️ Les entrées de log ne portent PAS le préfixe `currentPower`
|
||||
///
|
||||
/// `GetPowerBalance` rend `currentPowerProduction` / `currentPowerConsumption` ; les
|
||||
/// entrées de log, elles, s'appellent **`production`, `consumption`, `acquisition`,
|
||||
/// `storage`**. Réutiliser les clés de l'un pour l'autre ne produit **aucune erreur** :
|
||||
/// tout tombe sur le défaut `0`, et l'historique paraît vide alors que la base est
|
||||
/// pleine. C'est exactement ce qui a fait croire à un défaut de la box côté plugin — et
|
||||
/// c'est pour ça que ce piège est écrit ici plutôt que retenu.
|
||||
///
|
||||
/// Vérifié sur `.75` le 2026-08-27, avec les clés ci-dessous : **7 960 échantillons sur
|
||||
/// 90 jours** en `SampleRate15Mins`, dont 4 975 avec une production non nulle. Rien ne
|
||||
/// bloque l'historique 90 jours de ce côté.
|
||||
Future<List<PowerBalanceEntry>> fetchPowerBalanceLogs({
|
||||
required DateTime from,
|
||||
required DateTime to,
|
||||
@ -1034,6 +1047,32 @@ class NymeaService extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// `NymeaEnergy.GetChargingInfos` — l'objet de recharge **tel que la box le porte**.
|
||||
///
|
||||
/// Lecture indispensable avant toute écriture : `SetChargingInfo` reconstruit l'objet
|
||||
/// entier et un champ absent retombe sur son défaut C++ (cf. [setChargingInfo]). On ne
|
||||
/// peut donc pas « écrire un champ » sans avoir lu les autres.
|
||||
///
|
||||
/// \return l'objet de cette borne, ou `null` si la box n'en porte pas — auquel cas il
|
||||
/// n'y a rien à préserver, et c'est une information, pas un échec.
|
||||
Future<Map<String, dynamic>?> getChargingInfo(String evChargerId) async {
|
||||
if (_isSimulation || !_connected) return null;
|
||||
try {
|
||||
final r = await _sendRequest('NymeaEnergy.GetChargingInfos', {});
|
||||
final list = r['params']?['chargingInfos'];
|
||||
if (list is! List) return null;
|
||||
for (final e in list) {
|
||||
if (e is Map && e['evChargerId'] == evChargerId) {
|
||||
return Map<String, dynamic>.from(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
_log('[Recharge] GetChargingInfos: $e', force: true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// `NymeaEnergy.SetChargingInfo` — mode de recharge du véhicule.
|
||||
///
|
||||
/// ## Ce bloc envoyait une requête que la box REJETTE
|
||||
@ -1059,6 +1098,30 @@ class NymeaService extends ChangeNotifier {
|
||||
/// valeur du courant minimal ne se règle pas ici. Le mode se choisit, son paramètre
|
||||
/// non — et l'écran doit le dire au lieu d'offrir un réglage sans effet.
|
||||
///
|
||||
/// ## Écriture PARTIELLE : la box promet le contraire de ce qu'elle fait
|
||||
///
|
||||
/// `JSONRPC.Introspect` décrit la méthode ainsi : *« Only given properties will be set,
|
||||
/// others will be untouched. »* **C'est faux.** `ChargingInfo` est reconstruit
|
||||
/// intégralement à chaque écriture (`unpack<ChargingInfo>`), et un champ absent du JSON
|
||||
/// ne reste pas en place : il retombe sur le **défaut du membre C++**. Pour
|
||||
/// `targetPercentage`, ce défaut vaut **80**.
|
||||
///
|
||||
/// Et la substitution est **muette** : réponse `EnergyErrorNoError`, aucun
|
||||
/// avertissement (le seul du chemin d'écriture ne se déclenche qu'au-delà de 100), et
|
||||
/// la notification `ChargingInfoChanged` qui suit porte 80 comme si l'utilisateur
|
||||
/// l'avait choisi.
|
||||
///
|
||||
/// Reproduit sur `.75` le 2026-08-27, plugin `+etm22` : `targetPercentage` valait `0`,
|
||||
/// un `SetChargingInfo` portant le seul `chargingMode` l'a laissé à **80**.
|
||||
///
|
||||
/// D'où **lire, patcher, réécrire l'objet complet** — le même chemin que `priority`
|
||||
/// dans `LoadConfig`. Le seul champ qu'on ne renvoie PAS est `chargingState`, marqué
|
||||
/// `r:` au schéma : le réémettre ferait rejeter l'appel entier.
|
||||
///
|
||||
/// Conséquence pour l'écran : `targetPercentage` **n'est une cible choisie par
|
||||
/// l'utilisateur que s'il l'a lui-même écrite**. 80 peut être l'empreinte d'une
|
||||
/// écriture qui ne parlait pas de cible du tout.
|
||||
///
|
||||
/// \return `null` si la box a accepté, sinon de quoi l'annoncer. **Ne rien inventer**
|
||||
/// sur la cause : le détail reste au journal de la box.
|
||||
Future<String?> setChargingInfo({
|
||||
@ -1089,17 +1152,33 @@ class NymeaService extends ChangeNotifier {
|
||||
error = 'NoEvCharger';
|
||||
} else {
|
||||
try {
|
||||
// LIRE d'abord — une écriture partielle n'existe pas ici, tout champ omis
|
||||
// retombe sur son défaut C++. Une lecture qui échoue laisse `courant` vide :
|
||||
// on écrit alors le strict nécessaire, ce qui n'est pas pire que l'ancien
|
||||
// comportement et reste honnête sur ce qu'on sait.
|
||||
final courant = await getChargingInfo(evChargerId) ?? const {};
|
||||
|
||||
final info = <String, dynamic>{
|
||||
...courant,
|
||||
'evChargerId': evChargerId,
|
||||
'chargingMode': apiMode,
|
||||
};
|
||||
// Rien qui ne soit au schéma ne part : `minCurrent` n'existe pas, et une clé
|
||||
// inconnue ferait rejeter TOUT l'appel (nymea valide avant le handler).
|
||||
// `chargingState` est en lecture seule (`r:`) au schéma : le renvoyer ferait
|
||||
// rejeter TOUT l'appel. Même règle pour toute clé hors schéma — nymea valide
|
||||
// avant d'atteindre le handler.
|
||||
info.remove('chargingState');
|
||||
|
||||
if (deadline && mode != ChargingMode.boost) {
|
||||
info['targetPercentage'] = targetSoc;
|
||||
if (endTime != null) {
|
||||
info['endDateTime'] = endTime.millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
} else if (!info.containsKey('targetPercentage')) {
|
||||
// Sans échéance, l'app ne CHOISIT pas de cible — mais l'omettre en
|
||||
// choisirait une (80) à la place de l'utilisateur, sans rien lui dire. On
|
||||
// repose donc explicitement la valeur que la box portait déjà ; si elle n'en
|
||||
// portait aucune, `0` est le neutre, pas `80`.
|
||||
info['targetPercentage'] = 0;
|
||||
}
|
||||
final r = await _sendRequest(
|
||||
'NymeaEnergy.SetChargingInfo', {'chargingInfo': info});
|
||||
@ -1141,10 +1220,12 @@ class NymeaService extends ChangeNotifier {
|
||||
///
|
||||
/// ## Pourquoi il ne peut pas rester invisible
|
||||
///
|
||||
/// Le défaut d'usine est à **0,9**. Sur une installation dont la batterie plafonne à
|
||||
/// 50 %, cela gèle tout — et le symptôme, chez un client, est un HEMS qui ne pilote plus
|
||||
/// rien par grand soleil, **sans erreur ni panne**. Un seuil qui peut produire ça n'est
|
||||
/// pas un paramètre d'usine : c'est un réglage d'installation, et il doit se voir.
|
||||
/// Le défaut d'usine **était** à 0,9 ; il est passé à **0,2**. Sur une installation dont
|
||||
/// la batterie plafonne à 50 %, 0,9 gèle tout — et le symptôme, chez un client, est un
|
||||
/// HEMS qui ne pilote plus rien par grand soleil, **sans erreur ni panne**. Le nouveau
|
||||
/// défaut ne rattrape pas les box déjà déployées, qui gardent la valeur persistée, et le
|
||||
/// seuil reste réglable jusqu'à 1,0. Un seuil qui peut produire ça n'est pas un
|
||||
/// paramètre d'usine : c'est un réglage d'installation, et il doit se voir.
|
||||
///
|
||||
/// \return la fraction lue, ou `null` si la box ne l'expose pas.
|
||||
Future<double?> getBatteryLevelConsideration() async {
|
||||
|
||||
@ -13,10 +13,16 @@ import '../theme/etm_tokens.dart';
|
||||
/// seuil, le budget de surplus est **annulé** — pas réduit — donc ni ECS, ni PAC, ni
|
||||
/// bornes.
|
||||
///
|
||||
/// Le défaut d'usine est à **0,9**. Sur une installation dont la batterie plafonne à 50 %,
|
||||
/// cela gèle tout. Et le symptôme, chez un client, est le pire qui soit : un HEMS qui ne
|
||||
/// pilote plus rien **par grand soleil, sans erreur ni panne**. Rien à lire dans un
|
||||
/// journal, rien à diagnostiquer — juste une installation muette.
|
||||
/// Le défaut d'usine **était** à 0,9. Sur une installation dont la batterie plafonne à
|
||||
/// 50 %, cela gèle tout. Et le symptôme, chez un client, est le pire qui soit : un HEMS
|
||||
/// qui ne pilote plus rien **par grand soleil, sans erreur ni panne**. Rien à lire dans
|
||||
/// un journal, rien à diagnostiquer — juste une installation muette.
|
||||
///
|
||||
/// Il est passé à **0,2** côté plugin (`smartchargingmanager.cpp`, défaut de lecture des
|
||||
/// réglages). **Cela ne rend pas cette carte inutile, pour deux raisons :** le nouveau
|
||||
/// défaut ne s'applique qu'à une installation qui n'a jamais écrit le réglage — une box
|
||||
/// déjà déployée garde le 0,9 qu'elle a persisté — et le seuil reste réglable jusqu'à 1,0,
|
||||
/// donc reproductible à la main.
|
||||
///
|
||||
/// Un seuil qui peut produire ça n'est pas un paramètre d'usine. C'est un réglage
|
||||
/// d'installation, et il doit se voir avant de mordre, pas après.
|
||||
|
||||
@ -287,10 +287,20 @@ class _EVChargingCardState extends State<EVChargingCard> {
|
||||
],
|
||||
],
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// SOC progress
|
||||
_SocProgress(targetSoc: _targetSoc, currentSoc: 62),
|
||||
// Cible de charge — affichée SEULEMENT quand l'écran l'a lui-même écrite.
|
||||
//
|
||||
// `targetPercentage` n'est pas un réglage que l'app peut lire et présenter
|
||||
// comme un choix de l'utilisateur : `SetChargingInfo` reconstruit l'objet
|
||||
// entier, et le défaut C++ du champ vaut **80**. Une écriture qui ne parlait
|
||||
// que du mode de charge laisse donc 80 derrière elle, sans que rien ne le
|
||||
// signale — reproduit sur `.75` le 2026-08-27. Montrer « cible 80 % » hors
|
||||
// échéance afficherait cette empreinte comme une intention.
|
||||
//
|
||||
// Hors échéance, l'app n'a pas de cible : elle n'en montre pas.
|
||||
if (_deadlineEnabled) ...[
|
||||
const SizedBox(height: 14),
|
||||
_SocProgress(targetSoc: _targetSoc, currentSoc: 62),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user