etm-powersync-app/lib/models/energy_ratios_snapshot.dart
Patrick Schurig 71bb26c81b feat(saisie): l'horloge se qualifie, elle ne s'offre pas en réglage
« 12:00 heure de la box (Europe/London) — pas celle de cet appareil », et non
« Fuseau : Europe/Paris » : sous un champ, un intitulé de réglage laisse croire
qu'on choisit le fuseau DE CETTE échéance. Il n'y en a qu'un, il est système, et
il déplace d'un coup toutes les heures murales de l'installation — échéances,
frontière de la journée de reporting, et demain les plages tarifaires
(LM-1005-b-iii).

Affichage CONDITIONNEL : seulement quand les deux horloges divergent. C'est la
règle déjà appliquée deux fois cette semaine — ligne des ratios instantanés,
avertissement « partiel » du bilan — et sa raison tient en une phrase : un texte
permanent cesse d'être lu le jour exact où il devient vrai.

La divergence se DÉDUIT de ce que la box publie, sans base de fuseaux :
`periodStartedAt` est minuit de l'installation en epoch, donc rendu dans le
fuseau de l'appareil il tombe sur 00:00 si et seulement si les deux partagent le
même décalage. Exact pour l'instant présent, y compris les jours à 23 ou 25
heures — et c'est justement pourquoi nous avions demandé cette frontière plutôt
qu'un nom IANA.

Quatre silences testés : fuseaux identiques, échéance vide, box qui ne publie pas
la date, et l'intitulé « Fuseau : » qui ne doit jamais apparaître.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-06 09:22:33 +02:00

108 lines
4.9 KiB
Dart

/// **Les ratios publiés par la box, et depuis QUAND ils parlent.**
///
/// ## Deux bornes, et c'est leur écart qui porte la phrase
///
/// `NymeaEnergy.GetEnergyRatios` publie deux instants depuis `+etm56`, et ils ne disent pas
/// la même chose :
///
/// - [periodStartedAt] — le **début de la journée de reporting**, à minuit de
/// l'installation. C'est la frontière que l'app cherchait : elle ne se dérive pas d'un nom
/// de fuseau IANA, et elle reste juste les jours à 23 ou 25 heures ;
/// - [measuredSince] — depuis quand la **baseline** des ratios tient. Elle se repose à chaque
/// redémarrage de `nymead` et à chaque recul de compteur, donc **en plein jour**.
///
/// Relevé sur `.75` le 2026-09-05 : `periodStartedAt` à 00:00 Londres (01:00 Paris),
/// `measuredSince` à 07:04 Londres — **sept heures d'écart**. Un ratio étiqueté
/// « aujourd'hui » aurait donc parlé d'une fenêtre commencée à 08:04, et rien ne l'aurait
/// signalé.
///
/// C'est la même paire, pour la même raison, que `progress.measuredSince` / `targetWh` :
/// une borne de période et une borne de mesure ne sont pas la même chose, et quand elles
/// coïncident — le cas normal — il n'y a rien à dire de plus.
///
/// ## Ce que ces ratios NE sont pas
///
/// Ce ne sont **pas** des ratios journaliers. Ce sont des ratios *depuis la baseline*. Les
/// intituler « du jour » serait faux, et les afficher sans dire depuis quand est exactement
/// le silence que [measuredSince] existe pour fermer. Le journalier se calcule ailleurs, par
/// différence de compteurs sur la fenêtre [periodStartedAt] → maintenant (`DailyBalance`).
library;
class EnergyRatiosSnapshot {
/// Part de la consommation couverte sans le réseau, en %. `null` = non publié.
final double? autonomyRate;
/// Part de la production consommée sur place, en %. `null` = non publié.
final double? selfConsumptionRate;
/// Début de la journée de reporting — **minuit de l'installation**, publié et non dérivé.
final DateTime? periodStartedAt;
/// Depuis quand la baseline tient. Voir la note de tête.
final DateTime? measuredSince;
/// Fuseau de l'installation (`Europe/London` sur le banc). Le **nom**, pas un décalage :
/// c'est la box qui datera, l'app se contente de le dire.
final String? timeZone;
const EnergyRatiosSnapshot({
this.autonomyRate,
this.selfConsumptionRate,
this.periodStartedAt,
this.measuredSince,
this.timeZone,
});
static DateTime? _epoch(Object? v) => v is num
? DateTime.fromMillisecondsSinceEpoch(v.toInt() * 1000, isUtc: true)
.toLocal()
: null;
factory EnergyRatiosSnapshot.fromJson(Map<String, dynamic> m) =>
EnergyRatiosSnapshot(
autonomyRate: (m['autonomyRate'] as num?)?.toDouble(),
selfConsumptionRate: (m['selfConsumptionRate'] as num?)?.toDouble(),
periodStartedAt: _epoch(m['periodStartedAt']),
measuredSince: _epoch(m['measuredSince']),
timeZone: (m['timeZone'] is String && (m['timeZone'] as String).isNotEmpty)
? m['timeZone'] as String
: null,
);
/// **La baseline a été reposée APRÈS le début de la journée** — un redémarrage, un recul
/// de compteur. Les ratios ne parlent alors que depuis [measuredSince], et l'écran doit le
/// dire.
///
/// Tolérance d'une minute : les deux instants viennent de la même box, mais rien ne garantit
/// qu'ils tombent sur la même seconde au premier cycle après minuit.
bool get baselineRepriseEnJournee {
final debut = periodStartedAt;
final depuis = measuredSince;
if (debut == null || depuis == null) return false;
return depuis.difference(debut) > const Duration(minutes: 1);
}
/// **Les deux horloges divergent-elles ?** — sans base de données de fuseaux.
///
/// [periodStartedAt] est **minuit de l'installation**, publié en epoch. Rendu dans le
/// fuseau de l'appareil, il tombe donc sur 00:00 **si et seulement si** les deux partagent
/// le même décalage. Sur `.75` en `Europe/London` vu d'un téléphone français, il tombait
/// sur 01:00 ; les deux étant passés en `Europe/Paris`, il tombe sur 00:00.
///
/// C'est exact pour l'instant présent, y compris les jours de changement d'heure, et ça
/// n'exige **rien d'autre que ce que la box publie** — le nom IANA ne donnerait pas de
/// décalage, et une base embarquée pour le recalculer serait le mauvais geste.
///
/// `false` quand la date manque : on ne conclut pas d'une absence.
bool get fuseauDiffereDeLAppareil {
final d = periodStartedAt?.toLocal();
if (d == null) return false;
return d.hour != 0 || d.minute != 0;
}
/// Vrai quand la box publie de quoi dater la journée. Sans ça, l'app ne **peut pas**
/// calculer la bonne fenêtre — et elle le dit plutôt que de prendre minuit du téléphone
/// en silence.
bool get datePeriode => periodStartedAt != null;
}