:: Aurora Zine ::

numer 00 phile 01 z 01

Alert zadziałał. Mauser i tak się przelał.

Model liczył poziom wody bez czujnika, alert odpalił punktualnie, a zbiornik i tak się przelał.

grzegorz404

  • monitoring
  • alerting
  • postmortem
  • automatyka

Alert zadziałał. Mauser i tak się przelał.

25 lipca 2026, Żarnowo. Zbiornik IBC 1000 litrów — u mnie w domu nazywa się „mauser", bo tak się mówi i nikt tego nie zmieni. Napełniany pompą 230 V sterowaną z Domoticza. Druga pompa bierze z niego wodę i podlewa ogród. Całość dyryguje kilka skryptów pythonowych na Raspberry Pi 5 (host: centauri), spiętych cronem i systemd, z metrykami do Prometheusa, dashboardem w Grafanie i alertami na Mattermosta.

Domowa zabawka, ale zbudowana dokładnie tak, jak buduję rzeczy klientom. I dlatego jest o czym pisać: zaliczyła awarię, która w korpo kosztowałaby ludzi tydzień tłumaczenia się na callach.

O 14:00 monitoring wiedział wszystko. Alert PompaOverflowRisk przeszedł z pending w firing i poszedł na kanał. Pompa lała jeszcze cztery godziny. Zbiornik się przelał.

Model bez czujnika

Fundament tego systemu jest zgniły od pierwszego dnia i wiedziałem o tym: nie ma czujnika poziomu. Poziom wody nie jest mierzony, tylko całkowany z założeń:

  • tempo napełniania: 50 / 75 / 100 l/h (min / typowe / max — zależy od ciśnienia w studni),
  • tempo podlewania: 80 / 100 / 120 l/h,
  • czas pracy pomp parsowany z journala.

Bierzesz czas pracy, mnożysz przez stawkę, dodajesz, odejmujesz. Open-loop. Wychodzi z tego nie liczba, tylko przedział — i ten przedział rósł z każdą godziną, bo błąd się kumuluje i nie ma czego z nim zrobić. 25 lipca rano pasmo niepewności wynosiło 486–1000 L. Połowa zbiornika.

To jest miejsce, w którym system przestał być systemem pomiarowym, a stał się systemem opinii — tyle że opinia miała ładny wykres i dwa miejsca po przecinku.

Co się stało

24 lipca wieczorem podniosłem limit czasu jednego cyklu napełniania z 3 h na 8 h. Powód był rozsądny: przy 50 l/h trzy godziny to 150 litrów, cykl kończył się w połowie roboty i zbiornik nigdy nie dochodził do pełna.

Nie zauważyłem, że ten limit przez cały czas robił za coś innego. To nie był parametr wydajnościowy. To był przypadkowy dławik przeciwprzelewowy — jedyna rzecz, która pilnowała, żeby pompa nie lała bez końca. Podniosłem go i zdjąłem bezpiecznik, o którego istnieniu nie wiedziałem.

Nazajutrz cykl z 11:30:

lip 25 12:01:07 centauri pump-fill[2261]: fill: start, limit 28800s (8h)
lip 25 14:00:02 centauri pump-exporter[1187]: tank_estimated_liters=1000 pump_on=1
lip 25 14:00:12 centauri alertmanager: [FIRING:1] PompaOverflowRisk (zbiornik pelny, pompa pracuje)
lip 25 18:00:43 centauri pump-fill[2261]: fill: stop, pump runtime 6.01h

6,01 godziny ciągiem. O 14:00, cztery godziny przed końcem, model pokazywał jednocześnie tank_estimated_liters=1000 i pump_on=1 — czyli w jego własnym rozumieniu świata zbiornik był pełny, a pompa dalej lała. Alert to złapał. Reguła zadziałała idealnie, for się wypalił, powiadomienie wyszło na kanał.

I na tym się skończyło, bo nikt tego nie przeczytał. Sobotnie popołudnie, telefon w kuchni. Alert to powiadomienie, nie sterowanie. System wiedział i nadal robił szkodę, bo nikt go nie nauczył, że wiedza ma prowadzić do czynu. Między metryką a stycznikiem stał człowiek — i to była cała pętla sprzężenia zwrotnego.

Głębsza przyczyna: woda wpływała i nie wypływała

Osiem godzin lania nie przelałoby zbiornika, gdyby ogród pobierał wodę. Zajrzałem, ile razy podlewanie poszło od momentu, kiedy je „wdrożyłem".

Zero. Od 19 lipca ani razu. Około 24 nieudanych uruchomień.

Skrypt sterujący miał cztery niezależne wady. Każda pojedynczo wystarczała, żeby nic nie zadziałało:

1. Brak prawa wykonania.

-rw-r--r-- 1 grzeg grzeg 3841 lip 19 19:52 water_garden.py
lip 19 20:00:01 centauri CRON[3120]: (grzeg) CMD (/opt/pump/water_garden.py)
lip 19 20:00:01 centauri CRON[3120]: /bin/sh: 1: /opt/pump/water_garden.py: Permission denied

2. f-string z zagnieżdżonymi cudzysłowami. Składnia dozwolona od PEP 701, czyli od Pythona 3.12. Host ma 3.11.2, bo to Raspberry Pi OS i nikt tam nie kompiluje interpretera dla widzimisię:

  File "/opt/pump/water_garden.py", line 62
    log(f"switch {name} -> {r.json()["status"]}")
                                     ^
SyntaxError: f-string: unmatched '['

3. Wołanie API Domoticza bez logowania, a Domoticz od jakiegoś czasu ma włączoną autoryzację:

