17 Commits

Author SHA1 Message Date
Patrick Schurig
cd76df3a73 feat(etm16): s'aligner sur le nouveau contrat — table de paliers, stagesW, compteur et sonde
RELAIS : mon avertissement de reconstruction sur un réordonnancement est devenu FAUX, et
il est corrigé. `+etm16` compare la TABLE que les relais produisent (deriveStages), plus la
liste par index. Le miroir Dart suit, y compris le détail qui compte : les ThingIds d'une
combinaison se comparent comme un ENSEMBLE — collectés dans l'ordre de déclaration, fermer
{K1,K2} ou {K2,K1} est le même geste électrique. Mais l'appariement palier par palier
reste positionnel, ce qui préserve le seul cas où l'ordre signifie encore quelque chose :
deux relais de MÊME puissance, où il décide quel contact sert ce palier.

Conséquence pour l'écran : réordonner des contacteurs de puissances distinctes n'avertit
plus de rien. Avertir sur un geste devenu gratuit serait aussi trompeur que de se taire sur
un geste coûteux.

PALIERS : l'app ne dérive plus la combinatoire pour l'affichage — la box les publie
(mechanism.stagesW). Vérifié sur appareil : [0, 500, 1000, 1500, 2000, 2500, 3000, 3500].
deriveStages reste côté app pour la seule annonce d'impact, qui se fait avant l'écriture.
Tant que le brouillon n'a pas touché aux contacteurs, c'est la table PUBLIÉE qui s'affiche ;
dès qu'il y touche elle devient périmée, et l'écran montre une prévision annoncée comme
telle plutôt qu'une table qui décrit l'état d'avant.

COMPTEUR ET SONDE : ils ne « vont pas arriver », ils SONT au schéma de .75
(o:meterThingId, o:sensorThingId). Le grisage dérivé les a donc activés seul, sans nouvelle
version de l'app — exactement ce qui était promis. Mais un bloc actif sur un placeholder
mort est pire qu'un bloc grisé : les deux sont désormais câblés sur un vrai sélecteur,
filtré par INTERFACE (smartmeter, temperaturesensor) et jamais par nom de plugin.

Ni l'un ni l'autre n'entre dans sameHardware() : les rattacher ne reconstruit rien. Ni dans
le contrôle d'unicité : deux charges peuvent partager une sonde, un compteur divisionnaire
en couvrir plusieurs — le sélecteur ne grise donc rien ici, contrairement aux contacteurs.
Et la mesure sert à VÉRIFIER, jamais à décider : elle n'entre pas dans le budget, la charge
étant déjà comptée dans le racine dont elle n'est qu'une décomposition.

COMPTEURS CUMULÉS : la base purgée, les valeurs sont saines (55 730 / 50 / 6 327 / 62 007
kWh) et la tuile « compteur box invalide » a disparu d'elle-même. Le plafond de
plausibilité reste : un zéro réel s'affiche « 0,0 kWh », jamais « — ».

hasNeverRun était déjà traité comme un état distinct — « l'absence de cycle est dite pour
elle-même, jamais rendue par un âge de zéro ».

