Inhaltsverzeichnis

WWW: ESP32 - IR-Schnittstelle auslesen

Messdatenerfassung

Quelle intern

ESP32

Warum B und nicht A?

Anschlüsse

A B
1 15 PWM 1 14
2 34 analog 2 33 TX (zum Adapter-Tx)
3 22 SCL 3 22
4 3.3 V + 4 3.3 V +
5 GND - 5 GND -
6 21 SDA 6 21
7 26 interupt 7 27 RX (vom Adapter-Rx)
8 32 PWM 8 25

Lesekopf

Zähler

BILD vom ZÄHLER:

Stromzähler auslesen (E320, SML)

Verdrahtung (Variante B)

Lesekopf ESP32
Vcc 3,3 V
GND GND
Rx (Daten vom Zähler) GPIO27 (RX)
Tx (Senden zum Zähler) GPIO33 (TX)
OLED SDA GPIO21
OLED SCL GPIO22

Im Sketch:

Serial1.begin(9600, SERIAL_8N1, 27, 33);   // RX=27, TX=33

Stromzähler E320 (SML)

Daten

OBIS Bedeutung Einheit im Telegramm
1.8.0 Bezug (Zählerstand) Wh, Skalierer beachten
2.8.0 Einspeisung (Zählerstand) Wh, Skalierer beachten
16.7.0 Momentanleistung (negativ bei Einspeisung) W

PIN-Freischaltung (einmalig) Die PIN erhält man vom Messstellenbetreiber. Sie gehört nicht ins Wiki und nicht in den Quellcode.

  1. Lesekopf abnehmen (er deckt die optische Taste ab).
  2. Mit Handy-Taschenlampe kurz auf die optische Taste leuchten: Displaytest, dann Abfrage „PIn“.
  3. Erneut leuchten, die Anzeige beginnt bei 0. Jeder kurze Puls (unter 2 s) zählt die Ziffer um 1 hoch. Nach ca. 3 s ohne Puls wird die Ziffer übernommen und die nächste beginnt.
  4. Nach der PIN mit kurzen Pulsen durch die Liste blättern bis „inF“. Dort langer Puls (ca. 5 s), bis „inF on“ erscheint. Damit sendet die Schnittstelle den erweiterten Datensatz.
  5. Weiter zu „PIn on“, wieder ca. 5 s leuchten, bis „PIn oFF“ erscheint. Sonst verlangt der Zähler nach einem Stromausfall die PIN neu.
  6. Nach 120 s ohne Bedienung kehrt der Zähler zur Standardanzeige zurück. Dann PIN neu eingeben.
Bei LED-Handylicht erkennt der Sensor die Pulse manchmal schlecht. Hilfreich ist eine saubere Lichtquelle, direkt an der Taste, zwischen den Pulsen wirklich wegnehmen. Alternativ die IR-Diode des Lesekopfs über den ESP32 schalten, solange die BOOT-Taste gedrückt ist (Sketch unten). Das hat bei uns funktioniert.
const int PIN_LED   = 33;   // Adapter-Tx: LOW = IR-LED an
const int PIN_TASTE = 0;    // BOOT-Taste (gedrückt = LOW)
 
void setup() {
  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, HIGH);          // LED aus
  pinMode(PIN_TASTE, INPUT_PULLUP);
}
 
void loop() {
  digitalWrite(PIN_LED, digitalRead(PIN_TASTE) == LOW ? LOW : HIGH);
}

Sketch

/********************************************************************************************
 *  Stromzähler E320 (SML) -> OLED + EmonCMS    ESP32, statische IP, selbstheilend
 *
 *                                    2026-11-07
 *  Features:
 *   - NUR HIER ÄNDERN: SENSOR_ID, NODE, VERBINDUNGSTEST, Netzwerk-Auswahl (Bereich A/B/C)
 *   - VERBINDUNGSTEST true  = nur Testwert senden, 1 Versuch, Port-Test (80/443/8080)
 *     VERBINDUNGSTEST false = Zählerdaten (B_kWh, E_kWh, P_kWh) werden gesendet
 *   - Statische IP (kein DHCP), robuster WLAN-Start mit 3 Versuchen + Funk-Reset
 *   - Lesen (SML, 9600 Baud) in loop(), Senden in eigenem Task (blockiert das Lesen nicht)
 *   - Server ist nach dem Start erst nach ca. 5 min erreichbar:
 *       • in den ersten 10 min keine Funk-Resets
 *       • Boot-Watchdog erst nach 15 min
 *   - Auto-Neustart, falls:
 *       • beim Boot 15 min lang kein erfolgreicher Upload (nur im Normalbetrieb mit Zählerdaten)
 *       • WLAN im Betrieb > 2 min am Stück weg
 *
 *  Pinbelegung ESP32:  Lesekopf Rx -> GPIO27 (RX), Lesekopf Tx -> GPIO33 (TX),
 *                      Vcc -> 3,3 V, GND, OLED SDA = GPIO21, SCL = GPIO22
 ********************************************************************************************/
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClient.h>
#include <U8g2lib.h>
 
