/// Changer le **mécanisme** d'une charge — la bascule de charge utile, et rien d'autre. /// /// ## Pourquoi ça ne peut pas être un simple `adapter: …` /// /// `LoadConfig` est une **union discriminée** (LM-300) : chaque mécanisme a sa charge /// utile, et celles des autres doivent être **absentes**. La box ne se contente pas de les /// ignorer, elle **refuse en bloc** : /// /// ``` /// sg-ready : relays[] interdit (charge utile d'un autre mécanisme) /// etmvariableload : relays[] interdit (réservé à relay-router) /// : sgReady interdit (charge utile du mécanisme sg-ready) /// ``` /// /// Écrire `adapter` sans nettoyer le reste produit donc une configuration que la box /// rejette — et le refus porte sur **toutes** les charges de l'appel, sans motif. /// /// ## Ce que la bascule coûte /// /// `adapter` et `mode` sont comparés par `sameHardware()` : changer de mécanisme /// **reconstruit l'adaptateur**. Ses contacts passent par l'état sûr, ses verrous se /// réarment à froid. Sur une PAC à 900 s, c'est un quart d'heure de machine figée. L'écran /// doit l'annoncer **avant** de valider — jamais après. /// /// ## Le mode `fixed` d'`etmvariableload` n'est PAS proposé /// /// Le multipalier natif existe encore côté box, qualifié de *legacy* par le plugin. /// [basculerMecanisme] ne le produit jamais : une bascule vers `etmvariableload` donne /// toujours `dynamic`. Mais une configuration déployée qui l'utilise se **lit** sans être /// réécrite — [estHeritageFixed] la reconnaît, et seule une conversion explicitement /// demandée la change. Ouvrir un écran ne doit convertir personne. library; import '../models/load_config_entry.dart'; /// Les trois mécanismes qu'une charge peut porter. const List kMecanismes = ['relay-router', 'etmvariableload', 'sg-ready']; /// Libellés d'interface. Des **codes** côté contrat, des mots côté écran. const Map kMecanismeLabels = { 'relay-router': 'Routeur de relais', 'etmvariableload': 'Modulable', 'sg-ready': 'SG-Ready', }; /// Vrai si cette charge est un `etmvariableload` en **multipalier natif** (`fixed`) — /// la forme *legacy* que l'app lit sans la proposer ni la réécrire. bool estHeritageFixed(LoadConfigEntry e) => e.isVariableLoad && e.mode == 'fixed' && e.powerLevelsInt.isNotEmpty; /// Charge utile d'une charge basculée vers [vers]. /// /// Repart de la map d'origine — donc en préserve tout ce qui n'appartient à aucun /// mécanisme (`id`, `label`, `domain`, `priority`, `enabled`, `needs`) — puis **retire les /// clés du mécanisme quitté** et pose celles du nouveau. /// /// Ce qui est **récupéré** quand c'est possible, parce que le jeter ferait perdre à /// l'installateur un travail qu'il vient de faire : /// /// - vers `sg-ready` depuis un routeur : les **deux premiers relais** deviennent SG1 et /// SG2, dans l'encodage standard ; /// - vers `relay-router` depuis une PAC : les contacts SG-Ready redeviennent des lignes de /// relais, **puissance à saisir** (une estimation d'état n'est pas la puissance d'une /// résistance) ; /// - `minOnS`/`minOffS` survivent partout où ils ont un sens. /// /// La charge produite peut être **incomplète** — un `etmvariableload` sans `maxPowerW`, /// par exemple. C'est voulu : le formulaire la refusera et dira quoi remplir, ce qui vaut /// mieux que d'inventer une valeur de plaque. Map basculerMecanisme(LoadConfigEntry e, String vers) { assert(kMecanismes.contains(vers)); // Les contacts que l'on peut récupérer, tous mécanismes confondus. final contacts = []; void ajouter(String id) { if (id.trim().isEmpty) return; if (contacts.any((x) => normalizeThingId(x) == normalizeThingId(id))) return; contacts.add(id); } for (final r in e.relays) { ajouter(r.thingId); } for (final s in e.sgReadyStates) { for (final r in s.relays) { ajouter(r); } } switch (vers) { case 'relay-router': // Les puissances d'origine si elles existent ; sinon 0, que le formulaire refusera // en le disant — une estimation d'état SG-Ready n'est pas la puissance d'une // résistance, et la transposer serait une invention. final parThing = { for (final r in e.relays) normalizeThingId(r.thingId): r.powerW }; return e.patched( adapter: 'relay-router', mode: 'fixed', relays: [ for (final c in contacts) {'thingId': c, 'powerW': parThing[normalizeThingId(c)] ?? 0} ], sgReady: const {}, powerLevels: const [], maxPowerW: 0, ); case 'etmvariableload': // TOUJOURS `dynamic` : le multipalier natif est *legacy* et ne se propose pas. return e.patched( adapter: 'etmvariableload', mode: 'dynamic', relays: const >[], sgReady: const {}, powerLevels: const [], maxPowerW: e.maxPowerW > 0 ? e.maxPowerW : 0, ); case 'sg-ready': default: final sg1 = contacts.isNotEmpty ? contacts[0] : ''; final sg2 = contacts.length > 1 ? contacts[1] : ''; // Encodage SG-Ready standard, celui que l'écran sait étiqueter : // SG1 fermé en 1 (blocage), SG2 fermé en 3 (recommandation), les deux en 4. final estimations = { for (final s in e.sgReadyStates) s.state: s.estimatedPowerW }; return e.patched( adapter: 'sg-ready', mode: 'fixed', relays: const >[], powerLevels: const [], maxPowerW: 0, sgReady: { // 900 s par défaut : la valeur d'une PAC réelle, pas celle du banc. Mieux vaut // une protection trop longue qu'un compresseur en court-cycle. 'minStateHoldS': e.minStateHoldS > 0 ? e.minStateHoldS : 900, 'states': [ {'state': 1, 'relays': [if (sg1.isNotEmpty) sg1], 'estimatedPowerW': estimations[1] ?? 0.0}, {'state': 2, 'relays': [], 'estimatedPowerW': estimations[2] ?? 0.0}, {'state': 3, 'relays': [if (sg2.isNotEmpty) sg2], 'estimatedPowerW': estimations[3] ?? 0.0}, { 'state': 4, 'relays': [if (sg1.isNotEmpty) sg1, if (sg2.isNotEmpty) sg2], 'estimatedPowerW': estimations[4] ?? 0.0, }, ], }, ); } } /// Convertit un `etmvariableload` **legacy** (`fixed` + paliers) en modulation continue. /// /// Geste **explicite**, jamais automatique : basculer `fixed` → `dynamic` change /// l'arbitrage réel de la charge. La box refuse par ailleurs `dynamic` sans `maxPowerW` et /// refuse `powerLevels` avec — d'où le plafond, que la déclaration en paliers ne porte pas /// et qu'il faut donc fournir. /// /// \param maxPowerW plafond de modulation, strictement positif. Map convertirEnModulation(LoadConfigEntry e, int maxPowerW) { return e.patched( adapter: 'etmvariableload', mode: 'dynamic', powerLevels: const [], relays: const >[], sgReady: const {}, maxPowerW: maxPowerW, ); }