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
91 lines
3.2 KiB
Dart
91 lines
3.2 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
import '../services/nymea_service.dart';
|
|
import '../services/rpc_schema.dart';
|
|
|
|
/// Porte le [RpcSchema] de la box courante et le tient à jour au changement
|
|
/// d'installation.
|
|
///
|
|
/// ## Trois états, jamais deux
|
|
///
|
|
/// Un écran qui grise ses champs doit distinguer :
|
|
///
|
|
/// - **schéma lu, champ absent** → « cette box ne l'expose pas encore » ;
|
|
/// - **schéma lu, champ `r:`** → mesure affichable, jamais éditable ;
|
|
/// - **schéma non lu** → on ne sait pas, et il faut le dire.
|
|
///
|
|
/// Écraser le troisième cas sur l'un des deux premiers est exactement l'erreur que ce
|
|
/// provider existe pour empêcher : un champ grisé sans raison se lit comme un bug ou un
|
|
/// droit manquant, et un champ actif qui part en écriture vers une box qui n'en veut pas
|
|
/// fait rejeter **tout** l'appel (nymea refuse la requête entière sur clé inconnue).
|
|
class SchemaProvider extends ChangeNotifier {
|
|
NymeaService? _service;
|
|
|
|
RpcSchema _schema = RpcSchema.unknown;
|
|
bool _loading = false;
|
|
String? _error;
|
|
|
|
RpcSchema get schema => _schema;
|
|
bool get loading => _loading;
|
|
|
|
/// Message d'échec de lecture, s'il y en a eu un. Non nul ⇒ [RpcSchema.loaded] faux.
|
|
String? get error => _error;
|
|
|
|
void attach(NymeaService service) {
|
|
if (identical(_service, service)) return;
|
|
_service = service;
|
|
}
|
|
|
|
/// Lit le schéma. À appeler une fois la connexion établie — le résultat ne change pas
|
|
/// tant que la box ne redémarre pas avec un autre paquet.
|
|
Future<void> load() async {
|
|
final service = _service;
|
|
if (service == null) return;
|
|
_loading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
try {
|
|
final raw = await service.introspect();
|
|
if (raw == null) {
|
|
_schema = RpcSchema.unknown;
|
|
_error = 'introspection indisponible';
|
|
} else {
|
|
_schema = RpcSchema.fromIntrospect(raw, hello: service.helloPayload);
|
|
_error = null;
|
|
}
|
|
} catch (e) {
|
|
_schema = RpcSchema.unknown;
|
|
_error = e.toString();
|
|
} finally {
|
|
_loading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Ce qu'un écran peut annoncer honnêtement comme « version de la box ».
|
|
///
|
|
/// **Ce n'est pas la version du paquet du moteur.** `1.15.2+etm15` n'est exposée par
|
|
/// aucune méthode JSON-RPC ; elle ne se lit que par `dpkg` sur la box. Ce que le fil
|
|
/// porte, c'est la version d'API de l'expérience — qui est de toute façon le bon
|
|
/// discriminant, puisqu'elle décrit le contrat et non le paquet qui l'implémente.
|
|
String versionLine() {
|
|
final parts = <String>[];
|
|
final nymea = _schema.nymeaVersion ?? _service?.nymeaVersion;
|
|
if (nymea != null && nymea.isNotEmpty) parts.add('nymea $nymea');
|
|
final energy = _schema.experienceVersions['NymeaEnergy'];
|
|
if (energy != null) parts.add('NymeaEnergy $energy');
|
|
final ac = _schema.experienceVersions['AirConditioning'];
|
|
if (ac != null) parts.add('AirConditioning $ac');
|
|
if (parts.isEmpty) return 'version de la box inconnue';
|
|
return parts.join(' · ');
|
|
}
|
|
|
|
/// Remet à zéro au changement d'installation — le schéma appartient à la box quittée.
|
|
void clearForSwitch() {
|
|
_schema = RpcSchema.unknown;
|
|
_error = null;
|
|
_loading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|