/****************************************  NUR HIER ÄNDERUNGEN VORNEHMEN ******************************/
//
//                                        1) Geräte-Identität
#define SENSOR_ID 241                    // letzte IP-Stelle: 192.168.1.SENSOR_ID
const char* NODE = "241-ESP";            // Node-Name in EmonCMS
//
//                                        2) Verbindungstest
#define VERBINDUNGSTEST false            // true = nur Testwert senden, false = Zählerdaten senden
//
//                                        3) Netzwerk-Auswahl siehe weiter unten (Bereich A/B/C)
//
/**************************************** AB HIER KEINE ÄNDERUNGEN VORNEHMEN *****************************************************/
 
IPAddress ip(192, 168, 1, SENSOR_ID);    // feste IP aus SENSOR_ID
 
IPAddress SERVER_IP(192, 168, 1, 2);     // EmonCMS-Server (gilt für alle drei Netzwerkprofile)
const int   httpPort  = 80;
const char* EMON_PATH = "/emoncms/input/post.json";
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL  false   // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   true    // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false   // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "...";
  const char* apikey   = "...";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "...";
  const char* apikey   = "...";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "...";
  const char* apikey   = "...";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0,   0,   0,   0);
 
/*** Messintervall *************************************************************************/
const int Schreibpause = NETZ_A_MOBIL ? 10000 : 20000;  // A (mobil/Test) = 10 s, B/C (Schulnetz) = 20 s
 
/*** Hardware ******************************************************************************/
const int PIN_RX = 27;                   // Adapter-Rx (Daten vom Zähler)
const int PIN_TX = 33;                   // Adapter-Tx (wird nicht benutzt)
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);   // SDA=21, SCL=22
 
/*** Selbstheilungs-Parameter **************************************************************/
const uint32_t BOOT_UPLOAD_DEADLINE_MS = 900000;   // 15 min: Server ist erst nach ca. 5 min erreichbar
const uint32_t NO_RESET_AFTER_BOOT_MS  = 600000;   // 10 min: in dieser Zeit keine Funk-Resets
const uint32_t WIFI_DOWN_REBOOT_MS     = 120000;   // 2 min am Stück offline -> Neustart
const uint8_t  WIFI_BOOT_TRIES         = 3;
 
/*** Zustände ******************************************************************************/
static volatile bool     g_lastSendFailed = false;
static volatile bool     g_uploadedOnce   = false;
static volatile uint32_t g_wifiDownSince  = 0;
static volatile int      g_httpCode       = 0;
static uint32_t          g_bootMs         = 0;
 
struct Messwerte { double bezug, einsp, leistung; bool gueltig; uint32_t nr; };
Messwerte g_mw = {0, 0, 0, false, 0};
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
 
Messwerte holeMesswerte() {
  Messwerte c;
  portENTER_CRITICAL(&mux); c = g_mw; portEXIT_CRITICAL(&mux);
  return c;
}
 
/*** SML-Parser ****************************************************************************/
const uint8_t OBIS_BEZUG[]    = {0x77,0x07,0x01,0x00,0x01,0x08,0x00,0xFF};
const uint8_t OBIS_EINSPEIS[] = {0x77,0x07,0x01,0x00,0x02,0x08,0x00,0xFF};
const uint8_t OBIS_LEISTUNG[] = {0x77,0x07,0x01,0x00,0x10,0x07,0x00,0xFF};
const uint8_t START[8] = {0x1B,0x1B,0x1B,0x1B,0x01,0x01,0x01,0x01};
const uint8_t ENDM[5]  = {0x1B,0x1B,0x1B,0x1B,0x1A};
 
uint8_t buf[1024];
int  n = 0;
int  rest = -1;
bool imFrame = false;
 
