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
145 lines
5.0 KiB
Dart
145 lines
5.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../services/rpc_schema.dart';
|
|
import '../theme/etm_tokens.dart';
|
|
|
|
/// Enveloppe un champ dont la disponibilité vient du **schéma de la box**.
|
|
///
|
|
/// ## Un champ grisé sans raison se lit comme un bug
|
|
///
|
|
/// C'est la règle qui justifie ce widget. « Inactif » tout court laisse l'installateur
|
|
/// devant trois hypothèses également plausibles — panne, droit manquant, ou fonction
|
|
/// absente — et il choisira la mauvaise. On affiche donc **toujours** la raison, et le
|
|
/// nom du champ manquant quand on l'a : c'est ce nom qui rend le rapport actionnable.
|
|
///
|
|
/// ## Trois états, pas deux
|
|
///
|
|
/// - [FieldSupport.writable] — actif, l'enfant est rendu tel quel ;
|
|
/// - [FieldSupport.readOnly] — le champ existe et se lit, mais l'écrire est une erreur :
|
|
/// c'est une **mesure**, pas une fonction manquante ;
|
|
/// - [FieldSupport.absent] — cette box ne l'expose pas ;
|
|
/// - [FieldSupport.unknown] — le schéma n'a pas pu être lu ; on ne prétend ni l'un ni
|
|
/// l'autre.
|
|
class SchemaGated extends StatelessWidget {
|
|
/// Ce que le schéma dit du champ.
|
|
final FieldSupport support;
|
|
|
|
/// Nom du champ tel qu'il apparaîtrait au schéma (`endDateTime`, `minCurrent`…).
|
|
/// Affiché dans la raison — c'est lui qui permet de remonter un manque utilement.
|
|
final String fieldName;
|
|
|
|
/// Le champ actif.
|
|
final Widget child;
|
|
|
|
/// Libellé humain, pour la ligne de raison.
|
|
final String label;
|
|
|
|
const SchemaGated({
|
|
super.key,
|
|
required this.support,
|
|
required this.fieldName,
|
|
required this.label,
|
|
required this.child,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (support.isWritable) return child;
|
|
|
|
final muted = EtmTokens.mutedOf(context);
|
|
return Opacity(
|
|
opacity: 0.55,
|
|
child: IgnorePointer(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
child,
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(_icon, size: 13, color: muted),
|
|
const SizedBox(width: 5),
|
|
Expanded(
|
|
child: Text(
|
|
_reason,
|
|
style: TextStyle(fontSize: 11.5, color: muted, height: 1.3),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
IconData get _icon => switch (support) {
|
|
FieldSupport.readOnly => Icons.visibility_outlined,
|
|
FieldSupport.absent => Icons.cloud_off_outlined,
|
|
_ => Icons.help_outline,
|
|
};
|
|
|
|
String get _reason => switch (support) {
|
|
FieldSupport.readOnly =>
|
|
'Lecture seule — « $fieldName » est une mesure publiée par la box, '
|
|
'elle ne se règle pas ici.',
|
|
FieldSupport.absent =>
|
|
'Cette box ne l\'expose pas encore — champ « $fieldName » absent de son '
|
|
'schéma. Le réglage s\'activera seul après mise à jour du moteur.',
|
|
_ => 'Disponibilité inconnue — le schéma de la box n\'a pas pu être lu. '
|
|
'« $fieldName » est laissé inactif par prudence.',
|
|
};
|
|
}
|
|
|
|
/// Bandeau d'en-tête annonçant sur quoi l'écran s'appuie.
|
|
///
|
|
/// Dès qu'un écran se comporte différemment selon la version, il faut pouvoir dire
|
|
/// **laquelle on regarde** — sans quoi un rapport de terrain (« le champ est gris chez
|
|
/// moi ») n'est pas exploitable.
|
|
class SchemaVersionBanner extends StatelessWidget {
|
|
/// Ligne de version déjà composée (`SchemaProvider.versionLine()`).
|
|
final String versionLine;
|
|
|
|
/// Faux si le schéma n'a pas pu être lu — le bandeau le dit alors franchement.
|
|
final bool schemaLoaded;
|
|
|
|
const SchemaVersionBanner({
|
|
super.key,
|
|
required this.versionLine,
|
|
required this.schemaLoaded,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final muted = EtmTokens.mutedOf(context);
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
|
decoration: BoxDecoration(
|
|
color: EtmTokens.surfaceOf(context).withValues(alpha: 0.6),
|
|
borderRadius: BorderRadius.circular(EtmTokens.radiusCard),
|
|
border: Border.all(color: EtmTokens.lineOf(context)),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(schemaLoaded ? Icons.memory : Icons.help_outline,
|
|
size: 15, color: muted),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
schemaLoaded
|
|
? '$versionLine — les réglages disponibles sont déduits du schéma '
|
|
'publié par cette box.'
|
|
: '$versionLine — schéma illisible : les réglages sont laissés '
|
|
'inactifs par prudence, pas parce qu\'ils manquent.',
|
|
style: TextStyle(fontSize: 11.5, color: muted, height: 1.35),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|