Vérifié sur appareil contre .75 : compteur racine relu, « à configurer » vide, deux bornes
listées (Simulated wallbox + Terra AC Charger), stagesW de la box, les deux champs
writable, 5/5 sections de l'écran SG-Ready. Tests 98/98, analyze 0 erreur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
2026-08-26 10:54:16 +02:00
Patrick Schurig
aada8e2ba5 fix(config): glissement, sélecteur de mécanisme, chip de domaine, thème — les 4 points ouverts
1) GLISSEMENT — diagnostic d'abord, et il a donné DEUX réponses.

   Le geste n'est PAS avalé par le ListView parent : les deux montages du test
   (section seule / section imbriquée) glissent identiquement. La cause du symptôme était
   bien que .75 n'a qu'une charge par domaine — rien à réordonner.

   Mais l'enquête a trouvé un vrai défaut au passage : le centre de la poignée tombait dans
   le vide de 3 px séparant l'icône de la pastille de rang, et ReorderableDragStartListener
   défère le test de collision à ses enfants. Une bande morte invisible au milieu d'une
   poignée qui paraît d'un seul tenant. Corrigé par un Container transparent.

   Tentative faite et ANNULÉE : rendre les domaines glissables aussi. Deux
   ReorderableListView imbriqués se disputent le geste et l'extérieur gagne — le
   glissement des cartes cassait. Les domaines gardent leurs flèches, et une charge seule
   dans son domaine n'offre plus de prise qui mènerait nulle part (avec l'infobulle qui
   renvoie au bon geste).

   Vérifié SUR L'APPAREIL, contre .75, avec les deux charges mises dans le même domaine :
   [chauffe-eau, pac-terrain] → [pac-terrain, chauffe-eau], brouillon abandonné sans rien
   écrire. Classement ecs/hvac rétabli ensuite.

2) SÉLECTEUR DE MÉCANISME — actif, et c'était le morceau délicat.

   LoadConfig est une union discriminée : écrire `adapter` sans nettoyer le reste produit
   une configuration que la box refuse EN BLOC, et le refus emporte toutes les charges de
   l'appel. D'où basculerMecanisme(), qui retire les clés du mécanisme quitté et pose
   celles du nouveau, en récupérant ce qui peut l'être — les deux premiers contacts
   deviennent SG1/SG2 dans l'encodage standard, les relais gardent leur puissance.

   Ce qui n'est PAS transposé : l'estimation d'un état SG-Ready n'est pas la puissance
   d'une résistance. La charge produite est alors incomplète, et c'est voulu — le
   formulaire réclame plutôt que d'inventer une valeur de plaque.

   La bascule franchit sameHardware() : confirmation préalable qui annonce la
   reconstruction et la durée de réarmement (900 s par défaut vers SG-Ready — la valeur
   d'une PAC réelle, pas celle du banc).

   Le mode fixed d'etmvariableload reste LISIBLE sans être proposé : bloc « Configuration
   héritée » en lecture seule, et conversion seulement sur geste explicite, avec le
   plafond que la déclaration en paliers ne porte pas. Ouvrir l'écran ne convertit personne.

3) CHIP DE DOMAINE — il ne rendait qu'une icône muette : le domaine d'une charge était
   illisible sur sa carte, alors qu'il décide de son groupe et de son rang. Il porte
   maintenant son libellé, et sa couleur suit le thème.

4) THÈME — les 11 derniers sites (pas 9) basculés. Trois cas : contexte disponible
   (main, ac_screen) ; peintre sans contexte, à qui la couleur est passée résolue
   (energy_flow_widget) ; tables statiques, où la teinte devient nullable et se résout au
   rendu (categoryInfoMap, favoris). Plus AUCUN jeton hors thème dans lib/.

5) ZONES — non entreprises, comme convenu.

Tests 93/93, analyze 0 erreur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
2026-08-26 08:49:20 +02:00
Patrick Schurig
38475e0007 fix(dashboard+thème): compteurs réseau honnêtes, et le sélecteur clair/sombre s'applique partout
1) « Vers réseau » — TROIS défauts, dont un seul était visible.

   La valeur aberrante (9,10184409403335e+33 kWh) ne vient PAS de l'app : Energy.GetPowerBalance
   la publie telle quelle sur .75. Trois des quatre compteurs cumulés sont corrompus
   (totalReturn 9,1e+33, totalAcquisition 7,5e+33, totalConsumption -1,6e+33) alors que
   totalProduction reste sain, et que les états du Thing compteur sont tous plausibles
   (1429 kWh importés, 88 kWh exportés). C'est donc l'accumulation de l'experience-plugin
   énergie qui déraille, côté box — signalé, pas corrigé ici.

   Les deux autres défauts étaient bien dans l'app :
   - « Depuis réseau » lisait l'OPPOSÉ du compteur d'export au lieu de totalAcquisition,
     qui a son propre compteur. Dès que l'export était positif, la tuile affichait
     0,0 kWh en permanence, quelle que soit l'électricité réellement achetée ;
   - les deux tuiles étaient libellées « aujourd'hui » alors que ces compteurs sont
     CUMULÉS depuis l'origine, jamais remis à zéro.

   Corrigé : EnergyData porte gridExportKwh / gridImportKwh, transportés tels quels — l'app
   ne maquille pas une donnée que la box publie fausse. Un plafond de plausibilité
   (10 GWh, ~3000 ans de consommation) distingue « mesure » de « compteur cassé » : au-delà
   la tuile affiche « — · compteur box invalide ». Imprimer le nombre le présenterait comme
   une mesure ; l'effacer laisserait croire à zéro.