bool finde(const uint8_t *obis, double &out) {
  for (int i = 0; i < n - 30; i++) {
    if (memcmp(buf + i, obis, 8) != 0) continue;
    for (int j = i + 8; j < i + 24 && j < n - 2; j++) {
      if (buf[j] == 0x52 && (buf[j - 1] == 0x1E || buf[j - 1] == 0x1B)) {
        int8_t scaler = (int8_t)buf[j + 1];
        uint8_t typ = buf[j + 2];
        int len = (typ & 0x0F) - 1;
        if (len < 1 || len > 8 || j + 3 + len > n) return false;
        int64_t v = (buf[j + 3] & 0x80) ? -1 : 0;
        for (int k = 0; k < len; k++) v = (v << 8) | buf[j + 3 + k];
        out = (double)v;
        for (int s = scaler; s < 0; s++) out /= 10.0;
        for (int s = scaler; s > 0; s--) out *= 10.0;
        return true;
      }
    }
  }
  return false;
}
 
void anzeigen() {
  Messwerte m = holeMesswerte();
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_6x12_tf);
    u8g2.setCursor(0, 12); u8g2.printf("Bezug: %.1f kWh", m.bezug);
    u8g2.setCursor(0, 28); u8g2.printf("Einsp: %.1f kWh", m.einsp);
    u8g2.setCursor(0, 44); u8g2.printf("Leist: %.0f W", m.leistung);
    u8g2.setCursor(0, 60); u8g2.printf("%s #%lu H%d%s",
                                       WiFi.status() == WL_CONNECTED ? "WLAN" : "w/o",
                                       (unsigned long)m.nr, (int)g_httpCode,
                                       VERBINDUNGSTEST ? " TEST" : "");
  } while (u8g2.nextPage());
}
 
void auswerten() {
  double v;
  bool ok = false;
  Messwerte c = holeMesswerte();
  if (finde(OBIS_BEZUG, v))    { c.bezug    = v / 1000.0; ok = true; }
  if (finde(OBIS_EINSPEIS, v)) { c.einsp    = v / 1000.0; }
  if (finde(OBIS_LEISTUNG, v)) { c.leistung = v; }
  if (ok) c.gueltig = true;
  c.nr++;
  portENTER_CRITICAL(&mux); g_mw = c; portEXIT_CRITICAL(&mux);
  Serial.printf("#%lu (%d Bytes) Bezug %.3f | Einsp %.3f | %.0f W\n",
                (unsigned long)c.nr, n, c.bezug, c.einsp, c.leistung);
  anzeigen();
}
 
void byteEmpfangen(uint8_t b) {
  if (n < (int)sizeof(buf)) buf[n++] = b;
  else { n = 0; rest = -1; imFrame = false; return; }
 
  if (n >= 8 && memcmp(buf + n - 8, START, 8) == 0) {   // Telegrammanfang
    memmove(buf, buf + n - 8, 8);
    n = 8; rest = -1; imFrame = true;
    return;
  }
  if (!imFrame) { if (n > 16) { memmove(buf, buf + n - 8, 8); n = 8; } return; }
  if (rest > 0) {                                        // 3 Bytes nach dem Endmarker
    if (--rest == 0) { auswerten(); n = 0; rest = -1; imFrame = false; }
    return;
  }
  if (n >= 5 && memcmp(buf + n - 5, ENDM, 5) == 0) rest = 3;
}
 
/*** WLAN-Helfer ***************************************************************************/
bool ensureWifi(uint32_t timeoutMs = 15000) {
  if (WiFi.status() == WL_CONNECTED) { g_wifiDownSince = 0; return true; }
  Serial.print("[WIFI] Verbinde");
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {
    delay(250);
    Serial.print(".");
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\n[WIFI] OK: %s\n", WiFi.localIP().toString().c_str());
    g_wifiDownSince = 0;
    return true;
  }
  Serial.println("\n[WIFI] Timeout");
  if (!g_wifiDownSince) g_wifiDownSince = millis();
  return false;
}
 
void funkNeustart() {
  WiFi.disconnect(true);
  delay(200);
  WiFi.mode(WIFI_OFF);
  delay(200);
  WiFi.mode(WIFI_STA);
  delay(200);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
}
 
bool wifiBootConnect() {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.setSleep(false);
  WiFi.setTxPower(WIFI_POWER_19_5dBm);
 
  for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) {
    Serial.printf("[BOOT] WLAN-Versuch %u/%u\n", attempt, WIFI_BOOT_TRIES);
    funkNeustart();
    if (ensureWifi(20000)) {
      Serial.printf("[WIFI] OK: %s  RSSI:%d dBm\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
      return true;
    }
    int backoff = 1000 + random(0, 2000);
    Serial.printf("[BOOT] fehlgeschlagen -> Backoff %d ms\n", backoff);
    delay(backoff);
  }
  return false;
}
 
bool netSoftReset(uint32_t connectTimeoutMs = 15000) {
  Serial.println("[NET] Soft-Reset: Funk aus/an");
  funkNeustart();
  return ensureWifi(connectTimeoutMs);
}
 
bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50) {
  WiFiClient poke;
  if (!poke.connect(SERVER_IP, port)) return false;
  delay(graceMs);
  poke.stop();
  return true;
}
 
// Nur im VERBINDUNGSTEST: Ports des Servers und des Gateways prüfen
void portTest() {
  Serial.printf("[TEST] Eigene IP %s, Gateway %s, Subnetz %s, RSSI %d dBm\n",
                WiFi.localIP().toString().c_str(), WiFi.gatewayIP().toString().c_str(),
                WiFi.subnetMask().toString().c_str(), WiFi.RSSI());
  const uint16_t ports[] = {80, 443, 8080};
  for (uint16_t p : ports) {
    WiFiClient c;
    bool ok = c.connect(SERVER_IP, p, 3000);
    Serial.printf("[TEST] Server %s Port %u : %s\n", SERVER_IP.toString().c_str(), p,
                  ok ? "OFFEN" : "zu / keine Antwort");
    c.stop();
  }
  WiFiClient g;
  bool ok = g.connect(gateway, 80, 3000);
  Serial.printf("[TEST] Gateway %s Port 80 : %s\n", gateway.toString().c_str(),
                ok ? "OFFEN" : "zu / keine Antwort");
  g.stop();
}
 
// Nur im VERBINDUNGSTEST: misst, nach wie vielen Sekunden der Server nach dem Start antwortet
void warteBisServerAntwortet() {
  uint32_t t0 = millis();
  while (millis() - t0 < 600000) {                       // max. 10 min
    WiFiClient g, s;
    bool gw = g.connect(gateway, 80, 2000);   g.stop();
    bool sv = s.connect(SERVER_IP, 80, 2000); s.stop();
    Serial.printf("[DIAG] %lu s  Gateway:%s  Server:%s\n",
                  (millis() - t0) / 1000, gw ? "ok" : "-", sv ? "ok" : "-");
    if (sv) break;
    delay(3000);
  }
}
 
/*** SENDEN der Daten **********************************************************************/
void sendData(const Messwerte &m) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[HTTP] offline - Upload uebersprungen");
    g_lastSendFailed = true;
    return;
  }
 
  String path  = String(EMON_PATH);
  String query = "?node=" + String(NODE) + "&apikey=" + String(apikey);
  String body;
  if (VERBINDUNGSTEST) {
    body = "json={\"Test\":" + String(millis() / 1000) + "}";
  } else {
    body = "json={\"B_kWh\":" + String(m.bezug, 3) +
           ",\"E_kWh\":"      + String(m.einsp, 3) +
           ",\"P_kWh\":"      + String(m.leistung, 0) + "}";
  }
  Serial.printf("[HTTP] POST http://%s%s  node=%s  body=%s\n",
                SERVER_IP.toString().c_str(), path.c_str(), NODE, body.c_str());
 
  if (g_lastSendFailed && !VERBINDUNGSTEST) {
    bool ok = tcpPreflight(httpPort, 80);
    Serial.printf("[NET] Preflight %s\n", ok ? "OK" : "FAIL");
  }
 
  String hostStr = SERVER_IP.toString();
 
  auto doPostOnce = [&](int attempt) -> int {
    if (WiFi.status() != WL_CONNECTED) {
      ensureWifi(8000);
      if (WiFi.status() != WL_CONNECTED) {
        Serial.printf("[HTTP] WLAN down (Try %d) - skip\n", attempt);
        return -10000;
      }
    }
    WiFiClient client;
    client.setNoDelay(true);
 
    HTTPClient http;
    http.setConnectTimeout(5000);
    http.setTimeout(15000);                  // ms
    http.setReuse(false);
 
    if (!http.begin(client, hostStr, httpPort, path + query, false)) {
      Serial.println("[HTTP] begin() failed");
      return -2;
    }
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.addHeader("Connection", "close");
 
    int code = http.POST(body);
    String err = http.errorToString(code);
    String payload = (code > 0) ? http.getString() : String();
    http.end();
 
    g_httpCode = code;
    Serial.printf("[HTTP] Try %d -> code=%d (%s)\n", attempt, code, err.c_str());
    if (payload.length()) { Serial.print("[HTTP] resp: "); Serial.println(payload); }
    if (code == 200) {
      Serial.println(VERBINDUNGSTEST ? "Sent: Testwert" : "Sent: Zaehlerdaten");
      g_uploadedOnce   = true;
      g_lastSendFailed = false;
    }
    return code;
  };
 
  int code = -9999;
  for (int attempt = 1; attempt <= 3; ++attempt) {          // 3 normale Versuche
    code = doPostOnce(attempt);
    if (code == 200) return;
    if (VERBINDUNGSTEST) return;                             // im Test nur 1 Versuch
    delay(250 * attempt);
  }
 
  // Server ist nach dem Start erst nach ca. 5 min erreichbar: in den ersten 10 min keine Funk-Resets
  if (millis() < NO_RESET_AFTER_BOOT_MS) { g_lastSendFailed = true; return; }
 
  if (netSoftReset(15000)) {                                 // Soft-Reset + 4. Versuch
    code = doPostOnce(4);
    if (code == 200) return;
  }
  Serial.println("[NET] Soft-Reset #2 ...");                  // zweiter Soft-Reset + 5. Versuch
  if (netSoftReset(15000)) {
    code = doPostOnce(5);
    if (code == 200) return;
  }
  g_lastSendFailed = true;
  Serial.printf("[HTTP] FAILED after retries | Node=%s\n", NODE);
}
 
