AquaForte iSaver-X smart machen: ESP32, ESPHome, RS485 & leiser Lüfter

Für unseren Aufstellpool verwende ich eine Bali-400 Sandfilteranlage mit Aqua Plus 11 Pumpe. Die ist für einen 366 × 76 cm Pool natürlich deutlich überdimensioniert, aber der Plan ist, den Pool mit zunehmendem Alter der Kinder irgendwann gegen etwas mit mehr Beckenvolumen auszutauschen. Um die Pumpe bis dahin trotzdem sparsam, leise und zur Poolgröße passend zu betreiben, hängt sie an einem AquaForte iSaver-X. Dazu kommt eine Poolex MagFi 5 als Inverter-Wärmepumpe. Geschaltet und gemessen wird bei mir über Shelly Outdoor Steckdosen, damit Home Assistant den Verbrauch kennt und die Anlage nicht einfach nur stur nach Uhrzeit läuft.

Der iSaver-X kann von Haus aus schon einiges: Drehzahlprofile, Zeitschaltuhr und natürlich die eigentliche Frequenzregelung der Pumpe. Was ihm fehlt, ist die smarte Anbindung. Keine Home-Assistant-Integration, kein MQTT, keine lokale API. Genau das war für mich schade, weil der iSaver-X eigentlich das perfekte Bindeglied zwischen Poolpumpe, Wärmepumpe, PV-Überschuss und Nachtbetrieb ist.

Kurzfassung: Ich habe zuerst den lauten Lüfter gegen zwei Noctua NF-A4x20 getauscht und anschließend einen ESP32 D1 Mini mit einem TTL-zu-RS485-Modul intern verbaut. Der ESP32 liest und setzt die Drehzahl über die interne RS485-Schnittstelle des iSaver-X. Home Assistant bekommt dadurch Entitäten für Status, Drehzahl, Fehler und Buttons für die wichtigsten Betriebsmodi.

Ausgangslage und Ziel

Der eigentliche Auslöser war nicht „ich will die Pumpe vom Smartphone einschalten“. Das wäre nett, aber dafür hätte sich der Aufwand kaum gelohnt. Mir ging es darum, dass Home Assistant selbst entscheidet, welche Drehzahl gerade sinnvoll ist. Nachts leise, tagsüber normal, bei Heizbedarf mit ausreichendem Durchfluss und bei PV-Überschuss etwas höher.

KomponenteBei mir im EinsatzRolle im Setup
PoolIntex Frame 366 × 76 cmaktuelles Becken, bewusst mit Reserven geplant
FilteranlageOKU/Bali 400Sandfilteranlage mit Reserven für später
PumpeAqua Plus 11wird per iSaver-X heruntergeregelt
FrequenzumrichterAquaForte iSaver-XDrehzahlregelung der Filterpumpe
WärmepumpePoolex MagFi 5Temperatur halten, möglichst leise und effizient
Messung/SchaltungShelly OutdoorVerbrauch messen und Geräte schalten
Smart HomeHome AssistantAutomationen, Modi, Dashboards und Benachrichtigungen

Die wichtigsten Modi liegen bei mir grob zwischen 1300 und 2900 rpm. 1300 rpm reicht für normale, leise Filterung. Für die Wärmepumpe nutze ich mehr Durchfluss. Wenn viel PV-Überschuss vorhanden ist oder nach einem langen Badetag mit Sonnencreme mehr Umwälzung sinnvoll ist, darf die Pumpe entsprechend höher laufen.

ModusDrehzahlGedanke dahinter
Eco1300 rpmnormale Filterung, leise und sparsam
Nacht1500 rpmweiterhin Umwälzung und brauchbare Temperaturmessung
Wärmepumpe1950 rpmzuverlässiger Durchfluss für die Poolex MagFi 5
PV2400 rpmmehr Durchfluss, wenn ohnehin Überschuss vorhanden ist
Max2900 rpmRückspülen, Wartung oder manuelles Boost-Szenario

Sicherheit vorweg

