docs: maquettes de la refonte « Charges pilotables » + CLAUDE.md recalé sur la box
Trois maquettes, neuf écrans, après le passage sur appareil qui a montré que /energy/setup décrit autre chose que l'installation. - charges_pilotables_mockup.html : la liste à deux niveaux, avec le bandeau d'arbitrage hérité de /energy/loads (qui disparaît) et la télémétrie sur la carte de chaque charge. - configurer_mecanismes_mockup.html : les six « Configurer » — routeur de relais, modulable, SG-Ready, borne, zones HVAC, sélecteur d'appareils. - raccordement_reseau_mockup.html : compteur réseau et protection de surcharge. Décisions de conception que les images ne portent pas seules : les paliers n'existent que chez le routeur de relais et y sont DÉDUITS des combinaisons ; le mode fixed d'etmvariableload (legacy) n'est plus proposé mais reste lisible, parce qu'ouvrir un écran ne doit pas convertir une config déployée ; SG1/SG2 sont une convention d'app retrouvée par l'encodage standard, la box ne stocke qu'une liste de relais par état ; le compteur par charge est dessiné inactif avec sa raison ; la borne règle un mode par DÉFAUT, pas une commande. CLAUDE.md corrigé contre JSONRPC.Introspect (plugin 1.15.2+etm15) : - Energy.SetChargingMode n'existe pas — le « bug critique » décrivait un appel impossible. Energy.* n'a que cinq méthodes. - Le namespace EnergyPlugin.* n'existe pas : c'est NymeaEnergy.*, 20 méthodes et 14 notifications, toutes listées. - ChargingInfo : chargingMode / targetPercentage / endDateTime (Uint), et non mode / targetSoc / endTime. minCurrent n'existe nulle part. - phasePowerLimit est en AMPÈRES par phase, et 0 coupe toute la recharge intelligente. - AirConditioning expose 8 méthodes, pas 3. Toutes les méthodes citées ont été revérifiées une par une : aucune introuvable. Règle ajoutée : contrôler ce fichier contre Introspect avant chaque lot — trois fois aujourd'hui il a envoyé un agent dans le vide. Et docs/BRIEF_plugin_depuis_maquettes.md : les six points que les maquettes remontent vers le moteur, dont un seul blocage dur (pas de champ de courant minimum pour le mode Min+PV). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
This commit is contained in:
parent
47cc0b1f72
commit
054cbfab3c
124
CLAUDE.md
124
CLAUDE.md
@ -29,69 +29,130 @@ mais la source de vérité du tier est dans `powersync-optimizer`.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Bug critique — à corriger en priorité absolue
|
||||
## 🔴 Recharge EV — l'appel réel, vérifié contre la box
|
||||
|
||||
> **Ce bloc décrivait un bug qui n'existe plus, avec des champs qui n'ont jamais existé.**
|
||||
> `Energy.SetChargingMode` **n'est pas une méthode de nymea** — il n'y en a que cinq dans
|
||||
> `Energy.*`, et celle-là n'en fait pas partie : l'appel échouerait sur « No such method ».
|
||||
> Et le namespace `EnergyPlugin.*` n'existe pas non plus. Corrigé le 2026-08-25 contre
|
||||
> `JSONRPC.Introspect` sur `.75` (plugin **1.15.2+etm15**).
|
||||
|
||||
```dart
|
||||
// ❌ FAUX — ne déclenche pas le SmartChargingManager
|
||||
nymeaService.call('Energy.SetChargingMode', {'mode': 'pv'});
|
||||
|
||||
// ✅ CORRECT — appelle vraiment le plugin ETM
|
||||
nymeaService.call('EnergyPlugin.SetChargingInfo', {
|
||||
nymeaService.call('NymeaEnergy.SetChargingInfo', {
|
||||
'chargingInfo': {
|
||||
'evChargerId': chargerId,
|
||||
'mode': 'Eco', // Eco | EcoWithMinCurrent | Normal
|
||||
// EcoWithTargetTime | EcoMinWithTargetTime
|
||||
'minCurrent': 6, // requis pour EcoWithMinCurrent/*
|
||||
'targetSoc': 80, // requis pour *WithTargetTime
|
||||
'endTime': null, // requis pour *WithTargetTime
|
||||
'evChargerId': chargerId, // Uuid — requis
|
||||
'chargingMode': 'ChargingModeEco', // requis, énumération ci-dessous
|
||||
'targetPercentage': 80, // o: Uint — charge cible
|
||||
'endDateTime': 1786250400, // o: Uint — horodatage, PAS une chaîne
|
||||
'repeatDays': [1, 2, 3, 4, 5], // o: Int[]
|
||||
'assignedCarId': carId, // o: Uuid
|
||||
'spotMarketChargingEnabled': false, // o: Bool
|
||||
'dailySpotMarketPercentage': 30, // o: Uint
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Modes UI EV — 3 boutons + option deadline
|
||||
`chargingState` est en **lecture seule** (`r:`) dans la charge utile :
|
||||
`ChargingStateIdle` · `ChargingStateSurplusCharging` · `ChargingStateSpotMarketCharging` ·
|
||||
`ChargingStateTimeRequirement`.
|
||||
|
||||
### Énumération `ChargingMode` — les vrais noms
|
||||
|
||||
`ChargingModeNormal` · `ChargingModeEco` · `ChargingModeEcoWithTargetTime` ·
|
||||
`ChargingModeEcoWithMinCurrent` · `ChargingModeEcoMinWithTargetTime`
|
||||
|
||||
### Modes UI EV — 3 boutons + option échéance
|
||||
```
|
||||
[PV] [Min+PV] [Boost]
|
||||
↓
|
||||
Option deadline ? → afficher champs SOC cible + heure d'arrivée
|
||||
Option échéance ? → charge cible (targetPercentage) + heure (endDateTime)
|
||||
|
||||
PV → mode: Eco
|
||||
Min+PV → mode: EcoWithMinCurrent, minCurrent: 6
|
||||
Boost → mode: Normal
|
||||
PV + deadline → mode: EcoWithTargetTime, targetSoc, endTime
|
||||
Min+PV+deadline → mode: EcoMinWithTargetTime, minCurrent, targetSoc, endTime
|
||||
PV → ChargingModeEco
|
||||
Min+PV → ChargingModeEcoWithMinCurrent
|
||||
Boost → ChargingModeNormal
|
||||
PV + échéance → ChargingModeEcoWithTargetTime + targetPercentage + endDateTime
|
||||
Min+PV + échéance → ChargingModeEcoMinWithTargetTime + targetPercentage + endDateTime
|
||||
```
|
||||
|
||||
> ⚠️ **`minCurrent` n'existe nulle part.** Le mode `ChargingModeEcoWithMinCurrent` est bien
|
||||
> dans l'énumération, mais `ChargingInfo` ne porte **aucun champ** de courant minimum. Soit
|
||||
> il est fixé ailleurs (paramètre du Thing borne), soit il manque côté plugin. **Ne pas
|
||||
> proposer de réglage de courant minimum tant que ce n'est pas tranché** — un champ qui ne
|
||||
> part nulle part est pire que pas de champ.
|
||||
|
||||
---
|
||||
|
||||
## APIs JSON-RPC consommées
|
||||
|
||||
### `Energy.*` — nymea-experience-plugin-energy
|
||||
### `Energy.*` — nymea-experience-plugin-energy · **5 méthodes**
|
||||
| Méthode | Usage | Tier |
|
||||
|---|---|---|
|
||||
| `GetPowerBalance` | Dashboard temps réel | Community |
|
||||
| `GetPowerBalanceLogs(sampleRate, from, to)` | Historique — envoyer les bons `from/to` pour 90j | Community |
|
||||
| `GetThingPowerLogs(sampleRate, thingIds[], from, to)` | Historique par device | Community |
|
||||
| `GetRootMeter` | Lire le compteur racine configuré | Community |
|
||||
| `SetRootMeter(rootMeterThingId)` | Config installateur | Community |
|
||||
|
||||
### `EnergyPlugin.*` — powersync-energy-plugin-etm
|
||||
> `Energy.*` s'arrête là. **`SetChargingMode` n'existe pas** — ni ici, ni ailleurs.
|
||||
|
||||
### `NymeaEnergy.*` — powersync-energy-plugin-etm · **20 méthodes / 14 notifications**
|
||||
|
||||
> ⚠️ Le namespace est **`NymeaEnergy`**, pas `EnergyPlugin`. Ce dernier n'existe pas.
|
||||
> Source d'autorité : `energyplugin/nymeaenergyjsonhandler.cpp` et `JSONRPC.Introspect`.
|
||||
> **Ce n'est pas dans `OPTIMIZER_PROTOCOL.md`** — ce document décrit le protocole
|
||||
> plugin ↔ optimiseur (couche A ↔ couche B), pas la frontière JSON-RPC de l'app.
|
||||
|
||||
**Charges pilotées**
|
||||
| Méthode | Usage | Tier |
|
||||
|---|---|---|
|
||||
| `GetLoadConfig` | Lire les charges déclarées | Community |
|
||||
| `SetLoadConfig(loadConfigs[])` | Écrire — **remplace TOUT l'ensemble**, refus en bloc | Community |
|
||||
| `GetLoadTelemetry` | État runtime de l'arbitrage (budget, allocation, verrous, motifs) | Community |
|
||||
| `ClearLoadFault(loadId)` | Lever le verrou de défaut — **l'ack ne prouve rien** | Community |
|
||||
|
||||
**Recharge EV**
|
||||
| Méthode | Usage | Tier |
|
||||
|---|---|---|
|
||||
| `GetChargingInfos(evChargerId)` | Lire config EV | Community |
|
||||
| `SetChargingInfo(chargingInfo)` | **⚠️ CORRECTION CRITIQUE** | Community |
|
||||
| `SetChargingInfo(chargingInfo)` | Écrire config EV — voir le bloc plus haut | Community |
|
||||
| `GetChargingSchedules(evChargerId)` | Planning EV | Community |
|
||||
| `GetAvailableSpotMarketProviders()` | Liste providers | Community |
|
||||
| `SetSpotMarketConfiguration(enabled, providerId)` | Config tarif dynamique | Predict AI |
|
||||
| `GetSpotMarketScoreEntries(date)` | Cotations aWATTar | Predict AI |
|
||||
| `SetPhasePowerLimit(Uint)` | Config installateur | Community |
|
||||
| `SetAcquisitionTolerance(Double)` | Seuil surplus | Community |
|
||||
| `GetLockOnUnplug` / `SetLockOnUnplug(Bool)` | Verrouillage au débranchement | Community |
|
||||
|
||||
### `AirConditioning.*` — nymea-experience-plugin-airconditioning
|
||||
**Réglages globaux d'installation**
|
||||
| Méthode | Usage | Tier |
|
||||
|---|---|---|
|
||||
| `GetPhasePowerLimit` / `SetPhasePowerLimit(Uint)` | **AMPÈRES par phase**, pas des watts. `0` = désactivé, et coupe toute la recharge intelligente | Community |
|
||||
| `GetAcquisitionTolerance` / `SetAcquisitionTolerance(Double)` | Seuil de surplus au démarrage — **unité non documentée par la box** | Community |
|
||||
| `GetBatteryLevelConsideration` / `SetBatteryLevelConsideration(Double)` | Part du stockage prise en compte | Community |
|
||||
|
||||
**Tarifs dynamiques**
|
||||
| Méthode | Usage | Tier |
|
||||
|---|---|---|
|
||||
| `GetAvailableSpotMarketProviders` | Liste providers | Predict AI |
|
||||
| `GetSpotMarketConfiguration` / `SetSpotMarketConfiguration(enabled, providerId)` | Config tarif dynamique | Predict AI |
|
||||
| `GetSpotMarketScoreEntries(date)` | Cotations aWATTar | Predict AI |
|
||||
| `GetEnergyRatios` | Autoconsommation / autonomie | Community |
|
||||
|
||||
**Notifications** — `LoadConfigChanged` · `LoadTelemetryChanged` · `ChargingInfoAdded/Changed/Removed` ·
|
||||
`ChargingSchedulesChanged` · `EnergyRatiosChanged` · `PhasePowerLimitChanged` ·
|
||||
`AcquisitionToleranceChanged` · `BatteryLevelConsiderationChanged` · `LockOnUnplugChanged` ·
|
||||
`SpotMarketConfigurationChanged` · `SpotMarketStatusChanged` · `SpotMarketScoreEntriesChanged`
|
||||
|
||||
### `AirConditioning.*` — nymea-experience-plugin-airconditioning · **8 méthodes**
|
||||
| Méthode | Usage | Tier |
|
||||
|---|---|---|
|
||||
| `GetZones` | Afficher zones PAC/thermostat | Auto |
|
||||
| `SetZoneSetpointOverride` | Pilotage manuel | Auto |
|
||||
| `AddZone` / `RemoveZone` | CRUD de zones | Auto |
|
||||
| `SetZoneName` | Renommer | Auto |
|
||||
| `SetZoneThings(zoneId, thermostats[], valves[], indoorSensors[], outdoorSensors[], windowSensors[], notifications[])` | Rattacher les appareils | Auto |
|
||||
| `SetZoneSetpointOverride(zoneId, mode, setpointOverride, minutes)` | Dérogation manuelle | Auto |
|
||||
| `SetZoneStandbySetpoint` | Consigne de réduit | Auto |
|
||||
| `SetZoneWeekSchedule` | Planning 7 jours | Auto |
|
||||
|
||||
> `ZoneInfo` est **presque entièrement en lecture** (`r:`) : température, humidité, PM2.5, COV,
|
||||
> consigne courante, dérogation et sa fin, listes d'appareils. On écrit par les six `Set*`,
|
||||
> jamais par un objet complet.
|
||||
|
||||
### `Rules.*` — nymea core
|
||||
| Méthode | Usage | Tier |
|
||||
|---|---|---|
|
||||
@ -175,6 +236,9 @@ batteryGreen boostRed pvGreen minPvBlue accentTeal
|
||||
## Règles de modification
|
||||
- Tout nouvel écran → valider maquette avec Patrick avant de coder
|
||||
- Tout nouvel appel RPC → vérifier dans `INTERFACE.md` que la méthode existe
|
||||
- Ne jamais appeler `Energy.SetChargingMode` pour piloter un EV
|
||||
- Piloter un EV = `NymeaEnergy.SetChargingInfo` (`Energy.SetChargingMode` n'existe pas)
|
||||
- **Vérifier ce fichier contre `JSONRPC.Introspect` avant chaque lot.** Trois fois le
|
||||
2026-08-25, la doc locale était en retard sur la machine et a envoyé un agent dans le vide.
|
||||
La sonde tient en une ligne : `dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect`
|
||||
- Toujours tester la persistance : killer l'app et vérifier que les données survivent
|
||||
- Flavors à configurer : `com.etm-powersync.community` / `.auto` / `.predictai`
|
||||
|
||||
60
RECAP.md
60
RECAP.md
@ -260,6 +260,66 @@ autres (`cycleAge`, `sinceLastFrame`) sont court-circuités par l'éjection.
|
||||
la cause racine du défaut 2 ; sans ça, un glisser-déposer n'y écrira jamais un `priority`
|
||||
réel.
|
||||
|
||||
## Refonte décidée le 2026-08-25 (soir) — maquettes validées
|
||||
|
||||
Patrick a tranché la question ouverte du RECAP : **l'écran « Ordre de service des charges »
|
||||
(`/energy/loads`) disparaît.** Le rang se règle dans « Rôles & appareils → Charges
|
||||
pilotables » au glisser-déposer, le **regroupement à deux niveaux est conservé**, et le bloc
|
||||
de télémétrie descend sur la carte de chaque charge.
|
||||
|
||||
| Maquette | Contenu |
|
||||
|---|---|
|
||||
| `docs/mockups/charges_pilotables_mockup.html` | La liste : bandeau arbitrage, domaines glissables, cartes avec télémétrie, défaut, bouton Configurer |
|
||||
| `docs/mockups/configurer_mecanismes_mockup.html` | Les six « Configurer » : relais · modulable · SG-Ready · borne · zones HVAC · sélecteur d'appareils |
|
||||
| `docs/mockups/raccordement_reseau_mockup.html` | Compteur réseau + protection de surcharge |
|
||||
|
||||
**Décisions de conception qui ne se déduisent pas des maquettes :**
|
||||
|
||||
- **Les paliers n'existent que chez le routeur de relais**, et ils y sont *déduits* des
|
||||
combinaisons de contacts, jamais saisis. Le modulable ne connaît qu'un plafond
|
||||
(`maxPowerW`). Le mode `fixed` d'`etmvariableload` — que le plugin qualifie lui-même de
|
||||
*legacy* — **n'est plus proposé**, mais reste **lisible** : ouvrir l'écran ne doit pas
|
||||
convertir une config déployée.
|
||||
- **Le critère de choix entre les deux ECS**, formulé pour l'installateur : *où sont les
|
||||
contacteurs ?* Sur les sorties de la box → routeur de relais. Dans l'appareil, piloté par
|
||||
une consigne → modulable.
|
||||
- **SG1 / SG2 n'existent pas côté box.** `sgReady.states[].relays[]` n'est qu'une liste par
|
||||
état. Les étiquettes sont une convention d'app, retrouvée à la lecture par l'encodage
|
||||
standard (SG1 = fermé en état 1, SG2 = fermé en état 3). Un encodage non standard perd les
|
||||
noms, pas la configuration.
|
||||
- **Le compteur par charge figure dans les cinq écrans, inactif**, avec la raison sur place.
|
||||
Aucun champ n'existe côté box, et nymea rejette toute clé inconnue avant le handler.
|
||||
- **La borne règle un mode PAR DÉFAUT**, pas une commande — le pilotage vit sur la carte
|
||||
véhicule du tableau de bord.
|
||||
|
||||
## Ce qui remonte vers le plugin
|
||||
|
||||
Six points, rédigés en brief exploitable : `docs/BRIEF_plugin_depuis_maquettes.md`.
|
||||
Le seul **blocage dur** est le premier — `ChargingInfo` n'a aucun champ de courant minimum,
|
||||
donc le mode Min+PV reste sélectionnable mais non réglable.
|
||||
|
||||
Les cinq autres : motif de refus de `SetLoadConfig` qui ne franchit pas la frontière ·
|
||||
`WRITE_FAILED` qui masque `THING_MISSING` · paliers atteignables non exposés (l'app devra
|
||||
sinon dupliquer la combinatoire de `relayrouter.cpp`) · état matériel de `enabled:false` à
|
||||
confirmer pour relais et modulable · `estimatedPowerW` du banc encore fictif.
|
||||
|
||||
## Contrôle de la doc contre la machine — à refaire avant chaque lot
|
||||
|
||||
`CLAUDE.md` a été **corrigé contre `JSONRPC.Introspect`** le 2026-08-25. Trois fois dans la
|
||||
journée la doc locale était en retard sur la box :
|
||||
|
||||
1. le namespace `LoadConfig` cherché dans `OPTIMIZER_PROTOCOL.md`, où il n'est pas (note de
|
||||
périmètre ajoutée côté plugin, commit `afaa8c2`) ;
|
||||
2. le « bug critique » `Energy.SetChargingMode` — **cette méthode n'existe pas**, l'appel
|
||||
échouerait sur « No such method » ;
|
||||
3. les champs de `ChargingInfo` (`mode`/`minCurrent`/`targetSoc`/`endTime` au lieu de
|
||||
`chargingMode`/`targetPercentage`/`endDateTime`), le namespace `EnergyPlugin.*`
|
||||
inexistant, et `AirConditioning` donné pour 3 méthodes quand il en expose 8.
|
||||
|
||||
Toutes les méthodes citées par `CLAUDE.md` ont été revérifiées une par une : **aucune
|
||||
introuvable**. La sonde tient en une ligne —
|
||||
`dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect`.
|
||||
|
||||
## Vérifié / non vérifié (lot B app)
|
||||
|
||||
| Critère | Statut |
|
||||
|
||||
196
docs/BRIEF_plugin_depuis_maquettes.md
Normal file
196
docs/BRIEF_plugin_depuis_maquettes.md
Normal file
@ -0,0 +1,196 @@
|
||||
# Brief plugin — ce que les maquettes « Configurer » demandent au moteur
|
||||
|
||||
**Origine :** session du 2026-08-25, côté app. Écrit après le passage sur appareil et la
|
||||
conception des six écrans « Configurer » (`docs/mockups/`).
|
||||
**Cible :** `etm-powersync-energy-plugin-etm`, plugin déployé au moment du relevé :
|
||||
**1.15.2+etm15** sur `.75`.
|
||||
**Méthode :** chaque point est un constat vérifié contre la machine ou la source, pas une
|
||||
préférence d'UI. Les maquettes sont jointes — elles montrent où chaque manque se voit.
|
||||
|
||||
---
|
||||
|
||||
## 1. `SetChargingInfo` ne permet pas l'écran borne — seul blocage dur
|
||||
|
||||
**Ce que l'écran veut** (`configurer_mecanismes_mockup.html`, écran ③) : trois modes
|
||||
`[PV] [Min+PV] [Boost]`, une option d'échéance de départ avec charge cible et heure, et le
|
||||
basculement automatique vers les variantes `…WithTargetTime`.
|
||||
|
||||
**Ce que la box expose**, introspecté sur `.75` :
|
||||
|
||||
```
|
||||
ChargingInfo = chargingMode · evChargerId
|
||||
· o:assignedCarId · o:dailySpotMarketPercentage · o:endDateTime (Uint)
|
||||
· o:repeatDays · o:spotMarketChargingEnabled · o:targetPercentage
|
||||
· r:chargingState
|
||||
```
|
||||
|
||||
L'échéance est **partiellement là** : `targetPercentage` et `endDateTime` existent, et les
|
||||
modes `ChargingModeEcoWithTargetTime` / `ChargingModeEcoMinWithTargetTime` sont dans
|
||||
l'énumération. Ce qui manque est ailleurs :
|
||||
|
||||
**⛔ `minCurrent` n'existe nulle part.** `ChargingModeEcoWithMinCurrent` est un mode valide,
|
||||
mais aucun champ de `ChargingInfo` ne porte le courant à maintenir. Deux issues possibles,
|
||||
et elles ne demandent pas le même travail :
|
||||
|
||||
- le courant est déjà fixé ailleurs (paramètre du Thing borne, réglage global) → **le dire**,
|
||||
et l'app pointera l'utilisateur au bon endroit ;
|
||||
- il manque → **ajouter le champ**, et dire s'il est par borne ou global.
|
||||
|
||||
Tant que ce n'est pas tranché, l'app **ne proposera pas** de réglage de courant minimum : un
|
||||
champ qui ne part nulle part est pire que pas de champ. Le mode Min+PV reste donc à moitié
|
||||
livrable — sélectionnable, mais non réglable.
|
||||
|
||||
> Corollaire déjà traité côté app : `CLAUDE.md` documentait `mode` / `minCurrent` /
|
||||
> `targetSoc` / `endTime` sur un namespace `EnergyPlugin.*` inexistant. Corrigé le
|
||||
> 2026-08-25 contre `Introspect`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Le motif de refus de `SetLoadConfig` ne franchit pas la frontière
|
||||
|
||||
**Constat, reproduit deux fois.** Une charge invalide fait répondre :
|
||||
|
||||
```json
|
||||
{"params":{"energyError":"EnergyErrorInvalidParameter"},"status":"success"}
|
||||
```
|
||||
|
||||
…et rien d'autre. Le motif reste au journal de la box :
|
||||
|
||||
```
|
||||
W | NymeaEnergy: [NymeaEnergy] SetLoadConfig rejetée:
|
||||
"Config \"SONDE-TEST-A-SUPPRIMER\" invalide : etmvariableload fixed : powerLevels requis (non vide)"
|
||||
```
|
||||
|
||||
**Pourquoi ça compte maintenant.** Les six écrans « Configurer » écrivent tous par
|
||||
`SetLoadConfig`, et le refus est **en bloc** : une seule charge invalide fait échouer
|
||||
l'enregistrement de toutes. L'installateur apprendra qu'on a refusé, jamais pourquoi ni sur
|
||||
quelle charge — devant un client, avec un tableau électrique ouvert.
|
||||
|
||||
**Ce que l'app peut faire seule, et sa limite.** Rejouer `validateSet()` côté client couvre
|
||||
l'unicité des ids et des Things. Elle ne couvre pas les règles de mécanisme
|
||||
(`powerLevels` commençant par 0, `powerW > 0`, `relays[]` interdit en modulable, `mode`
|
||||
imposé…) sans dupliquer `LoadConfig::validate()` — deuxième implémentation, deuxième
|
||||
divergence.
|
||||
|
||||
**Demande.** Le même remède que la télémétrie : un **code** et des **paramètres**, pas une
|
||||
phrase. Par exemple, à côté de `energyError` :
|
||||
|
||||
```json
|
||||
{"energyError":"EnergyErrorInvalidParameter",
|
||||
"rejection":{"loadId":"chauffe-eau","code":"POWERLEVELS_REQUIRED","params":{"mode":"fixed"}}}
|
||||
```
|
||||
|
||||
Le libellé français est fabriqué par l'app, comme pour `decision.code`.
|
||||
|
||||
---
|
||||
|
||||
## 3. `WRITE_FAILED` masque `THING_MISSING`
|
||||
|
||||
**Constat**, provoqué délibérément sur `.75` (`tools/rpc/fault_probe.dart`) : une charge
|
||||
`etmvariableload` dont le ThingId n'existe pas publie **`WRITE_FAILED`**, pas
|
||||
`THING_MISSING`. L'écriture est tentée, échoue faute de Thing, l'échelle s'épuise, et
|
||||
`m_faulted` prend le pas — l'ordre des `if` dans `etmvariableloadadapter.cpp:148-150` :
|
||||
|
||||
```cpp
|
||||
if (m_faulted) v.faultCode = "WRITE_FAILED";
|
||||
else if (!thingManager->findConfiguredThing(...)) v.faultCode = "THING_MISSING";
|
||||
```
|
||||
|
||||
**Pourquoi ça compte.** Les deux codes appellent **deux gestes différents** chez le client.
|
||||
« Échec d'écriture » envoie chercher un défaut de liaison — bus Modbus, alimentation,
|
||||
contacteur. « Appareil absent » envoie dans la configuration nymea. L'app rend fidèlement ce
|
||||
que la box dit, et la box envoie au mauvais endroit.
|
||||
|
||||
**Demande.** Faire primer la cause structurelle : un Thing absent est un `THING_MISSING`,
|
||||
même si des écritures ont échoué entre-temps. Ou publier les deux.
|
||||
|
||||
---
|
||||
|
||||
## 4. Les paliers atteignables ne sont pas exposés
|
||||
|
||||
**Constat.** `mechanism` (kind `relay`) publie `stageW` et `maxStageW` — **la liste des
|
||||
paliers atteignables n'existe dans aucun champ** :
|
||||
|
||||
```
|
||||
mechanism = kind · o:stageW · o:maxStageW · o:setpointW · o:percent · o:maxPowerW
|
||||
· o:state · o:estimatedPowerW · o:chargingEnabled · o:currentA
|
||||
· o:phaseCount · o:pluggedIn
|
||||
```
|
||||
|
||||
**Ce que la maquette affiche** (écran ①, routeur de relais) :
|
||||
|
||||
> 3 relais → paliers atteignables : **0 · 500 · 1 000 · 1 500 · 2 000 · 2 500 · 3 000 · 3 500 W**
|
||||
|
||||
C'est l'aide la plus utile du formulaire : elle dit à l'installateur ce que son câblage
|
||||
permet, avant qu'il n'enregistre.
|
||||
|
||||
**Le risque.** Faute d'exposition, l'app **dérivera** cette liste — donc réimplémentera la
|
||||
combinatoire de `relayrouter.cpp` (toutes les combinaisons 2^N, doublons écartés, plafond
|
||||
16 relais). Deux implémentations d'une même règle divergeront : à la première correction
|
||||
côté moteur, l'app annoncera des paliers que la box n'atteint plus.
|
||||
|
||||
**Demande.** Exposer la liste — soit dans la télémétrie (`mechanism.stagesW: [...]`), soit en
|
||||
retour d'un `GetLoadConfig` enrichi. À défaut, **le dire explicitement**, et l'app
|
||||
documentera sa dérivation comme un miroir à resynchroniser.
|
||||
|
||||
---
|
||||
|
||||
## 5. `enabled: false` — ce que deviennent les contacts
|
||||
|
||||
**Ce que l'app sait déjà**, et qui est bon : la charge quitte l'arbitrage, ne consomme aucun
|
||||
budget, et disparaît de `loads[]` **au cycle suivant** — pas dans la seconde.
|
||||
|
||||
**Ce qu'il reste à établir côté app :** l'état matériel. Le relevé du lot B-bis (§4) répond
|
||||
sur `sg-ready` — contacts K1/K2 mesurés ouverts après désactivation, l'état sûr passe bien
|
||||
par `applySafeState()` avant retrait de l'adaptateur. **Reste à confirmer pour
|
||||
`relay-router` et `etmvariableload`**, sur matériel.
|
||||
|
||||
**Pourquoi ça compte.** La maquette écrit aujourd'hui « à la désactivation, le retrait passe
|
||||
par l'état sûr : tous les relais ouverts ». Si c'est faux pour un mécanisme, l'écran laisse
|
||||
croire à une coupure qui n'a pas eu lieu — sur un chauffe-eau, c'est une promesse de
|
||||
sécurité.
|
||||
|
||||
**Demande.** Confirmer, par mécanisme, ce que produit `enabled: false` sur le matériel. Une
|
||||
ligne de journal par adaptateur suffit ; c'est déjà le cas pour SG-Ready.
|
||||
|
||||
---
|
||||
|
||||
## 6. `estimatedPowerW` SG-Ready reste fictif sur `.75`, et il alimente le recrédit
|
||||
|
||||
**Constat, déjà relevé côté plugin** (`RELEVE_LOTBbis.md` §7) : les valeurs {état 3 : 1500 W,
|
||||
état 4 : 3000 W} et `minStateHoldS: 300` sont reconduites du bloc codé en dur supprimé, où
|
||||
elles décrivaient une PAC **inventée**. Aucune PAC n'est sous tension sur K1/K2.
|
||||
|
||||
**Ce que ça change côté app.** L'écran SG-Ready (écran ②) présente `estimatedPowerW` comme
|
||||
une **donnée d'installation**, saisie par l'installateur d'après la plaque signalétique. Sur
|
||||
le banc, c'est une valeur de test — et elle sert de base au **recrédit de budget** et au
|
||||
choix d'état, donc elle décale l'allocation de tout le waterfall en aval.
|
||||
|
||||
**Demande.** Rien à changer dans le code : c'est un point de **données de banc**. Mais tant
|
||||
qu'il n'est pas clos, aucune capture de `.75` ne doit servir d'exemple d'installation, et le
|
||||
§4 de `TEST_TERRAIN.md` reste non transposable.
|
||||
|
||||
---
|
||||
|
||||
## Ce que l'app livre sans rien attendre
|
||||
|
||||
Pour situer ce qui est bloqué et ce qui ne l'est pas :
|
||||
|
||||
| Écran | État |
|
||||
|---|---|
|
||||
| Liste « Charges pilotables » à deux niveaux, télémétrie sur carte | livrable |
|
||||
| Configurer · routeur de relais | livrable — sauf l'aperçu des paliers (point 4) |
|
||||
| Configurer · modulable | livrable |
|
||||
| Configurer · SG-Ready | livrable |
|
||||
| Configurer · borne de recharge | **bloqué sur le point 1** pour le mode Min+PV |
|
||||
| Zones HVAC + sélecteur d'appareils | livrable côté RPC — **aucun matériel HVAC sur `.75`** |
|
||||
| Raccordement réseau / protection de surcharge | livrable, mais voir ci-dessous |
|
||||
|
||||
**Et une question de périmètre, pas un défaut.** La protection de surcharge ne déleste
|
||||
aujourd'hui **que les bornes de recharge** (`m_evChargers` dans
|
||||
`SmartChargingManager::verifyOverloadProtection`). Le chauffe-eau, la PAC et les charges
|
||||
modulables ne sont pas réduits par elle. L'écran « Raccordement » le dit explicitement plutôt
|
||||
que de promettre une protection générale. **Étendre le délestage au waterfall est une
|
||||
décision produit** — si elle est prise, l'app aura besoin d'un ordre de délestage, qui n'est
|
||||
pas l'ordre de service : le premier retire de la puissance en surcharge, le second en
|
||||
répartit en surplus. Deux listes qui peuvent légitimement différer.
|
||||
317
docs/mockups/charges_pilotables_mockup.html
Normal file
317
docs/mockups/charges_pilotables_mockup.html
Normal file
@ -0,0 +1,317 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ETM PowerSync — Charges pilotables (liste à deux niveaux)</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#081f2b; --panel:#0d2b3b; --card:#12384a; --card2:#0f3242;
|
||||
--line:rgba(255,255,255,.08); --line2:rgba(255,255,255,.14);
|
||||
--blue:#31a3dd; --yellow:#fec113; --green:#35c98a; --red:#e07a7a;
|
||||
--txt:#e8f1f6; --muted:#90a8b6; --muted2:#6f8b9a; --r:16px;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:
|
||||
radial-gradient(900px 500px at 15% -10%, #0e3145 0%, transparent 60%),
|
||||
radial-gradient(900px 600px at 110% 10%, #0b2535 0%, transparent 55%),
|
||||
var(--bg);
|
||||
font-family:'Inter',system-ui,sans-serif;color:var(--txt);min-height:100vh;padding:34px 20px 60px}
|
||||
.head{max-width:1180px;margin:0 auto 26px}
|
||||
.head h1{font-family:'Poppins';font-weight:700;font-size:20px;margin:0 0 6px}
|
||||
.head p{margin:0 0 8px;color:var(--muted);font-size:13.5px;line-height:1.55;max-width:860px}
|
||||
.head code{color:#9fd4ee}
|
||||
.head b{color:var(--yellow)}
|
||||
.stage{display:flex;gap:34px;justify-content:center;flex-wrap:wrap;align-items:flex-start}
|
||||
.col{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:392px}
|
||||
.cap{color:var(--muted2);font-size:12px;font-family:'Poppins';font-weight:500;letter-spacing:.4px;text-transform:uppercase}
|
||||
|
||||
.phone{width:392px;background:var(--panel);border:1px solid var(--line2);border-radius:30px;
|
||||
overflow:hidden;box-shadow:0 30px 80px -30px rgba(0,0,0,.7);display:flex;flex-direction:column;height:860px}
|
||||
.appbar{display:flex;align-items:center;gap:12px;padding:18px 16px 14px;border-bottom:1px solid var(--line);
|
||||
background:linear-gradient(#102f40,#0d2b3b)}
|
||||
.appbar .ic{width:26px;height:26px;display:grid;place-items:center;color:var(--muted);font-size:18px}
|
||||
.appbar h2{font-family:'Poppins';font-weight:600;font-size:15.5px;margin:0;flex:1;text-align:center}
|
||||
.scroll{overflow:auto;padding:16px 15px 26px}
|
||||
.scroll::-webkit-scrollbar{width:0}
|
||||
|
||||
.sect{font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);letter-spacing:.3px;
|
||||
margin:20px 0 9px;display:flex;align-items:baseline;gap:8px}
|
||||
.sect:first-child{margin-top:2px}
|
||||
.sect small{font-weight:500;color:var(--muted2);letter-spacing:0;font-size:11px}
|
||||
|
||||
/* ─ bandeau arbitrage (hérite de l'écran supprimé) ─ */
|
||||
.arb{background:var(--card2);border:1px solid var(--line);border-radius:13px;padding:12px 13px}
|
||||
.arb .top{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
||||
.arb .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(53,201,138,.15)}
|
||||
.arb .top b{font-family:'Poppins';font-size:12.5px;font-weight:600}
|
||||
.arb .top i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
|
||||
.arb .grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
|
||||
.arb .cell{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:7px 4px;text-align:center}
|
||||
.arb .cell u{display:block;text-decoration:none;font-size:9.5px;color:var(--muted2);margin-bottom:2px}
|
||||
.arb .cell b{font-family:'Poppins';font-weight:600;font-size:11.5px}
|
||||
|
||||
/* ─ domaine ─ */
|
||||
.dom{display:flex;align-items:center;gap:8px;margin:16px 0 8px;padding:0 2px}
|
||||
.dom .grip{color:var(--muted2);font-size:15px;cursor:grab}
|
||||
.dom h3{font-family:'Poppins';font-weight:600;font-size:13px;margin:0;flex:1}
|
||||
.dom .arw{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2);color:var(--muted);
|
||||
display:grid;place-items:center;font-size:13px;background:transparent;cursor:pointer}
|
||||
.dom.unclassed h3{color:var(--muted2)}
|
||||
|
||||
/* ─ carte de charge ─ */
|
||||
.load{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:12px;margin-bottom:9px}
|
||||
.load .r1{display:flex;align-items:center;gap:10px}
|
||||
.load .grip{color:var(--muted2);font-size:15px;cursor:grab}
|
||||
.rank{width:23px;height:23px;border-radius:7px;background:rgba(49,163,221,.16);color:var(--blue);
|
||||
font-family:'Poppins';font-weight:700;font-size:11.5px;display:grid;place-items:center;flex:none}
|
||||
.load .nm{flex:1;min-width:0}
|
||||
.load .nm b{font-family:'Poppins';font-weight:600;font-size:13.5px;display:block}
|
||||
.load .nm span{font-size:11px;color:var(--muted2)}
|
||||
.sw{width:38px;height:22px;border-radius:20px;background:rgba(53,201,138,.85);position:relative;cursor:pointer;flex:none}
|
||||
.sw::after{content:"";position:absolute;top:2px;left:18px;width:18px;height:18px;border-radius:50%;background:#fff}
|
||||
.sw.off{background:rgba(255,255,255,.16)}
|
||||
.sw.off::after{left:2px}
|
||||
|
||||
.tel{margin-top:10px;padding-top:9px;border-top:1px dashed var(--line);font-size:11.5px;line-height:1.5}
|
||||
.tel .a{font-family:'Poppins';font-weight:600;color:var(--green);font-size:12px}
|
||||
.tel .m{color:var(--muted)}
|
||||
.tel .why{color:var(--muted2);margin-top:2px}
|
||||
.tel .lock{color:var(--yellow);margin-top:2px}
|
||||
.tel.stale .a{color:var(--muted2)}
|
||||
.tel.stale{opacity:.75}
|
||||
|
||||
.fault{margin-top:9px;background:rgba(224,122,122,.1);border:1px solid rgba(224,122,122,.3);
|
||||
border-radius:10px;padding:9px 10px;font-size:11px;line-height:1.45}
|
||||
.fault b{color:var(--red);font-family:'Poppins';font-size:11.5px;display:block;margin-bottom:2px}
|
||||
.fault button{margin-top:7px;background:transparent;border:1px solid rgba(224,122,122,.45);color:var(--red);
|
||||
font-family:'Poppins';font-weight:600;font-size:11px;padding:6px 11px;border-radius:8px;cursor:pointer}
|
||||
|
||||
.cfg{margin-top:10px;width:100%;background:transparent;border:1px solid rgba(49,163,221,.42);color:var(--blue);
|
||||
font-family:'Poppins';font-weight:600;font-size:12px;padding:9px;border-radius:10px;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;gap:7px}
|
||||
|
||||
.noload{background:var(--card2);border:1px dashed var(--line2);border-radius:12px;padding:11px 12px;
|
||||
display:flex;align-items:center;gap:10px}
|
||||
.noload .nm b{font-family:'Poppins';font-weight:600;font-size:13px;display:block;color:var(--muted)}
|
||||
.noload .nm span{font-size:11px;color:var(--muted2)}
|
||||
.noload button{margin-left:auto;background:var(--blue);color:#04222f;border:0;border-radius:9px;
|
||||
font-family:'Poppins';font-weight:600;font-size:11.5px;padding:8px 13px;cursor:pointer}
|
||||
|
||||
/* ─ formulaire ─ */
|
||||
.field{margin-top:18px}
|
||||
.field:first-child{margin-top:4px}
|
||||
.field>label{display:block;font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);
|
||||
letter-spacing:.3px;margin-bottom:9px}
|
||||
.field>label small{color:var(--muted2);font-weight:500;letter-spacing:0}
|
||||
.saved-flag{font-size:10.5px;color:var(--green);background:rgba(53,201,138,.12);border:1px solid rgba(53,201,138,.3);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
.derived-flag{font-size:10.5px;color:var(--muted2);background:rgba(255,255,255,.05);border:1px solid var(--line2);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
.todo-flag{font-size:10.5px;color:var(--yellow);background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.32);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
|
||||
.inp{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
|
||||
font-family:'Poppins';font-weight:600;font-size:13.5px;padding:11px 12px}
|
||||
.sel{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
|
||||
font-family:'Inter';font-size:13px;padding:11px 12px;display:flex;align-items:center;gap:8px}
|
||||
.sel i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
|
||||
|
||||
.seg{display:flex;background:var(--card2);border:1px solid var(--line);border-radius:11px;padding:4px;gap:4px}
|
||||
.seg button{flex:1;border:0;background:transparent;color:var(--muted);font-family:'Poppins';font-weight:600;
|
||||
font-size:11.5px;padding:9px 4px;border-radius:8px;cursor:pointer}
|
||||
.seg button.on{background:var(--blue);color:#04222f}
|
||||
.mode-hint{margin-top:9px;font-size:11.5px;color:var(--muted2);line-height:1.5}
|
||||
|
||||
.relay{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
||||
.relay .k{width:32px;font-family:'Poppins';font-weight:700;font-size:12px;color:var(--muted2);flex:none}
|
||||
.relay .thing{flex:1;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
|
||||
font-size:12.5px;padding:10px 11px;display:flex;align-items:center;gap:6px}
|
||||
.relay .thing i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:10px}
|
||||
.relay .w{width:92px;flex:none;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
|
||||
color:var(--txt);font-family:'Poppins';font-weight:600;font-size:13px;padding:10px 8px;text-align:center}
|
||||
.relay .del{color:var(--muted2);font-size:14px;cursor:pointer;flex:none;width:16px;text-align:center}
|
||||
.addrow{width:100%;background:transparent;border:1px dashed var(--line2);color:var(--muted);
|
||||
font-family:'Poppins';font-weight:600;font-size:12px;padding:10px;border-radius:10px;cursor:pointer}
|
||||
|
||||
.preview{margin-top:11px;border:1px solid rgba(53,201,138,.22);background:rgba(53,201,138,.06);
|
||||
border-radius:11px;padding:11px 12px;font-size:11.5px;color:var(--muted);line-height:1.55}
|
||||
.preview b{color:var(--green);font-family:'Poppins'}
|
||||
.preview em{display:block;font-style:normal;color:var(--muted2);font-size:10.5px;margin-top:6px}
|
||||
|
||||
.stepper{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border:1px solid var(--line);
|
||||
border-radius:11px;background:var(--card2);margin-bottom:8px}
|
||||
.stepper .lab{font-size:12.5px}.stepper .lab small{display:block;color:var(--muted2);font-size:10.5px;margin-top:1px}
|
||||
.stepper .ctl{display:flex;align-items:center;gap:13px}
|
||||
.stepper .ctl b{font-family:'Poppins';font-weight:600;font-size:13.5px;min-width:56px;text-align:right}
|
||||
.pm{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2);color:var(--blue);background:transparent;
|
||||
font-size:16px;display:grid;place-items:center;cursor:pointer}
|
||||
|
||||
.blocked{border:1px dashed rgba(254,193,19,.35);background:rgba(254,193,19,.05);border-radius:12px;padding:12px}
|
||||
.blocked .row{display:flex;align-items:center;gap:10px;opacity:.55}
|
||||
.blocked .row .lab{font-size:12.5px;flex:1}
|
||||
.blocked p{margin:9px 0 0;font-size:10.8px;color:var(--muted2);line-height:1.5}
|
||||
.blocked p b{color:var(--yellow)}
|
||||
|
||||
.danger{margin-top:9px;font-size:10.8px;color:var(--muted2);line-height:1.5;
|
||||
border-left:2px solid rgba(224,122,122,.5);padding-left:9px}
|
||||
.danger b{color:var(--red)}
|
||||
|
||||
.save{display:flex;gap:10px;margin-top:24px}
|
||||
.btn{text-align:center;font-family:'Poppins';font-weight:600;font-size:13px;padding:12px;border-radius:11px;cursor:pointer}
|
||||
.btn.go{background:var(--blue);color:#04222f;flex:2;border:0}
|
||||
.btn.alt{flex:1.3;background:transparent;border:1px solid var(--line2);color:var(--txt)}
|
||||
|
||||
.note{width:392px;font-size:11.5px;color:var(--muted2);line-height:1.55;text-align:left}
|
||||
.note b{color:var(--yellow)}
|
||||
.note code{color:#9fd4ee}
|
||||
.note ul{margin:7px 0 0;padding-left:16px}
|
||||
.note li{margin-bottom:5px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="head">
|
||||
<h1>Charges pilotables — l'ordre, la télémétrie et le mécanisme au même endroit</h1>
|
||||
<p>Décision du 2026-08-25 : <b>l'écran « Ordre de service des charges » disparaît</b>. Le rang se règle
|
||||
ici au glisser-déposer, le regroupement <b>à deux niveaux</b> est conservé (domaines ordonnés entre eux,
|
||||
charges ordonnées dedans), et le bloc de télémétrie descend sur la carte de chaque charge.</p>
|
||||
<p>Les six écrans « Configurer » qu'ouvre le bouton de chaque carte sont dans
|
||||
<code>configurer_mecanismes_mockup.html</code>.</p>
|
||||
<p><b>Le changement de fond n'est pas visuel :</b> ces cartes doivent <b>être</b> les charges lues par
|
||||
<code>NymeaEnergy.GetLoadConfig</code>, pas des <code>LoadDescriptor</code> reconstruits depuis des
|
||||
<code>EmsRole</code>. C'est la cause racine du défaut constaté sur appareil — l'écran actuel affichait
|
||||
« 6/6 rôles configurés · tout est assigné » en pointant sur des appareils absents de la box, pendant que
|
||||
celle-ci arbitrait deux charges qui n'apparaissaient nulle part.</p>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
|
||||
<!-- ══════════ ÉCRAN 1 ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Rôles & appareils</h2><div class="ic">⟳</div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="sect">Arbitrage en direct <small>· dernier cycle il y a 41 s</small></div>
|
||||
<div class="arb">
|
||||
<div class="top"><span class="dot"></span><b>Budget de surplus du cycle</b><i>2 571 W entrants</i></div>
|
||||
<div class="grid">
|
||||
<div class="cell"><u>Surplus</u><b>2 571</b></div>
|
||||
<div class="cell"><u>Réservé VE</u><b>0</b></div>
|
||||
<div class="cell"><u>Recrédité</u><b>500</b></div>
|
||||
<div class="cell"><u>Alloué</u><b>3 000</b></div>
|
||||
<div class="cell"><u>Reliquat</u><b>71</b></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sect">Charges pilotables <small>· glissez les domaines, puis les charges</small></div>
|
||||
|
||||
<!-- domaine 1 -->
|
||||
<div class="dom">
|
||||
<span class="grip">⠿</span>
|
||||
<h3>Eau chaude sanitaire</h3>
|
||||
<button class="arw">˄</button><button class="arw">˅</button>
|
||||
</div>
|
||||
|
||||
<div class="load">
|
||||
<div class="r1">
|
||||
<span class="grip">⠿</span><span class="rank">1</span>
|
||||
<div class="nm"><b>Chauffe-eau</b><span>Routeur de relais · 3 relais · min ON 60 s</span></div>
|
||||
<span class="sw"></span>
|
||||
</div>
|
||||
<div class="tel">
|
||||
<span class="a">Alloué 3 000 W</span> <span class="m">· palier 3 000 W sur 3 500 W</span>
|
||||
<div class="why">Consigne de 3 000 W servie par le surplus, arrondie sur un palier déclaré
|
||||
(3 071 W de budget).</div>
|
||||
<div class="lock">🔒 Maintien ON — 60 s restantes <span style="color:var(--muted2)">(au dernier cycle)</span></div>
|
||||
</div>
|
||||
<button class="cfg">⚙ Configurer</button>
|
||||
</div>
|
||||
|
||||
<!-- domaine 2 -->
|
||||
<div class="dom">
|
||||
<span class="grip">⠿</span>
|
||||
<h3>Chauffage</h3>
|
||||
<button class="arw">˄</button><button class="arw">˅</button>
|
||||
</div>
|
||||
|
||||
<div class="load">
|
||||
<div class="r1">
|
||||
<span class="grip">⠿</span><span class="rank">2</span>
|
||||
<div class="nm"><b>PAC banc</b><span>SG-Ready · maintien d'état 300 s</span></div>
|
||||
<span class="sw"></span>
|
||||
</div>
|
||||
<div class="tel">
|
||||
<span class="a">Alloué 3 000 W</span> <span class="m">· état SG-Ready 4 — estimation 3 000 W</span>
|
||||
<div class="why">Pompe à chaleur en état 4 (forcé) — estimation 3 000 W sur 3 477 W de budget.</div>
|
||||
</div>
|
||||
<button class="cfg">⚙ Configurer</button>
|
||||
</div>
|
||||
|
||||
<!-- domaine 3 : non classé + défaut -->
|
||||
<div class="dom unclassed">
|
||||
<span class="grip">⠿</span>
|
||||
<h3>Non classé</h3>
|
||||
<button class="arw">˄</button><button class="arw">˅</button>
|
||||
</div>
|
||||
|
||||
<div class="load">
|
||||
<div class="r1">
|
||||
<span class="grip">⠿</span><span class="rank">3</span>
|
||||
<div class="nm"><b>Sèche-serviettes</b><span>Routeur de relais · 1 relais</span></div>
|
||||
<span class="sw off"></span>
|
||||
</div>
|
||||
<div class="tel stale">
|
||||
<span class="a">Non arbitrée</span> <span class="m">· absente du dernier cycle publié</span>
|
||||
<div class="why">Charge désactivée : son rôle reste décrit, elle ne consomme aucun budget.</div>
|
||||
</div>
|
||||
<button class="cfg">⚙ Configurer</button>
|
||||
</div>
|
||||
|
||||
<div class="load" style="border-color:rgba(224,122,122,.35)">
|
||||
<div class="r1">
|
||||
<span class="grip">⠿</span><span class="rank">4</span>
|
||||
<div class="nm"><b>Plancher chauffant</b><span>Charge modulable</span></div>
|
||||
<span class="sw"></span>
|
||||
</div>
|
||||
<div class="fault">
|
||||
<b>⚠ En défaut</b>
|
||||
É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é.
|
||||
<button>🔓 Lever le défaut</button>
|
||||
</div>
|
||||
<button class="cfg">⚙ Configurer</button>
|
||||
</div>
|
||||
|
||||
<div class="sect">Sans appareil</div>
|
||||
<div class="noload">
|
||||
<div class="nm"><b>Borne de recharge</b><span>Aucune charge déclarée sur la box</span></div>
|
||||
<button>+ Déclarer</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap">Charges pilotables — deux niveaux + télémétrie</div>
|
||||
<div class="note">
|
||||
<b>Ce qui part sur la box :</b> le glisser-déposer produit une suite d'entiers <code>priority</code> à plat —
|
||||
la box ne sait rien des domaines, le rang de domaine se déduit de l'ordre d'apparition. Le toggle écrit
|
||||
<code>enabled</code>.
|
||||
<ul>
|
||||
<li><b>« Non arbitrée »</b> n'est pas une erreur : une charge de <code>GetLoadConfig</code> absente de
|
||||
<code>loads[]</code> se lit <b>désactivée</b>, jamais perdue. L'inclusion ne vaut que dans un sens.</li>
|
||||
<li><b>« Lever le défaut »</b> n'apparaît que si <code>available: false</code>, et son résultat n'est
|
||||
jamais annoncé sur le retour du RPC : mesuré sur le banc, <code>ClearLoadFault</code> répond
|
||||
<code>EnergyErrorNoError</code>, lève réellement le verrou… et le cycle suivant le repose si la cause
|
||||
demeure. Le verdict vient de la télémétrie relue, deux cycles plus tard.</li>
|
||||
<li><b>« Déclarer »</b> mène au même formulaire que « Configurer », vide.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
998
docs/mockups/configurer_mecanismes_mockup.html
Normal file
998
docs/mockups/configurer_mecanismes_mockup.html
Normal file
@ -0,0 +1,998 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ETM PowerSync — Configurer : les cinq mécanismes</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#081f2b; --panel:#0d2b3b; --card:#12384a; --card2:#0f3242;
|
||||
--line:rgba(255,255,255,.08); --line2:rgba(255,255,255,.14);
|
||||
--blue:#31a3dd; --yellow:#fec113; --green:#35c98a; --red:#e07a7a; --violet:#9d8ce0;
|
||||
--txt:#e8f1f6; --muted:#90a8b6; --muted2:#6f8b9a;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:
|
||||
radial-gradient(900px 500px at 15% -10%, #0e3145 0%, transparent 60%),
|
||||
radial-gradient(900px 600px at 110% 10%, #0b2535 0%, transparent 55%),
|
||||
var(--bg);
|
||||
font-family:'Inter',system-ui,sans-serif;color:var(--txt);min-height:100vh;padding:34px 20px 60px}
|
||||
.head{max-width:1300px;margin:0 auto 26px}
|
||||
.head h1{font-family:'Poppins';font-weight:700;font-size:20px;margin:0 0 6px}
|
||||
.head p{margin:0 0 8px;color:var(--muted);font-size:13.5px;line-height:1.55;max-width:900px}
|
||||
.head code{color:#9fd4ee} .head b{color:var(--yellow)}
|
||||
.stage{display:flex;gap:34px;justify-content:center;flex-wrap:wrap;align-items:flex-start}
|
||||
.col{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:392px}
|
||||
.cap{color:var(--muted2);font-size:12px;font-family:'Poppins';font-weight:500;letter-spacing:.4px;text-transform:uppercase}
|
||||
.cap b{color:var(--blue)}
|
||||
|
||||
.phone{width:392px;background:var(--panel);border:1px solid var(--line2);border-radius:30px;
|
||||
overflow:hidden;box-shadow:0 30px 80px -30px rgba(0,0,0,.7);display:flex;flex-direction:column;height:880px}
|
||||
.appbar{display:flex;align-items:center;gap:12px;padding:18px 16px 14px;border-bottom:1px solid var(--line);
|
||||
background:linear-gradient(#102f40,#0d2b3b)}
|
||||
.appbar .ic{width:26px;height:26px;display:grid;place-items:center;color:var(--muted);font-size:18px}
|
||||
.appbar h2{font-family:'Poppins';font-weight:600;font-size:15px;margin:0;flex:1;text-align:center}
|
||||
.scroll{overflow:auto;padding:16px 15px 26px}
|
||||
.scroll::-webkit-scrollbar{width:0}
|
||||
|
||||
.field{margin-top:18px}
|
||||
.field:first-child{margin-top:4px}
|
||||
.field>label{display:block;font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);
|
||||
letter-spacing:.3px;margin-bottom:9px}
|
||||
.field>label small{color:var(--muted2);font-weight:500;letter-spacing:0}
|
||||
.saved-flag{font-size:10.5px;color:var(--green);background:rgba(53,201,138,.12);border:1px solid rgba(53,201,138,.3);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
.other-flag{font-size:10.5px;color:var(--violet);background:rgba(157,140,224,.12);border:1px solid rgba(157,140,224,.32);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
.todo-flag{font-size:10.5px;color:var(--yellow);background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.32);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
.derived-flag{font-size:10.5px;color:var(--muted2);background:rgba(255,255,255,.05);border:1px solid var(--line2);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
|
||||
.inp{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
|
||||
font-family:'Poppins';font-weight:600;font-size:13.5px;padding:11px 12px}
|
||||
.sel{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
|
||||
font-family:'Inter';font-size:12.5px;padding:11px 12px;display:flex;align-items:center;gap:8px}
|
||||
.sel i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
|
||||
.mode-hint{margin-top:9px;font-size:11.5px;color:var(--muted2);line-height:1.5}
|
||||
.mode-hint b{color:var(--muted)}
|
||||
|
||||
.seg{display:flex;background:var(--card2);border:1px solid var(--line);border-radius:11px;padding:4px;gap:4px}
|
||||
.seg button{flex:1;border:0;background:transparent;color:var(--muted);font-family:'Poppins';font-weight:600;
|
||||
font-size:11.5px;padding:9px 4px;border-radius:8px;cursor:pointer}
|
||||
.seg button.on{background:var(--blue);color:#04222f}
|
||||
|
||||
/* table d'états SG-Ready */
|
||||
.st{border:1px solid var(--line);border-radius:12px;background:var(--card2);margin-bottom:9px;padding:11px 12px}
|
||||
.st .h{display:flex;align-items:center;gap:9px;margin-bottom:8px}
|
||||
.st .badge{width:24px;height:24px;border-radius:7px;font-family:'Poppins';font-weight:700;font-size:12px;
|
||||
display:grid;place-items:center;flex:none}
|
||||
.st .badge.s1{background:rgba(224,122,122,.18);color:var(--red)}
|
||||
.st .badge.s2{background:rgba(255,255,255,.08);color:var(--muted)}
|
||||
.st .badge.s3{background:rgba(53,201,138,.16);color:var(--green)}
|
||||
.st .badge.s4{background:rgba(254,193,19,.16);color:var(--yellow)}
|
||||
.st .h b{font-family:'Poppins';font-weight:600;font-size:12.5px;flex:1}
|
||||
.st .h span{font-size:10.5px;color:var(--muted2)}
|
||||
.st .contacts{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.kchip{font-size:11px;border-radius:8px;padding:5px 9px;border:1px solid var(--line2);
|
||||
background:var(--panel);color:var(--muted);font-family:'Poppins';font-weight:600;cursor:pointer}
|
||||
.kchip.on{border-color:var(--blue);color:var(--blue);background:rgba(49,163,221,.1)}
|
||||
.st .pw{display:flex;align-items:center;gap:9px}
|
||||
.st .pw label{font-size:11px;color:var(--muted2);flex:1}
|
||||
.st .pw input{width:96px;background:var(--panel);border:1px solid var(--line2);border-radius:9px;color:var(--txt);
|
||||
font-family:'Poppins';font-weight:600;font-size:12.5px;padding:8px;text-align:center}
|
||||
.st.locked{opacity:.62}
|
||||
|
||||
.stepper{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border:1px solid var(--line);
|
||||
border-radius:11px;background:var(--card2);margin-bottom:8px}
|
||||
.stepper .lab{font-size:12.5px}.stepper .lab small{display:block;color:var(--muted2);font-size:10.5px;margin-top:1px}
|
||||
.stepper .ctl{display:flex;align-items:center;gap:13px}
|
||||
.stepper .ctl b{font-family:'Poppins';font-weight:600;font-size:13.5px;min-width:56px;text-align:right}
|
||||
.pm{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2);color:var(--blue);background:transparent;
|
||||
font-size:16px;display:grid;place-items:center;cursor:pointer}
|
||||
.sw{width:38px;height:22px;border-radius:20px;background:rgba(53,201,138,.85);position:relative;cursor:pointer;flex:none}
|
||||
.sw::after{content:"";position:absolute;top:2px;left:18px;width:18px;height:18px;border-radius:50%;background:#fff}
|
||||
.sw.off{background:rgba(255,255,255,.16)} .sw.off::after{left:2px}
|
||||
|
||||
.warn{margin-top:10px;border:1px solid rgba(224,122,122,.32);background:rgba(224,122,122,.07);
|
||||
border-radius:11px;padding:10px 11px;font-size:11px;color:var(--muted);line-height:1.5}
|
||||
.warn b{color:var(--red);display:block;font-family:'Poppins';font-size:11.5px;margin-bottom:3px}
|
||||
.info{margin-top:10px;border:1px solid rgba(49,163,221,.26);background:rgba(49,163,221,.06);
|
||||
border-radius:11px;padding:10px 11px;font-size:11px;color:var(--muted);line-height:1.5}
|
||||
.info b{color:var(--blue);display:block;font-family:'Poppins';font-size:11.5px;margin-bottom:3px}
|
||||
.blocked{border:1px dashed rgba(254,193,19,.35);background:rgba(254,193,19,.05);border-radius:12px;padding:12px}
|
||||
.blocked p{margin:9px 0 0;font-size:10.8px;color:var(--muted2);line-height:1.5} .blocked p b{color:var(--yellow)}
|
||||
.blocked .row{display:flex;align-items:center;gap:10px;opacity:.55}
|
||||
.blocked .row .lab{font-size:12.5px;flex:1}
|
||||
|
||||
/* EV */
|
||||
.evmode{display:flex;gap:8px}
|
||||
.evmode button{flex:1;border:1px solid var(--line2);background:var(--card2);color:var(--muted);
|
||||
font-family:'Poppins';font-weight:600;font-size:12px;padding:14px 6px;border-radius:12px;cursor:pointer;
|
||||
display:flex;flex-direction:column;align-items:center;gap:5px}
|
||||
.evmode button.on{border-color:var(--green);background:rgba(53,201,138,.1);color:var(--green)}
|
||||
.evmode button u{text-decoration:none;font-size:17px}
|
||||
.evmode button small{font-weight:500;font-size:9.5px;color:var(--muted2)}
|
||||
.evmode button.on small{color:rgba(53,201,138,.75)}
|
||||
.maps{margin-top:10px;font-size:10.8px;color:var(--muted2);line-height:1.6}
|
||||
.maps code{color:#9fd4ee}
|
||||
.state{display:flex;align-items:center;gap:9px;background:var(--card2);border:1px solid var(--line);
|
||||
border-radius:11px;padding:11px 12px}
|
||||
.state .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(53,201,138,.15)}
|
||||
.state b{font-family:'Poppins';font-size:12.5px;font-weight:600;flex:1}
|
||||
.state i{font-style:normal;font-size:10.5px;color:var(--muted2)}
|
||||
|
||||
/* zones */
|
||||
.zone{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:12px;margin-bottom:9px}
|
||||
.zone .z1{display:flex;align-items:center;gap:10px}
|
||||
.zone .z1 b{font-family:'Poppins';font-weight:600;font-size:13.5px;flex:1}
|
||||
.zone .temp{font-family:'Poppins';font-weight:700;font-size:17px}
|
||||
.zone .temp small{font-size:11px;font-weight:500;color:var(--muted2);display:block;text-align:right}
|
||||
.zone .z2{margin-top:9px;display:flex;gap:6px;flex-wrap:wrap}
|
||||
.tchip{font-size:10.5px;border-radius:7px;padding:4px 8px;border:1px solid var(--line);
|
||||
background:var(--card2);color:var(--muted2)}
|
||||
.zone .ovr{margin-top:9px;padding-top:9px;border-top:1px dashed var(--line);font-size:11px;color:var(--yellow)}
|
||||
.empty{border:1px dashed var(--line2);border-radius:13px;padding:22px 14px;text-align:center}
|
||||
.empty b{display:block;font-family:'Poppins';font-size:13px;margin-bottom:5px}
|
||||
.empty p{margin:0 0 13px;font-size:11.5px;color:var(--muted2);line-height:1.5}
|
||||
.empty button{background:var(--blue);color:#04222f;border:0;border-radius:10px;font-family:'Poppins';
|
||||
font-weight:600;font-size:12.5px;padding:10px 16px;cursor:pointer}
|
||||
.rowsel{display:flex;align-items:center;gap:9px;background:var(--card2);border:1px solid var(--line);
|
||||
border-radius:11px;padding:10px 11px;margin-bottom:7px}
|
||||
.rowsel .lab{font-size:12px;flex:1}
|
||||
.rowsel .cnt{font-size:11px;color:var(--muted2)}
|
||||
.rowsel i{font-style:normal;color:var(--muted2);font-size:11px}
|
||||
|
||||
/* lignes de relais */
|
||||
.relay{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
||||
.relay .k{width:30px;font-family:'Poppins';font-weight:700;font-size:12px;color:var(--muted2);flex:none}
|
||||
.relay .thing{flex:1;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
|
||||
font-size:12.5px;padding:10px 11px;display:flex;align-items:center;gap:6px;min-width:0}
|
||||
.relay .thing i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:10px}
|
||||
.relay .w{width:88px;flex:none;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
|
||||
color:var(--txt);font-family:'Poppins';font-weight:600;font-size:13px;padding:10px 8px;text-align:center}
|
||||
.relay .del{color:var(--muted2);font-size:14px;cursor:pointer;flex:none;width:14px;text-align:center}
|
||||
.addrow{width:100%;background:transparent;border:1px dashed var(--line2);color:var(--muted);
|
||||
font-family:'Poppins';font-weight:600;font-size:12px;padding:10px;border-radius:10px;cursor:pointer}
|
||||
.preview{margin-top:11px;border:1px solid rgba(53,201,138,.22);background:rgba(53,201,138,.06);
|
||||
border-radius:11px;padding:11px 12px;font-size:11.5px;color:var(--muted);line-height:1.55}
|
||||
.preview b{color:var(--green);font-family:'Poppins'}
|
||||
.preview em{display:block;font-style:normal;color:var(--muted2);font-size:10.5px;margin-top:6px}
|
||||
|
||||
/* paliers (modulable) */
|
||||
.chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||
.pchip{display:flex;align-items:center;gap:8px;background:var(--card2);border:1px solid var(--line2);
|
||||
border-radius:9px;padding:9px 11px;font-family:'Poppins';font-weight:600;font-size:13px}
|
||||
.pchip i{font-style:normal;color:var(--muted2);font-weight:500;font-size:10.5px}
|
||||
.pchip.off{opacity:.6} .pchip .x{color:var(--muted2);cursor:pointer;font-size:12px}
|
||||
.add{width:34px;height:34px;border-radius:9px;border:1px dashed var(--line2);color:var(--muted);
|
||||
display:grid;place-items:center;font-size:18px;background:transparent;cursor:pointer}
|
||||
.assist-btn{margin-top:11px;display:flex;align-items:center;gap:8px;width:100%;justify-content:center;
|
||||
background:transparent;border:1px dashed rgba(49,163,221,.4);color:var(--blue);font-family:'Poppins';
|
||||
font-weight:600;font-size:12px;padding:10px;border-radius:10px;cursor:pointer}
|
||||
|
||||
/* sélecteur de things */
|
||||
.search{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:11px;color:var(--muted2);
|
||||
font-size:12.5px;padding:11px 12px;margin-bottom:11px}
|
||||
.filters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
|
||||
.fchip{font-size:11px;border-radius:20px;padding:5px 11px;border:1px solid var(--line2);
|
||||
background:var(--card2);color:var(--muted);font-family:'Poppins';font-weight:600;cursor:pointer}
|
||||
.fchip.on{border-color:var(--blue);color:var(--blue);background:rgba(49,163,221,.1)}
|
||||
.pickrow{display:flex;align-items:center;gap:11px;background:var(--card2);border:1px solid var(--line);
|
||||
border-radius:11px;padding:11px 12px;margin-bottom:7px}
|
||||
.cb{width:19px;height:19px;border-radius:6px;border:2px solid var(--muted2);flex:none}
|
||||
.cb.on{border-color:var(--blue);background:var(--blue);position:relative}
|
||||
.cb.on::after{content:"✓";position:absolute;inset:0;display:grid;place-items:center;color:#04222f;
|
||||
font-size:12px;font-weight:700}
|
||||
.pickrow .nm{flex:1;min-width:0}
|
||||
.pickrow .nm b{display:block;font-size:12.5px;font-family:'Poppins';font-weight:600}
|
||||
.pickrow .nm span{font-size:10.5px;color:var(--muted2)}
|
||||
.pickrow .taken{font-size:10px;color:var(--yellow);background:rgba(254,193,19,.1);
|
||||
border:1px solid rgba(254,193,19,.3);border-radius:20px;padding:3px 8px;font-weight:600}
|
||||
.pickrow.dim{opacity:.5}
|
||||
|
||||
.save{display:flex;gap:10px;margin-top:24px}
|
||||
.btn{text-align:center;font-family:'Poppins';font-weight:600;font-size:13px;padding:12px;border-radius:11px;cursor:pointer}
|
||||
.btn.go{background:var(--blue);color:#04222f;flex:2;border:0}
|
||||
.btn.alt{flex:1.3;background:transparent;border:1px solid var(--line2);color:var(--txt)}
|
||||
|
||||
.note{width:392px;font-size:11.5px;color:var(--muted2);line-height:1.55;text-align:left}
|
||||
.note b{color:var(--yellow)} .note code{color:#9fd4ee}
|
||||
.note ul{margin:7px 0 0;padding-left:16px} .note li{margin-bottom:5px}
|
||||
.pill{display:inline-block;font-size:10px;font-family:'Poppins';font-weight:600;border-radius:20px;
|
||||
padding:2px 9px;margin-bottom:6px}
|
||||
.pill.load{background:rgba(53,201,138,.12);color:var(--green);border:1px solid rgba(53,201,138,.3)}
|
||||
.pill.ev{background:rgba(157,140,224,.12);color:var(--violet);border:1px solid rgba(157,140,224,.32)}
|
||||
.pill.ac{background:rgba(49,163,221,.12);color:var(--blue);border:1px solid rgba(49,163,221,.32)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="head">
|
||||
<h1>Six écrans, trois contrats — et ils ne se ressemblent pas</h1>
|
||||
<p>Le bouton <b>Configurer</b> ne peut pas être un formulaire unique. SG-Ready est une
|
||||
<code>LoadConfig</code> comme le routeur de relais ; la borne de recharge et les zones de climatisation
|
||||
n'en sont pas du tout — elles vivent sur d'autres namespaces, avec d'autres champs et d'autres tiers.</p>
|
||||
<p><b>Deux écarts relevés en introspectant <code>.75</code> (plugin 1.15.2+etm15) :</b>
|
||||
① le <code>ChargingInfo</code> réellement déployé ne porte <b>ni <code>mode</code>, ni <code>minCurrent</code>,
|
||||
ni <code>targetSoc</code>, ni <code>endTime</code></b> — les noms écrits dans <code>CLAUDE.md</code> ne sont pas
|
||||
ceux de la box. ② <code>AirConditioning</code> expose <b>8 méthodes</b>, pas 3.</p>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
|
||||
<!-- ══════════ ECS RELAIS / MULTIPALIER ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Configurer · Chauffe-eau</h2><div class="ic"></div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom de la charge <span class="saved-flag">enregistré · label</span></label>
|
||||
<input class="inp" value="Chauffe-eau">
|
||||
<div class="mode-hint">Le seul texte de l'installation que l'utilisateur voit dans chaque motif
|
||||
de décision.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Domaine <span class="saved-flag">enregistré · domain</span></label>
|
||||
<div class="sel">Eau chaude sanitaire <i>▾</i></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Mécanisme <span class="saved-flag">enregistré · adapter</span></label>
|
||||
<div class="seg">
|
||||
<button class="on">Routeur de relais</button><button>Modulable</button><button>SG-Ready</button>
|
||||
</div>
|
||||
<div class="info">
|
||||
<b>Ce sont vos contacteurs</b>
|
||||
La box ouvre et ferme des contacts qu'elle commande directement, et compose les puissances
|
||||
en les combinant. C'est le seul mécanisme où l'app déclare du <b>matériel</b>.
|
||||
<span style="display:block;margin-top:5px;color:var(--muted2)">Si les étages sont à l'intérieur
|
||||
de l'appareil et qu'on lui envoie une consigne, ce n'est pas ici : c'est « modulable ».</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Relais commandés <span class="saved-flag">enregistré · relays[]</span><br>
|
||||
<small>un contacteur par ligne — puissance de la résistance câblée derrière</small></label>
|
||||
|
||||
<div class="relay">
|
||||
<span class="k">K1</span>
|
||||
<div class="thing">Relay1 <i>Digital Output</i></div>
|
||||
<input class="w" value="500 W">
|
||||
<span class="del">×</span>
|
||||
</div>
|
||||
<div class="relay">
|
||||
<span class="k">K2</span>
|
||||
<div class="thing">Relay2 <i>Digital Output</i></div>
|
||||
<input class="w" value="1 000 W">
|
||||
<span class="del">×</span>
|
||||
</div>
|
||||
<div class="relay">
|
||||
<span class="k">K3</span>
|
||||
<div class="thing">Relay3 <i>Digital Output</i></div>
|
||||
<input class="w" value="2 000 W">
|
||||
<span class="del">×</span>
|
||||
</div>
|
||||
<button class="addrow">+ Ajouter un relais</button>
|
||||
|
||||
<div class="preview">
|
||||
3 relais → paliers atteignables :
|
||||
<b>0 · 500 · 1 000 · 1 500 · 2 000 · 2 500 · 3 000 · 3 500 W</b>
|
||||
<em>Déduit, jamais enregistré. La box compose elle-même les paliers en essayant toutes les
|
||||
combinaisons de contacts (2<sup>N</sup>, plafond 16 relais). Deux relais de même puissance ne
|
||||
créent pas de palier supplémentaire — les doublons sont écartés.</em>
|
||||
</div>
|
||||
|
||||
<div class="mode-hint"><b>Un ECS simple, c'est un seul relais</b> — deux paliers, 0 et P. Une
|
||||
résistance multipalier en déclare 2, 3 ou 4. La puissance saisie est celle de la
|
||||
<b>résistance</b>, pas le calibre du contacteur : rien dans nymea ne dit ce qu'un relais
|
||||
commande.</div>
|
||||
|
||||
<div class="warn">
|
||||
<b>⚠ Ce que la box refuse, en bloc et sans le dire</b>
|
||||
<code>relays[]</code> vide · un <code>powerW</code> ≤ 0 · un même Thing sur deux lignes ·
|
||||
un <code>mode</code> autre que <code>fixed</code>. Le motif n'apparaît qu'au journal de la box —
|
||||
le formulaire doit donc valider <b>avant</b> d'envoyer.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Temporisations <span class="saved-flag">enregistré · minOnS / minOffS</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Maintien ON minimal<small>anti court-cycle à l'enclenchement</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>60 s</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<div class="lab">Maintien OFF minimal<small>délai avant réenclenchement</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>60 s</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
<div class="mode-hint">Ces deux durées produisent les verrous <b>LOCK_MIN_ON</b> et
|
||||
<b>LOCK_MIN_OFF</b> que la carte de la charge affiche en secondes restantes.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row">
|
||||
<div class="lab">Rattacher un compteur à cette charge</div>
|
||||
<span class="sw off"></span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:9px">
|
||||
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
|
||||
</div>
|
||||
<p><b>Aucun champ n'existe côté box.</b> <code>SetLoadConfig</code> n'accepte que
|
||||
<code>adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS ·
|
||||
needs · powerLevels · relays · sgReady</code>. nymea rejette toute clé inconnue <b>avant</b> le
|
||||
handler : inventer <code>meterThingId</code> ferait échouer l'enregistrement de <b>toutes</b> les
|
||||
charges. <span style="display:block;margin-top:5px">Ici le besoin est le plus fort des trois
|
||||
mécanismes : la box <b>estime</b> la consommation depuis les paliers déclarés, elle ne la
|
||||
mesure pas.</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Arbitrage <span class="saved-flag">enregistré · enabled</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Inclure dans l'arbitrage<small>rang 1 · Eau chaude sanitaire</small></div>
|
||||
<div class="ctl"><span class="sw"></span></div>
|
||||
</div>
|
||||
<div class="mode-hint">À la désactivation, le retrait passe par l'état sûr : <b>tous les relais
|
||||
ouverts</b>. Vérifié sur matériel côté plugin.</div>
|
||||
</div>
|
||||
|
||||
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap"><b>Routeur de relais</b> — ECS simple ou multipalier</div>
|
||||
<div class="note">
|
||||
<span class="pill load">NymeaEnergy.SetLoadConfig</span><br>
|
||||
Le seul mécanisme où l'app déclare des <b>contacteurs</b>. C'est aussi le seul où les paliers ont un
|
||||
sens : ils naissent des combinaisons de relais, ils ne se saisissent pas.
|
||||
<ul>
|
||||
<li><b><code>mode</code> doit valoir <code>fixed</code></b> — la box refuse toute autre valeur sur
|
||||
ce mécanisme. Le champ n'a donc pas à être proposé : il est déterminé par le choix du mécanisme.</li>
|
||||
<li><b>Toucher aux relais franchit <code>sameHardware()</code></b> : reconstruction de l'adaptateur
|
||||
et réarmement des verrous à froid — contrairement à <code>label</code>, <code>domain</code>,
|
||||
<code>priority</code> et <code>enabled</code>, qui passent en mise à jour sur place.</li>
|
||||
<li>Configuration réelle du banc <code>.75</code> : trois relais 500 / 1 000 / 2 000 W, huit paliers,
|
||||
<code>maxStageW: 3 500</code> — la télémétrie l'a confirmé pendant le passage sur appareil.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ ECS MODULABLE ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Configurer · Chauffe-eau</h2><div class="ic"></div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom de la charge <span class="saved-flag">enregistré · label</span></label>
|
||||
<input class="inp" value="Chauffe-eau">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Mécanisme <span class="saved-flag">enregistré · adapter</span></label>
|
||||
<div class="seg">
|
||||
<button>Routeur de relais</button><button class="on">Modulable</button><button>SG-Ready</button>
|
||||
</div>
|
||||
<div class="info">
|
||||
<b>Une consigne continue, pas des étages</b>
|
||||
La box envoie une <b>valeur en watts</b>, n'importe laquelle entre 0 et le plafond, et l'appareil
|
||||
module. Aucun contact n'est commandé, aucun palier n'est déclaré.
|
||||
<span style="display:block;margin-top:5px;color:var(--muted2)">Des paliers ⇒ ce sont des
|
||||
contacteurs ⇒ c'est « routeur de relais ». Les deux mécanismes ne se recouvrent pas.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Appareil piloté <span class="saved-flag">enregistré · id</span><br>
|
||||
<small>un seul Thing, portant l'interface etmvariableload</small></label>
|
||||
<div class="sel">Chauffe-eau Modbus · etmvariableload <i>▾</i></div>
|
||||
<div class="warn">
|
||||
<b>⚠ Changer d'appareil, c'est changer de charge</b>
|
||||
L'identifiant de la charge <b>est</b> le ThingId piloté. Choisir un autre appareil ne modifie donc
|
||||
pas cette charge : cela en crée une autre et supprime celle-ci — avec son passage par l'état sûr
|
||||
(consigne 0 W). À dire avant d'enregistrer, pas après.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Puissance nominale <span class="saved-flag">enregistré · maxPowerW</span><br>
|
||||
<small>plafond de modulation — la consigne ira de 0 à cette valeur</small></label>
|
||||
<div class="amp">
|
||||
<button class="pm">−</button>
|
||||
<div class="v">3 000<small>W</small></div>
|
||||
<button class="pm">+</button>
|
||||
</div>
|
||||
<div class="mode-hint">Requis, et strictement positif : la box refuse une charge modulable sans
|
||||
plafond. C'est la plaque signalétique de la résistance, pas une estimation.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Temporisations <span class="saved-flag">enregistré · minOnS / minOffS</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Maintien ON minimal<small>anti court-cycle</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>0 s</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<div class="lab">Maintien OFF minimal<small>délai avant reprise</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>0 s</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row">
|
||||
<div class="lab">Rattacher un compteur à cette charge</div>
|
||||
<span class="sw off"></span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:9px">
|
||||
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
|
||||
</div>
|
||||
<p><b>Aucun champ n'existe côté box.</b> <code>SetLoadConfig</code> n'accepte que
|
||||
<code>adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS ·
|
||||
needs · powerLevels · relays · sgReady</code>, et nymea rejette toute clé inconnue <b>avant</b>
|
||||
le handler. <span style="display:block;margin-top:5px">Ici la question se double : l'interface
|
||||
remonte déjà la <b>puissance réelle</b> de l'appareil — un compteur externe mesurerait-il autre
|
||||
chose ?</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Arbitrage <span class="saved-flag">enregistré · enabled</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Inclure dans l'arbitrage<small>rang 1 · Eau chaude sanitaire</small></div>
|
||||
<div class="ctl"><span class="sw"></span></div>
|
||||
</div>
|
||||
<div class="mode-hint">À la désactivation, le retrait passe par l'état sûr : <b>consigne 0 W</b>.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Configuration héritée <span class="derived-flag">lecture seule · mode fixed</span><br>
|
||||
<small>n'apparaît que si la box renvoie déjà une charge en paliers</small></label>
|
||||
<div class="blocked">
|
||||
<div class="row" style="opacity:.8">
|
||||
<div class="lab">Paliers déclarés : 0 · 600 · 1 200 · 1 800 · 2 400 W</div>
|
||||
</div>
|
||||
<p><b>L'app ne propose plus ce mode</b> — le plugin lui-même le qualifie de <i>legacy</i> — mais
|
||||
elle ne le réécrit pas en douce : basculer une charge de <code>fixed</code> vers
|
||||
<code>dynamic</code> change l'arbitrage réel, et cela demande un geste explicite.
|
||||
<span style="display:block;margin-top:6px">La conversion exige aussi une
|
||||
<b>puissance nominale</b>, que la déclaration en paliers ne porte pas : la box refuse
|
||||
<code>dynamic</code> sans <code>maxPowerW</code>, et refuse <code>powerLevels</code> avec.</span>
|
||||
</p>
|
||||
<div class="row" style="opacity:1;margin-top:10px">
|
||||
<div class="sel" style="flex:1;border-color:rgba(254,193,19,.4);color:var(--yellow)">
|
||||
Convertir en modulation continue…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap"><b>Modulable</b> — une consigne continue</div>
|
||||
<div class="note">
|
||||
<span class="pill load">NymeaEnergy.SetLoadConfig</span><br>
|
||||
<code>etmvariableload</code> : <b>un seul appareil</b>, une consigne en watts, aucun palier.
|
||||
<code>relays[]</code> y est interdit — la box refuse en bloc.
|
||||
<ul>
|
||||
<li><b>Le critère de choix entre les deux mécanismes</b>, et c'est le seul qui compte pour
|
||||
l'installateur : <b>où sont les contacteurs ?</b> Sur les sorties de la box → routeur de relais, et
|
||||
il déclare {Thing, W} par contacteur. Dans l'appareil, piloté par une consigne → modulable, et il
|
||||
ne déclare qu'un plafond.</li>
|
||||
<li><b>Le mode <code>fixed</code> existe encore côté box</b> — « multipalier natif, <i>legacy</i> »
|
||||
dans les mots du plugin. L'app doit savoir le <b>lire</b> sans le proposer : une config déployée
|
||||
qui l'utilise ne doit pas être convertie par le simple fait d'ouvrir l'écran.</li>
|
||||
<li>Trois niveaux, trois responsables (contrat rév. 2) : le <b>câblage</b> au wizard Things, la
|
||||
<b>déclaration</b> à cet écran, le <b>runtime</b> à l'interface. « Le thing décide du matériel ;
|
||||
l'energymanager décide de la puissance. »</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ SG-READY ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Configurer · PAC banc</h2><div class="ic"></div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom de la charge <span class="saved-flag">enregistré · label</span></label>
|
||||
<input class="inp" value="PAC banc">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Mécanisme <span class="saved-flag">enregistré · adapter</span></label>
|
||||
<div class="seg">
|
||||
<button>Routeur de relais</button><button>Modulable</button><button class="on">SG-Ready</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Contacts SG-Ready <span class="derived-flag">déduit · pas de champ dédié</span><br>
|
||||
<small>les deux entrées de la pompe à chaleur, et le relais qui les commande</small></label>
|
||||
<div class="relay">
|
||||
<span class="k">SG1</span>
|
||||
<div class="thing">Relay4 <i>Digital Output</i></div>
|
||||
</div>
|
||||
<div class="relay">
|
||||
<span class="k">SG2</span>
|
||||
<div class="thing">Relay5 <i>Digital Output</i></div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<b>« SG1 » et « SG2 » n'existent pas côté box</b>
|
||||
<code>sgReady.states[]</code> ne stocke qu'une <b>liste de relais par état</b>, sans nom ni ordre.
|
||||
Les deux étiquettes sont une convention de cet écran, et elles se retrouvent à la lecture :
|
||||
<b>SG1 = le contact fermé en état 1</b> (blocage), <b>SG2 = celui fermé en état 3</b>
|
||||
(recommandation). C'est l'encodage SG-Ready standard, et c'est celui du banc.
|
||||
</div>
|
||||
<div class="warn">
|
||||
<b>⚠ Un encodage non standard perd les étiquettes</b>
|
||||
Si la configuration lue ne correspond pas à ce schéma — trois contacts, ou un état 1 sans contact
|
||||
fermé — l'écran doit renoncer aux noms SG1/SG2 et afficher les relais tels quels, plutôt que
|
||||
d'attribuer une étiquette au hasard. Le tableau ci-dessous reste éditable ; ce sont les noms qui
|
||||
disparaissent, pas la configuration.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Table d'états <span class="saved-flag">enregistré · sgReady.states[]</span><br>
|
||||
<small>quels contacts fermer pour chaque état, et ce que la PAC consomme alors</small></label>
|
||||
|
||||
<div class="st">
|
||||
<div class="h"><span class="badge s1">1</span><b>Blocage</b><span>arrêt forcé</span></div>
|
||||
<div class="contacts">
|
||||
<span class="kchip on">SG1 fermé</span><span class="kchip">SG2 ouvert</span>
|
||||
</div>
|
||||
<div class="pw"><label>Puissance estimée</label><input value="0 W"></div>
|
||||
</div>
|
||||
|
||||
<div class="st">
|
||||
<div class="h"><span class="badge s2">2</span><b>Normal</b><span>aucune consigne</span></div>
|
||||
<div class="contacts">
|
||||
<span class="kchip">SG1 ouvert</span><span class="kchip">SG2 ouvert</span>
|
||||
</div>
|
||||
<div class="pw"><label>Puissance estimée</label><input value="0 W"></div>
|
||||
</div>
|
||||
|
||||
<div class="st">
|
||||
<div class="h"><span class="badge s3">3</span><b>Recommandation</b><span>incitation douce</span></div>
|
||||
<div class="contacts">
|
||||
<span class="kchip">SG1 ouvert</span><span class="kchip on">SG2 fermé</span>
|
||||
</div>
|
||||
<div class="pw"><label>Puissance estimée</label><input value="1 500 W"></div>
|
||||
</div>
|
||||
|
||||
<div class="st">
|
||||
<div class="h"><span class="badge s4">4</span><b>Forcé</b><span>marche imposée</span></div>
|
||||
<div class="contacts">
|
||||
<span class="kchip on">SG1 fermé</span><span class="kchip on">SG2 fermé</span>
|
||||
</div>
|
||||
<div class="pw"><label>Puissance estimée</label><input value="3 000 W"></div>
|
||||
</div>
|
||||
|
||||
<div class="warn">
|
||||
<b>⚠ L'état 2 doit être exprimable</b>
|
||||
Si l'encodage des contacts ne permet pas de représenter l'état « normal », la pompe à chaleur
|
||||
n'est <b>jamais</b> pilotable : la box publie <code>UNUSABLE_ENCODING</code> et la charge sort de
|
||||
l'arbitrage définitivement (ECS-110). Le formulaire doit le refuser avant l'envoi.
|
||||
</div>
|
||||
<div class="info">
|
||||
<b>Une estimation, jamais un engagement</b>
|
||||
<code>estimatedPowerW</code> sert de base au <b>recrédit de budget</b> et au choix d'état : une
|
||||
valeur fausse ne décale pas seulement la PAC, elle décale l'allocation de tout le waterfall en
|
||||
aval. À relever sur la plaque signalétique, puis à mesurer sous les deux régimes.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Anti court-cycle <span class="saved-flag">enregistré · sgReady.minStateHoldS</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Maintien d'état minimal<small>durée avant tout changement d'état</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>300 s</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
<div class="mode-hint">Pendant ce maintien, la télémétrie publie le verrou
|
||||
<b>LOCK_MIN_STATE_HOLD</b> avec les secondes restantes — c'est ce que l'écran affiche
|
||||
« au dernier cycle », sans le décompter lui-même.</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field">
|
||||
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row">
|
||||
<div class="lab">Rattacher un compteur à cette charge</div>
|
||||
<span class="sw off"></span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:9px">
|
||||
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
|
||||
</div>
|
||||
<p><b>Aucun champ n'existe côté box.</b> <code>SetLoadConfig</code> n'accepte que <code>adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS · needs · powerLevels · relays · sgReady</code>. nymea rejette toute clé inconnue <b>avant</b> le handler : inventer <code>meterThingId</code> ferait échouer l'enregistrement de <b>toutes</b> les charges.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Arbitrage <span class="saved-flag">enregistré · enabled</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Inclure dans l'arbitrage<small>rang 2 · Chauffage</small></div>
|
||||
<div class="ctl"><span class="sw"></span></div>
|
||||
</div>
|
||||
<div class="mode-hint">À la désactivation, le retrait passe par l'état sûr : <b>état 2</b>,
|
||||
jamais le blocage. Vérifié sur matériel côté plugin.</div>
|
||||
</div>
|
||||
|
||||
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap"><b>SG-Ready</b> — même contrat que le relais</div>
|
||||
<div class="note">
|
||||
<span class="pill load">NymeaEnergy.SetLoadConfig</span><br>
|
||||
Même famille que le routeur de relais : c'est une <code>LoadConfig</code>, elle est arbitrée par le
|
||||
waterfall, elle apparaît dans <code>GetLoadTelemetry</code>, elle a un rang et un domaine.
|
||||
<ul>
|
||||
<li>Ce que les contacts commandent, ce sont des <b>états normés 1→4</b>, pas des watts. Les watts
|
||||
n'y sont qu'une <b>estimation déclarée</b> — d'où le libellé.</li>
|
||||
<li>Toucher aux contacts ou à la table d'états franchit <code>sameHardware()</code> :
|
||||
<b>reconstruction de l'adaptateur</b> et réarmement des verrous à froid.</li>
|
||||
<li><b>Le choix des Things se fait une fois, en haut</b> — pas état par état. Ce qui se coche dans le
|
||||
tableau, c'est <b>fermé / ouvert</b>, et c'est ça qui devient <code>relays[]</code> à
|
||||
l'enregistrement : la liste des contacts fermés pour cet état.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ BORNE DE RECHARGE ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Configurer · Borne de recharge</h2><div class="ic"></div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="field">
|
||||
<label>Appareil <span class="other-flag">chargingInfo · evChargerId</span></label>
|
||||
<div class="sel">Terra AC Charger (TCP) <i>▾</i></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>État actuel <span class="derived-flag">lecture seule · chargingState</span></label>
|
||||
<div class="state"><span class="dot"></span><b>Recharge sur surplus</b><i>SurplusCharging</i></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Mode de recharge par défaut <span class="other-flag">chargingInfo · chargingMode</span><br>
|
||||
<small>ce que la borne applique quand personne n'a rien demandé</small></label>
|
||||
<div class="evmode">
|
||||
<button class="on"><u>☀</u>PV<small>surplus seul</small></button>
|
||||
<button><u>⚡</u>Min+PV<small>courant mini</small></button>
|
||||
<button><u>🚀</u>Boost<small>pleine puissance</small></button>
|
||||
</div>
|
||||
<div class="maps">
|
||||
PV → <code>ChargingModeEco</code> · Min+PV → <code>ChargingModeEcoWithMinCurrent</code> ·
|
||||
Boost → <code>ChargingModeNormal</code><br>
|
||||
avec échéance → <code>ChargingModeEcoWithTargetTime</code> /
|
||||
<code>ChargingModeEcoMinWithTargetTime</code>
|
||||
</div>
|
||||
<div class="mode-hint"><b>C'est un défaut, pas une commande.</b> Le pilotage au jour le jour se
|
||||
fait depuis la carte véhicule du tableau de bord ; ici on règle ce vers quoi la borne revient.
|
||||
Les deux écrivent le même <code>chargingMode</code> — d'où l'importance de ne pas laisser croire
|
||||
que ce bouton agit à l'instant.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Échéance de départ <span class="other-flag">targetPercentage · endDateTime · repeatDays</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Fixer une heure de départ<small>bascule le mode en « …WithTargetTime »</small></div>
|
||||
<div class="ctl"><span class="sw"></span></div>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<div class="lab">Charge cible<small>targetPercentage</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>80 %</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<div class="lab">Prête à<small>endDateTime</small></div>
|
||||
<div class="ctl"><b>07:30</b></div>
|
||||
</div>
|
||||
<div class="sel">Répéter : Lun · Mar · Mer · Jeu · Ven <i>▾</i></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Courant minimum <span class="todo-flag">champ absent de la box</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row">
|
||||
<div class="lab">Courant maintenu en Min+PV</div>
|
||||
<div class="ctl" style="display:flex;align-items:center;gap:12px">
|
||||
<button class="pm">−</button><b style="font-family:Poppins;font-size:13.5px">6 A</b><button class="pm">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<p><b>Le mode existe, le réglage n'existe pas.</b> <code>ChargingModeEcoWithMinCurrent</code> est
|
||||
bien dans l'énumération de la box, mais <code>ChargingInfo</code> n'a <b>aucun champ</b> de courant
|
||||
minimum — ni <code>minCurrent</code>, ni équivalent. Soit la valeur est fixée ailleurs (paramètre
|
||||
du Thing borne), soit le champ manque. <b>À trancher avec l'agent plugin avant de livrer ce
|
||||
mode</b> : proposer un réglage qui ne part nulle part serait pire que de ne pas le proposer.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field">
|
||||
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row">
|
||||
<div class="lab">Rattacher un compteur à la borne</div>
|
||||
<span class="sw off"></span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:9px">
|
||||
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
|
||||
</div>
|
||||
<p><b>Aucun champ n'existe côté box.</b> <code>ChargingInfo</code> ne porte que <code>chargingMode · evChargerId · assignedCarId · dailySpotMarketPercentage · endDateTime · repeatDays · spotMarketChargingEnabled · targetPercentage</code>, plus <code>chargingState</code> en lecture. Rien pour un compteur — et la borne mesure déjà, ce qui pose la question de <b>quelle</b> mesure fait foi.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Tarif dynamique <span class="other-flag">spotMarketChargingEnabled · dailySpotMarketPercentage</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Recharger aux heures les moins chères<small>Predict AI</small></div>
|
||||
<div class="ctl"><span class="sw off"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap"><b>Borne</b> — un autre contrat</div>
|
||||
<div class="note">
|
||||
<span class="pill ev">NymeaEnergy.SetChargingInfo</span><br>
|
||||
<b>Ce n'est pas une <code>LoadConfig</code>.</b> Le véhicule n'a ni <code>adapter</code>, ni
|
||||
<code>priority</code>, ni <code>domain</code> — et il <b>n'apparaît pas</b> dans
|
||||
<code>GetLoadTelemetry</code> en beta : il est décidé en amont du waterfall, et sa puissance est
|
||||
seulement <b>réservée</b> dans <code>budget.evReservedW</code>. Il rejoindra <code>loads[]</code> à
|
||||
l'étape 3g.
|
||||
<ul>
|
||||
<li><b>Les noms de champs de <code>CLAUDE.md</code> sont périmés.</b> La box déploie
|
||||
<code>chargingMode</code> (pas <code>mode</code>), <code>targetPercentage</code> (pas
|
||||
<code>targetSoc</code>), <code>endDateTime</code> en <b>Uint</b> (pas <code>endTime</code>), plus
|
||||
<code>assignedCarId</code>, <code>repeatDays</code> et deux champs spot market. À corriger dans
|
||||
<code>CLAUDE.md</code>.</li>
|
||||
<li>Le namespace est <code>NymeaEnergy.*</code>, pas <code>EnergyPlugin.*</code>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ ZONES HVAC ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Zones de climatisation</h2><div class="ic">+</div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="field" style="margin-top:2px">
|
||||
<label>Zones déclarées <span class="other-flag">AirConditioning.GetZones</span></label>
|
||||
|
||||
<div class="zone">
|
||||
<div class="z1">
|
||||
<b>Séjour</b>
|
||||
<div class="temp">20,4 °<small>consigne 21,0 °</small></div>
|
||||
</div>
|
||||
<div class="z2">
|
||||
<span class="tchip">2 thermostats</span><span class="tchip">1 vanne</span>
|
||||
<span class="tchip">1 sonde int.</span><span class="tchip">1 contact fenêtre</span>
|
||||
</div>
|
||||
<div class="ovr">⏱ Dérogation 22,0 ° — encore 45 min</div>
|
||||
</div>
|
||||
|
||||
<div class="zone">
|
||||
<div class="z1">
|
||||
<b>Chambres</b>
|
||||
<div class="temp">18,9 °<small>consigne 19,0 °</small></div>
|
||||
</div>
|
||||
<div class="z2">
|
||||
<span class="tchip">3 thermostats</span><span class="tchip">1 sonde int.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone">
|
||||
<div class="z1">
|
||||
<b>Bureau</b>
|
||||
<div class="temp">21,1 °<small>réduit 17,0 °</small></div>
|
||||
</div>
|
||||
<div class="z2">
|
||||
<span class="tchip">1 thermostat</span><span class="tchip">humidité 47 %</span>
|
||||
<span class="tchip">COV 320</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Zone « Séjour » <span class="saved-flag">SetZoneThings · SetZoneName</span></label>
|
||||
<input class="inp" value="Séjour" style="margin-bottom:9px">
|
||||
<div class="rowsel"><span class="lab">Thermostats</span><span class="cnt">2 appareils</span><i>▾</i></div>
|
||||
<div class="rowsel"><span class="lab">Vannes</span><span class="cnt">1 appareil</span><i>▾</i></div>
|
||||
<div class="rowsel"><span class="lab">Sondes intérieures</span><span class="cnt">1 appareil</span><i>▾</i></div>
|
||||
<div class="rowsel"><span class="lab">Sondes extérieures</span><span class="cnt">aucune</span><i>▾</i></div>
|
||||
<div class="rowsel"><span class="lab">Contacts de fenêtre</span><span class="cnt">1 appareil</span><i>▾</i></div>
|
||||
<div class="rowsel"><span class="lab">Notifications</span><span class="cnt">aucune</span><i>▾</i></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field">
|
||||
<label>Compteur dédié <span class="todo-flag">prévu · pas encore supporté</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row">
|
||||
<div class="lab">Rattacher un compteur à la zone</div>
|
||||
<span class="sw off"></span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:9px">
|
||||
<div class="sel" style="flex:1">Choisir un appareil de mesure… <i>▾</i></div>
|
||||
</div>
|
||||
<p><b>Aucun champ n'existe côté box.</b> <code>ZoneInfo</code> rattache des <code>indoorSensors</code> et <code>outdoorSensors</code> — des sondes de <b>température</b>, pas de consommation. Un compteur de zone est un champ à ajouter, et il faudra dire s'il mesure la zone ou l'appareil qui la chauffe.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Dérogation <span class="saved-flag">SetZoneSetpointOverride</span></label>
|
||||
<div class="seg">
|
||||
<button>Aucune</button><button class="on">Minutée</button>
|
||||
<button>Illimitée</button><button>Éventuelle</button>
|
||||
</div>
|
||||
<div class="stepper" style="margin-top:9px">
|
||||
<div class="lab">Consigne dérogée<small>setpointOverride</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>22,0 °</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<div class="lab">Durée<small>minutes — mode minuté seul</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>60 min</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Consigne de réduit <span class="saved-flag">SetZoneStandbySetpoint</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Hors planning<small>standbySetpoint</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>17,0 °</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Planning hebdomadaire <span class="saved-flag">SetZoneWeekSchedule</span></label>
|
||||
<div class="sel">7 jours · 4 créneaux par jour <i>›</i></div>
|
||||
<div class="mode-hint">Écran dédié — la forme exacte de <code>TemperatureWeekSchedule</code> reste
|
||||
à introspecter, le type n'est pas détaillé dans le schéma de cette box.</div>
|
||||
</div>
|
||||
|
||||
<div class="info" style="margin-top:20px">
|
||||
<b>Sur le banc, il n'y a aucune zone</b>
|
||||
<code>AirConditioning.GetZones</code> renvoie <code>zones: []</code> sur <code>.75</code>.
|
||||
L'état vide ci-dessous est donc le <b>premier</b> écran que verra un installateur.
|
||||
</div>
|
||||
|
||||
<div class="empty" style="margin-top:12px">
|
||||
<b>Aucune zone déclarée</b>
|
||||
<p>Une zone regroupe des thermostats, des vannes et des sondes sous une même consigne.
|
||||
Sans zone, la climatisation n'est pas pilotée par l'énergie.</p>
|
||||
<button>+ Créer une zone</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap"><b>HVAC</b> — encore un autre contrat</div>
|
||||
<div class="note">
|
||||
<span class="pill ac">AirConditioning.* · tier Auto</span><br>
|
||||
Troisième contrat : <b>ni <code>LoadConfig</code>, ni <code>ChargingInfo</code></b>. Une zone n'a pas de
|
||||
rang de service ni de domaine — c'est un regroupement de Things sous une consigne de température.
|
||||
<ul>
|
||||
<li><b>8 méthodes, pas 3</b> comme l'annonce <code>CLAUDE.md</code> : s'ajoutent
|
||||
<code>AddZone</code>, <code>RemoveZone</code>, <code>SetZoneName</code>,
|
||||
<code>SetZoneThings</code> et <code>SetZoneStandbySetpoint</code>. Le CRUD complet est donc
|
||||
possible depuis l'app.</li>
|
||||
<li>Une zone expose <b>en lecture seule</b> température, humidité, PM2.5, COV, statut, et la
|
||||
dérogation en cours avec sa fin. Tout le reste des champs de <code>ZoneInfo</code> est
|
||||
<code>r:</code> — on écrit par les six méthodes <code>Set*</code>, jamais par un objet complet.</li>
|
||||
<li><b>Lien avec l'énergie à établir :</b> rien dans <code>ZoneInfo</code> ne relie une zone à une
|
||||
<code>LoadConfig</code>. La PAC SG-Ready et les zones vivent côte à côte sans se connaître — si
|
||||
l'ordre de service doit tenir compte des zones, c'est une question de conception, pas de câblage.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ══════════ SÉLECTEUR DE THINGS (ZONE) ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Séjour · Thermostats</h2><div class="ic">✓</div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="field" style="margin-top:2px">
|
||||
<label>Rôle dans la zone <span class="saved-flag">SetZoneThings</span></label>
|
||||
<div class="filters">
|
||||
<span class="fchip on">Thermostats</span>
|
||||
<span class="fchip">Vannes</span>
|
||||
<span class="fchip">Sondes int.</span>
|
||||
<span class="fchip">Sondes ext.</span>
|
||||
<span class="fchip">Fenêtres</span>
|
||||
<span class="fchip">Notifications</span>
|
||||
</div>
|
||||
<div class="mode-hint">Six listes distinctes, six paramètres de <code>SetZoneThings</code>. On
|
||||
n'envoie que celles qu'on a touchées — les autres sont optionnelles et restent en place.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Appareils compatibles <span class="derived-flag">filtré · interfaces nymea</span><br>
|
||||
<small>ThingClass portant l'interface « thermostat »</small></label>
|
||||
<div class="search">🔍 Rechercher un appareil…</div>
|
||||
|
||||
<div class="pickrow">
|
||||
<span class="cb on"></span>
|
||||
<div class="nm"><b>Thermostat Séjour Nord</b><span>Danfoss Ally · thermostat</span></div>
|
||||
</div>
|
||||
<div class="pickrow">
|
||||
<span class="cb on"></span>
|
||||
<div class="nm"><b>Thermostat Séjour Sud</b><span>Danfoss Ally · thermostat</span></div>
|
||||
</div>
|
||||
<div class="pickrow dim">
|
||||
<span class="cb"></span>
|
||||
<div class="nm"><b>Thermostat Chambre 1</b><span>Danfoss Ally · thermostat</span></div>
|
||||
<span class="taken">zone « Chambres »</span>
|
||||
</div>
|
||||
<div class="pickrow">
|
||||
<span class="cb"></span>
|
||||
<div class="nm"><b>Thermostat Bureau</b><span>Shelly TRV · thermostat</span></div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<b>Un appareil dans deux zones ?</b>
|
||||
Le schéma ne l'interdit pas — <code>SetZoneThings</code> prend une liste d'UUID sans contrainte
|
||||
d'unicité déclarée. Tant que ce n'est pas éprouvé sur la box, l'app <b>signale</b> l'appartenance
|
||||
existante sans bloquer, et ne prétend pas connaître la règle.
|
||||
</div>
|
||||
|
||||
<div class="warn">
|
||||
<b>⚠ Sur le banc, ces listes sont vides</b>
|
||||
Les 58 ThingClass de <code>.75</code> ne portent <b>aucune</b> interface thermostat, vanne ou
|
||||
sonde de température — il n'y a pas de matériel HVAC dessus. Les appareils ci-dessus sont
|
||||
fictifs : cet écran n'est pas éprouvable en l'état, et la première vérification à faire est
|
||||
d'y brancher un thermostat réel.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Compteur de zone <span class="todo-flag">prévu · pas encore supporté</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row">
|
||||
<div class="lab">Rattacher un compteur à la zone</div>
|
||||
<span class="sw off"></span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:9px">
|
||||
<div class="sel" style="flex:1">Filtrer sur « energymeter »… <i>▾</i></div>
|
||||
</div>
|
||||
<p><b>Le filtre, lui, existe déjà.</b> <code>.75</code> porte les interfaces
|
||||
<code>energymeter</code>, <code>smartmeter</code>, <code>smartmeterconsumer</code> et
|
||||
<code>smartmeterproducer</code> : le jour où le champ est ajouté, l'app saura proposer les bons
|
||||
appareils sans table maintenue à la main. C'est le <b>champ de destination</b> qui manque, pas la
|
||||
façon de choisir.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Valider (2)</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap"><b>Zone</b> — choisir les appareils</div>
|
||||
<div class="note">
|
||||
<span class="pill ac">AirConditioning.SetZoneThings</span><br>
|
||||
Le même écran sert aux six rôles : thermostats, vannes, sondes intérieures, sondes extérieures,
|
||||
contacts de fenêtre, notifications. Seul le filtre d'interface change.
|
||||
<ul>
|
||||
<li><b>Filtrer par interface, pas par nom de classe.</b> <code>ThingClass.interfaces</code> est une
|
||||
liste en lecture sur chaque classe ; s'en servir évite la table de correspondance codée en dur que
|
||||
le brief interdit — et qui vieillirait à chaque plugin ajouté.</li>
|
||||
<li><b>Ne rien attendre du vocabulaire affiché.</b> Mesuré sur <code>.75</code> : sur 784 chaînes,
|
||||
le français en traduit <b>zéro</b>. Les <code>displayName</code> sont des identifiants anglais, à
|
||||
afficher tels quels.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
321
docs/mockups/raccordement_reseau_mockup.html
Normal file
321
docs/mockups/raccordement_reseau_mockup.html
Normal file
@ -0,0 +1,321 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ETM PowerSync — Raccordement réseau et protection de surcharge</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#081f2b; --panel:#0d2b3b; --card:#12384a; --card2:#0f3242;
|
||||
--line:rgba(255,255,255,.08); --line2:rgba(255,255,255,.14);
|
||||
--blue:#31a3dd; --yellow:#fec113; --green:#35c98a; --red:#e07a7a; --violet:#9d8ce0;
|
||||
--txt:#e8f1f6; --muted:#90a8b6; --muted2:#6f8b9a;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:
|
||||
radial-gradient(900px 500px at 15% -10%, #0e3145 0%, transparent 60%),
|
||||
radial-gradient(900px 600px at 110% 10%, #0b2535 0%, transparent 55%),
|
||||
var(--bg);
|
||||
font-family:'Inter',system-ui,sans-serif;color:var(--txt);min-height:100vh;padding:34px 20px 60px}
|
||||
.head{max-width:1000px;margin:0 auto 26px}
|
||||
.head h1{font-family:'Poppins';font-weight:700;font-size:20px;margin:0 0 6px}
|
||||
.head p{margin:0 0 8px;color:var(--muted);font-size:13.5px;line-height:1.55;max-width:880px}
|
||||
.head code{color:#9fd4ee} .head b{color:var(--yellow)}
|
||||
.stage{display:flex;gap:34px;justify-content:center;flex-wrap:wrap;align-items:flex-start}
|
||||
.col{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:392px}
|
||||
.cap{color:var(--muted2);font-size:12px;font-family:'Poppins';font-weight:500;letter-spacing:.4px;text-transform:uppercase}
|
||||
.cap b{color:var(--blue)}
|
||||
|
||||
.phone{width:392px;background:var(--panel);border:1px solid var(--line2);border-radius:30px;
|
||||
overflow:hidden;box-shadow:0 30px 80px -30px rgba(0,0,0,.7);display:flex;flex-direction:column;height:850px}
|
||||
.appbar{display:flex;align-items:center;gap:12px;padding:18px 16px 14px;border-bottom:1px solid var(--line);
|
||||
background:linear-gradient(#102f40,#0d2b3b)}
|
||||
.appbar .ic{width:26px;height:26px;display:grid;place-items:center;color:var(--muted);font-size:18px}
|
||||
.appbar h2{font-family:'Poppins';font-weight:600;font-size:15px;margin:0;flex:1;text-align:center}
|
||||
.scroll{overflow:auto;padding:16px 15px 26px}
|
||||
.scroll::-webkit-scrollbar{width:0}
|
||||
|
||||
.sect{font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);letter-spacing:.3px;
|
||||
margin:20px 0 9px}
|
||||
.sect:first-child{margin-top:2px}
|
||||
|
||||
.field{margin-top:18px} .field:first-child{margin-top:4px}
|
||||
.field>label{display:block;font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);
|
||||
letter-spacing:.3px;margin-bottom:9px}
|
||||
.field>label small{color:var(--muted2);font-weight:500;letter-spacing:0}
|
||||
.saved-flag{font-size:10.5px;color:var(--green);background:rgba(53,201,138,.12);border:1px solid rgba(53,201,138,.3);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
.derived-flag{font-size:10.5px;color:var(--muted2);background:rgba(255,255,255,.05);border:1px solid var(--line2);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
.todo-flag{font-size:10.5px;color:var(--yellow);background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.32);
|
||||
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
|
||||
|
||||
.sel{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
|
||||
font-family:'Inter';font-size:12.5px;padding:11px 12px;display:flex;align-items:center;gap:8px}
|
||||
.sel i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
|
||||
.mode-hint{margin-top:9px;font-size:11.5px;color:var(--muted2);line-height:1.5}
|
||||
.mode-hint b{color:var(--muted)}
|
||||
|
||||
.meter{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:13px}
|
||||
.meter .m1{display:flex;align-items:center;gap:11px}
|
||||
.meter .ico{width:38px;height:38px;border-radius:11px;background:rgba(254,193,19,.13);display:grid;
|
||||
place-items:center;font-size:19px;flex:none}
|
||||
.meter .m1 .nm{flex:1;min-width:0}
|
||||
.meter .m1 .nm b{display:block;font-family:'Poppins';font-weight:600;font-size:13.5px}
|
||||
.meter .m1 .nm span{font-size:11px;color:var(--muted2)}
|
||||
.tag{font-size:10.5px;color:var(--blue);background:rgba(49,163,221,.12);border:1px solid rgba(49,163,221,.3);
|
||||
border-radius:20px;padding:3px 9px;font-family:'Poppins';font-weight:600}
|
||||
.phases{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:11px}
|
||||
.ph{background:var(--card2);border:1px solid var(--line);border-radius:10px;padding:8px 5px;text-align:center}
|
||||
.ph u{display:block;text-decoration:none;font-size:9.5px;color:var(--muted2);margin-bottom:2px}
|
||||
.ph b{font-family:'Poppins';font-weight:600;font-size:12.5px}
|
||||
.ph.hot b{color:var(--red)}
|
||||
.gorow{margin-top:11px;display:flex;align-items:center;gap:9px;background:var(--card2);border:1px solid var(--line2);
|
||||
border-radius:11px;padding:11px 12px;cursor:pointer}
|
||||
.gorow .lab{flex:1;font-size:12.5px}
|
||||
.gorow .val{font-family:'Poppins';font-weight:600;font-size:12.5px;color:var(--yellow)}
|
||||
.gorow i{font-style:normal;color:var(--muted2)}
|
||||
|
||||
.amp{display:flex;align-items:center;justify-content:space-between;padding:14px;border:1px solid var(--line2);
|
||||
border-radius:13px;background:var(--card2)}
|
||||
.amp .v{font-family:'Poppins';font-weight:700;font-size:26px}
|
||||
.amp .v small{font-size:14px;font-weight:600;color:var(--muted);margin-left:3px}
|
||||
.pm{width:34px;height:34px;border-radius:10px;border:1px solid var(--line2);color:var(--blue);background:transparent;
|
||||
font-size:19px;display:grid;place-items:center;cursor:pointer}
|
||||
.presets{display:flex;gap:6px;margin-top:9px;flex-wrap:wrap}
|
||||
.preset{font-size:11px;border-radius:20px;padding:6px 12px;border:1px solid var(--line2);
|
||||
background:var(--card2);color:var(--muted);font-family:'Poppins';font-weight:600;cursor:pointer}
|
||||
.preset.on{border-color:var(--blue);color:var(--blue);background:rgba(49,163,221,.1)}
|
||||
|
||||
.stepper{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border:1px solid var(--line);
|
||||
border-radius:11px;background:var(--card2);margin-bottom:8px}
|
||||
.stepper .lab{font-size:12.5px}.stepper .lab small{display:block;color:var(--muted2);font-size:10.5px;margin-top:1px}
|
||||
.stepper .ctl{display:flex;align-items:center;gap:13px}
|
||||
.stepper .ctl b{font-family:'Poppins';font-weight:600;font-size:13.5px;min-width:64px;text-align:right}
|
||||
.stepper .pm{width:26px;height:26px;border-radius:8px;font-size:16px}
|
||||
.sw{width:38px;height:22px;border-radius:20px;background:rgba(53,201,138,.85);position:relative;cursor:pointer;flex:none}
|
||||
.sw::after{content:"";position:absolute;top:2px;left:18px;width:18px;height:18px;border-radius:50%;background:#fff}
|
||||
.sw.off{background:rgba(255,255,255,.16)} .sw.off::after{left:2px}
|
||||
|
||||
.warn{margin-top:10px;border:1px solid rgba(224,122,122,.32);background:rgba(224,122,122,.07);
|
||||
border-radius:11px;padding:10px 11px;font-size:11px;color:var(--muted);line-height:1.5}
|
||||
.warn b{color:var(--red);display:block;font-family:'Poppins';font-size:11.5px;margin-bottom:3px}
|
||||
.info{margin-top:10px;border:1px solid rgba(49,163,221,.26);background:rgba(49,163,221,.06);
|
||||
border-radius:11px;padding:10px 11px;font-size:11px;color:var(--muted);line-height:1.5}
|
||||
.info b{color:var(--blue);display:block;font-family:'Poppins';font-size:11.5px;margin-bottom:3px}
|
||||
.blocked{border:1px dashed rgba(254,193,19,.35);background:rgba(254,193,19,.05);border-radius:12px;padding:12px}
|
||||
.blocked p{margin:9px 0 0;font-size:10.8px;color:var(--muted2);line-height:1.5} .blocked p b{color:var(--yellow)}
|
||||
.blocked .row{display:flex;align-items:center;gap:10px;opacity:.55}
|
||||
.blocked .row .lab{font-size:12.5px;flex:1}
|
||||
|
||||
.save{display:flex;gap:10px;margin-top:24px}
|
||||
.btn{text-align:center;font-family:'Poppins';font-weight:600;font-size:13px;padding:12px;border-radius:11px;cursor:pointer}
|
||||
.btn.go{background:var(--blue);color:#04222f;flex:2;border:0}
|
||||
.btn.alt{flex:1.3;background:transparent;border:1px solid var(--line2);color:var(--txt)}
|
||||
|
||||
.note{width:392px;font-size:11.5px;color:var(--muted2);line-height:1.55;text-align:left}
|
||||
.note b{color:var(--yellow)} .note code{color:#9fd4ee}
|
||||
.note ul{margin:7px 0 0;padding-left:16px} .note li{margin-bottom:5px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="head">
|
||||
<h1>Raccordement réseau — la limite du disjoncteur, et ce qu'elle protège vraiment</h1>
|
||||
<p>Le réglage existe déjà côté box : <code>NymeaEnergy.SetPhasePowerLimit</code>. Sa place naturelle est
|
||||
bien le <b>compteur réseau</b>, puisqu'il n'a de sens qu'avec lui — la protection est inopérante sans
|
||||
compteur racine.</p>
|
||||
<p><b>Trois pièges relevés en lisant la box et le plugin :</b> ① le champ s'appelle « power » mais son unité
|
||||
est l'<b>ampère par phase</b> ; ② <b>0 = désactivé</b>, et cela coupe aussi toute la recharge intelligente ;
|
||||
③ la protection ne déleste <b>que le véhicule</b> — aucune charge du waterfall n'est réduite par elle.</p>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
|
||||
<!-- ══════════ ENTRÉE : CARTE COMPTEUR RÉSEAU ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Rôles & appareils</h2><div class="ic">⟳</div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="sect">Compteurs · mesure — hors priorité</div>
|
||||
|
||||
<div class="meter">
|
||||
<div class="m1">
|
||||
<div class="ico">⚡</div>
|
||||
<div class="nm"><b>Compteur réseau</b><span>Fronius Three Phase Meter · SunSpec</span></div>
|
||||
<span class="tag">rootMeter</span>
|
||||
</div>
|
||||
|
||||
<div class="phases">
|
||||
<div class="ph"><u>Phase A</u><b>9,2 A</b></div>
|
||||
<div class="ph"><u>Phase B</u><b>4,1 A</b></div>
|
||||
<div class="ph hot"><u>Phase C</u><b>28,7 A</b></div>
|
||||
</div>
|
||||
|
||||
<div class="gorow">
|
||||
<span class="lab">Raccordement & protection</span>
|
||||
<span class="val">32 A / phase</span>
|
||||
<i>›</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<b>Pourquoi ici</b>
|
||||
La limite n'a de sens qu'attachée au compteur racine : la protection lit
|
||||
<code>currentPowerPhaseA/B/C</code> sur ce Thing. Sans compteur réseau, la box journalise
|
||||
« <i>overload protection is not available</i> » et ne fait rien.
|
||||
</div>
|
||||
|
||||
<div class="sect">Compteur solaire</div>
|
||||
<div class="meter" style="opacity:.75">
|
||||
<div class="m1">
|
||||
<div class="ico" style="background:rgba(53,201,138,.13)">☀</div>
|
||||
<div class="nm"><b>Compteur solaire</b><span>Fronius Three Phase Inverter</span></div>
|
||||
<span class="tag" style="color:var(--green);background:rgba(53,201,138,.12);
|
||||
border-color:rgba(53,201,138,.3)">auto</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap">L'entrée — sur la carte du compteur réseau</div>
|
||||
<div class="note">
|
||||
Les trois ampérages par phase sont <b>déjà lisibles</b> : ce sont les états
|
||||
<code>currentPowerPhaseA/B/C</code> du Thing compteur, divisés par la tension. Les afficher ici rend la
|
||||
limite concrète — l'installateur voit tout de suite laquelle de ses phases approche.
|
||||
<ul>
|
||||
<li><b>Rien de tout ça n'est encore branché dans l'app</b> : l'écran actuel n'affiche que le nom du
|
||||
Thing et le badge <code>rootMeter</code>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ ÉCRAN RACCORDEMENT ══════════ -->
|
||||
<div class="col">
|
||||
<div class="phone">
|
||||
<div class="appbar"><div class="ic">‹</div><h2>Raccordement & protection</h2><div class="ic"></div></div>
|
||||
<div class="scroll">
|
||||
|
||||
<div class="field">
|
||||
<label>Courant maximal par phase <span class="saved-flag">enregistré · phasePowerLimit</span><br>
|
||||
<small>le calibre du disjoncteur de branchement</small></label>
|
||||
<div class="amp">
|
||||
<button class="pm">−</button>
|
||||
<div class="v">32<small>A</small></div>
|
||||
<button class="pm">+</button>
|
||||
</div>
|
||||
<div class="presets">
|
||||
<span class="preset">15 A</span><span class="preset">30 A</span>
|
||||
<span class="preset on">32 A</span><span class="preset">45 A</span>
|
||||
<span class="preset">60 A</span><span class="preset">63 A</span>
|
||||
</div>
|
||||
<div class="mode-hint"><b>Des ampères, pas des watts.</b> Le champ s'appelle
|
||||
<code>phasePowerLimit</code> mais la box attend un courant par phase — sa propre description le
|
||||
dit : « <i>maximum allowed Ampere per phase</i> ». Un formulaire en kVA se tromperait de grandeur.</div>
|
||||
|
||||
<div class="info">
|
||||
<b>Équivalence indicative</b>
|
||||
32 A × 230 V × 3 phases ≈ <b>22 kVA</b>
|
||||
<span style="display:block;margin-top:5px;color:var(--muted2)">Déduit, jamais enregistré. La box
|
||||
convertit avec <b>230 V codés en dur</b> quand la tension mesurée n'est pas disponible ; le nombre
|
||||
de phases vient des états du compteur, pas d'un réglage.</span>
|
||||
</div>
|
||||
|
||||
<div class="warn">
|
||||
<b>⚠ 0 A ne veut pas dire « pas de limite »</b>
|
||||
0 <b>désactive la protection</b> — et avec elle <b>toute la recharge intelligente</b>
|
||||
(« <i>All smart charging will be disabled</i> »). C'est la valeur actuelle du banc
|
||||
<code>.75</code>, et c'est pour ça que son journal répète
|
||||
« <i>overload protection is disabled</i> ». Un champ vide ne doit jamais être enregistré comme 0
|
||||
par inadvertance.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Ce que la protection déleste <span class="derived-flag">comportement observé</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Borne de recharge<small>bridée, puis coupée si nécessaire</small></div>
|
||||
<div class="ctl"><span class="sw"></span></div>
|
||||
</div>
|
||||
<div class="stepper" style="opacity:.55">
|
||||
<div class="lab">Chauffe-eau, PAC, charges modulables<small>non concernées aujourd'hui</small></div>
|
||||
<div class="ctl"><span class="sw off"></span></div>
|
||||
</div>
|
||||
<div class="warn">
|
||||
<b>⚠ Ce n'est pas un délestage général</b>
|
||||
Quand une phase dépasse, la box parcourt <b>uniquement ses bornes de recharge</b>
|
||||
(<code>m_evChargers</code>) et réduit leur courant, jusqu'à couper si le minimum n'est plus
|
||||
tenable. Les charges du waterfall — routeur de relais, modulable, SG-Ready — ne sont
|
||||
<b>pas</b> réduites par cette protection.
|
||||
<span style="display:block;margin-top:6px">Étendre le délestage à ces charges est une question
|
||||
<b>côté plugin</b>, pas côté app. Tant qu'elle n'est pas tranchée, l'écran ne doit pas laisser
|
||||
croire que le raccordement est protégé dans son ensemble.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Seuil de démarrage sur surplus <span class="saved-flag">enregistré · acquisitionTolerance</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Surplus requis pour lancer<small>valeur banc : 0,5</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>0,5</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
<div class="mode-hint">La box décrit « <i>how much solar surplus is required to start the
|
||||
charging</i> » sans donner d'unité. <b>À faire préciser avant d'écrire un suffixe</b> : afficher
|
||||
« 0,5 kW » ou « 0,5 A » sur une supposition serait inventer la grandeur.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Prise en compte de la batterie <span class="saved-flag">enregistré · batteryLevelConsideration</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Part du stockage considérée<small>valeur banc : 0,9</small></div>
|
||||
<div class="ctl"><button class="pm">−</button><b>0,9</b><button class="pm">+</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Verrouillage au débranchement <span class="saved-flag">enregistré · lockOnUnplug</span></label>
|
||||
<div class="stepper">
|
||||
<div class="lab">Verrouiller la borne au retrait du câble<small>lockOnUnplug</small></div>
|
||||
<div class="ctl"><span class="sw off"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Délestage par charge <span class="todo-flag">n'existe pas</span></label>
|
||||
<div class="blocked">
|
||||
<div class="row"><div class="lab">Ordre de délestage en surcharge</div><span class="sw off"></span></div>
|
||||
<p><b>À ne pas confondre avec l'ordre de service.</b> L'ordre de « Charges pilotables » répartit
|
||||
un <b>surplus</b> ; un ordre de délestage retirerait de la puissance en <b>surcharge</b>. Ce
|
||||
sont deux listes qui peuvent différer — et la seconde n'a aujourd'hui aucun support côté box.
|
||||
À spécifier avec l'agent plugin si le délestage doit dépasser le véhicule.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save"><div class="btn alt">Annuler</div><div class="btn go">Enregistrer</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap"><b>Raccordement</b> — la limite et ses voisins</div>
|
||||
<div class="note">
|
||||
Quatre réglages globaux vivent au même endroit parce qu'ils partagent le même public — l'installateur —
|
||||
et le même namespace : <code>SetPhasePowerLimit</code>, <code>SetAcquisitionTolerance</code>,
|
||||
<code>SetBatteryLevelConsideration</code>, <code>SetLockOnUnplug</code>.
|
||||
<ul>
|
||||
<li><b>Valeurs lues sur <code>.75</code> le 2026-08-25 :</b> <code>phasePowerLimit: 0</code> (non
|
||||
réglé), <code>acquisitionTolerance: 0.5</code>, <code>batteryLevelConsideration: 0.9</code>.</li>
|
||||
<li><b>Deux unités restent à faire confirmer</b> par l'agent plugin avant d'écrire un suffixe à
|
||||
l'écran : celle de la tolérance d'acquisition, et celle de la prise en compte batterie (un ratio
|
||||
0–1, ou un pourcentage ?).</li>
|
||||
<li>La protection exige un compteur racine <b>et</b> des états par phase. Un compteur monophasé ou
|
||||
sans détail de phase la rend inopérante — l'écran doit le dire au lieu d'afficher un réglage sans
|
||||
effet.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user