$ curl -s -o /dev/null -w '%{http_code}\n' \
    'http://127.0.0.1:8080/json.htm?type=command&switchcmd=Set%20On&idx=42'
401

4. Zły format polecenia. Nawet po zalogowaniu ten URL nic by nie przełączył, bo Domoticz oczekuje param=switchlight, a switchcmd przyjmuje On, nie Set On:

# było:  /json.htm?type=command&switchcmd=Set On&idx=42
# ma być: /json.htm?type=command&param=switchlight&idx=42&switchcmd=On

Wisienka: powiadomienia o błędach też nie dochodziły.

lip 19 20:00:02 centauri notify[3121]: delivery to mattermost failed

Cztery błędy w jednym pliku to nie jest pech. Cztery błędy w jednym pliku to jednoznaczny dowód, że ten skrypt nie został uruchomiony ani razu — ani ręcznie, ani na sucho, ani python -c "import". Napisany, zacommitowany, wrzucony do crona, odhaczony w głowie jako zrobiony. „Napisane" to nie „działa", a mój własny cron przez sześć dni grzecznie logował mi to do twarzy.

I jeszcze harmonogram, wpisany źle wszędzie

Przy okazji sekcji zwłok wyszło, że realny harmonogram podlewania z crona to:

0 20,2,8,14 * * *   # 4 cykle x 1,5 h = 6 h/dobe

a config modelu i kod liczący bilans miały wpisane [6, 18] — dwa cykle, 3 h na dobę. Nie jedno miejsce się rozjechało: rozjechały się wszystkie naraz, więc nie było czego z czym porównać.

Konsekwencje były dwie i obie paskudne:

  • model odejmował połowę realnego zużycia, czyli systematycznie zaniżał to, ile wody ubywa, a więc systematycznie uważał, że jest jej mniej niż w rzeczywistości i trzeba dolewać;
  • dwa z czterech cykli napełniania były martwe — 30-minutowy bootstrap zjadał całe 30-minutowe okno, więc pompa startowała dokładnie wtedy, kiedy okno się zamykało.

Przy paśmie 486–1000 L brałem do decyzji ten koniec, który pasował do tezy „trzeba dolać". Gdybym brał ten, który boli — 1000 L i lej dalej — awaria wyszłaby na sucho, na kartce, w piątek wieczorem.

Naprawa

Nie dokładałem kolejnego alertu, bo problemem nie był brak alertu. Problemem był brak decyzji.

Sprzężenie zwrotne przed laniem. Skrypt napełniający pyta teraz model o wolne miejsce, zanim włączy pompę, liczy czas po maksymalnej stawce (100 l/h) i trzyma 50 L zapasu. Za mało miejsca — skraca cykl. Brak miejsca — pomija cykl w całości:

lip 25 20:00:03 centauri pump-fill[4402]: tank: wolne 200L @ 100l/h -> skracam napelnianie 28468s -> 7200s
lip 26 02:00:02 centauri pump-fill[4517]: tank: wolne 40L (zapas 50L) -> cykl pominiety

Rekalibracja na obserwację człowieka. Podszedłem, zajrzałem, oceniłem 800 L i wpisałem to jako punkt odniesienia. Prymitywne, ale jedno spojrzenie do zbiornika ma mniejszy błąd niż sześć dni całkowania.

Poprawka godzin — w configu i w kodzie, żeby model widział wszystkie cztery cykle podlewania.

Przepisanie skryptu podlewania jako klon działającego skryptu drugiej pompy. Bez ambicji, bez ulepszeń. Ten drugi skrypt lał wodę bezbłędnie od tygodni — była jedna sprawdzona ścieżka i trzeba było z niej skorzystać, zamiast pisać drugą od zera.

przed po
pasmo modelu 486–1000 L 740–760 L
szerokość pasma 514 L 20 L
cykl nocny 7,9 h 1,5 h
podlewanie w modelu 3 h/dobę 6 h/dobę
udane uruchomienia podlewania od 19.07 0 z ~24

Pasmo zwęziło się z połowy zbiornika do dwóch procent. Nie dlatego, że model zmądrzał — dalej jest open-loop, dalej zgaduje. Po prostu przestał zgadywać na dystansie sześciu dni.

Co z tego zostaje

Alert był poprawny. Reguła była poprawna. Dostarczenie zadziałało. I zbiornik i tak się przelał, bo powiadomienie nie ma rąk. Jeżeli twój system potrafi wykryć stan, w którym robi sobie krzywdę, i jedyną reakcją jest wiadomość na czacie — to nie jest automatyka, to jest świadek.

Model bez pomiaru jest opinią, nawet jeśli ma dwa miejsca po przecinku i ładny wykres w Grafanie. Przy paśmie na pół zbiornika liczby dają złudzenie kontroli, a złudzenie kontroli jest gorsze niż jej brak, bo brak kontroli przynajmniej zmusza cię, żeby podejść i sprawdzić.

I ta ostatnia, najgorsza: docelowo zamawiam pływak za 50 zł. Kawałek plastiku na dźwigni, który przerywa obwód, kiedy woda dojdzie do góry. Nie potrzebuje prądu z zasilacza, sieci, Prometheusa, poprawnego f-stringa ani interpretera w wersji 3.12. Nie da się go zapomnieć zrestartować.

Cały ten stack — Domoticz, exporter, reguły alertowe, dashboard, Mattermost — nadal ma sens. Powie mi, że studnia słabnie, że pompa pracuje dłużej niż zwykle, że coś się psuje. Ale on jest od wiedzy. Od tego, żeby zbiornik się nie przelał, jest kawałek plastiku za pięć dych.

Najtwardsze zabezpieczenie w całym systemie kosztuje mniej niż godzina mojej pracy i nie ma ani jednej linijki kodu.