Der Lüfterumbau ist vergleichsweise harmlos, weil der Lüfterhalter außen am Kühlkörper sitzt und die Elektronik dafür nicht geöffnet werden muss. Beim ESP32-Umbau sieht das anders aus. Der iSaver-X ist ein Frequenzumrichter und im Gerät liegen gefährliche Spannungen an. Gerät spannungsfrei machen, warten, messen und im Zweifel jemanden fragen, der solche Arbeiten fachlich bewerten kann. Garantie oder Gewährleistung kann durch den Umbau natürlich ebenfalls ein Thema sein.

Teil 1: Den iSaver-X leiser machen

Bevor ich den iSaver-X smart gemacht habe, musste erstmal der Lüfter dran glauben. Der originale 40-mm-Lüfter war bei mir das lauteste Bauteil der ganzen Pooltechnik. Nicht die Aqua Plus 11, nicht die Wärmepumpe, sondern dieser kleine hochfrequente Lüfter auf der Rückseite.

Die Halterung ist bei meinem Gerät für zwei 40-mm-Lüfter vorbereitet, ab Werk war aber nur einer bestückt. Ich habe deshalb direkt zwei Noctua NF-A4x20 genommen. Die Stecker habe ich abgeschnitten und die beiden Lüfter parallel mit der vorhandenen 12-V-Versorgung verlötet. Widerstände oder Low-Noise-Adapter habe ich nicht verwendet, weil der iSaver-X die Lüfter temperaturabhängig schaltet.

BenötigtHinweis
1–2× Noctua NF-A4x20bei mir zwei Stück, 12 V vorher am eigenen Gerät prüfen
Schrumpfschlauchfür die verlöteten Adern
Lötkolben und Lötzinnein Set mit Multimeter und Schraubendrehern ist praktisch
Seitenschneider / Cutterzum Kürzen und Abisolieren
Multimeterzum Prüfen der Lüfterspannung
Feuerzeug oder Heißluftfür den Schrumpfschlauch
  1. Modell prüfen und nachsehen, ob die Aufnahme für zwei 40-mm-Lüfter vorbereitet ist.
  2. Lüfterkabel vorsichtig freilegen und prüfen, ob dort wirklich 12 V anliegen. Dafür muss das Gerät kurz bestromt werden.
  3. Gerät wieder stromlos machen und warten. Danach den Originallüfter möglichst nah am Lüfter trennen, damit vom originalen Kabel möglichst viel übrig bleibt.
  4. Noctua-Stecker abschneiden, Adern freilegen, verzinnen, Schrumpfschlauch vorher nicht vergessen und beide Lüfter parallel anlöten.
  5. Lötstellen isolieren, Lüfter in die Halterung setzen und die Halterung wieder am Kühlkörper verschrauben.

Einen direkten Test bekommt man nicht immer sofort, weil die Lüfter temperaturgesteuert laufen. Im normalen Gartenbetrieb sind die Noctuas bei mir praktisch nicht mehr zu hören. Die Wärme der Kühlrippen wird trotzdem ordentlich abtransportiert.

Teil 2: Den iSaver-X per ESP32 und RS485 smart machen

Der zweite Teil ist etwas aufwändiger. Grundlage ist ein öffentliches GitHub-Projekt, das die RS485-Kommunikation für iSaver/iSaver-X dokumentiert hat. Das ursprüngliche Beispiel nutzt einen ESP8266. Ich habe es für meinen ESP32 D1 Mini angepasst, andere UART-Pins verwendet, das Polling auf 3 Sekunden gesetzt und die Konfiguration für mein Home-Assistant-Setup erweitert.

Wichtig: Das ist kein normales Modbus RTU, auch wenn RS485 verwendet wird. Der iSaver-X spricht ein proprietäres Protokoll. Standard-Modbus-Integrationen helfen hier also nicht direkt weiter.

