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
100 lines
4.4 KiB
Dart
100 lines
4.4 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:etm_powersync_app/providers/load_config_provider.dart';
|
|
import 'package:etm_powersync_app/services/load_priority.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
List<Map<String, dynamic>> _configs(String fixture) {
|
|
final raw = File('test/fixtures/$fixture').readAsStringSync();
|
|
final map = jsonDecode(raw) as Map<String, dynamic>;
|
|
return (map['loadConfigs'] as List).cast<Map<String, dynamic>>();
|
|
}
|
|
|
|
void main() {
|
|
// ───────────────────────────────────────────────────────────────────────────
|
|
group('§7-4 — domaine', () {
|
|
test('les deux charges de .75 arrivent « non classées », jamais rangées d\'office',
|
|
() {
|
|
final p = LoadConfigProvider()..adoptForTest(_configs('loadconfig_hems75_v3.json'));
|
|
|
|
// `domain: ""` sort toujours de GetLoadConfig (sérialisation par méta-objet) ;
|
|
// vide ET absent se lisent tous deux « non classée ».
|
|
expect(p.groups.length, 1);
|
|
expect(p.groups.single.isUnclassified, isTrue);
|
|
expect(p.groups.single.loads.map((e) => e.id),
|
|
['chauffe-eau', 'pac-terrain']);
|
|
expect(p.hasPendingChanges, isFalse,
|
|
reason: 'une simple lecture ne doit jamais produire de brouillon');
|
|
});
|
|
|
|
test('classer une charge la déplace dans son domaine — dans le brouillon seulement',
|
|
() {
|
|
final p = LoadConfigProvider()..adoptForTest(_configs('loadconfig_hems75_v3.json'));
|
|
p.assignDomain('chauffe-eau', 'ecs');
|
|
|
|
expect(p.hasPendingChanges, isTrue);
|
|
expect(p.groups.map((g) => g.domain), [kUnclassifiedDomain, 'ecs']);
|
|
expect(p.groups.first.loads.single.id, 'pac-terrain');
|
|
expect(p.groups.last.loads.single.domain, 'ecs');
|
|
});
|
|
|
|
test('la charge utile du mécanisme traverse l\'affectation intacte', () {
|
|
final source = _configs('loadconfig_hems75_v3.json');
|
|
final p = LoadConfigProvider()..adoptForTest(source);
|
|
p.assignDomain('pac-terrain', 'hvac');
|
|
|
|
final payload = flattenToPayload(p.groups);
|
|
final pac = payload.firstWhere((m) => m['id'] == 'pac-terrain');
|
|
final origin = source.firstWhere((m) => m['id'] == 'pac-terrain');
|
|
|
|
// Seuls `domain` et `priority` changent : la table d'états SG-Ready, les relais
|
|
// vides, `needs` et `powerLevels` sont transportés tels quels. Une
|
|
// re-sérialisation depuis un modèle typé les amputerait en silence.
|
|
expect(pac['domain'], 'hvac');
|
|
for (final key in origin.keys) {
|
|
if (key == 'domain' || key == 'priority') continue;
|
|
expect(pac[key], equals(origin[key]),
|
|
reason: 'la clé $key a été altérée par l\'affectation de domaine');
|
|
}
|
|
});
|
|
|
|
test('déclasser une charge la ramène au bucket « non classé »', () {
|
|
final p = LoadConfigProvider()..adoptForTest(_configs('loadconfig_hems75_v3.json'));
|
|
p.assignDomain('chauffe-eau', 'ecs');
|
|
p.assignDomain('chauffe-eau', kUnclassifiedDomain);
|
|
|
|
final ecs = p.groups.where((g) => g.domain == 'ecs');
|
|
expect(ecs, isEmpty, reason: 'un groupe vidé ne doit pas subsister');
|
|
expect(
|
|
p.groups
|
|
.firstWhere((g) => g.isUnclassified)
|
|
.loads
|
|
.map((e) => e.id)
|
|
.toSet(),
|
|
{'chauffe-eau', 'pac-terrain'});
|
|
});
|
|
|
|
test('l\'aplatissement renumérote 1..N sans jamais réordonner les groupes', () {
|
|
final p = LoadConfigProvider()..adoptForTest(_configs('loadconfig_hems75_v3.json'));
|
|
p.assignDomain('chauffe-eau', 'ecs');
|
|
|
|
final payload = flattenToPayload(p.groups);
|
|
expect(payload.map((m) => m['id']), ['pac-terrain', 'chauffe-eau']);
|
|
expect(payload.map((m) => m['priority']), [1, 2]);
|
|
});
|
|
|
|
test('les domaines proposés sont exactement l\'énumération fermée du plugin', () {
|
|
// `SetLoadConfig` rejette EN BLOC une valeur hors énumération : proposer un
|
|
// domaine inventé ferait échouer l'enregistrement de toutes les charges.
|
|
expect(kAssignableDomains.toSet(), {'ev', 'ecs', 'hvac', 'heating', 'battery'});
|
|
});
|
|
|
|
test('l\'ordre de mise en service ne contient pas la batterie', () {
|
|
// Classable, mais sa place ne s'exprime pas en watts : elle s'exprime en cible
|
|
// de SOC, hors périmètre de l'ordre de service.
|
|
expect(kDefaultDomainOrder, isNot(contains('battery')));
|
|
});
|
|
});
|
|
}
|