/// Rendu en français des **codes** de la télémétrie d'arbitrage. /// /// La box transporte un code et des paramètres numériques ; la phrase est fabriquée ici. /// Ce n'est pas une préférence de style : la locale nymea est **par connexion** (deux /// utilisateurs d'une même box peuvent avoir deux langues), et le handler qui émet ces /// codes est un `JsonHandler` sans `pluginId` — structurellement hors du catalogue de /// traduction nymea. Une phrase composée en C++ ne pourrait servir personne correctement. /// /// ## La règle qui gouverne ce fichier /// /// **Un code que cette version d'app ne connaît pas s'affiche en repli lisible — jamais /// une phrase vide, jamais un plantage.** Une box en avance sur l'app est un cas normal /// en parc déployé : le paquet du plugin se met à jour par `apt`, l'app par le magasin. /// /// Le repli vaut aussi pour un code **connu dont un paramètre manque** : composer /// « Consigne de W servie par le surplus » serait pire qu'afficher le code brut, parce /// que la phrase trouée a l'air d'une donnée alors que le repli a l'air de ce qu'il est. library; import '../l10n/app_localizations.dart'; import '../models/load_telemetry.dart'; /// Phrase de décision, ou repli explicite. String decisionText(L10n t, LoadDecision d) { final p = d.params; switch (d.code) { case 'EV_SURPLUS': return t.decisionEvSurplus; case 'EV_SPOT_MARKET': return t.decisionEvSpotMarket; case 'EV_DEADLINE': return t.decisionEvDeadline; case 'EV_ECO_MIN': return t.decisionEvEcoMin; case 'EV_IDLE': return t.decisionEvIdle; case 'DEGRADED_L2': return t.decisionDegradedL2; case 'SAFE_STATE_RELAY': return t.decisionSafeStateRelay; case 'SAFE_STATE_SETPOINT': return t.decisionSafeStateSetpoint; case 'SAFE_STATE_SG_READY': return t.decisionSafeStateSgReady; case 'LOAD_UNAVAILABLE': final frozen = _w(d, 'frozenW'); if (frozen == null) break; return t.decisionLoadUnavailable(frozen); case 'LOCK_MIN_ON': final applied = _w(d, 'appliedW'); final budget = _w(d, 'budgetW'); if (applied == null || budget == null) break; return t.decisionLockMinOn(applied, budget); case 'LOCK_MIN_OFF': final requested = _w(d, 'requestedW'); final budget = _w(d, 'budgetW'); if (requested == null || budget == null) break; return t.decisionLockMinOff(requested, budget); case 'LOCK_MIN_OFF_CAPPED': final applied = _w(d, 'appliedW'); final requested = _w(d, 'requestedW'); final budget = _w(d, 'budgetW'); if (applied == null || requested == null || budget == null) break; return t.decisionLockMinOffCapped(applied, requested, budget); case 'SURPLUS_SETPOINT': final setpoint = _w(d, 'setpointW'); final budget = _w(d, 'budgetW'); if (setpoint == null || budget == null) break; // `stepped` distingue une consigne servie telle quelle d'une consigne arrondie sur // un palier déclaré. Absent, on ne prétend ni l'un ni l'autre : la variante neutre. return d.boolParam('stepped') == true ? t.decisionSurplusSetpointStepped(setpoint, budget) : t.decisionSurplusSetpoint(setpoint, budget); case 'SURPLUS_INSUFFICIENT': final budget = _w(d, 'budgetW'); if (budget == null) break; return t.decisionSurplusInsufficient(budget); case 'LOCK_MIN_STATE_HOLD': final state = _i(d, 'state'); if (state == null) break; return t.decisionLockMinStateHold(state); case 'SG_FORCED': final estimated = _w(d, 'estimatedW'); final budget = _w(d, 'budgetW'); if (estimated == null || budget == null) break; return t.decisionSgForced(estimated, budget); case 'SG_RECOMMENDED': final estimated = _w(d, 'estimatedW'); final budget = _w(d, 'budgetW'); if (estimated == null || budget == null) break; return t.decisionSgRecommended(estimated, budget); case 'SG_NORMAL': final budget = _w(d, 'budgetW'); if (budget == null) break; return t.decisionSgNormal(budget); // « Il y a du surplus, mais pas au bon format. » // // Le sens est à distinguer soigneusement de SURPLUS_INSUFFICIENT et SG_NORMAL, qui ne // sortent désormais que pour un budget ≤ 0 — « il n'y a rien ». Ici il y a de quoi, // simplement pas assez pour le plus petit cran commandable : la charge ne prend rien // et le budget file à la suivante. Les confondre ferait chercher un défaut de // production là où il n'y a qu'une granularité. // // Deux variantes, selon que le mécanisme se commande en watts ou en états. La présence // de `state` les départage — et si elle manque sur un mécanisme à états, le repli // rendra le code et ses paramètres plutôt qu'une phrase fausse. // ── Réserve batterie : préparé, pas encore branché ────────────────────── // // Le motif viendra quand la règle montera dans le budget. Son CODE n'est pas tranché // côté plugin, donc le deviner reviendrait à écrire une branche morte qui ne // s'activerait jamais — ou pire, à en activer une sur un code homonyme. // // Ce qui est prêt, c'est la PHRASE, et c'est la partie difficile : elle doit nommer la // cause (« une règle interdit de distribuer ») et non décrire l'effet (« rien n'est // piloté »), sans quoi elle se confond avec « pas de surplus ». Le jour où le code est // publié, il n'y a qu'à l'ajouter à [kCodesReserveBatterie]. // // En attendant, le repli fait exactement son travail : un code inconnu sort en clair // avec ses paramètres, ce qui ne peut PAS se lire comme « aucun surplus ». C'est // vérifié par un test plutôt qu'espéré. case _ when kCodesReserveBatterie.contains(d.code): final soc = _i(d, 'batteryLevel') ?? _i(d, 'soc'); final seuil = _i(d, 'threshold') ?? _i(d, 'batteryLevelConsideration'); return (soc == null || seuil == null) ? t.decisionBatteryReserveShort : t.decisionBatteryReserve(soc, seuil); case 'BELOW_MIN_POWER': final budget = _w(d, 'budgetW'); final min = _w(d, 'minPowerW'); if (budget == null || min == null) break; final state = _i(d, 'state'); return state == null ? t.decisionBelowMinPower(budget, min) : t.decisionBelowMinPowerState(budget, state, min); } // Code inconnu, ou code connu dont un paramètre attendu manque. return t.decisionUnknown(d.code.isEmpty ? '—' : d.code, formatParams(p)); } /// Codes que le plugin publiera pour « budget annulé par la réserve batterie ». /// /// **Volontairement vide.** Le nom n'est pas arrêté côté plugin ; y mettre une supposition /// créerait soit une branche morte, soit — pire — une branche qui s'activerait sur un /// homonyme. Le jour où le code est publié, une seule ligne suffit ici, et la phrase est /// déjà écrite et relue. const Set kCodesReserveBatterie = {}; /// Rendu déterministe des paramètres bruts, pour le repli. /// /// Trié par clé : deux affichages du même motif doivent être identiques, sinon l'écran /// « bouge » d'une trame à l'autre sans qu'il se soit rien passé. String formatParams(Map params) { if (params.isEmpty) return ''; final keys = params.keys.toList()..sort(); final body = [for (final k in keys) '$k = ${params[k]}'].join(', '); return ' ($body)'; } /// Phrase de défaut, ou repli sur le code brut. String faultText(L10n t, String code) => switch (code) { 'WRITE_FAILED' => t.faultWriteFailed, 'THING_MISSING' => t.faultThingMissing, 'UNUSABLE_ENCODING' => t.faultUnusableEncoding, _ => t.faultUnknown(code), }; /// Verrou actif. Les secondes affichées sont celles **du cycle publié** : l'app ne les /// décompte pas localement, parce que le plugin exclut volontairement ce champ de sa /// détection de changement — un décompte local serait une extrapolation présentée comme /// une mesure. String lockText(L10n t, LoadLock lock) => switch (lock.kind) { 'minOn' => t.lockMinOn(lock.remainingS), 'minOff' => t.lockMinOff(lock.remainingS), 'minStateHold' => t.lockMinStateHold(lock.remainingS), _ => t.lockUnknown(lock.kind, lock.remainingS), }; /// État du mécanisme. `null` quand la variante ne porte pas de quoi composer une phrase — /// l'appelant n'affiche alors rien plutôt qu'une ligne creuse. String? mechanismText(L10n t, LoadMechanism m) { switch (m.kind) { case 'relay': final stage = m.stageW; final max = m.maxStageW; if (stage == null || max == null) break; return t.mechanismRelayStage(stage.round(), max.round()); case 'variable': final setpoint = m.setpointW; final percent = m.percent; if (setpoint == null || percent == null) break; return t.mechanismVariableSetpoint(setpoint.round(), percent.round()); case 'sgReady': final state = m.state; final estimated = m.estimatedPowerW; if (state == null || estimated == null) break; return t.mechanismSgReadyState(state, estimated.round()); case 'evcharger': final current = m.currentA; final phases = m.phaseCount; if (current == null || phases == null) break; return t.mechanismEvCharger(current, phases); } return m.kind.isEmpty ? null : t.mechanismUnknown(m.kind); } /// Paramètre en watts, arrondi. `null` si absent ou non numérique — c'est ce qui /// déclenche le repli plutôt qu'une phrase trouée. int? _w(LoadDecision d, String key) => d.numParam(key)?.round(); int? _i(LoadDecision d, String key) => d.numParam(key)?.toInt();