BauteilVerwendungHinweis
ESP32 D1 MiniESPHome-GerätUSB-C/Micro-USB je nach Boardvariante
TTL-zu-RS485-ModulUART ↔ RS485bei mir ein Modul mit Auto-Direction
JST-PH 2,0 mm, 9 PinRS485-Anschluss am iSaver-XPinbelegung zählen, nicht blind Kabelfarben vertrauen
JST-XH 2,54 mm, 4 Pin5-V-Versorgungfreier Anschluss auf der Platine, vorher messen
Dupont-Kabel oder LitzeVerdrahtungich habe teilweise mit Dupont gearbeitet
LochrasterplatineTräger im Gehäuseoptional, macht den Einbau sauberer
HeißklebepistoleBefestigungESP und RS485-Modul bei mir unten an der Innenseite der Front

ESP32 am Schreibtisch vorbereiten

Ich würde den ESP32 immer zuerst am Schreibtisch flashen und OTA testen. Wenn das Board einmal im iSaver-X sitzt, möchte man es nicht wegen eines simplen WLAN- oder OTA-Problems wieder ausbauen.

$ ls /dev/cu.*
/dev/cu.Bluetooth-Incoming-Port /dev/cu.usbserial-0001

Unter macOS wurde mein Board als CP2102 USB to UART Bridge erkannt. Danach habe ich den Chip mit esptool geprüft. Die MAC-Adresse ist hier anonymisiert:

$ esptool --chip esp32 --port /dev/cu.usbserial-0001 chip-id
esptool v5.3.0
Connected to ESP32 on /dev/cu.usbserial-0001:
Chip type:          ESP32-D0WD-V3 (revision v3.1)
Features:           Wi-Fi, BT, Dual Core + LP Core, 240MHz
MAC:                d4:e9:f4:xx:xx:xx

Danach kann die ESPHome-YAML kompiliert und zunächst per USB übertragen werden. Sobald WLAN und API funktionieren, lässt sich ein OTA-Test per IP machen. Das ist besonders praktisch, wenn mDNS im Netzwerk oder bei UniFi mal nicht sauber auflöst.

pip install esphome
esphome run esp32.yaml
esphome run esp32.yaml --device 192.168.x.xxx
esphome logs esp32.yaml --device 192.168.x.xxx
INFO Connecting to 192.168.x.xxx port 3232...
INFO Connected to 192.168.x.xxx
INFO Uploading firmware.bin (815808 bytes)
Uploading: [============================================================] 100% Done...
INFO OTA successful
INFO Successfully uploaded program.

Pinbelegung am iSaver-X

Der 9-polige JST-PH-Stecker ist für die RS485-Kommunikation zuständig, liefert aber keine 5-V-Versorgung für den ESP32. Für die reine RS485-Anbindung werden nur A, B und GND benötigt.

PinFunktionFür diesen Umbau
1GNDoptional / Masse
2V/I Analogeingangnicht als ESP-Versorgung nutzen
3RS485 Bja
4RS485 Aja
5GNDja, gemeinsame Masse
6IN1nicht genutzt
7IN2nicht genutzt
8IN3nicht genutzt
9IN4nicht genutzt

Bei meinem vorbereiteten Kabel habe ich die Belegung so umgesetzt: Pin 3 auf B-, Pin 4 auf A+ und Pin 5 auf GND. Die Farben können bei anderen Kabeln abweichen. Also lieber einmal mehr nach Pin-Nummer zählen als später lange nach einem „No Response“ suchen.

iSaver-X JST-PH-9  ->  RS485-Modul
Pin 3 / B           ->  B-
Pin 4 / A           ->  A+
Pin 5 / GND         ->  GND

ESP32 D1 Mini       ->  RS485-Modul
GPIO17              ->  TXD
GPIO16              ->  RXD
GND                 ->  GND
5V/VCC              ->  VCC

Stromversorgung aus dem Gerät

Der 9-polige Stecker ist nicht die Lösung für die Versorgung. Auf der Hauptplatine gibt es bei meinem Gerät aber einen freien weißen JST-XH-4-Pin-Anschluss, an dem 5 V anliegen. Darüber versorge ich den ESP32 und das RS485-Modul. Vorher natürlich messen, denn Platinenrevisionen können sich ändern.

