Pomiar poziomu wody w zbiorniku czujnik ciśnieniowy TL-136, ESPHome i Home Assistant
Mark
Autor
Pomiar poziomu wody w zbiorniku czujnik ciśnieniowy TL-136, ESPHome i Home Assistant

Podoba Ci się ten artykuł?

Tworzenie poradników zajmuje dużo czasu, a na stronie nie chce reklam. Jeśli pomogłem i masz na to ochotę — postaw mi kawę . To motywuje do dalszego pisania!

 

MarkLabs · Deszczówka i nawadnianie · Część 2

Pomiar poziomu wody w zbiorniku 3000 l: TL-136, 4–20 mA, ADS1115 i ESPHome krok po kroku

W pierwszej części opisałem sam zbiornik na deszczówkę, jego montaż i przygotowanie instalacji pod przyszły system nawadniania. Tym razem przechodzę do pierwszego elementu właściwego sterownika: pomiaru poziomu wody. Nie będzie to jednak opis „jak powinno działać”. Pokażę dokładnie układ, który zbudowałem, wartości zmierzone multimetrem, błędy po drodze, konfigurację ESPHome i finalną weryfikację w prawdziwym zbiorniku.

Najważniejszy wynik tego etapu:
przy pomiarze ręcznym otrzymałem dokładnie 48,0 cm wody od dna zbiornika. ESPHome wyliczał około 42,7 cm słupa nad membraną sondy. Po doliczeniu rzeczywistego offsetu montażowego sondy 5,3 cm wynik również wyniósł około 48,0 cm.

To ważne, bo w takim projekcie łatwo zbudować system, który „pokazuje jakieś liczby”. Mnie interesował pomiar, który da się zweryfikować zwykłą miarką i który później będzie można bez obaw wykorzystać do ochrony pompy, automatycznego podlewania, alarmów niskiego poziomu oraz decyzji, czy system ma w ogóle uruchamiać daną sekcję ogrodu.

Jeśli nie czytałeś pierwszej części, znajdziesz ją tutaj:
zbiornik na deszczówkę 3000 l – montaż i początek projektu.
Tam opisałem również przygotowanie przewodów podczas prac ziemnych. Do miejsca przyszłego sterownika została poprowadzona instalacja, dzięki czemu sam pomiar poziomu nie kończy się na prototypie na biurku.

Co dokładnie budujemy?

Cel jest prosty do opisania, ale wymaga kilku etapów po drodze. Sonda hydrostatyczna TL-136 nie wysyła do ESP32 informacji „w zbiorniku jest 48 cm wody”. Jej wyjściem jest przemysłowy sygnał prądowy 4–20 mA. Musimy zamienić go na napięcie, zmierzyć to napięcie przetwornikiem analogowo-cyfrowym, przekazać wynik do ESP32, a dopiero tam przeliczyć go na miliampery, centymetry i procent.

TL-136 Home Assistant, TL-136 ESPHome, czujnik poziomu wody ESP32, sonda hydrostatyczna 4–20 mA ESPHome, ADS1115 ESPHome, pomiar poziomu deszczówki Home Assistant, czujnik poziomu wody w zbiorniku na deszczówkę

W obecnym etapie prac łańcuch wygląda więc tak:
woda → TL-136 → pętla 4–20 mA → rezystor pomiarowy → ADS1115 → I²C → ESP32 → ESPHome → Home Assistant.
Mały OLED 0,96 cala pełni na razie rolę lokalnego wyświetlacza diagnostycznego. Docelowo nie będzie to finalny interfejs sterownika – później zastąpi go kolorowy ekran dotykowy TFT.

Części, z których zbudowałem prototyp

W tym artykule celowo podaję konkretne elementy, bo przy 4–20 mA bardzo łatwo znaleźć w sieci ogólny schemat, ale znacznie trudniej odtworzyć cały działający zestaw. Poniżej są części użyte lub przewidziane w tym etapie projektu.

Element Wersja użyta w projekcie Rola
Sonda hydrostatyczna TL-136, 0–1 m, 4–20 mA, 24 V DC Pomiar ciśnienia słupa wody
Mikrokontroler ESP32 DevKit / ESP-WROOM-32 ESPHome, obliczenia, komunikacja z HA
ADC ADS1115, 16 bit, I²C, 4 kanały Pomiar napięcia na rezystorze
Rezystor pomiarowy – prototyp 120 Ω Konwersja 4–20 mA na napięcie
Rezystor pomiarowy – wersja uporządkowana 120 Ω Docelowe uproszczenie toru wejściowego
Rezystor wejściowy 1 kΩ Ograniczenie prądu wejściowego / część filtra
Kondensator 10 nF Prosty filtr wejścia ADC
Wyświetlacz testowy OLED 0,96″ SSD1306 128×64 I²C Lokalny odczyt procentu
Zasilanie sondy 24 V DC Zasilanie pętli TL-136
Rezystor pomiarowy:
w całym torze pomiarowym używam pojedynczego rezystora 120 Ω. Ta sama wartość jest użyta w obliczeniach ESPHome, dzięki czemu schemat, opis i kod pozostają ze sobą spójne.

TL-136 – dlaczego akurat sonda hydrostatyczna 4–20 mA?

Mój egzemplarz TL-136 ma zakres 0–1 m i wyjście 4–20 mA. Ten zakres pasuje do niskiego zbiornika, bo interesuje mnie mniej więcej metr słupa wody, a nie kilka czy kilkanaście metrów. Z punktu widzenia tego projektu szczególnie ważny jest jednak nie sam model, lecz sposób przesyłania pomiaru.

Czujnik nie wysyła delikatnego sygnału 0–1 V ani nie próbuje prowadzić magistrali I²C przez ogród. Wysyła prąd w pętli. Dla dolnego końca zakresu jest to około 4 mA, dla górnego około 20 mA. To rozwiązanie bardzo dobrze pasuje do sytuacji, w której sonda znajduje się w zbiorniku, a elektronika sterownika jest kilkanaście metrów dalej.

W mojej instalacji od zbiornika do miejsca przyszłego sterownika jest około 15 m. Przewód został przygotowany już podczas prac ziemnych. Z tego samego wielożyłowego kabla korzystać będą później inne elementy systemu, ale dla TL-136 przeznaczona jest osobna para żył. Nie łączę przewodów sygnałowych sondy „jak popadnie” z przewodami zaworów.

Ważne przy TL-136:
z przewodu sondy wychodzi również cienka rurka odpowietrzająca. Nie należy jej zatykać ani zalewać. Koniec przewodu powinien znaleźć się w suchym miejscu, gdzie czujnik ma odniesienie do ciśnienia atmosferycznego.