/*** Sende-Task (läuft getrennt vom Lesen) *************************************************/
void emonTask(void *) {
  uint32_t zyklus = 0;
  for (;;) {
    vTaskDelay(Schreibpause / portTICK_PERIOD_MS);
    ensureWifi(10000);
    if ((++zyklus % 18) == 0) Serial.printf("[WIFI] RSSI: %d dBm\n", WiFi.RSSI());
    Messwerte m = holeMesswerte();
    if (!VERBINDUNGSTEST && !m.gueltig) continue;   // im Normalbetrieb erst senden, wenn Daten da sind
    sendData(m);
  }
}
 
/*** SETUP *********************************************************************************/
void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("\n=== Boot: Stromzaehler-Lesekopf startet ===");
  Serial.printf("Node = %s, IP = %s, Server = %s%s\n", NODE, ip.toString().c_str(),
                SERVER_IP.toString().c_str(), EMON_PATH);
  if (VERBINDUNGSTEST) Serial.println("*** VERBINDUNGSTEST aktiv: es wird nur ein Testwert gesendet ***");
 
  u8g2.begin();
  anzeigen();
 
  randomSeed((uint32_t)ESP.getEfuseMac());
  if (!wifiBootConnect()) {
    Serial.println("[BOOT] WLAN nicht erreichbar -> Neustart");
    delay(800);
    ESP.restart();
  }
 
  if (VERBINDUNGSTEST) { portTest(); warteBisServerAntwortet(); }
 
  Serial1.setRxBufferSize(1024);
  Serial1.begin(9600, SERIAL_8N1, PIN_RX, PIN_TX);
 
  g_bootMs = millis();
  xTaskCreate(emonTask, "emon", 8192, NULL, 1, NULL);
}
 
/*** LOOP: nur Lesen + Watchdogs ***********************************************************/
void loop() {
  while (Serial1.available()) byteEmpfangen(Serial1.read());
 
  uint32_t now = millis();
  if (!VERBINDUNGSTEST && g_mw.gueltig && !g_uploadedOnce && (now - g_bootMs > BOOT_UPLOAD_DEADLINE_MS)) {
    Serial.println("[WD] Kein erfolgreicher Upload binnen 15 min -> Reboot");
    delay(500);
    ESP.restart();
  }
  if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_REBOOT_MS)) {
    Serial.println("[WD] WLAN > 120 s weg -> Reboot");
    delay(500);
    ESP.restart();
  }
}

emonCMS

Fehlersuche

Symptom Ursache / Lösung
Kompilierfehler extended character is not valid in an identifier Unsichtbare geschützte Leerzeichen durch Kopieren-Schaltfläche. Code mit der Maus markieren statt „Kopieren“ klicken.
Keine Bytes im Serial Monitor Kopf verrutscht, Rx/Tx vertauscht oder falsche GPIOs. Mit Pin-Test prüfen.
1B 1B 1B 1B 01 01 01 01 kommt, aber nur kWh, keine Leistung PIN-Freischaltung fehlt (inF on).
Anzeige bleibt stehen, Telegramme laufen weiter Telegrammerkennung per Pause funktioniert nicht, Marker-Erkennung im Sketch nutzen.
HTTP code=-1 direkt nach dem Start Server ca. 5 min nicht erreichbar, abwarten.
HTTP code=200, Antwort ok, aber keine Inputs Falscher Node oder Write-Key.

Echo-Test des Lesekopfs (ohne Zähler): ESP32 sendet 100 Bytes 0x55, der Empfänger sieht die eigene IR-Diode und meldet ca. 100 empfangene Bytes. Das zeigt nur, dass Diode und Empfänger arbeiten, nicht die Strecke zum Zähler.

Wärmezähler Sharky 775 (in Arbeit)