Ich habe mir dafür ein kleines Y-Kabel gebaut: 5 V und GND vom JST-XH-Stecker gehen auf den ESP32 und auf das RS485-Modul. Wichtig ist am Ende nur, dass GND sauber gemeinsam geführt wird und nichts lose im Gehäuse herumliegt.

Einbau im iSaver-X

Nach dem Trennen vom Strom habe ich das Gerät geöffnet und mir zuerst Fotos vom Originalzustand gemacht. Das spart Nerven, wenn man zwischendurch Kabel löst, um die Front weiter öffnen zu können. Bei mir habe ich die Erdung zur Front, die beiden gesteckten Stromkabel und einen blauen JST-XH-Stecker zur Front gelöst, um besser arbeiten zu können.

Den ESP32 und das RS485-Modul habe ich auf einer kleinen Lochrasterplatine vorbereitet und unten an der Innenseite der Front mit Heißkleber fixiert. Das ist nicht die edelste Lösung, aber für diesen kleinen, leichten Aufbau ausreichend. Wichtig ist, dass keine Kontakte das Gehäuse berühren und sich durch Vibrationen nichts lösen kann.

Der ESP sitzt damit im Metallgehäuse. Bei mir reicht das WLAN trotzdem, weil der Access Point etwa 10 m entfernt ist und der ESP in Richtung Kunststofffront sitzt. Wenn der Empfang schlecht ist, würde ich entweder einen ESP mit externer Antenne verwenden oder das Modul außerhalb des iSaver-X in ein separates Gehäuse setzen.

Der erste RS485-Test

Beim ersten Test war die TX-LED am RS485-Modul sofort aktiv. Das bedeutet erstmal nur: Der ESP32 sendet. Wenn RX nicht blinkt, antwortet der iSaver-X nicht. Bei mir waren anfangs A und B vertauscht. Nach dem Tausch blinkten TX und RX abwechselnd und im ESPHome-Log kam endlich „Online“.

Gekürzte und anonymisierte Beispielausgabe aus den ESPHome-Logs:

[09:23:49.674][S][text_sensor]: 'iSaver Modbus Status' >> 'No Response'
[13:14:31.534][S][text_sensor]: 'iSaver Modbus Status' >> 'Wrong Data Header'
[13:14:32.695][S][text_sensor]: 'iSaver Modbus Status' >> 'No Response'
[13:12:32.609][S][text_sensor]: 'iSaver Modbus Status' >> 'Online'
[13:12:32.610][S][text_sensor]: 'iSaver Error' >> 'No Error'
[13:12:46.626][S][sensor]: 'iSaver Current RPM' >> 1650 RPM
[13:13:40.628][S][sensor]: 'iSaver Current RPM' >> 1300 RPM
[13:14:33.614][S][sensor]: 'iSaver Current RPM' >> 2900 RPM

Mein Merksatz danach: TX alleine heißt nur, dass der ESP sendet. TX und RX im Wechsel heißt, dass auf dem Bus wenigstens etwas zurückkommt. Wenn der Status trotzdem nicht passt, sind A/B, GND und Timing die ersten Kandidaten.

Finale ESPHome-YAML

Die produktive YAML ist unten vollständig enthalten und zusätzlich als Datei esp32.yaml im Downloadpaket. WLAN, API-Key, OTA-Passwort und Fallback-Hotspot sind über !secret anonymisiert. Vor dem Flashen müssen diese Werte in deiner secrets.yaml vorhanden sein.

Wichtige Änderungen gegenüber dem ursprünglichen ESP8266-Beispiel: ESP32 statt ESP8266, GPIO17/GPIO16 für UART, 3-Sekunden-Polling, weniger Log-Traffic, Diagnose-Sensoren und eine einfache Soft-Sperre, damit Polling und Schreibbefehl nicht gleichzeitig auf UART/RS485 zugreifen.

uart:
  id: modbus_uart
  tx_pin: GPIO17
  rx_pin: GPIO16
  baud_rate: 1200
  data_bits: 8
  stop_bits: 1
  parity: NONE
  rx_buffer_size: 256
