diff --git a/tests/tools/etm-powersync-trace-replay-test.py b/tests/tools/etm-powersync-trace-replay-test.py index a1faa1b..044df0a 100644 --- a/tests/tools/etm-powersync-trace-replay-test.py +++ b/tests/tools/etm-powersync-trace-replay-test.py @@ -228,5 +228,35 @@ class TestTraceReplay(unittest.TestCase): self.assertNotIn('requêtes rejouées : 0', out) +class TestComparaison(unittest.TestCase): + """La comparaison seule (fonctions de l'outil, sans serveur).""" + + @classmethod + def setUpClass(cls): + from importlib.machinery import SourceFileLoader + cls.outil = SourceFileLoader('trace_replay', OUTIL).load_module() + + def test_action_state_comparee_sur_son_etat_seul(self): + # Brief 4 T7 : le plan du rule-based porte la puissance ESTIMÉE d'une action `state`, + # l'optimiseur de référence ne la donne pas (le §6 ne la définit pas) — ce n'est pas un écart. + o = self.outil + maintenant = o.instant('2026-06-08T13:00:00Z') + creneau = {'from': '2026-06-08T13:00:00Z', 'to': '2026-06-08T13:01:00Z'} + regles = {'result': {'slots': [dict(creneau, actions=[ + {'loadId': 'pac', 'kind': 'state', 'state': 3, 'powerW': 1500}, + {'loadId': 'ecs', 'kind': 'setpoint', 'powerW': 1000}])]}} + optimiseur = {'result': {'slots': [dict(creneau, actions=[ + {'loadId': 'pac', 'kind': 'state', 'state': 3}, + {'loadId': 'ecs', 'kind': 'setpoint', 'powerW': 1000.0}])]}} + a = o.actions_courantes(regles, maintenant) + self.assertEqual(o.differences(a, o.actions_courantes(optimiseur, maintenant), 0.5), []) + optimiseur['result']['slots'][0]['actions'][0]['state'] = 4 + self.assertEqual(o.differences(a, o.actions_courantes(optimiseur, maintenant), 0.5), ['pac : état 3 → 4']) + optimiseur['result']['slots'][0]['actions'][0]['state'] = 3 + optimiseur['result']['slots'][0]['actions'][1]['powerW'] = 1200 + self.assertEqual(o.differences(a, o.actions_courantes(optimiseur, maintenant), 0.5), + ['ecs : 1000 W → 1200 W']) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/tools/etm-powersync-trace-replay b/tools/etm-powersync-trace-replay index ea045d8..d3cb6c1 100755 --- a/tools/etm-powersync-trace-replay +++ b/tools/etm-powersync-trace-replay @@ -13,7 +13,10 @@ de la trace (jamais d'écrasement), puis un résumé : requêtes, taux d'abstent médian et max, cycles dont les actions diffèrent du plan enregistré. « Actions du plan » : celles du créneau qui couvre l'instant de la requête (`timestamp`) — -charge, kind, puissance (à --tolerance-w près), état SG-Ready. Une abstention (`abstain`, aucun +charge, kind, puissance d'une consigne (`setpoint`, à --tolerance-w près), état d'une action +`state` (SG-Ready). La puissance d'une action `state` n'est pas comparée : le protocole ne la +définit pas (§6), et le plan du rule-based la porte (puissance estimée) quand un optimiseur ne la +donne pas. Une abstention (`abstain`, aucun créneau, aucun créneau courant) n'a pas d'actions. Une ligne dont la réponse enregistrée est une erreur (`no-response`, `timeout`…) n'est pas comparable : comptée à part. @@ -156,8 +159,9 @@ def actions_courantes(reponse, maintenant): continue actions = [] for a in creneau.get('actions') or []: - actions.append((str(a.get('loadId')), str(a.get('kind', 'setpoint')), - a.get('powerW'), a.get('state'))) + kind = str(a.get('kind', 'setpoint')) + actions.append((str(a.get('loadId')), kind, + None if kind == 'state' else a.get('powerW'), a.get('state'))) return sorted(actions, key=lambda x: (x[0], x[1])) return []