2) Thème : 126 sites basculés des constantes claires vers les variantes contextuelles
   (EtmTokens.bg → bgOf(context), card → surfaceOf, ink → inkOf, muted → mutedOf,
   faint → faintOf, line → lineOf) dans ac_screen, things_screen, energy_screen,
   favorites_screen, main_drawer, ev_charging_card, energy_flow_widget. Ces écrans
   restaient en palette claire quel que soit le sélecteur.

   9 sites RESTENT en constante, et c'est délibéré : ce sont des tables statiques et des
   helpers de haut niveau (categoryInfoMap, _options des favoris, _modeColor, peintres du
   flux) où la couleur est une identité d'icône et où aucun BuildContext n'est en portée.
   Les faire suivre le thème demande de threader le contexte jusqu'à ces tables — un
   refactor à part entière, pas un correctif de fin de soirée.

Tests 80/80, analyze 0 erreur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
2026-08-25 23:13:20 +02:00
Patrick Schurig
2a8ad95120 feat(config): lot C — un seul écran de configuration, et le grisage vient du schéma
FUSION. L'écran « Ordre de service » est supprimé ; sa route redirige. C'est son CHEMIN DE
DONNÉES (LoadConfigProvider) qui a migré dans la présentation de « Rôles & appareils »,
jamais l'inverse. EmsRole fige six rôles dont dhw ET heatPump séparés — or LM-201 dit qu'une
PAC sans ballon séparé est UNE charge : le modèle des rôles aurait exigé deux charges sur un
canal unique, ce que validateSet() refuse à raison. Les charges viennent donc de
GetLoadConfig. Les compteurs restent sur EnergySetupProvider (autre contrat, SetRootMeter).

La note « le pilote peut bousculer l'ordre les jours Tempo rouge ou à l'approche d'une
échéance » est retirée : aucun de ces comportements n'existe dans le moteur.

ANNONCE AVANT ÉCRITURE. Miroir exact de EnergyArbitrator::sameHardware(). Le piège, vérifié
sur .75 : la comparaison des relais est INDEXÉE, donc réordonner relays[] sans rien changer
d'autre reconstruit l'adaptateur — contacts ouverts (applySafeState, force) et verrous
réarmés à froid. Un glisser-déposer dans la liste des contacteurs est un geste matériel.
L'écran le nomme, avec la durée de réarmement.

GRISAGE PAR INTROSPECTION. RpcSchema lit JSONRPC.Introspect à la connexion ; trois états et
non deux — écrivable, r: (mesure, pas fonction manquante), absent — plus « inconnu » quand le
schéma n'a pas pu être lu. Chaque champ grisé porte sa raison et le nom du champ manquant.
La version affichée est celle de l'API d'expérience (NymeaEnergy 0.8) : le paquet +etm15
n'est exposé par aucune méthode.