interval:
  - interval: 3s
    then:
      - lambda: |-
          if (id(rs485_busy)) {
            return;
          }

          id(rs485_busy) = true;
          uint8_t packet[8] = {0xAA, 0xC3, 0x07, 0xD1, 0x00, 0x00, 0x0D, 0x4D};

Use-Cases in Home Assistant

Der Umbau ist technisch fertig, sobald Home Assistant die Drehzahl lesen und setzen kann. Sinnvoll wird es aber erst durch die Automationen. Nachts geht der Pool bei mir in den Eco-Modus und wechselt nur dann in einen Nacht-Wärmepumpenmodus, wenn die Wassertemperatur unter einen bestimmten Wert fällt und der Strompreis nicht zu hoch ist.

Die Wärmepumpe begrenze ich indirekt über die Solltemperatur. Wenn die Leistungsaufnahme zu hoch wird, passt Home Assistant den Offset an. So soll die Poolex MagFi 5 nachts nicht unnötig hochdrehen. In meinem Setup kann ich sie damit z.B. in einem Bereich halten, in dem sie deutlich leiser läuft.

Tagsüber läuft die Pumpe in einem normalen Tagesmodus. Zusätzlich habe ich Buttons beziehungsweise Modi für Poolpflege und Boost. Das nutze ich nach Stoßchlorung, Flockung oder wenn die Kinder den ganzen Tag mit Sonnencreme im Pool waren. Nach einer definierten Dauer fällt die Anlage wieder zurück. Genau das ist der eigentliche Mehrwert: Ich muss nicht mehr ständig daran denken.

Troubleshooting

SymptomWahrscheinliche UrsacheWas ich prüfen würde
TX blinkt, RX nichtESP sendet, iSaver-X antwortet nichtA/B tauschen, GND prüfen, iSaver-X eingeschaltet?
No Responsekeine gültige Antwort auf dem BusRS485-Leitungen, gemeinsame Masse und Versorgung prüfen
Wrong Data HeaderAntwort unvollständig oder TimingproblemPolling reduzieren, Schreib- und Lesebefehle entkoppeln
OTA klappt nichtmDNS/UniFi/VLAN-Auflösungesphome run esp32.yaml --device 192.168.x.xxx nutzen
UniFi zeigt alten NamenCache oder alter Device-TrackerClient-Alias setzen oder alte Einträge bereinigen
WLAN schwachESP sitzt im Gehäuse ungünstigESP näher an die Kunststofffront, sonst externe Antenne

Gerade A/B ist ein Klassiker. Wenn man sich eigentlich sicher ist, lohnt sich das Tauschen trotzdem. Bei mir war das genau der Punkt, der aus „der ESP sendet, aber nichts passiert“ ein funktionierendes Setup gemacht hat.

Kosten und Aufwand

PositionGrobe Kosten
ESP32 D1 Minica. 5–8 €
TTL-zu-RS485-Modulca. 2–5 €
JST-Stecker, Litze, Schrumpfschlaucheinige Euro
Lochrasterplatine / Kleinteileeinige Euro
Noctua NF-A4x20je nach Anzahl ca. 15 € pro Stück

Der Smart-Umbau selbst ist preislich überschaubar. Der Lüfterumbau kostet durch die Noctuas mehr, bringt aber sofort einen spürbaren Gewinn, weil der hochfrequente Originalton weg ist.

Fazit

Ich würde den Umbau wieder machen. Nicht, weil ich jetzt per Smartphone die Drehzahl der Poolpumpe ändern kann. Das mache ich praktisch nie. Der eigentliche Vorteil ist, dass Home Assistant die Entscheidung übernimmt und der iSaver-X endlich in die restliche Logik passt.

Der iSaver-X ist dadurch für mich das Gerät geworden, das ich mir ursprünglich gewünscht hätte: ein leiser Frequenzumrichter, der sich lokal steuern lässt und automatisch auf Nachtbetrieb, Wärmepumpe, PV-Überschuss und Poolpflege reagiert.

