Ctrl K
ComunidadGuías…

test_watch_compare_etapes.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Caractérise l'ordre et les frontières des étapes de surveillance."""

import copy
import datetime as dt
import os
import tempfile
import unittest
from unittest import mock

_IMPORT_HOME = tempfile.TemporaryDirectory(prefix="blink-watch-compare-import-")
with mock.patch.dict(os.environ, {"BLINK_BOOTSTRAP": "none", "BLINK_HOME": _IMPORT_HOME.name}):
    import runtime
    import watch


MAINTENANT = dt.datetime(2026, 9, 8, 12, tzinfo=dt.timezone.utc)


class DateFixe(dt.datetime):
    @classmethod
    def now(cls, tz=None):
        return MAINTENANT.astimezone(tz) if tz else MAINTENANT.replace(tzinfo=None)


def camera(**changements):
    return {"online": True, "armed": True, "battery": "ok", "system_armed": True,
            **changements}


class TestsComparaisonEtapes(unittest.TestCase):
    def setUp(self):
        langue = mock.patch.object(runtime, "lire_langue", return_value="fr")
        self.langue = langue.start()
        self.addCleanup(langue.stop)
        horloge = mock.patch.object(watch.dt, "datetime", DateFixe)
        horloge.start()
        self.addCleanup(horloge.stop)

    @staticmethod
    def comparer(avant, courant, ignores=()):
        return watch.compare(avant, courant, dt.timezone.utc, set(ignores))

    def test_ordre_alertes_modules_cameras_systeme_puis_silence(self):
        avant = {"at": (MAINTENANT - dt.timedelta(hours=2)).isoformat(),
                 "cameras": {nom: camera() for nom in ("Zebra", "Alpha", "Delta", "Charlie")}}
        courant = {
            "modules": [{"name": "Z-module", "online": False},
                        {"name": "A-module", "online": False}],
            "cameras": {
                "Zebra": camera(online=False, armed=False, battery="low", system_armed=False),
                "Delta": camera(system_armed=False),
                "Alpha": camera(online=False, armed=False, battery="low", system_armed=False),
                "Charlie": camera(system_armed=False),
            },
            "last_clip": {nom: (MAINTENANT - dt.timedelta(days=2)).isoformat()
                          for nom in ("Delta", "Charlie")},
        }
        self.assertEqual(self.comparer(avant, courant), ([
            "Module « Z-module » hors ligne.",
            "Module « A-module » hors ligne.",
            "Caméra « Alpha » hors ligne.",
            "Caméra « Alpha » : batterie « low ».",
            "Caméra « Alpha » : détection coupée.",
            "Caméra « Zebra » hors ligne.",
            "Caméra « Zebra » : batterie « low ».",
            "Caméra « Zebra » : détection coupée.",
            "Système entièrement désarmé.",
            "Caméra « Delta » : aucun clip depuis 2 jour(s).",
            "Caméra « Charlie » : aucun clip depuis 2 jour(s).",
        ], []))

    def test_ordre_retours_modules_puis_cameras_sans_retour_batterie(self):
        avant = {"modules": [{"name": nom, "online": False} for nom in ("A", "Z")],
                 "cameras": {nom: camera(online=False, armed=False, battery="low")
                             for nom in ("Zebra", "Alpha")}}
        courant = {"modules": [{"name": nom, "online": True} for nom in ("Z", "A")],
                   "cameras": {nom: camera() for nom in ("Zebra", "Alpha")}}
        self.assertEqual(self.comparer(avant, courant), ([], [
            "Module « Z » de nouveau en ligne.",
            "Module « A » de nouveau en ligne.",
            "Caméra « Alpha » de nouveau en ligne.",
            "Caméra « Alpha » : détection réactivée.",
            "Caméra « Zebra » de nouveau en ligne.",
            "Caméra « Zebra » : détection réactivée.",
        ]))

    def test_premiere_observation_repetition_et_retour(self):
        incident = {"cameras": {"Salon": camera(online=False, armed=False, battery="low",
                                                system_armed=False)}}
        self.assertEqual(self.comparer({}, incident), ([
            "Caméra « Salon » hors ligne.",
            "Caméra « Salon » : batterie « low ».",
            "Caméra « Salon » : détection coupée.",
            "Système entièrement désarmé.",
        ], []))
        self.assertEqual(self.comparer(incident, incident), ([], []))
        self.assertEqual(self.comparer(incident, {"cameras": {"Salon": camera()}}), ([], [
            "Caméra « Salon » de nouveau en ligne.",
            "Caméra « Salon » : détection réactivée.",
        ]))

    def test_anglais_couvre_les_messages_des_quatre_etapes(self):
        self.langue.return_value = "en"
        courant = {
            "modules": [{"name": "Maison", "online": False}],
            "cameras": {
                "Salon": camera(online=False, armed=False, battery="low", system_armed=False),
                "Terrasse": camera(system_armed=False),
            },
            "last_clip": {"Terrasse": (MAINTENANT - dt.timedelta(days=3)).isoformat()},
        }
        self.assertEqual(self.comparer({}, courant), ([
            'Module "Maison" offline.',
            'Camera "Salon" offline.',
            'Camera "Salon": battery "low".',
            'Camera "Salon": detection disabled.',
            "System fully disarmed.",
            'Camera "Terrasse": no clip for 3 day(s).',
        ], []))
        retabli = {"modules": [{"name": "Maison", "online": True}],
                   "cameras": {"Salon": camera(), "Terrasse": camera()}}
        self.assertEqual(self.comparer(courant, retabli), ([], [
            'Module "Maison" back online.',
            'Camera "Salon" back online.',
            'Camera "Salon": detection re-enabled.',
        ]))

    def test_sourdine_supprime_alertes_et_retours_camera_pas_les_modules(self):
        avant = {"modules": [{"name": "Maison", "online": False}],
                 "cameras": {"Salon": camera(online=False, armed=False)}}
        courant = {"modules": [{"name": "Maison", "online": True},
                               {"name": "Annexe", "online": False}],
                   "cameras": {"Salon": camera(battery="low", system_armed=False)},
                   "last_clip": {"Salon": (MAINTENANT - dt.timedelta(days=4)).isoformat()}}
        self.assertEqual(self.comparer(avant, courant, {"Salon"}), (
            ["Module « Annexe » hors ligne."], ["Module « Maison » de nouveau en ligne."]))

    def test_sourdine_ne_rend_pas_unique_un_ancien_nom_ambigu(self):
        avant = {"cameras": {"Jardin": camera(online=False, battery="low")}}
        courant = {"cameras": {
            "Jardin maison": camera(name="Jardin", network_id="1", device_id="10",
                                     online=False, battery="low"),
            "Jardin annexe": camera(name="Jardin", network_id="2", device_id="20"),
        }}
        self.assertEqual(self.comparer(avant, courant, {"Jardin annexe"}), ([
            "Caméra « Jardin maison » hors ligne.",
            "Caméra « Jardin maison » : batterie « low ».",
        ], []))

    def test_identite_suit_renommage_sans_emprunter_etat_autre_reseau(self):
        avant = {"cameras": {
            "Autre module": camera(name="Jardin", network_id="2", device_id="10",
                                    online=False, armed=False),
            "Ancien libellé": camera(name="Jardin", network_id="1", device_id="10"),
        }}
        courant = {"cameras": {
            "Nouveau libellé": camera(name="Terrasse", network_id="1", device_id="10"),
        }}
        self.assertEqual(self.comparer(avant, courant), ([], []))

    def test_ancien_nom_unique_est_repris_sans_identite_mais_pas_avec_autre_identite(self):
        courant = {"cameras": {"Jardin": camera(name="Jardin", network_id="1", device_id="10",
                                                online=False, battery="low")}}
        historique = camera(online=False, battery="low")
        self.assertEqual(self.comparer({"cameras": {"Jardin": historique}}, courant), ([], []))
        autre = {**historique, "name": "Jardin", "network_id": "2", "device_id": "10"}
        self.assertEqual(self.comparer({"cameras": {"Jardin": autre}}, courant), ([
            "Caméra « Jardin » hors ligne.", "Caméra « Jardin » : batterie « low ».",
        ], []))

    def test_sourdine_suit_une_identite_renommee(self):
        avant = {"cameras": {
            "Ancien": camera(name="Jardin", network_id="1", device_id="10"),
        }}
        courant = {"cameras": {
            "Nouveau": camera(name="Terrasse", network_id="1", device_id="10",
                              online=False, armed=False, battery="low", system_armed=False),
        }}
        self.assertEqual(self.comparer(avant, courant, {"Ancien"}), ([], []))

    def test_seuil_silence_utilise_les_jours_complets(self):
        seuil = dt.timedelta(days=watch.SILENCE_DAYS)
        for age, attendu in ((seuil - dt.timedelta(seconds=1), []),
                              (seuil, [f"Caméra « Jardin » : aucun clip depuis {watch.SILENCE_DAYS} jour(s)."]),
                              (seuil + dt.timedelta(hours=23),
                               [f"Caméra « Jardin » : aucun clip depuis {watch.SILENCE_DAYS} jour(s)."]),
                              (-dt.timedelta(seconds=1), [])):
            with self.subTest(age=age):
                courant = {"cameras": {"Jardin": camera()},
                           "last_clip": {"Jardin": (MAINTENANT - age).isoformat()}}
                self.assertEqual(self.comparer({}, courant), (attendu, []))

    def test_silence_alerte_au_franchissement_pas_apres_seuil_deja_franchi(self):
        dernier = MAINTENANT - dt.timedelta(days=watch.SILENCE_DAYS, hours=1)
        seuil = dernier + dt.timedelta(days=watch.SILENCE_DAYS)
        courant = {"cameras": {"Jardin": camera()}, "last_clip": {"Jardin": dernier.isoformat()}}
        for passage, attendu in ((seuil - dt.timedelta(seconds=1),
                                  [f"Caméra « Jardin » : aucun clip depuis {watch.SILENCE_DAYS} jour(s)."]),
                                 (seuil, []), (MAINTENANT, [])):
            with self.subTest(passage=passage):
                self.assertEqual(self.comparer({"at": passage.isoformat()}, courant), (attendu, []))

    def test_silence_ancien_passage_absent_vide_ou_invalide_permet_premiere_alerte(self):
        courant = {"cameras": {"Jardin": camera()},
                   "last_clip": {"Jardin": (MAINTENANT - dt.timedelta(days=5)).isoformat()}}
        for avant in ({}, {"at": None}, {"at": ""}, {"at": "date incorrecte"}):
            with self.subTest(avant=avant):
                self.assertEqual(self.comparer(avant, courant), (
                    ["Caméra « Jardin » : aucun clip depuis 5 jour(s)."], []))

    def test_silence_ignore_absentes_sourdines_hors_ligne_desarmees_et_dates_invalides(self):
        vieux = (MAINTENANT - dt.timedelta(days=5)).isoformat()
        cameras = {"Sourdine": camera(), "Hors ligne": camera(online=False),
                   "Désarmée": camera(armed=False), "Date invalide": camera(),
                   "Sans clip": camera()}
        courant = {"cameras": cameras,
                   "last_clip": {"Absente": vieux, "Sourdine": vieux, "Hors ligne": vieux,
                                 "Désarmée": vieux, "Date invalide": "date incorrecte"}}
        self.assertEqual(self.comparer({"cameras": cameras}, courant, {"Sourdine"}), ([], []))

    def test_systeme_desarme_consulte_aussi_ancien_etat_des_cameras_en_sourdine(self):
        avant = {"cameras": {"Visible": camera(system_armed=False), "Muette": camera()}}
        courant = {"cameras": {"Visible": camera(system_armed=False), "Muette": camera()}}
        self.assertEqual(self.comparer(avant, courant, {"Muette"}), (
            ["Système entièrement désarmé."], []))

    def test_comparaison_ne_modifie_pas_les_etats_ni_la_sourdine(self):
        avant = {"cameras": {"Ancien": camera(name="Jardin", network_id="1", device_id="10")}}
        courant = {"modules": [{"name": "Maison", "online": False}],
                   "cameras": {"Nouveau": camera(name="Terrasse", network_id="1", device_id="10")}}
        ignores = {"Ancien"}
        copie = copy.deepcopy((avant, courant, ignores))
        watch.compare(avant, courant, dt.timezone.utc, ignores)
        self.assertEqual((avant, courant, ignores), copie)


if __name__ == "__main__":
    unittest.main()