ADS1115 – po co dodatkowy ADC, skoro ESP32 ma wejścia analogowe?

ADS1115jpg

Chciałem mieć osobny, przewidywalny tor pomiarowy i możliwość późniejszego wykorzystania kolejnych kanałów. ADS1115 jest 16-bitowym przetwornikiem ADC komunikującym się po I²C. W prototypie pracuje pod adresem 0x48, a OLED pod 0x3C, więc oba urządzenia mogą być podłączone do tych samych linii SDA i SCL.

Na ESP32 wykorzystuję GPIO21 jako SDA i GPIO22 jako SCL. ADS1115 zasilałem z 3,3 V, dzięki czemu poziomy logiczne I²C są bezpieczne dla ESP32. W ESPHome finalnie używam kanału A3_GND.

Sam ADS1115 nie mierzy prądu 4–20 mA bezpośrednio. Najpierw trzeba ten prąd przepuścić przez rezystor pomiarowy. Dopiero spadek napięcia na rezystorze trafia do wejścia ADC.

Jak 4–20 mA zamienia się na napięcie?

To najważniejszy fragment całej elektroniki. W pętli umieszczamy rezystor pomiarowy. Z prawa Ohma:

U = I × R

Gdyby użyć dokładnie 120 Ω, otrzymujemy:

4 mA: 0,004 A × 120 Ω = 0,480 V
20 mA: 0,020 A × 120 Ω = 2,400 V

W torze pomiarowym zastosowałem pojedynczy rezystor 120 Ω. Dzięki temu zakres 4–20 mA daje bardzo wygodny zakres napięć około 0,48–2,40 V.

zależność wysokość → prąd → napięcie dla toru z rezystorem 120
Zależność wysokość → prąd → napięcie dla toru z rezystorem 120 Ω.

Dla rezystora 120 Ω czujnik przy 4 mA daje około 0,480 V, a przy 20 mA około 2,400 V. Taki poziom napięcia jest wygodny dla ADS1115 zasilanego 3,3 V.

Poprawne połączenie pętli – najważniejszy punkt całego układu

W czasie budowy właśnie tutaj najłatwiej o błąd. ADS1115 nie może znaleźć się szeregowo w pętli. Mierzy on napięcie powstałe na rezystorze. Prawidłowy układ dla pomiaru niskostronnego wygląda logicznie tak:

Pomiar poziomu wody w zbiorniku – TL-136, ESPHome iHome Assistant

+24 V DC
   │
   └──── czerwony przewód TL-136
                  │
                TL-136
                  │
              czarny przewód
                  │
                  ●  ← punkt pomiarowy
                  │
                  ├──── 1 kΩ ───── A3 ADS1115
                  │                  │
                  │                10 nF
                  │                  │
                120 Ω              GND
                  │                  │
0 V zasilacza ────●──────────────────┘
                  │
                  ├──── GND ADS1115
                  └──── GND ESP32
Nie podawaj 24 V na ADS1115 ani ESP32.
24 V zasila pętlę sondy. ADS1115 pracuje z 3,3 V, a ESP32 podczas prototypowania może być zasilane np. z USB. W nieizolowanym układzie pomiarowym masa ADS1115 i ESP32 musi mieć wspólne odniesienie z 0 V zasilacza pętli.

Rezystor 1 kΩ pomiędzy punktem pomiarowym a wejściem A3 oraz kondensator 10 nF do masy tworzą bardzo prosty filtr wejściowy. Przy pomiarze poziomu wody nie potrzebujemy reakcji w mikrosekundach. Woda w zbiorniku zmienia poziom powoli, więc możemy pozwolić sobie zarówno na filtr sprzętowy, jak i później programowy.

Pierwszy test bez ESP32 – najpierw multimetr

Zanim podłączyłem ADS1115, chciałem wiedzieć, czy sonda w ogóle pracuje poprawnie. To bardzo ważna zasada diagnostyki: nie dokładać od razu pięciu warstw elektroniki i oprogramowania. Najpierw sprawdzamy to, co da się zmierzyć bezpośrednio.

1. Pomiar rezystancji

Połączenie 120 Ω zmierzyłem multimetrem. Wynik: 120 Ω. To od tej chwili stało się dla mnie wartością ważniejszą niż napis na rezystorach.

2. Sonda poza wodą

Przy zerowym słupie wody napięcie na rezystorze 120 Ω powinno wynosić około 0,480 V. To odpowiada dokładnie 4,00 mA, czyli dolnej granicy zakresu czujnika.

Policzmy:

I = 0,480 V / 120 Ω = 0,004 A = 4,00 mA

Czyli po ustabilizowaniu czujnik był praktycznie idealnie na dolnym końcu zakresu.

3. Test w naczyniu

Następnie sonda trafiła do naczynia z wodą. Przy słupie wody rzędu około 20–21 cm napięcie na rezystorze 120 Ω powinno znajdować się w okolicy 0,86–0,88 V, zależnie od dokładnej wysokości słupa nad membraną.

schemat podlaczenia esp32, ADS1115 czujnik poziomu wody TL-136 home assistantschemat podlaczenia esp32, ADS1115 czujnik poziomu wody TL-136 home assistant

dla około 20 cm:
I ≈ 7,2 mA
U ≈ 7,2 mA × 120 Ω = 0,864 V

Biorąc pod uwagę sposób ręcznego określania punktu pomiarowego sondy i wysokości wody w naczyniu, wynik był bardzo dobry. Ten prosty test dał mi najważniejszą odpowiedź: sonda reaguje liniowo i cały pomysł z rezystorem pomiarowym działa.

Praktyczna lekcja:
przy tego typu pomiarach nie oceniaj czujnika po pierwszej liczbie, którą zobaczysz na mierniku. Daj układowi chwilę, wykonaj kilka pomiarów i dopiero wtedy wyciągaj wnioski.

ADS1115 i ESP32 – pierwsze uruchomienie I²C

Po pomiarach multimetrem dołożyłem ADS1115 i ESP32. Magistrala I²C pracuje na GPIO21 i GPIO22. W logu ESPHome skaner znalazł dwa urządzenia:

Found device at address 0x3C
Found device at address 0x48

0x3C to OLED, a 0x48 to ADS1115. Sam fakt znalezienia adresu nie oznacza jednak jeszcze, że cały układ działa poprawnie. W jednej z prób skaner widział ADS1115, ale komponent zgłaszał Communication failed. Po poprawieniu fizycznych połączeń błąd zniknął.

To kolejna rzecz, którą warto zapamiętać: jeśli I²C raz działa, raz nie działa albo urządzenie jest widoczne tylko czasami, zanim zaczniemy przepisywać YAML, sprawdzamy przewody, zasilanie i styki.

Dlaczego ostatecznie używam A3?