Die meiste Zeit ging nicht für das Löten drauf, sondern für Kleinigkeiten: A/B vertauscht, OTA-Test, „No Response“, „Wrong Data Header“ und die Frage, wo man sauber 5 V abgreift. Genau deshalb habe ich diese Punkte hier aufgeschrieben. Vielleicht spart es dem nächsten ein paar Stunden Fehlersuche.

FAQ

Kann ich den Lüfterumbau unabhängig vom ESP32-Umbau machen?

Ja. Der Lüfterhalter sitzt außen am Kühlkörper. Für den reinen Lüftertausch muss das eigentliche Elektronikgehäuse nicht geöffnet werden.

Brauche ich zwingend einen ESP32?

Nein. Das ursprüngliche Projekt nutzt einen ESP8266. Ich habe mich für einen ESP32 D1 Mini entschieden, weil ich diese Boards ohnehin gerne für solche IoT-Aufgaben verwende und genug Reserven vorhanden sind.

Kommt die Versorgung aus dem 9-poligen JST-PH-Stecker?

Nein. Der 9-polige Stecker liefert bei meinem Gerät RS485 und GND, aber keine 5 V für den ESP32. Die 5 V habe ich über einen separaten freien JST-XH-Anschluss auf der Platine abgegriffen.

Was mache ich, wenn nur TX blinkt?

Dann sendet der ESP32, bekommt aber keine sinnvolle Antwort. Als erstes würde ich A/B tauschen, GND prüfen und kontrollieren, ob der iSaver-X überhaupt eingeschaltet beziehungsweise betriebsbereit ist.

Ist das normales Modbus RTU?

Nein. Es läuft elektrisch über RS485, aber das Protokoll ist proprietär. Deshalb wird in der YAML direkt per UART mit Byte-Frames gearbeitet.

Quellen und Dank

Die Grundlage für die RS485-Kommunikation stammt aus dem GitHub-Projekt backuprestore/isaver-isaverx-RS485-modbus. Ich habe die Idee auf meinen ESP32-/ESPHome-Aufbau übertragen, angepasst und für mein Home-Assistant-Setup erweitert. Danke an den Contributor für die Vorarbeit.


Anhang: vollständige esp32.yaml

Für WordPress würde ich diese Datei zusätzlich als Download anbieten. Hier ist sie zur Dokumentation vollständig enthalten:

substitutions:
  device_name: isaver
  friendly_name: "iSaver X"

  # Pinbelegung ESP32 D1 Mini / ESP32 DevKit:
  # GPIO17 -> TXD am RS485-Modul
  # GPIO16 -> RXD am RS485-Modul
  #
  # RS485-Modul -> iSaver X JST-PH-9:
  # A  -> Pin 4 / A
  # B  -> Pin 3 / B
  # GND -> Pin 5 / GND
  #
  # Hinweis:
  # Der 9-polige JST-Stecker liefert keine Versorgungsspannung.
  # ESP32 und RS485-Modul müssen separat mit 5V/GND versorgt werden.

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}

  on_boot:
    priority: -100
    then:
      - lambda: |-
          // Nach einem Neustart nur den zuletzt gelesenen Zustand anzeigen.
          // Es wird bewusst kein RPM-Befehl an den iSaver gesendet.
          if (!isnan(id(pump_rpm_state).state) && id(pump_rpm_state).state > 0) {
            id(pump_rpm).publish_state(id(pump_rpm_state).state);
          }

esp32:
  board: esp32dev
  framework:
    type: esp-idf
    advanced:
      minimum_chip_revision: "3.1"
      sram1_as_iram: true

logger:
  level: WARN

api:
  encryption:
    key: !secret isaver_api_key

ota:
  - platform: esphome
    password: !secret isaver_ota_password

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

  power_save_mode: none
  fast_connect: true

  # Optional: feste Zieladresse für OTA, falls mDNS/UniFi zickt.
  # use_address: 192.168.1.175

  ap:
    ssid: "iSaver Fallback"
    password: !secret isaver_fallback_password