CORRIGÉ AU PASSAGE — les modes de recharge étaient INOPÉRANTS. setChargingInfo() appelait
EnergyPlugin.SetChargingInfo (namespace inexistant) avec mode/targetSoc/endTime/minCurrent
au lieu de chargingMode/targetPercentage/endDateTime, et minCurrent n'existe dans aucun
champ. Vérifié par appel réel : « Missing required key: chargingMode ». Le refus était de
plus avalé et l'état local mis à jour quand même. Désormais l'état ne suit que si la box
accepte, et le refus remonte sans en inventer la cause.

ZONES — constaté, pas corrigé. L'hypothèse « champs grisés » est fausse : AirConditioning
publie 8 méthodes toutes en écriture. ac_screen.dart n'en appelle aucune, ses 4 pièces sont
des constantes de maquette, et .75 déclare zéro zone. getZones() ajouté + bandeau qui lit le
vrai compte et annonce la maquette. Le câblage est un lot en soi, non exerçable sans zone.

Tests 80/80, analyze 0 erreur. Relevé : docs/RELEVE_LOTC.md — dont §6, ce qui n'a PAS pu
être vérifié : l'IU n'a pas pu être pilotée sur l'appareil, MIUI refusant INJECT_EVENTS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
2026-08-25 22:17:16 +02:00
Patrick Schurig
dee20896c5 feat(télémétrie): lot B app — consommation de la télémétrie d'arbitrage
Le lot plugin B-bis (1.15.2+etm15) est sur .75 et GetLoadConfig/GetLoadTelemetry
concordent : les critères 4 à 8 du brief deviennent atteignables.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
2026-08-25 19:20:30 +02:00
Patrick Schurig
7ca42973f2 feat(installateur): ordre de service des charges + locale + ossature i18n
Lot B côté app, partie NON bloquée par le lot plugin (voir §Limites).

§1 Locale (indépendant) — JSONRPC.Hello portait {} : l'app était en en_US de
fait alors que nymea gère la locale PAR CONNEXION (o:locale). Envoi de la
locale à chaque Hello, donc aussi après reconnexion subie (m_clientLocales
repart au défaut de la box à chaque socket). Locale demandée ET retenue
journalisées : un .qm introuvable rend exactement comme une absence de
traduction, sans cette trace un catalogue mal packagé passe inaperçu.

§2 i18n — flutter_localizations + intl + ARB (français seul). Aucune chaîne
en dur dans le nouvel écran. Les codes reçus de la box (domaine, mécanisme)
sont des clés ARB avec repli explicite sur code brut si l'app ne les connaît
pas : une box en avance sur l'app est un cas normal en parc déployé.

§3 Modèle — LoadConfigEntry est une VUE en lecture au-dessus de la map brute ;
l'écriture repart de la map d'origine (patched()). Pas de couche de filtrage
préventive : l'aller-retour neutre passe contre .75, donc l'écho verbatim est
sûr par construction (§7-1).

§4 Priorité à deux niveaux — groupByDomain / flattenToPayload. Le rang de
domaine se déduit de l'ordre d'apparition à plat ; la box ne sait rien des
groupes. Config non contiguë : signalée, ordre réel affiché, normalisation
seulement sur confirmation explicite. Miroir client de validateSet().

§5 Écritures asynchrones — LoadConfigProvider ne conclut jamais sur
l'acquittement du RPC : pending → LoadConfigChanged → confirmed|diverged.

NymeaEnergy.GetLoadConfig / SetLoadConfig deviennent RÉELS (ils étaient des
stubs journalisés). Conséquence traitée : EnergySetupProvider.persist()
n'émet plus setLoadConfig(buildLoadDescriptors()) — SetLoadConfig remplace
TOUT l'ensemble, et _commit() l'appelait à chaque changement d'UI : une
reconstruction depuis le modèle typé aurait écrasé la config réelle de la
box sans geste explicite.

Écran /energy/loads, gaté installateur (drawer + garde de routeur).