Podczas prób kanał A0 nie dawał poprawnego odczytu w konfiguracji, na której pracowałem. Nie traciłem czasu na rozbieranie modułu i dochodzenie, czy problemem jest sam kanał, pin, ścieżka czy styk. ADS1115 ma cztery wejścia. Przepiąłem sygnał na A3, w ESPHome ustawiłem A3_GND i otrzymałem stabilny pomiar.

Nie wyciągam z tego wniosku, że każdy A0 w ADS1115 jest problematyczny – oczywiście nie jest. W moim prototypie ważniejsze było doprowadzenie całego toru do poprawnego pomiaru. A3 działa, więc to na nim kontynuuję projekt.

Test w prawdziwym zbiorniku – tutaj wszystko się rozstrzygnęło

Test warsztatowy był potrzebny, ale prawdziwa weryfikacja przyszła dopiero po zamontowaniu sondy w zbiorniku. Sonda znajduje się około 5 cm nad dnem. Po dokładnym porównaniu pomiaru elektronicznego z miarką wyszło, że punkt pomiarowy należy traktować jako położony około 5,3 cm nad dnem.

W gotowym torze pomiarowym najważniejsze są trzy rzeczy: rezystor 120 Ω, zakres 4–20 mA oraz poprawka wynikająca z położenia sondy nad dnem.

schemat podlaczenia esp32, ADS1115 czujnik poziomu wody TL-136 home assistant

Rezystor pomiarowy
120 Ω
Zakres czujnika
4–20 mA
Offset sondy
5,3 cm
Poziom od dna
48,0 cm
Prąd pętli
10,83 mA
Słup nad sondą
42,7 cm
Poziom od dna
48,0 cm

Wyszedłem więc do zbiornika z miarką i zmierzyłem wodę ręcznie. Wynik: dokładnie 48 cm. To był dla mnie moment, w którym cały tor można było uznać za działający. Czujnik zamontowałem w rurce elektroinstalacyjnej uprzednio ponawiercanej, w celu ochrony przed uszkodzeniami mechanicznymi. Pomiar poziomu wody w zbiorniku – TL-136, ESPHome i Home Assistant

Całość prezentuje się tak :

Pomiar poziomu wody w zbiorniku – TL-136, ESPHome i Home Assistant

 

P

Po co dodaję 5,3 cm zamiast zostawić 42,7 cm?

Bo chcę, aby Home Assistant pokazywał rzeczywistą wysokość lustra wody liczoną od dna zbiornika. TL-136 mierzy ciśnienie nad swoją membraną. Jeśli membrana jest kilka centymetrów wyżej, to bez offsetu wynik zawsze będzie o te kilka centymetrów niższy.

Dlatego w kodzie:

poziom rzeczywisty = słup wody nad sondą + 5,3 cm

Poniżej punktu pomiarowego i tak może z czasem pojawić się osad, więc nie traktuję tych kilku centymetrów jako aktywnego zakresu sondy. Do prezentacji wysokości chcę jednak mieć pełny poziom od dna – dokładnie taki, jaki zobaczę na miarce.

Jak ESPHome przelicza napięcie na centymetry?

Obliczenie składa się z trzech prostych etapów.

Etap 1 – napięcie na ADS1115 zamieniamy na prąd:

I[mA] = U[V] / 120 Ω × 1000

Etap 2 – prąd zamieniamy na wysokość nad membraną:

wysokość[cm] = (I[mA] – 4) × 6,25

Skąd 6,25? Zakres 0–100 cm odpowiada zakresowi 4–20 mA. Mamy więc 16 mA rozpiętości:

100 cm / 16 mA = 6,25 cm/mA

Etap 3 – dodajemy offset montażowy:

poziom od dna = wysokość nad membraną + 5,3 cm

To wszystko. Nie potrzebujemy żadnych magicznych współczynników. Najważniejsze jest, aby używać rzeczywistej wartości rezystora i rzeczywiście zmierzonego położenia sondy.

Pełny YAML ESPHome używany na obecnym etapie

Poniżej jest pełna konfiguracja dla obecnego etapu projektu: ESP32, ADS1115 na A3, pojedynczy rezystor pomiarowy 120 Ω, filtr medianowy, obliczenie prądu, słupa nad sondą, rzeczywistej wysokości od dna i procentu. OLED SSD1306 jest obrócony o 180°. Główną wartością na ekranie jest procent napełnienia, a w dolnym wierszu pokazuję dodatkowo napięcie z ADS1115 oraz wysokość słupa wody nad sondą. Wszystkie wartości pozostają dostępne również w Home Assistant.

W tej wersji 100 cm = 100%. To na razie założenie robocze do testów interfejsu. Gdy zmierzę rzeczywisty maksymalny poziom lustra przy pełnym zbiorniku / przelewie, zmienię tę wartość. Procent wysokości nie oznacza też automatycznie procentu objętości w profilowanym zbiorniku.
esphome:
  name: sterownik-nawadniania
  friendly_name: Sterownik nawadniania

esp32:
  board: esp32dev
  framework:
    type: esp-idf

logger:

api:
  encryption:
    key: !secret api_encryption_key

ota:
  - platform: esphome
    password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

captive_portal:

web_server:

i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true
  frequency: 50kHz

ads1115:
  - address: 0x48
    id: ads1115_1

