Patrick Schurig dee20896c5 feat(télémétrie): lot B app — consommation de la télémétrie d'arbitrage
Le lot plugin B-bis (1.15.2+etm15) est sur .75 et GetLoadConfig/GetLoadTelemetry
concordent : les critères 4 à 8 du brief deviennent atteignables.

- models/load_telemetry.dart : vue en lecture. timestamp/budget/mechanism/lock/
  faultCode nullables — omis porte un sens que zéro ne porterait pas.
- services/telemetry_text.dart : rendu français des CODES (19 motifs, 3 défauts,
  3 verrous, 4 mécanismes). Repli lisible sur un code inconnu ET sur un code connu
  privé d'un paramètre : une phrase trouée a l'air d'une donnée, le code brut a
  l'air de ce qu'il est.
- providers/load_telemetry_provider.dart : instantané + LoadTelemetryChanged,
  fraîcheur à trois signaux (âge du timestamp, silence des trames, timestamp qui
  cesse d'avancer), machine à états de ClearLoadFault qui ne conclut JAMAIS sur
  l'acquittement du RPC.
- load_config_provider : assignDomain() en brouillon, énumération fermée.
- load_order_screen : budget, fraîcheur, mode dégradé, bloc par charge, encart
  défaut, menu de domaine.
- nymea_service : getLoadTelemetry(), clearLoadFault(), aiguillage de la
  notification. Une box sans la méthode s'annonce comme telle, pas comme en panne.

Seuil de fraîcheur à 180 s : le battement de cœur du plugin est de 60 s et le
timestamp avance d'un cycle par minute. Un seuil plus court crierait « données
anciennes » sur une installation vivante.

Fixtures v3 prises sur .75 après déploiement, dont un cycle porteur d'un verrou —
« lock » étant optionnel, un dump pris au mauvais moment ne l'éprouverait pas.

57 tests (30 avant), flutter analyze inchangé à 25 issues.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
2026-08-25 19:20:30 +02:00

308 lines
13 KiB
Plaintext

{
"@@locale": "fr",
"installerLoadsTitle": "Rôles & appareils",
"@installerLoadsTitle": {
"description": "Titre de l'écran installateur de configuration des charges pilotables"
},
"installerLoadsSubtitle": "Ordre de service des charges pilotables",
"loadsEmpty": "Aucune charge pilotable déclarée sur cette installation.",
"loadsLoadError": "Lecture de la configuration impossible : {error}",
"@loadsLoadError": {
"placeholders": { "error": { "type": "String" } }
},
"sectionUnclassified": "Non classé",
"@sectionUnclassified": {
"description": "Bucket des charges sans o:domain — jamais rangées d'office dans un domaine"
},
"domainEv": "Véhicule électrique",
"domainEcs": "Eau chaude sanitaire",
"domainHvac": "Climatisation",
"domainHeating": "Chauffage",
"domainUnknown": "Domaine « {code} »",
"@domainUnknown": {
"description": "Repli quand la box renvoie un domaine que cette version d'app ne connaît pas",
"placeholders": { "code": { "type": "String" } }
},
"adapterRelayRouter": "Routeur de relais",
"adapterEtmVariableLoad": "Charge modulable",
"adapterSgReady": "SG-Ready",
"adapterEvCharger": "Borne de recharge",
"adapterUnknown": "Mécanisme « {code} »",
"@adapterUnknown": {
"placeholders": { "code": { "type": "String" } }
},
"loadEnabled": "Active",
"loadDisabled": "Exclue de l'arbitrage",
"loadPriority": "Rang {rank}",
"@loadPriority": {
"placeholders": { "rank": { "type": "int" } }
},
"loadMinOn": "Maintien ON minimal : {seconds} s",
"@loadMinOn": {
"placeholders": { "seconds": { "type": "int" } }
},
"loadMinOff": "Maintien OFF minimal : {seconds} s",
"@loadMinOff": {
"placeholders": { "seconds": { "type": "int" } }
},
"loadRelayCount": "{count, plural, =1{1 relais} other{{count} relais}}",
"@loadRelayCount": {
"placeholders": { "count": { "type": "int" } }
},
"loadMeterSlot": "Compteur dédié : non rattachable",
"@loadMeterSlot": {
"description": "Emplacement visuel réservé — LoadConfig n'a aucun champ pour rattacher un compteur à une charge"
},
"orderNotGroupedTitle": "Ordre non groupé",
"orderNotGroupedBody": "L'ordre en place sur la box n'est pas groupé par domaine : les charges d'un même domaine ne se suivent pas. L'ordre réel est affiché ci-dessous, tel que la box l'applique.",
"orderNotGroupedAction": "Normaliser l'ordre",
"orderNotGroupedWarning": "Normaliser modifiera l'ordre d'arbitrage réel de l'installation. Aucune écriture ne part tant que vous n'avez pas confirmé.",
"orderNormalizeConfirm": "Normaliser et enregistrer",
"cancel": "Annuler",
"saveOrder": "Enregistrer l'ordre",
"savePending": "Envoi en cours…",
"saveWaitingBox": "En attente de confirmation de la box…",
"saveConfirmed": "Configuration confirmée par la box.",
"saveRejected": "La box a refusé : {reason}",
"@saveRejected": {
"placeholders": { "reason": { "type": "String" } }
},
"saveDiverged": "La box a enregistré un état différent de celui demandé. L'affichage montre ce que la box applique réellement.",
"validationDuplicateId": "L'identifiant de charge « {id} » apparaît deux fois.",
"@validationDuplicateId": {
"placeholders": { "id": { "type": "String" } }
},
"validationThingClaimedTwice": "L'appareil {thingId} est commandé par deux charges actives, « {first} » et « {second} » : deux commandes contradictoires et une puissance comptée deux fois.",
"@validationThingClaimedTwice": {
"placeholders": {
"thingId": { "type": "String" },
"first": { "type": "String" },
"second": { "type": "String" }
}
},
"telemetryUnavailableTitle": "Télémétrie indisponible",
"telemetryUnavailableBody": "Cette box n'expose pas GetLoadTelemetry. Puissance allouée, verrous, disponibilité et motifs de décision ne sont pas lisibles depuis l'app.",
"telemetryStale": "Données anciennes — dernier point il y a {seconds} s",
"@telemetryStale": {
"placeholders": { "seconds": { "type": "int" } }
},
"telemetryDegradedMode": "Mode dégradé L2 — compteur muet, planification suspendue",
"faultTitle": "En défaut",
"faultClearAction": "Lever le défaut",
"faultCleared": "Défaut levé — confirmé par la télémétrie relue.",
"faultClearNoop": "Aucun défaut à lever sur cette charge.",
"faultUnknown": "Défaut « {code} »",
"@faultUnknown": {
"placeholders": { "code": { "type": "String" } }
},
"decisionUnknown": "Décision « {code} »{params}",
"@decisionUnknown": {
"description": "Repli obligatoire quand la box renvoie un decision.code absent de cet ARB — box plus récente que l'app. Affiche le code brut et ses paramètres, jamais une phrase vide.",
"placeholders": {
"code": { "type": "String" },
"params": { "type": "String" }
}
},
"telemetryTitle": "Arbitrage en direct",
"telemetryNeverRan": "Aucun cycle d'arbitrage depuis le démarrage du service.",
"@telemetryNeverRan": {
"description": "timestamp absent — l'absence de cycle est une information, pas un zéro"
},
"telemetryLastCycle": "Dernier cycle il y a {seconds} s",
"@telemetryLastCycle": {
"placeholders": { "seconds": { "type": "int" } }
},
"telemetryBudgetTitle": "Budget de surplus du cycle",
"telemetryBudgetAbsent": "Aucun budget publié pour ce cycle : la planification est suspendue.",
"budgetSurplus": "Surplus entrant",
"budgetEvReserved": "Réservé au véhicule",
"budgetRecredited": "Recrédité",
"budgetAllocated": "Alloué",
"budgetRemaining": "Reliquat",
"watts": "{watts} W",
"@watts": {
"placeholders": { "watts": { "type": "int", "format": "decimalPattern" } }
},
"telemetryAllocated": "Alloué {watts} W",
"@telemetryAllocated": {
"placeholders": { "watts": { "type": "int", "format": "decimalPattern" } }
},
"telemetryNotArbitrated": "Non arbitrée — absente du dernier cycle publié",
"@telemetryNotArbitrated": {
"description": "Charge présente dans GetLoadConfig mais absente de loads[] : elle est désactivée, jamais perdue"
},
"lockMinOn": "Maintien ON — {seconds} s restantes",
"@lockMinOn": {
"placeholders": { "seconds": { "type": "int" } }
},
"lockMinOff": "Maintien OFF — {seconds} s restantes",
"@lockMinOff": {
"placeholders": { "seconds": { "type": "int" } }
},
"lockMinStateHold": "Maintien d'état — {seconds} s restantes",
"@lockMinStateHold": {
"placeholders": { "seconds": { "type": "int" } }
},
"lockUnknown": "Verrou « {kind} » — {seconds} s restantes",
"@lockUnknown": {
"placeholders": {
"kind": { "type": "String" },
"seconds": { "type": "int" }
}
},
"lockAsOfCycle": "au dernier cycle",
"@lockAsOfCycle": {
"description": "Les secondes restantes ne sont PAS décomptées côté app : elles datent du cycle publié"
},
"mechanismRelayStage": "Palier {stage} W sur {max} W",
"@mechanismRelayStage": {
"placeholders": {
"stage": { "type": "int", "format": "decimalPattern" },
"max": { "type": "int", "format": "decimalPattern" }
}
},
"mechanismVariableSetpoint": "Consigne {setpoint} W ({percent} %)",
"@mechanismVariableSetpoint": {
"placeholders": {
"setpoint": { "type": "int", "format": "decimalPattern" },
"percent": { "type": "int" }
}
},
"mechanismSgReadyState": "État SG-Ready {state} — estimation {estimated} W",
"@mechanismSgReadyState": {
"placeholders": {
"state": { "type": "int" },
"estimated": { "type": "int", "format": "decimalPattern" }
}
},
"mechanismEvCharger": "{current} A sur {phases} phase(s)",
"@mechanismEvCharger": {
"placeholders": {
"current": { "type": "double", "format": "decimalPattern" },
"phases": { "type": "int" }
}
},
"mechanismUnknown": "Mécanisme « {kind} »",
"@mechanismUnknown": {
"placeholders": { "kind": { "type": "String" } }
},
"decisionEvSurplus": "Véhicule rechargé sur le surplus photovoltaïque.",
"decisionEvSpotMarket": "Véhicule rechargé sur un créneau tarifaire favorable.",
"decisionEvDeadline": "Véhicule prioritaire : l'échéance approche.",
"decisionEvEcoMin": "Pas de surplus : le courant minimum est maintenu.",
"decisionEvIdle": "Pas de surplus : la recharge est suspendue.",
"decisionLoadUnavailable": "Charge en défaut : aucune commande n'est émise, et {frozen} W restent comptés au budget.",
"@decisionLoadUnavailable": {
"placeholders": { "frozen": { "type": "int", "format": "decimalPattern" } }
},
"decisionLockMinOn": "Le maintien ON a relevé la consigne à {applied} W ; le budget en payait {budget} W.",
"@decisionLockMinOn": {
"placeholders": {
"applied": { "type": "int", "format": "decimalPattern" },
"budget": { "type": "int", "format": "decimalPattern" }
}
},
"decisionLockMinOff": "Le maintien OFF interdit l'enclenchement ; le budget payait pourtant {requested} W sur {budget} W disponibles.",
"@decisionLockMinOff": {
"placeholders": {
"requested": { "type": "int", "format": "decimalPattern" },
"budget": { "type": "int", "format": "decimalPattern" }
}
},
"decisionLockMinOffCapped": "Le maintien OFF plafonne la consigne à {applied} W ; {requested} W étaient demandés sur {budget} W de budget.",
"@decisionLockMinOffCapped": {
"placeholders": {
"applied": { "type": "int", "format": "decimalPattern" },
"requested": { "type": "int", "format": "decimalPattern" },
"budget": { "type": "int", "format": "decimalPattern" }
}
},
"decisionSurplusSetpoint": "Consigne de {setpoint} W servie par le surplus ({budget} W de budget).",
"@decisionSurplusSetpoint": {
"placeholders": {
"setpoint": { "type": "int", "format": "decimalPattern" },
"budget": { "type": "int", "format": "decimalPattern" }
}
},
"decisionSurplusSetpointStepped": "Consigne de {setpoint} W servie par le surplus, arrondie sur un palier déclaré ({budget} W de budget).",
"@decisionSurplusSetpointStepped": {
"placeholders": {
"setpoint": { "type": "int", "format": "decimalPattern" },
"budget": { "type": "int", "format": "decimalPattern" }
}
},
"decisionSurplusInsufficient": "Le budget de {budget} W ne payait aucun palier — aucun verrou n'a mordu.",
"@decisionSurplusInsufficient": {
"placeholders": { "budget": { "type": "int", "format": "decimalPattern" } }
},
"decisionLockMinStateHold": "État {state} maintenu par protection anti court-cycle.",
"@decisionLockMinStateHold": {
"placeholders": { "state": { "type": "int" } }
},
"decisionSgForced": "Pompe à chaleur en état 4 (forcé) — estimation {estimated} W sur {budget} W de budget.",
"@decisionSgForced": {
"placeholders": {
"estimated": { "type": "int", "format": "decimalPattern" },
"budget": { "type": "int", "format": "decimalPattern" }
}
},
"decisionSgRecommended": "Pompe à chaleur en état 3 (recommandation) — estimation {estimated} W sur {budget} W de budget.",
"@decisionSgRecommended": {
"placeholders": {
"estimated": { "type": "int", "format": "decimalPattern" },
"budget": { "type": "int", "format": "decimalPattern" }
}
},
"decisionSgNormal": "Pompe à chaleur en état 2 (normal, sans consigne) — {budget} W de budget.",
"@decisionSgNormal": {
"placeholders": { "budget": { "type": "int", "format": "decimalPattern" } }
},
"decisionDegradedL2": "Consigne de repli : le compteur est muet et le watchdog a suspendu la planification.",
"decisionSafeStateRelay": "État sûr avant retrait : tous les relais sont ouverts.",
"decisionSafeStateSetpoint": "État sûr avant retrait : consigne ramenée à 0 W.",
"decisionSafeStateSgReady": "État sûr avant retrait : état 2 (normal), jamais le blocage.",
"faultWriteFailed": "Échec d'écriture : l'échelle de commande est épuisée, plus aucune commande n'est émise tant que le défaut n'est pas levé.",
"faultThingMissing": "L'appareil commandé est absent de la configuration nymea.",
"faultUnusableEncoding": "Encodage SG-Ready inexploitable : l'état 2 ne peut pas être exprimé, cette pompe à chaleur n'est pas pilotable.",
"faultClearPending": "Levée demandée — en attente de la télémétrie…",
"faultClearStillFaulty": "La charge est toujours en défaut après la levée.",
"faultClearRejected": "La box a refusé la levée : {reason}",
"@faultClearRejected": {
"placeholders": { "reason": { "type": "String" } }
},
"faultClearHint": "La levée est une action délibérée : le défaut ne se lève jamais tout seul.",
"domainBattery": "Batterie",
"domainAssignTitle": "Classer « {label} »",
"@domainAssignTitle": {
"placeholders": { "label": { "type": "String" } }
},
"domainAssignAction": "Changer de domaine",
"domainAssignNone": "Retirer du classement",
"domainAssignHint": "Le domaine est une intention déclarée par l'installateur. L'arbitre ne la lit pas : elle ne sert qu'à grouper l'ordre de service.",
"localeRequested": "Locale demandée : {requested} · retenue par la box : {effective}",
"@localeRequested": {
"placeholders": {
"requested": { "type": "String" },
"effective": { "type": "String" }
}
}
}