etm-powersync-app/lib/models/energy_ratios_snapshot.dart
Patrick Schurig b1c5336bbe docs: le test de fuseau porte sur le DÉCALAGE — ne pas le réutiliser pour un tarif
Europe/Berlin et Europe/Paris ne divergeront jamais : la ligne d'échéance restera
muette, et à raison — elle qualifie une horloge, et deux fuseaux au même décalage
donnent la même heure. Avertir parce que les NOMS diffèrent alors que les HEURES
coïncident serait le bruit que la condition existe pour éviter.

Mais une plage tarifaire suit le FOURNISSEUR, pas la box : « même décalage, autre
autorité » y reste une divergence réelle, et le jour où l'un des deux change de
règle d'heure d'été, la plage bouge sans que la box ait bougé. Le lot tarifaire
aura donc besoin d'une comparaison de provenance et d'un fuseau de référence
publié avec la configuration tarifaire.

Noté dans la doc du getter, là où quelqu'un ira le réutiliser.

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

123 lines
6.0 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.
///
/// ## ⚠️ Ne PAS réutiliser tel quel pour une plage tarifaire
///
/// Ce test porte sur le **décalage**, pas sur le nom — et c'est juste ici : une échéance
/// qualifie une **horloge**, donc deux fuseaux au même décalage donnent la même heure et
/// il n'y a rien à signaler. `Europe/Berlin` et `Europe/Paris` ne divergeront jamais, été
/// comme hiver : afficher un avertissement parce que les *noms* diffèrent alors que les
/// *heures* coïncident serait exactement le bruit que cette condition existe pour éviter.
///
/// Une **plage tarifaire** pose l'autre question. Elle est définie par le fournisseur, donc
/// elle suit **son** fuseau et non celui de la box — et « même décalage, autre autorité »
/// reste une divergence réelle : le jour où l'un des deux change de règle d'heure d'été, la
/// plage bouge sans que la box ait bougé. Le lot tarifaire aura donc besoin d'une
/// comparaison de **provenance**, pas de décalage, et d'un fuseau de référence publié avec
/// la configuration tarifaire — jamais codé en dur côté app.
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;
}