sensor:

  - platform: ads1115
    ads1115_id: ads1115_1
    multiplexer: A3_GND
    gain: 4.096
    sample_rate: 128
    name: "TL136 Napięcie"
    id: tl136_voltage
    unit_of_measurement: "V"
    device_class: voltage
    state_class: measurement
    accuracy_decimals: 3
    update_interval: 1s
    filters:
      - median:
          window_size: 5
          send_every: 1
          send_first_at: 1

  - platform: template
    name: "TL136 Prąd"
    id: tl136_current
    unit_of_measurement: "mA"
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (isnan(id(tl136_voltage).state)) {
        return NAN;
      }

      const float shunt_resistance = 120.0f;

      float current_ma =
        (id(tl136_voltage).state / shunt_resistance) * 1000.0f;

      return current_ma;

  - platform: template
    name: "TL136 Słup wody nad sondą"
    id: tl136_water_column
    unit_of_measurement: "cm"
    state_class: measurement
    accuracy_decimals: 1
    icon: "mdi:waves"
    update_interval: 1s
    lambda: |-
      if (isnan(id(tl136_voltage).state)) {
        return NAN;
      }

      const float shunt_resistance = 120.0f;

      float current_ma =
        (id(tl136_voltage).state / shunt_resistance) * 1000.0f;

      float water_column_cm =
        (current_ma - 4.0f) * 6.25f;

      if (water_column_cm < 0.0f) {
        water_column_cm = 0.0f;
      }

      if (water_column_cm > 100.0f) {
        water_column_cm = 100.0f;
      }

      return water_column_cm;

  - platform: template
    name: "Poziom wody w zbiorniku"
    id: tank_water_level
    unit_of_measurement: "cm"
    state_class: measurement
    accuracy_decimals: 1
    icon: "mdi:waves-arrow-up"
    update_interval: 1s
    lambda: |-
      if (isnan(id(tl136_voltage).state)) {
        return NAN;
      }

      const float shunt_resistance = 120.0f;
      const float sensor_offset_cm = 5.3f;

      float current_ma =
        (id(tl136_voltage).state / shunt_resistance) * 1000.0f;

      float water_column_cm =
        (current_ma - 4.0f) * 6.25f;

      if (water_column_cm < 0.0f) {
        water_column_cm = 0.0f;
      }

      if (water_column_cm > 100.0f) {
        water_column_cm = 100.0f;
      }

      float real_level_cm =
        water_column_cm + sensor_offset_cm;

      return real_level_cm;

  - platform: template
    name: "Poziom wody w zbiorniku procent"
    id: tank_water_percent
    unit_of_measurement: "%"
    state_class: measurement
    accuracy_decimals: 0
    icon: "mdi:water-percent"
    update_interval: 1s
    lambda: |-
      if (isnan(id(tank_water_level).state)) {
        return NAN;
      }

      const float full_level_cm = 100.0f;

      float percent =
        (id(tank_water_level).state / full_level_cm) * 100.0f;

      if (percent < 0.0f) {
        percent = 0.0f;
      }

      if (percent > 100.0f) {
        percent = 100.0f;
      }

      return percent;

font:

  - file: "gfonts://Roboto"
    id: font_small
    size: 10

  - file: "gfonts://Roboto"
    id: font_data
    size: 11

  - file: "gfonts://Roboto"
    id: font_percent
    size: 32

  - file: "gfonts://Roboto"
    id: font_medium
    size: 14

display:

  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    id: oled_display
    rotation: 180°
    update_interval: 2s

    lambda: |-
      if (
        isnan(id(tank_water_percent).state) ||
        isnan(id(tl136_voltage).state) ||
        isnan(id(tl136_water_column).state)
      ) {

        it.print(
          64,
          7,
          id(font_small),
          TextAlign::TOP_CENTER,
          "ZBIORNIK"
        );

        it.print(
          64,
          27,
          id(font_medium),
          TextAlign::TOP_CENTER,
          "BRAK POMIARU"
        );

        return;
      }

      it.print(
        64,
        0,
        id(font_small),
        TextAlign::TOP_CENTER,
        "ZBIORNIK"
      );

      it.printf(
        64,
        12,
        id(font_percent),
        TextAlign::TOP_CENTER,
        "%.0f%%",
        id(tank_water_percent).state
      );

      it.printf(
        2,
        52,
        id(font_data),
        TextAlign::TOP_LEFT,
        "%.3fV",
        id(tl136_voltage).state
      );

      it.printf(
        126,
        52,
        id(font_data),
        TextAlign::TOP_RIGHT,
        "%.1fcm",
        id(tl136_water_column).state
      );

Co widzę w Home Assistant?

Na tym etapie ESPHome publikuje kilka osobnych encji. Celowo zostawiam także wartości diagnostyczne, bo podczas budowy bardzo ułatwiają sprawdzenie, czy błąd leży w sondzie, rezystorze, ADS1115 czy dopiero w przeliczeniu.

TL136 Napięcie – surowe napięcie z ADS1115
TL136 Prąd – przeliczona wartość pętli 4–20 mA
TL136 Słup wody nad sondą – wysokość liczona od membrany
Poziom wody w zbiorniku – rzeczywista wysokość od dna
Poziom wody w zbiorniku procent – procent używany także na OLED

Samą listę encji traktuję wyłącznie jako warstwę diagnostyczną. Ponieważ pomiar został już zweryfikowany fizycznie, w dalszej części tej samej publikacji dokładam gotowy dashboard z dynamiczną grafiką zbiornika i wykresem historii. Status pompy, zaworów i sekcji nawadniania pojawi się dopiero wtedy, gdy te elementy rzeczywiście dołączą do sterownika.

Dashboard Home Assistant – z surowych encji do czytelnego widoku zbiornika

schemat podlaczenia esp32, ADS1115 czujnik poziomu wody TL-136 home assistant

Skoro sam pomiar został już zweryfikowany miarką, mogę wreszcie zrobić coś bardziej użytecznego niż lista pięciu encji. W Home Assistant przygotowałem kartę, która w jednym miejscu pokazuje aktualny procent, poziom w centymetrach, dynamiczny „zbiornik” z wysokością wody, status zakresu, a po prawej diagnostykę toru TL-136. Pod spodem dodałem historię poziomu z ostatnich 24 godzin.

Celowo nie pokazuję jeszcze litrów. Mój zbiornik jest profilowany, dlatego procent wysokości nie jest tym samym co procent objętości. Dopiero po zdobyciu tabeli wysokość → objętość albo wykonaniu własnej kalibracji dodam wiarygodną liczbę litrów.

Ta wersja dashboardu korzysta z HACS.
Potrzebne są dwie karty frontendowe:
custom:button-card oraz
custom:apexcharts-card.
Pierwsza buduje dynamiczny wygląd zbiornika, a druga pokazuje historię poziomu.

Jak dodać kartę?

W HACS przechodzę do sekcji Frontend i instaluję Button Card oraz ApexCharts Card. Po instalacji odświeżam przeglądarkę. Następnie otwieram dashboard Home Assistant, włączam edycję, wybieram dodanie karty ręcznej i wklejam poniższy YAML.

W kodzie używam identyfikatorów encji utworzonych przez mój węzeł ESPHome. Home Assistant może nadać u Ciebie nieco inne entity_id, dlatego przed wklejeniem warto sprawdzić je w Ustawienia → Urządzenia i usługi → Encje. W karcie występuje pięć sensorów: procent, poziom rzeczywisty, prąd pętli, napięcie ADS1115 oraz słup wody nad sondą.