captive_portal:

globals:
  # Einfache Soft-Sperre, damit Polling und Schreibbefehl nicht gleichzeitig
  # auf UART/RS485 zugreifen. Das reduziert sporadische "Wrong Data Header"
  # oder "No Response" direkt nach einem RPM-Wechsel.
  - id: rs485_busy
    type: bool
    restore_value: no
    initial_value: "false"

  - id: rs485_last_rx_ms
    type: uint32_t
    restore_value: no
    initial_value: "0"

  - id: rs485_error_counter
    type: uint32_t
    restore_value: no
    initial_value: "0"

uart:
  id: modbus_uart
  tx_pin: GPIO17
  rx_pin: GPIO16
  baud_rate: 1200
  data_bits: 8
  stop_bits: 1
  parity: NONE
  rx_buffer_size: 256

text_sensor:
  - platform: template
    name: "iSaver Modbus Status"
    id: modbus_status
    icon: "mdi:lan-connect"

  - platform: template
    name: "iSaver Error"
    id: pump_error_text
    icon: "mdi:alert-circle-outline"

  - platform: version
    name: "iSaver ESPHome Version"
    hide_timestamp: true

sensor:
  - platform: template
    name: "iSaver Current RPM"
    id: pump_rpm_state
    unit_of_measurement: "RPM"
    icon: "mdi:fan"
    accuracy_decimals: 0
    update_interval: never

  - platform: template
    name: "iSaver RS485 Error Counter"
    id: rs485_error_counter_sensor
    icon: "mdi:counter"
    accuracy_decimals: 0
    update_interval: 60s
    lambda: |-
      return (float) id(rs485_error_counter);

  - platform: template
    name: "iSaver Last Response"
    id: rs485_last_response_age
    unit_of_measurement: "s"
    icon: "mdi:timer-outline"
    accuracy_decimals: 0
    update_interval: 10s
    lambda: |-
      if (id(rs485_last_rx_ms) == 0) {
        return NAN;
      }
      return (float) ((millis() - id(rs485_last_rx_ms)) / 1000);

  - platform: wifi_signal
    name: "iSaver WiFi Signal"
    id: wifi_signal_db
    update_interval: 60s

  - platform: uptime
    name: "iSaver Uptime"
    id: isaver_uptime
    update_interval: 60s

number:
  - platform: template
    name: "iSaver RPM"
    id: pump_rpm
    icon: "mdi:speedometer"
    min_value: 0
    max_value: 2900
    step: 50
    optimistic: false
    set_action:
      then:
        - lambda: |-
            if (id(rs485_busy)) {
              ESP_LOGW("isaver", "RS485 busy, skipping RPM command");
              return;
            }

            id(rs485_busy) = true;

            uint16_t rpm = (uint16_t)x;

            // Beim iSaver bedeutet ein Wert unterhalb der Minimaldrehzahl "OFF".
            if (rpm < 1200) {
              rpm = 1;
            }

            // Wenn die gemessene Drehzahl schon dem Ziel entspricht, nicht unnötig schreiben.
            if (!isnan(id(pump_rpm_state).state) &&
                (uint16_t) id(pump_rpm_state).state == rpm) {
              id(rs485_busy) = false;
              return;
            }

            uint8_t rpm0 = (rpm >> 8) & 0xFF;
            uint8_t rpm1 = rpm & 0xFF;

            uint8_t data[6] = {0xAA, 0xD0, 0x0B, 0xB9, rpm0, rpm1};

            uint16_t crc = 0xFFFF;
            for (int i = 0; i < 6; i++) {
              crc ^= data[i];
              for (int j = 0; j < 8; j++) {
                if (crc & 0x0001) {
                  crc >>= 1;
                  crc ^= 0xA001;
                } else {
                  crc >>= 1;
                }
              }
            }

            uint8_t packet[8] = {
              0xAA,
              0xD0,
              0x0B,
              0xB9,
              rpm0,
              rpm1,
              (uint8_t)(crc & 0xFF),
              (uint8_t)((crc >> 8) & 0xFF)
            };

            auto uart = id(modbus_uart);

            while (uart->available() > 0) {
              uint8_t t;
              uart->read_array(&t, 1);
            }

            uart->write_array(packet, 8);
            uart->flush();

            // Kurzer Abstand, damit das nächste Polling nicht direkt in die Antwort läuft.
            delay(80);

            id(rs485_busy) = false;