Limites — le lot plugin n'est PAS déployé sur .75 : GetLoadTelemetry,
LoadTelemetryChanged et o:domain sont absents (19 méthodes / 13 notifications
inchangées). Toute la moitié télémétrie du brief est donc hors d'atteinte, et
o:domain n'étant pas au schéma, la box rejetterait la clé (jsonvalidator.cpp:143).
Fixtures multi-domaines synthétiques : .75 n'a qu'une charge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 11:34:53 +02:00
Patrick Schurig
b4a6c97d3d feat(installateur): lot C — squelette discovery de things (pending SDM validation)
- nymea_models : discoveryParamTypes, isDiscoveryAddable, ThingDescriptor (+alreadyAdded), thingErrorMessage FR
- nymea_service : discoverThings() (0-résultat = "aucun appareil", distinct d'erreur), addThingFromDescriptor()
- thing_catalog : bouton "Découvrir" actif pour Discovery+JustAdd
- thing_discovery_screen (nouveau) : form slaveAddress (sans sélecteur master) -> scan global -> descriptors (master résolu) -> o:thingId = grisé "déjà ajouté" -> AddThing
- retrait d'un discoverThings mort (collision)

NON VALIDÉ SUR MATÉRIEL : 3 points marqués "À REVALIDER SUR SDM RÉEL" (résolution master par paramTypeId, timeout scan, title/description). Merge bloqué jusqu'à descriptor SDM réel sur hems .75.
Chaîne RPC prouvée live (GPIO : 26 descriptors ajoutables).
Réf. UI_DATA_CONTRACT.md §5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:15:44 +02:00
Patrick Schurig
b7a7213ee4 feat(installateur): catalogue de things (lecture seule) + filtres
- nymea_models : NymeaVendor + NymeaThingClass étendu (vendorId, setupMethod, createMethods)
- nymea_service : getVendors() + getAllThingClasses() (typées, erreurs FR, guards sim/déco)
- thing_category : +simpleheatpump/smartgridheatpump -> hvac (interfaces réelles)
- thing_catalog_screen : liste groupée + 3 filtres combinables (fabricant/type/recherche) + détail read-only
- main.dart : /settings/system/things -> ThingCatalogScreen (remplace le stub)

Type dérivé des interfaces réelles (pas d'enum), fabricant résolu par vendorId (doublon ABB géré).
Aucun AddThing/Discovery/Pairing (lots B/C à venir).
Validé contre nymea réel (hems .75) : 6 vendors, 45 classes, 18 interfaces.
Réf. UI_DATA_CONTRACT.md §5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:00:12 +02:00
Patrick Schurig
3a535e05fa feat(installateur): écran Protocoles — CRUD masters Modbus RTU
- modbus_models.dart : ModbusRtuMaster, SerialPort, enums parity/dataBits/stopBits, modbusErrorMessage()
- nymea_service : 5 RPC typées (GetSerialPorts, GetModbusRtuMasters, Add/Reconfigure/Remove), erreurs ModbusRtuError -> FR, guards sim/déco
- protocols_screen : liste + empty state, form add/edit (dropdowns contraints), suppression confirmée (avert. orphelinage), garde-fou installateur
- main.dart : route /settings/system/protocols (remplace le stub)

Validé contre nymea réel (hems .75) : champs/enums/modbusError confirmés, cas port USB absent géré.
Réf. UI_DATA_CONTRACT.md §6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:25:58 +02:00
Patrick Schurig
3821bf3fdb refactor(energy): retire le calcul de ratios app-side, isole l'interim derrière EnergyRatiosInterim
- supprime selfRate/autoRate de _parsePowerBalance, le getter autoconsommationW, les .abs() interim
- ratios dérivés par Δ-de-cumuls (§2.1/§8.1) dans lib/services/energy_ratios.dart
- reseed jour-roulant & Δ non-monotone ; gardes : dén≤0→null, pas de NaN, clamp [0,100]
- selfConsumptionPower net-signé sans .abs() (§2.2) ; signes nymea bruts préservés (§7.1)
- seam swappable vers un state plugin en Phase 2 (une seule fonction)

Interim (Phase 1). Gestion du signe d'affichage renvoyée à la Phase 4 (§1.1).
Réf. UI_DATA_CONTRACT.md rev.5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:51:50 +02:00
Patrick Schurig
c638ec6c52 feat: connexions multi-HEMS + rôles & appareils (beta add/config)
Lot Connexions multi-HEMS :
- modèle Installation + InstallationStore (persistance par UUID ;
  métadonnées SharedPreferences, token via flutter_secure_storage)
- ConnectionManager au-dessus de nymea_service : mono-connexion, switchTo,
  connectNew, enterDemo (démo = active factice), contrôle d'identité DHCP
- nymea_service : connect() réveillé (ws://4444, token par UUID), capture
  Hello (uuid/name/initialSetupRequired), authenticate/createUser, resetState
- écran Installations (3 états) + déverrouillage installateur depuis la racine
- écran Connexion : auth 2 branches (JSONRPC.Authenticate / CreateUser),
  détection auto via initialSetupRequired, segmenteur de repli
- gate de routage (redirect + refreshListenable merge[cm,svc]) ;
  hasActiveConnection = isSimulation || (connected && authenticated)
- en-tête drawer cliquable → Installations
- invalidation des caches au switch (service.resetState + clearForSwitch
  rôles/scheduler/tariff), sans persist()

Lot Rôles & appareils :
- EnergySetupProvider refondu en tri-état (present/absent/non-configuré)
- écran roles_devices_screen (3 zones, drag-priorité, inférence solaire/batterie)
- LoadDescriptor (etmvariableload, rév.2 : PAC exclue → SG-Ready) ;
  Energy.SetRootMeter réel, Get/SetLoadConfig en stub loggé

Correctifs :
- bug signe énergie : production +, consommation NÉGATIVE sur nymea 1.15.2
  → consumptionW/home en .abs() (autoconso n'est plus clouée à 0)
- UUID Hello brace-wrapped normalisé

Tests : flutter analyze 0 erreur ; gate + zéro-fuite au switch (5/5 verts).
Validé en vrai : login auth .120, .75 ouvert, switch, déverrouillage installateur.
Note : embarque aussi le WIP dashboard/thème déjà présent dans l'arbre.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:39:08 +02:00
Patrick Schurig ETM-Schurig
dd798ae85e fix: graphe énergie — double axe kW/SOC% visible + simulation SOC réaliste
NymeaService
- batterySOCSource : retourne {'thingId': 'sim-battery', 'stateName': 'soc'}
  en simulation au lieu de null — le SOC est désormais fetchable
- fetchHistory en simulation : données SOC réalistes en % (0-100) avec
  courbe charge solaire 8h-15h (20→85%) puis décharge soir (85→20%)
  au lieu de valeurs sinus 100-900 W incorrectes

EnergyScreen
- initState : appelle startSimulation() si non connecté (comme les autres écrans)
- leftTitles : interval explicite (yMax/4) + SideTitleWidget pour ancrage correct
- gridData : horizontalInterval aligné sur yInterval
- rightTitles (SOC %) : SideTitleWidget + interval aligné
- bottomTitles : SideTitleWidget sur les deux graphes
- barChart leftTitles : interval + SideTitleWidget

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 22:33:18 +02:00
Patrick Schurig ETM-Schurig
80500e21e6 feat: favoris — persistance SharedPreferences + restyle EtmTokens
Persistance (FavoriteWidget)
- FavoriteWidget.fromJson / toJson ajoutés dans nymea_models.dart
- NymeaService : constructeur appelle _loadFavorites() au démarrage
- _saveFavorites() appelé après addFavorite / removeFavorite / reorderFavorites
- Clé SharedPreferences : 'etm_favorites_v1'
- Résistance aux données corrompues (try/catch sur fromJson)

Restyle (favorites_screen.dart)
- AppTheme → EtmTokens sur toutes les couleurs et typographies
- Valeurs en IBM Plex Mono (size 36 pour les grandes métriques)
- Cartes avec EtmTokens.cardShadow + border radius 22
- _EmptyState : bouton vert + étoile + message
- _AddSheet : DraggableScrollableSheet restyled, sans emoji, items visuellement
  distingués (ajouté vs disponible)
- ReorderableDelayedDragStartListener pour le drag discret
- Simulation auto-démarrée si non connecté (comme Dashboard/Things)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 22:07:14 +02:00
e42412fef8 feat: EV charging UI — 3 modes PV/Min+PV/Boost + deadline option
- setChargingInfo complet avec EcoWithMinCurrent + EcoMinWithTargetTime
- UI 3 boutons + toggle Cible + slider SOC 0-100% + time picker
2026-04-05 07:55:13 +02:00
c19c9d1a98 feat: navigation drawer, EMS setup, scheduler, tarifs, paramètres app
- Drawer custom (overlay Stack) avec mode installateur PIN SHA-256
- GoRouter + ShellRoute : navigation préservée entre onglets
- 6 providers : NavigationProvider, InstallerModeProvider, AppSettingsProvider,
  EnergySetupProvider, SchedulerProvider, TariffProvider
- Écrans Energy Manager : RoleConfigFlow (3 étapes), Scheduler, Tarifs, Timeline
- Écrans Paramètres : Apparence, Écrans actifs, AppSettingsScreen
- DrawerMenuButton présent dans les 5 AppBars principaux
- Simulation : _thingClasses générées avec interfaces EMS pour filtrage des rôles
- Compteur solaire : ajout smartmeter aux interfaces compatibles
- Thème ETM (etm_theme.dart), ProLockBadge, widgets PowerBar/RoleCard/TimelineSlotCard
- Dépendances : go_router, shared_preferences, crypto, url_launcher

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:52:32 +01:00
8862dc2a72 feat: historique énergie, navigation Things, actions nymea
Énergie :
- Écran Énergie reécrit : line chart (production/conso/autoconso/batterie)
  et bar chart (bilan Wh par période) avec onglets 15 min / 1 h / 1 j / 1 sem
- Datepicker pour sélectionner une période historique (chip dismissible)
- Timelines des deux graphiques alignées (même x=i → data[i].timestamp)
- PowerBalanceEntry + fetchPowerBalanceLogs() + simulation sinusoïdale
- Overflow fixes : energy_flow_widget (Expanded sur titre), production_card

Things :
- Navigation 3 niveaux : ThingsScreen → CategoryOverviewScreen → ThingDetailScreen
- Catégorie Cars ajoutée, carrousel corrigé (clamp RangeError)
- ThingDetailScreen : executeAction, setStateValue, activeThumbColor fix
- NymeaTile widget, state_history_chart widget (générique Logging.GetLogEntries)

Modèles / service :
- HistoryEntry, PowerBalanceEntry ajoutés
- fetchHistory(), fetchPowerBalanceLogs() dans NymeaService
- interfaceToCategoryMap étendu (Cars, etc.)
- AppTheme : nouvelles couleurs (accentTeal, boostRed, pvGreen, minPvBlue…)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 07:15:48 +01:00
etm
d5dc0c7ca5 Initial commit : Flutter app nymea energy monitor
- NymeaService : auth complète (Hello → Authenticate → SetNotificationStatus)
- Token top-level dans chaque requête JSON-RPC (fix critique GetThings)
- Persistance token via shared_preferences par hôte
- Dashboard : champs utilisateur/mot de passe dans le dialog de connexion
- ThingDetailScreen : renommer, réglages (settingsTypes) et supprimer
- NymeaThingClass : champ settingsTypes parsé depuis l'API
- NymeaThing : copyWith(name) + settingValue()
- Fix overflow _StateChip dans ThingsScreen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:57:46 +01:00