/// 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); } // Code inconnu, ou code connu dont un paramètre attendu manque. return t.decisionUnknown(d.code.isEmpty ? '—' : d.code, formatParams(p)); } /// 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();