type: vertical-stack
cards:

  # ==========================================================
  # GŁÓWNA KARTA ZBIORNIKA
  #
  # Wymaga:
  # - custom:button-card
  #
  # Jeżeli Home Assistant utworzył inne entity_id, podmień
  # pięć identyfikatorów sensor.* poniżej.
  # ==========================================================

  - type: custom:button-card

    entity: sensor.sterownik_nawadniania_poziom_wody_w_zbiorniku_procent

    show_icon: false
    show_name: false
    show_state: false

    tap_action:
      action: more-info

    hold_action:
      action: more-info

    styles:
      card:
        - padding: 0
        - overflow: hidden
        - border-radius: 24px
        - border: 1px solid rgba(56, 189, 248, 0.22)
        - background: >
            linear-gradient(
              145deg,
              rgba(4, 16, 28, 0.98),
              rgba(8, 28, 45, 0.98)
            )
        - box-shadow: >
            0 18px 50px rgba(0, 0, 0, 0.28)

      grid:
        - grid-template-areas: '"tank"'
        - grid-template-columns: 1fr
        - grid-template-rows: auto

      custom_fields:
        tank:
          - width: 100%

    custom_fields:

      tank: |
        [[[
          const percentEntity =
            states['sensor.sterownik_nawadniania_poziom_wody_w_zbiorniku_procent'];

          const levelEntity =
            states['sensor.sterownik_nawadniania_poziom_wody_w_zbiorniku'];

          const currentEntity =
            states['sensor.sterownik_nawadniania_tl136_prad'];

          const voltageEntity =
            states['sensor.sterownik_nawadniania_tl136_napiecie'];

          const columnEntity =
            states['sensor.sterownik_nawadniania_tl136_slup_wody_nad_sonda'];

          const unavailable = (e) =>
            !e ||
            e.state === 'unknown' ||
            e.state === 'unavailable';

          const n = (e, fallback = NaN) =>
            unavailable(e) ? fallback : Number(e.state);

          const percentRaw = n(percentEntity);
          const level = n(levelEntity);
          const current = n(currentEntity);
          const voltage = n(voltageEntity);
          const column = n(columnEntity);

          const percent = Number.isFinite(percentRaw)
            ? Math.max(0, Math.min(100, percentRaw))
            : 0;

          const online = !unavailable(percentEntity);

          let status = 'BRAK DANYCH';
          let statusClass = 'status-offline';

          if (online) {
            if (percent <= 10) {
              status = 'KRYTYCZNIE NISKI';
              statusClass = 'status-critical';
            } else if (percent <= 25) {
              status = 'NISKI';
              statusClass = 'status-low';
            } else if (percent < 90) {
              status = 'POZIOM PRAWIDŁOWY';
              statusClass = 'status-normal';
            } else if (percent < 100) {
              status = 'PRAWIE PEŁNY';
              statusClass = 'status-high';
            } else {
              status = 'PEŁNY';
              statusClass = 'status-full';
            }
          }

          const fmt = (value, digits, unit) =>
            Number.isFinite(value)
              ? `${value.toFixed(digits)} ${unit}`
              : '—';

          const updated =
            percentEntity?.last_updated
              ? new Date(percentEntity.last_updated).toLocaleTimeString(
                  undefined,
                  { hour: '2-digit', minute: '2-digit', second: '2-digit' }
                )
              : '—';

          return `
            <div class="rain-root">

              <div class="rain-head">
                <div>
                  <div class="rain-kicker">STEROWNIK NAWADNIANIA</div>
                  <div class="rain-title">Zbiornik deszczówki</div>
                  <div class="rain-subtitle">
                    TL-136 · 4–20 mA · ADS1115 · ESP32
                  </div>
                </div>

                <div class="rain-online ${online ? 'online' : 'offline'}">
                  <span class="rain-dot"></span>
                  ${online ? 'ONLINE' : 'BRAK DANYCH'}
                </div>
              </div>

              <div class="rain-main">

                <div class="rain-left">

                  <div class="rain-value-label">POZIOM WODY</div>

                  <div class="rain-value">
                    ${online ? Math.round(percent) : '—'}
                    <span>%</span>
                  </div>

                  <div class="rain-cm">
                    ${fmt(level, 1, 'cm')}
                  </div>

                  <div class="rain-status ${statusClass}">
                    ${status}
                  </div>

                  <div class="rain-progress">
                    <div
                      class="rain-progress-fill"
                      style="width:${percent}%"
                    ></div>
                  </div>

                  <div class="rain-progress-scale">
                    <span>0%</span>
                    <span>100%</span>
                  </div>

                </div>

                <div class="rain-center">

                  <div class="tank-wrap">

                    <div class="tank-cap"></div>

                    <div class="tank-shell">

                      <div
                        class="tank-water"
                        style="height:${percent}%"
                      >
                        <div class="tank-wave tank-wave-one"></div>
                        <div class="tank-wave tank-wave-two"></div>
                      </div>

                      <div class="tank-scale">
                        <span style="bottom:95%">100</span>
                        <span style="bottom:72%">75</span>
                        <span style="bottom:48%">50</span>
                        <span style="bottom:24%">25</span>
                        <span style="bottom:1%">0</span>
                      </div>

                      <div class="tank-probe">
                        <div class="tank-probe-wire"></div>
                        <div class="tank-probe-body"></div>
                      </div>

                      <div class="tank-badge">
                        <strong>${online ? Math.round(percent) : '—'}%</strong>
                        <span>${fmt(level, 1, 'cm')}</span>
                      </div>

                    </div>

                    <div class="tank-base"></div>

                  </div>

                </div>

                <div class="rain-right">

                  <div class="diag-title">DIAGNOSTYKA TL-136</div>

                  <div class="diag-row">
                    <span>Prąd pętli</span>
                    <strong>${fmt(current, 2, 'mA')}</strong>
                  </div>

                  <div class="diag-row">
                    <span>Napięcie ADS1115</span>
                    <strong>${fmt(voltage, 3, 'V')}</strong>
                  </div>

                  <div class="diag-row">
                    <span>Słup nad sondą</span>
                    <strong>${fmt(column, 1, 'cm')}</strong>
                  </div>

                  <div class="diag-row">
                    <span>Offset sondy</span>
                    <strong>5.3 cm</strong>
                  </div>

                  <div class="diag-row">
                    <span>Wejście ADC</span>
                    <strong>A3</strong>
                  </div>

                  <div class="diag-row">
                    <span>Aktualizacja</span>
                    <strong>${updated}</strong>
                  </div>

                </div>

              </div>

              <div class="rain-foot">
                <span>ESP32</span>
                <span>ADS1115 · 0x48</span>
                <span>OLED · 0x3C</span>
                <span>TL-136 · 0–1 m</span>
              </div>

            </div>
          `;
        ]]]

    extra_styles: |

      .rain-root {
        box-sizing: border-box;
        width: 100%;
        padding: 24px;
        color: #eaf6ff;
        font-family:
          Inter,
          -apple-system,
          BlinkMacSystemFont,
          "Segoe UI",
          sans-serif;
      }

      .rain-head {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: 18px;
        margin-bottom: 24px;
      }

      .rain-kicker {
        color: #38bdf8;
        font-size: 11px;
        font-weight: 800;
        letter-spacing: 0.18em;
      }

      .rain-title {
        margin-top: 5px;
        color: #ffffff;
        font-size: clamp(24px, 4vw, 36px);
        line-height: 1.05;
        font-weight: 800;
      }

      .rain-subtitle {
        margin-top: 7px;
        color: #85a8be;
        font-size: 13px;
      }

      .rain-online {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 8px 12px;
        border-radius: 999px;
        font-size: 11px;
        font-weight: 800;
        letter-spacing: 0.06em;
        white-space: nowrap;
      }

      .rain-online.online {
        color: #86efac;
        background: rgba(34, 197, 94, 0.10);
        border: 1px solid rgba(34, 197, 94, 0.34);
      }

      .rain-online.offline {
        color: #fca5a5;
        background: rgba(239, 68, 68, 0.10);
        border: 1px solid rgba(239, 68, 68, 0.32);
      }

      .rain-dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: currentColor;
        box-shadow: 0 0 12px currentColor;
      }

      .rain-main {
        display: grid;
        grid-template-columns:
          minmax(190px, 0.85fr)
          minmax(220px, 1.15fr)
          minmax(210px, 0.95fr);
        gap: 18px;
        align-items: stretch;
      }

      .rain-left,
      .rain-right {
        box-sizing: border-box;
        min-width: 0;
        padding: 18px;
        border: 1px solid rgba(148, 191, 218, 0.15);
        border-radius: 18px;
        background: rgba(255,255,255,0.025);
      }

      .rain-left {
        display: flex;
        flex-direction: column;
        justify-content: center;
      }

      .rain-value-label,
      .diag-title {
        color: #38bdf8;
        font-size: 12px;
        font-weight: 800;
        letter-spacing: 0.09em;
      }

      .rain-value {
        margin-top: 12px;
        color: #ffffff;
        font-size: clamp(54px, 8vw, 86px);
        line-height: 0.95;
        font-weight: 850;
        letter-spacing: -0.04em;
      }

      .rain-value span {
        margin-left: 3px;
        color: #7dd3fc;
        font-size: 0.45em;
        font-weight: 800;
      }

      .rain-cm {
        margin-top: 10px;
        color: #7dd3fc;
        font-size: 22px;
        font-weight: 750;
      }

      .rain-status {
        align-self: flex-start;
        margin-top: 14px;
        padding: 7px 10px;
        border-radius: 9px;
        font-size: 10px;
        font-weight: 850;
        letter-spacing: 0.07em;
      }

      .status-critical {
        color: #fecaca;
        background: rgba(239,68,68,0.16);
      }

      .status-low {
        color: #fed7aa;
        background: rgba(249,115,22,0.15);
      }

      .status-normal {
        color: #bae6fd;
        background: rgba(14,165,233,0.14);
      }

      .status-high {
        color: #a5f3fc;
        background: rgba(6,182,212,0.15);
      }

      .status-full {
        color: #bbf7d0;
        background: rgba(34,197,94,0.15);
      }

      .status-offline {
        color: #cbd5e1;
        background: rgba(148,163,184,0.12);
      }

      .rain-progress {
        height: 9px;
        margin-top: 20px;
        overflow: hidden;
        border-radius: 999px;
        background: rgba(255,255,255,0.08);
      }

      .rain-progress-fill {
        height: 100%;
        border-radius: inherit;
        background:
          linear-gradient(
            90deg,
            #0284c7,
            #38bdf8,
            #67e8f9
          );
        box-shadow: 0 0 18px rgba(56,189,248,0.45);
        transition: width 0.8s ease;
      }

      .rain-progress-scale {
        display: flex;
        justify-content: space-between;
        margin-top: 6px;
        color: #66869a;
        font-size: 10px;
      }

      .rain-center {
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 330px;
        padding: 6px 0;
      }

      .tank-wrap {
        position: relative;
        width: min(100%, 300px);
        height: 325px;
      }

      .tank-cap {
        position: absolute;
        z-index: 4;
        left: 50%;
        top: 0;
        width: 95px;
        height: 22px;
        transform: translateX(-50%);
        border-radius: 12px 12px 4px 4px;
        border: 1px solid rgba(148,191,218,0.32);
        background:
          linear-gradient(
            180deg,
            rgba(94,123,141,0.85),
            rgba(25,47,60,0.95)
          );
      }

      .tank-shell {
        position: absolute;
        z-index: 2;
        left: 6%;
        right: 6%;
        top: 17px;
        bottom: 16px;
        overflow: hidden;
        border: 2px solid rgba(159,199,222,0.42);
        border-radius: 38px 38px 30px 30px;
        background:
          linear-gradient(
            180deg,
            rgba(172,202,219,0.16),
            rgba(83,117,135,0.06)
          );
        box-shadow:
          inset 0 0 35px rgba(255,255,255,0.05),
          0 18px 42px rgba(0,0,0,0.25);
      }

      .tank-water {
        position: absolute;
        z-index: 1;
        left: 0;
        right: 0;
        bottom: 0;
        min-height: 0;
        background:
          linear-gradient(
            180deg,
            rgba(56,189,248,0.72),
            rgba(2,132,199,0.85) 38%,
            rgba(3,105,161,0.95)
          );
        box-shadow:
          inset 0 10px 25px rgba(103,232,249,0.30);
        transition: height 0.9s ease;
      }

      .tank-wave {
        position: absolute;
        left: -30%;
        width: 160%;
        height: 26px;
        border-radius: 45%;
        background: rgba(165,243,252,0.34);
      }

      .tank-wave-one {
        top: -11px;
        animation: waveOne 7s linear infinite;
      }

      .tank-wave-two {
        top: -7px;
        opacity: 0.45;
        animation: waveTwo 11s linear infinite reverse;
      }

      @keyframes waveOne {
        from { transform: translateX(-9%) rotate(0deg); }
        50%  { transform: translateX(7%) rotate(1deg); }
        to   { transform: translateX(-9%) rotate(0deg); }
      }

      @keyframes waveTwo {
        from { transform: translateX(8%); }
        50%  { transform: translateX(-8%); }
        to   { transform: translateX(8%); }
      }

      .tank-scale {
        position: absolute;
        z-index: 5;
        left: 11px;
        top: 12px;
        bottom: 12px;
        width: 36px;
        color: rgba(225,244,255,0.58);
        font-size: 9px;
      }

      .tank-scale span {
        position: absolute;
        left: 0;
      }

      .tank-scale span::after {
        content: "";
        position: absolute;
        left: 23px;
        top: 50%;
        width: 12px;
        border-top: 1px solid rgba(225,244,255,0.35);
      }

      .tank-probe {
        position: absolute;
        z-index: 5;
        right: 33px;
        top: 20px;
        bottom: 17px;
        width: 20px;
      }

      .tank-probe-wire {
        position: absolute;
        left: 9px;
        top: 0;
        width: 2px;
        height: 62%;
        background: rgba(203,213,225,0.75);
      }

      .tank-probe-body {
        position: absolute;
        left: 4px;
        bottom: 26%;
        width: 12px;
        height: 58px;
        border-radius: 7px;
        border: 1px solid rgba(255,255,255,0.50);
        background:
          linear-gradient(
            90deg,
            #64748b,
            #e2e8f0,
            #64748b
          );
      }

      .tank-badge {
        position: absolute;
        z-index: 6;
        left: 50%;
        top: 50%;
        display: flex;
        min-width: 86px;
        transform: translate(-50%, -50%);
        flex-direction: column;
        align-items: center;
        padding: 10px 13px;
        border: 1px solid rgba(125,211,252,0.35);
        border-radius: 12px;
        background: rgba(5,20,33,0.74);
        backdrop-filter: blur(6px);
        box-shadow: 0 10px 30px rgba(0,0,0,0.28);
      }

      .tank-badge strong {
        color: #ffffff;
        font-size: 24px;
      }

      .tank-badge span {
        margin-top: 2px;
        color: #9bdcfb;
        font-size: 12px;
      }

      .tank-base {
        position: absolute;
        z-index: 1;
        left: 11%;
        right: 11%;
        bottom: 6px;
        height: 18px;
        border-radius: 50%;
        background: rgba(56,189,248,0.10);
        filter: blur(5px);
      }

      .rain-right {
        display: flex;
        flex-direction: column;
        justify-content: center;
      }

      .diag-title {
        margin-bottom: 8px;
      }

      .diag-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 14px;
        padding: 12px 0;
        border-bottom: 1px solid rgba(148,191,218,0.11);
        font-size: 12px;
      }

      .diag-row:last-child {
        border-bottom: 0;
      }

      .diag-row span {
        color: #8ba9bb;
      }

      .diag-row strong {
        color: #eaf6ff;
        text-align: right;
      }

      .rain-foot {
        display: flex;
        flex-wrap: wrap;
        gap: 8px 16px;
        margin-top: 18px;
        padding-top: 14px;
        border-top: 1px solid rgba(148,191,218,0.12);
        color: #68889a;
        font-size: 10px;
        letter-spacing: 0.05em;
      }

      @media (max-width: 860px) {
        .rain-root {
          padding: 18px;
        }

        .rain-main {
          grid-template-columns: 1fr;
        }

        .rain-center {
          order: -1;
          min-height: 300px;
        }

        .tank-wrap {
          height: 290px;
        }
      }

      @media (max-width: 520px) {
        .rain-head {
          flex-direction: column;
        }

        .rain-online {
          align-self: flex-start;
        }

        .rain-center {
          min-height: 270px;
        }

        .tank-wrap {
          height: 265px;
        }

        .rain-value {
          font-size: 58px;
        }
      }


  # ==========================================================
  # HISTORIA POZIOMU Z OSTATNICH 24 GODZIN
  #
  # Wymaga:
  # - custom:apexcharts-card
  # ==========================================================

  - type: custom:apexcharts-card

    graph_span: 24h

    header:
      show: true
      title: "Poziom wody — ostatnie 24 godziny"
      show_states: true
      colorize_states: true

    yaxis:
      - min: 0
        max: 100
        decimals: 0

    apex_config:
      chart:
        height: 230

      grid:
        borderColor: "rgba(148, 191, 218, 0.14)"

      stroke:
        curve: smooth

      fill:
        type: gradient
        gradient:
          shadeIntensity: 0.25
          opacityFrom: 0.45
          opacityTo: 0.05

      tooltip:
        x:
          format: "dd.MM HH:mm"

    series:
      - entity: sensor.sterownik_nawadniania_poziom_wody_w_zbiorniku_procent
        name: "Poziom"
        type: area
        curve: smooth
        stroke_width: 3

        group_by:
          duration: 5min
          func: avg