switch:
  - platform: template
    name: "iSaver Power"
    id: pump_power
    icon: "mdi:power"
    optimistic: false
    turn_on_action:
      - number.set:
          id: pump_rpm
          value: 1800
    turn_off_action:
      - number.set:
          id: pump_rpm
          value: 0

button:
  - platform: restart
    name: "iSaver ESP Restart"

  - platform: template
    name: "iSaver ECO 1300"
    icon: "mdi:leaf"
    on_press:
      - number.set:
          id: pump_rpm
          value: 1300

  - platform: template
    name: "iSaver Night 1500"
    icon: "mdi:weather-night"
    on_press:
      - number.set:
          id: pump_rpm
          value: 1500

  - platform: template
    name: "iSaver WP 1950"
    icon: "mdi:heat-pump"
    on_press:
      - number.set:
          id: pump_rpm
          value: 1950

  - platform: template
    name: "iSaver PV 2400"
    icon: "mdi:solar-power-variant"
    on_press:
      - number.set:
          id: pump_rpm
          value: 2400

  - platform: template
    name: "iSaver Max 2900"
    icon: "mdi:fan-speed-3"
    on_press:
      - number.set:
          id: pump_rpm
          value: 2900

interval:
  - interval: 3s
    then:
      - lambda: |-
          if (id(rs485_busy)) {
            return;
          }

          id(rs485_busy) = true;

          // Request Frame aus dem ursprünglichen iSaver/iSaverX-RS485-Projekt.
          uint8_t packet[8] = {0xAA, 0xC3, 0x07, 0xD1, 0x00, 0x00, 0x0D, 0x4D};

          auto uart = id(modbus_uart);

          while (uart->available() > 0) {
            uint8_t t;
            uart->read_array(&t, 1);
          }

          uart->write_array(packet, 8);
          uart->flush();

          uint32_t start_time = millis();

          while (uart->available() < 7 && (millis() - start_time) < 250) {
            delay(10);
          }

          if (uart->available() >= 7) {
            uint8_t response[7];
            uart->read_array(response, 7);

            if ((response[0] == 0xAA) && (response[1] == 0xC3)) {
              id(rs485_last_rx_ms) = millis();

              if (id(modbus_status).state != "Online") {
                id(modbus_status).publish_state("Online");
              }

              uint16_t error = ((uint16_t)response[2] << 8) | (uint16_t)response[3];

              if (error == 0) {
                if (id(pump_error_text).state != "No Error") {
                  id(pump_error_text).publish_state("No Error");
                }
              } else {
                char error_buffer[24];
                snprintf(error_buffer, sizeof(error_buffer), "Error 0x%04X", error);
                if (id(pump_error_text).state != error_buffer) {
                  id(pump_error_text).publish_state(error_buffer);
                }
              }

              bool power = response[4] == 1;

              if (power != id(pump_power).state) {
                id(pump_power).publish_state(power);
              }

              float rpm = (float)(((uint16_t)response[5] << 8) | (uint16_t)response[6]);

              if (isnan(id(pump_rpm_state).state) || rpm != id(pump_rpm_state).state) {
                id(pump_rpm_state).publish_state(rpm);
                id(pump_rpm).publish_state(rpm);
              }

            } else {
              id(rs485_error_counter)++;

              if (id(modbus_status).state != "Wrong Data Header") {
                id(modbus_status).publish_state("Wrong Data Header");
              }
            }

          } else {
            id(rs485_error_counter)++;

            if (id(modbus_status).state != "No Response") {
              id(modbus_status).publish_state("No Response");
            }
          }

          id(rs485_busy) = false;

Schreibe einen Kommentar