Co ta karta pokazuje?

Środkowy rysunek zbiornika jest generowany bez osobnego pliku PNG – to HTML i CSS wewnątrz custom:button-card. Wysokość niebieskiego pola jest liczona bezpośrednio z sensora procentowego, więc przy 48% woda zajmuje około 48% wysokości grafiki. Obok widzę równocześnie wartość w centymetrach oraz surowe parametry toru pomiarowego. Dzięki temu dashboard nie ukrywa diagnostyki pod efektowną grafiką.

Status zmienia się zależnie od poziomu. Do 10% karta oznacza stan jako krytycznie niski, do 25% jako niski, poniżej 90% jako prawidłowy, a powyżej tego poziomu informuje, że zbiornik jest prawie pełny lub pełny. Te granice są na razie wartościami interfejsu – później zostaną powiązane z realną logiką pompy i nawadniania.

Druga karta to wykres z ostatnich 24 godzin. Dane grupuję w pięciominutowe przedziały, bo przy zbiorniku nie ma sensu rysować każdej sekundowej próbki. Dzięki temu wykres jest czytelny, a w przyszłości pozwoli łatwo zobaczyć deszcz, pobór wody podczas podlewania i tempo opróżniania zbiornika.

Ważne:
obecne 48% oznacza procent roboczej wysokości, ponieważ w ESPHome na tym etapie przyjąłem 100 cm = 100%. Nie należy jeszcze interpretować tej wartości jako 48% z 3000 litrów.

OLED to tylko etap przejściowy

Mały OLED 0,96 cala jest bardzo wygodny w czasie budowy. Obróciłem go programowo o 180°, ponieważ tak pasuje do montażu prototypu. Główną, dużą wartością jest procent napełnienia zbiornika. Na dole zostawiłem dwie informacje diagnostyczne: napięcie odczytane przez ADS1115 oraz wysokość słupa wody nad sondą. Dzięki temu bez otwierania Home Assistant od razu widzę zarówno wynik końcowy, jak i dwie wartości pozwalające szybko ocenić tor pomiarowy.

Nie jest to jednak finalny interfejs. Docelowy sterownik ma dostać kolorowy dotykowy ekran TFT. Na nim chcę później pokazać poziom zbiornika, pracę pompy, sekcje podlewania, tryb automatyczny i ręczny, statusy czujników, alarmy, a także możliwość lokalnego sterowania bez telefonu.

Prototyp ESP32 ADS1115 OLED i koncepcja docelowego sterownika z ekranem dotykowym
Obecny układ to pierwszy działający prototyp toru pomiarowego. Finalny sterownik będzie znacznie większym projektem.

Błędy po drodze – czyli co zrobiłbym od razu przy drugim egzemplarzu

Ten etap projektu był dobrym przykładem, dlaczego warto uruchamiać elektronikę warstwami. Gdybym od początku podłączył sondę, ADS1115, OLED, ESP32 i Home Assistant, pierwszy błąd wyglądałby jak „nic nie działa”. Tymczasem każdy fragment można sprawdzić oddzielnie.

Najpierw sprawdziłbym rezystancję bocznika. Potem napięcie bez ADS1115. Następnie sam skan I²C. Dopiero po tym wejście ADC. Jeśli ESPHome pokazuje prawie 0 V, a multimetr na punkcie pomiarowym pokazuje 0,49 V lub więcej, wiadomo, że nie ma sensu podejrzewać sondy – trzeba sprawdzić drogę punkt pomiarowy → wejście ADS oraz wspólną masę.

Podczas prób kanał A0 nie dawał poprawnego odczytu w konfiguracji, na której pracowałem. Zamiast tracić czas na rozbieranie modułu i szukanie przyczyny, przeniosłem sygnał TL-136 na A3. Po ustawieniu w ESPHome A3_GND otrzymałem stabilny pomiar około 1,32 V, odpowiadający rzeczywistej wysokości wody.

Najprostsza procedura diagnostyczna:
1) zmierz rezystor, 2) zmierz napięcie na rezystorze, 3) sprawdź 3,3 V na ADS1115, 4) sprawdź skan I²C, 5) sprawdź napięcie na wybranym wejściu ADS, 6) dopiero wtedy analizuj wzory i YAML.

Czy potrzebny jest rezystor 0,1%?

Do tego zastosowania wystarczy pojedynczy rezystor 120 Ω o rozsądnej tolerancji. Przy 20 mA wydzieli się na nim około 0,048 W, więc zwykły rezystor 0,25 W ma duży zapas mocy. W finalnym układzie ważniejsza od bardzo wysokiej mocy jest stabilność wartości i poprawne odwzorowanie tej samej wartości 120 Ω w kodzie ESPHome.

Czy z wysokości można od razu policzyć litry?

Jeszcze nie. I celowo tego na tym etapie nie robię.

Zbiornik ma nominalnie 3000 l, ale jego kształt nie jest prostym prostopadłościanem. Jest profilowany. To oznacza, że 50% wysokości nie musi odpowiadać 1500 l. Żeby wyświetlać litry uczciwie, potrzebuję charakterystyki wysokość → objętość dla konkretnego zbiornika albo własnej tabeli kalibracyjnej.

Dlatego dzisiaj Home Assistant pokazuje coś, co potrafię zweryfikować: centymetry i roboczy procent wysokości. Litry pojawią się dopiero wtedy, gdy będę mógł uzasadnić ich wyliczenie.

Co dalej z tym sterownikiem?

Pomiar poziomu to dopiero początek. Docelowo ten sam sterownik ma obsługiwać cały system podlewania, a nie tylko raportować stan zbiornika. W kolejnych etapach dojdą zawory sekcji, sterowanie pompą, logika ochrony przed pracą przy zbyt niskim poziomie, tryby ręczne i automatyczne, a także dodatkowe pomiary – np. ciśnienie lub przepływ.

ADS1115 ma jeszcze wolne kanały, więc można go wykorzystać również do kolejnych analogowych sygnałów, np. drugiego przetwornika 4–20 mA. Czujnik przepływu impulsowego podłączyłbym jednak do GPIO, a nie do ADS1115. Jeśli kiedyś zabraknie wejść analogowych, kolejne ADS1115 można umieścić na tej samej magistrali I²C z innymi adresami – ale sama magistrala I²C pozostanie lokalnie w obudowie sterownika. Nie będę prowadził jej przez kilkanaście metrów ogrodu.

Kolejnym ważnym krokiem będzie również uporządkowanie wersji prototypowej w docelowej obudowie i dodanie ochrony wejść dla przewodów wychodzących na zewnątrz. Długi kabel w ogrodzie to nie to samo co przewód Dupont długości 10 cm na stole.

Podsumowanie

Na początku tego etapu miałem sondę TL-136, kilka rezystorów, ADS1115, ESP32 i pomysł, żeby wiedzieć, ile naprawdę wody jest w zbiorniku. Po kilku testach mam już coś znacznie ważniejszego: zweryfikowany tor pomiarowy.

W finalnym torze pomiarowym używam pojedynczego rezystora 120 Ω. Daje on około 0,480 V przy 4 mA i około 2,400 V przy 20 mA, czyli bardzo wygodny zakres dla ADS1115. Po uwzględnieniu położenia sondy 5,3 cm nad dnem odczyt rzeczywistego poziomu wody może być bezpośrednio prezentowany w ESPHome i Home Assistant. Przy ręcznym pomiarze poziomu w zbiorniku otrzymałem 48 cm i tę wartość traktuję jako punkt odniesienia dla kalibracji całego układu.

I właśnie tak chcę budować kolejne części tego projektu: nie od „ładnego dashboardu”, ale od fizycznego pomiaru, który da się sprawdzić. Dopiero potem dokładamy automatykę i interfejs.

IMG_20260820_095239

TL-136 Home Assistant, TL-136 ESPHome, czujnik poziomu wody ESP32, sonda hydrostatyczna 4–20 mA ESPHome, ADS1115 ESPHome, pomiar poziomu deszczówki Home Assistant, czujnik poziomu wody w zbiorniku na deszczówkę

W następnej części

Zaczniemy zamieniać pomiar poziomu w prawdziwy sterownik nawadniania.

Do projektu będą dochodzić kolejne elementy, a obecny OLED pozostanie jedynie etapem prototypowym przed docelowym ekranem dotykowym TFT.

Źródła techniczne wykorzystane do weryfikacji konfiguracji

Dokumentacja ESPHome: komponent ADS1115 (adresy I²C, A3_GND, gain, sample rate i ograniczenia wejść analogowych) oraz komponent SSD1306. Do części o dashboardzie wykorzystałem dokumentację projektu custom:button-card (pola własne, HTML/CSS i szablony JavaScript) oraz ApexCharts Card (serie historyczne i grupowanie danych). Szczegóły konkretnego TL-136 w tym artykule pochodzą z oznaczeń na rzeczywistym egzemplarzu użytym w projekcie oraz z wykonanych pomiarów.

 

Społeczność MarkLabs

Dyskutuj na forum

Pytania, doświadczenia i rozwiązania do tego artykułu publikujemy teraz na forum MarkLabs.

Otwórz dyskusję na forum

Wymagane jest bezpłatne konto MarkLabs. Dotychczasowe komentarze bloga nie są przenoszone.

buycoffee.to