/* ============================================================ BLE-YC01 → WiFi/MQTT Bridge für Home Assistant Board: ESP32 (Arduino IDE, ESP32 Arduino Core >= 2.x) v1.6 – Fix: MQTT Timeout + Doppelter Connect während BLE ============================================================ Messgrößen: pH | EC | TDS | ORP | Chlor | Temperatur | Batterie BLE Service: 0xFF01 Characteristic: 0xFF02 (Notify + Read) MQTT Broker: Mosquitto (Home Assistant Add-on) HA-Discovery wird beim ersten Start automatisch gesendet. ============================================================ Benötigte Libraries (Arduino IDE → Bibliotheken verwalten): ┌─────────────────────────────────────────────────────────┐ │ 1. PubSubClient by Nick O'Leary v2.7 oder 2.8 │ │ 2. ArduinoJson by Benoit Blanchon (>= 6.x) │ │ 3. ESP32 BLE Arduino → im ESP32-Core enthalten │ │ 4. time.h → im ESP32-Core enthalten │ │ 5. WebServer → im ESP32-Core enthalten │ └─────────────────────────────────────────────────────────┘ Board-Einstellungen (Arduino IDE): Board: ESP32 Dev Module Partition Scheme: Huge APP (3MB No OTA/1MB SPIFFS) Upload Speed: 921600 HTTP Statusseite: z.B. http://192.168.2.170/status ============================================================ */ // !! MUSS vor #include stehen !! #define MQTT_MAX_PACKET_SIZE 1100 #include #include #include #include #include #include #include #include #include // ============================================================ // KONFIGURATION ← Hier alle Werte anpassen // ============================================================ // --- WLAN --- #define WIFI_SSID "WLAN eintragen" #define WIFI_PASSWORD "WLAN Passwort eintragen" // --- Statische IP --- IPAddress STATIC_IP (192, 168, 2, 170); IPAddress SUBNET (255, 255, 255, 0); IPAddress GATEWAY (192, 168, 2, 1); IPAddress DNS1 (192, 168, 2, 1); IPAddress DNS2 ( 8, 8, 8, 8); // --- NTP Zeitserver (Deutschland) --- #define NTP_SERVER "pool.ntp.org" #define NTP_UTC_OFFSET 3600 #define NTP_DST_OFFSET 3600 // --- MQTT Broker (Home Assistant IP) --- IPAddress MQTT_SERVER (192, 168, 2, 14); #define MQTT_PORT 1883 #define MQTT_USER "mqtt Benutzer" #define MQTT_PASSWORD "mqtt Passwort" // --- BLE-YC01 MAC-Adresse --- #define YC01_MAC "C0:00:00:00:A0:F4" // --- Abfrageintervall & Zeitfenster --- #define POLL_MINUTES 15 #define ACTIVE_HOUR_FROM 7 #define ACTIVE_HOUR_TO 23 // --- BLE Timeout --- #define BLE_TIMEOUT_MS 20000 // ============================================================ // KONSTANTEN (nicht ändern) // ============================================================ static BLEUUID SVC_UUID("0000FF01-0000-1000-8000-00805f9b34fb"); static BLEUUID CHR_UUID("0000FF02-0000-1000-8000-00805f9b34fb"); #define MQTT_STATE "homeassistant/sensor/ble_yc01/state" #define MQTT_AVAIL "homeassistant/sensor/ble_yc01/availability" #define HA_DISC_PREFIX "homeassistant" #define DEVICE_ID "ble_yc01" #define DEVICE_NAME "BLE-YC01 Pool Sensor" #define DEVICE_MODEL "BLE-YC01 6-in-1" #define DEVICE_MFR "Yieryi" #define POLL_MS ((unsigned long)POLL_MINUTES * 60UL * 1000UL) // ============================================================ // GLOBALE VARIABLEN // ============================================================ WiFiClient wifiClient; PubSubClient mqtt(wifiClient); WebServer httpServer(80); BLEClient* bleClient = nullptr; BLERemoteCharacteristic* bleChar = nullptr; bool dataReady = false; bool discoverySent = false; float v_ph = 0, v_temp = 0, v_ec = 0; float v_tds = 0, v_orp = 0, v_bat = 0, v_cl = 0; unsigned long lastPollMs = 0; String lastMeasTime = "--:--"; // ============================================================ // NTP – Zeitsynchronisation // ============================================================ void syncNTP() { Serial.println("[NTP] Synchronisiere Uhrzeit..."); configTime(NTP_UTC_OFFSET, NTP_DST_OFFSET, NTP_SERVER); struct tm ti; uint8_t retries = 0; while (!getLocalTime(&ti)) { delay(500); Serial.print("."); if (++retries > 20) { Serial.println("\n[NTP] WARNUNG: Sync fehlgeschlagen"); return; } } Serial.printf("\n[NTP] Zeit: %02d.%02d.%04d %02d:%02d:%02d\n", ti.tm_mday, ti.tm_mon + 1, ti.tm_year + 1900, ti.tm_hour, ti.tm_min, ti.tm_sec); } bool isActiveHour() { struct tm ti; if (!getLocalTime(&ti)) return true; return (ti.tm_hour >= ACTIVE_HOUR_FROM && ti.tm_hour < ACTIVE_HOUR_TO); } String currentTime() { struct tm ti; if (!getLocalTime(&ti)) return "--:--"; char buf[6]; snprintf(buf, sizeof(buf), "%02d:%02d", ti.tm_hour, ti.tm_min); return String(buf); } // ============================================================ // BLE-YC01 Datendekodierung // ============================================================ bool decodeYC01(uint8_t* d, size_t len) { if (len < 22) { Serial.printf("[BLE] Paket zu kurz: %d Bytes (min. 22)\n", len); return false; } for (int i = (int)len - 1; i > 0; i--) { uint8_t cur = d[i]; uint8_t prev = d[i - 1]; d[i] = ~( ((cur & 0x55) << 1) | ((prev & 0xAA) >> 1) ); d[i - 1] = ~( ((prev & 0x55) << 1) | ((cur & 0xAA) >> 1) ); } v_ph = ((d[3] << 8) | d[4]) / 100.0f; v_ec = (float)((d[5] << 8) | d[6]); v_tds = (float)((d[7] << 8) | d[8]); v_cl = ((d[11] << 8) | d[12]) / 10.0f; v_temp = ((d[13] << 8) | d[14]) / 10.0f; v_bat = ((d[15] << 8) | d[16]) / 31.9f; v_orp = (float)((d[20] << 8) | d[21]); if (((d[11] << 8) | d[12]) == 0xFFFF) v_cl = NAN; if (v_bat > 100.0f) v_bat = 100.0f; if (v_bat < 0.0f) v_bat = 0.0f; Serial.printf("[YC01] pH:%.2f Temp:%.1f°C EC:%.0fµS/cm TDS:%.0fppm ORP:%.0fmV Cl:%.1fmg/L Bat:%.0f%%\n", v_ph, v_temp, v_ec, v_tds, v_orp, isnan(v_cl) ? 0.0f : v_cl, v_bat); return true; } // ============================================================ // BLE Notification Callback // ============================================================ void notifyCallback(BLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { uint8_t buf[64]; if (length > sizeof(buf)) length = sizeof(buf); memcpy(buf, pData, length); if (decodeYC01(buf, length)) dataReady = true; } // ============================================================ // WiFi mit statischer IP verbinden // ============================================================ void connectWiFi() { Serial.println("[WiFi] Konfiguriere statische IP..."); if (!WiFi.config(STATIC_IP, GATEWAY, SUBNET, DNS1, DNS2)) { Serial.println("[WiFi] WARNUNG: Statische IP konnte nicht gesetzt werden!"); } WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.printf("[WiFi] Verbinde mit '%s'", WIFI_SSID); uint8_t retries = 0; while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); if (++retries > 40) { Serial.println("\n[WiFi] Timeout – Neustart!"); ESP.restart(); } } Serial.printf("\n[WiFi] Verbunden! IP: %s RSSI: %d dBm\n", WiFi.localIP().toString().c_str(), WiFi.RSSI()); } // ============================================================ // MQTT verbinden / reconnect // v1.6: explizites disconnect() VOR connect() verhindert // "session taken over" Doppelverbindung // ============================================================ bool connectMQTT() { mqtt.setServer(MQTT_SERVER, MQTT_PORT); mqtt.setKeepAlive(60); if (mqtt.connected()) { Serial.println("[MQTT] Bereits verbunden."); return true; } // Sauber trennen bevor neu verbunden wird if (mqtt.state() != MQTT_DISCONNECTED) { Serial.println("[MQTT] Trenne bestehende Session..."); mqtt.disconnect(); delay(200); } char clientId[32]; snprintf(clientId, sizeof(clientId), "ESP32_YC01_%08X", (uint32_t)ESP.getEfuseMac()); uint8_t tries = 0; while (!mqtt.connected()) { Serial.printf("[MQTT] Verbinde %s:%d (Versuch %d)...", MQTT_SERVER.toString().c_str(), MQTT_PORT, tries + 1); bool ok = mqtt.connect(clientId, MQTT_USER, MQTT_PASSWORD, MQTT_AVAIL, 1, true, "offline"); if (ok) { Serial.println(" OK!"); delay(100); // kurz warten nach connect (verhindert Race-Condition) mqtt.publish(MQTT_AVAIL, "online", true); if (!discoverySent) sendHADiscovery(); return true; } else { Serial.printf(" Fehler (rc=%d) – Retry in 5s\n", mqtt.state()); delay(5000); if (++tries > 5) { Serial.println("[MQTT] Zu viele Fehler – ESP32 restart"); ESP.restart(); } } } return false; } // ============================================================ // Home Assistant MQTT Auto-Discovery // ============================================================ void sendHADiscovery() { struct Sensor { const char* id; const char* name; const char* unit; const char* device_class; const char* icon; const char* tpl; }; Sensor sensors[] = { { "ph", "pH", "pH", "", "mdi:ph", "{{ value_json.ph | round(2) }}" }, { "temp", "Temperatur", "°C", "temperature", "mdi:thermometer", "{{ value_json.temp_c | round(1) }}" }, { "ec", "EC", "µS/cm", "", "mdi:water-opacity", "{{ value_json.ec | round(0) }}" }, { "tds", "TDS", "ppm", "", "mdi:water-opacity", "{{ value_json.tds | round(0) }}" }, { "orp", "ORP", "mV", "voltage", "mdi:flash", "{{ value_json.orp | round(0) }}" }, { "cl", "Chlor", "mg/L", "", "mdi:water-check", "{{ value_json.cl | round(1) }}" }, { "battery", "Batterie", "%", "battery", "mdi:battery", "{{ value_json.battery | round(0) }}"}, }; Serial.println("[MQTT] Sende HA Auto-Discovery..."); for (auto& s : sensors) { char topic[128]; snprintf(topic, sizeof(topic), "%s/sensor/%s_%s/config", HA_DISC_PREFIX, DEVICE_ID, s.id); StaticJsonDocument<900> doc; doc["name"] = String(DEVICE_NAME) + " " + s.name; doc["unique_id"] = String(DEVICE_ID) + "_" + s.id; doc["state_topic"] = MQTT_STATE; doc["availability_topic"] = MQTT_AVAIL; doc["payload_available"] = "online"; doc["payload_not_available"] = "offline"; doc["value_template"] = s.tpl; doc["unit_of_measurement"] = s.unit; doc["state_class"] = "measurement"; doc["icon"] = s.icon; if (strlen(s.device_class) > 0) doc["device_class"] = s.device_class; JsonObject dev = doc.createNestedObject("device"); dev["identifiers"][0] = DEVICE_ID; dev["name"] = DEVICE_NAME; dev["model"] = DEVICE_MODEL; dev["manufacturer"] = DEVICE_MFR; char payload[900]; size_t written = serializeJson(doc, payload, sizeof(payload)); Serial.printf("[MQTT] Discovery '%s': %d Bytes\n", s.name, written); if (mqtt.publish(topic, payload, true)) { Serial.printf("[MQTT] HA-Discovery: %s ✓\n", s.name); } else { Serial.printf("[MQTT] HA-Discovery: %s FEHLER! (rc=%d)\n", s.name, mqtt.state()); } delay(100); } discoverySent = true; Serial.println("[MQTT] Auto-Discovery abgeschlossen."); } // ============================================================ // BLE verbinden & Daten lesen // v1.6: MQTT wird VOR dem BLE-Scan bewusst getrennt, // damit kein Keepalive-Timeout im Hintergrund auftritt // ============================================================ bool readBLE() { // v1.6: MQTT vor BLE sauber trennen → kein "exceeded timeout" // im Mosquitto-Log und kein "session taken over" danach if (mqtt.connected()) { Serial.println("[BLE] Trenne MQTT vor BLE-Scan..."); mqtt.publish(MQTT_AVAIL, "offline", true); delay(50); mqtt.disconnect(); delay(100); } Serial.printf("[BLE] Verbinde mit %s ...\n", YC01_MAC); if (bleClient == nullptr) { bleClient = BLEDevice::createClient(); } BLEAddress addr(YC01_MAC); if (!bleClient->connect(addr)) { Serial.println("[BLE] Verbindung fehlgeschlagen!"); return false; } Serial.println("[BLE] Verbunden."); delay(1500); BLERemoteService* svc = bleClient->getService(SVC_UUID); if (!svc) { Serial.println("[BLE] Service 0xFF01 nicht gefunden!"); bleClient->disconnect(); return false; } bleChar = svc->getCharacteristic(CHR_UUID); if (!bleChar) { Serial.println("[BLE] Charakteristik 0xFF02 nicht gefunden!"); bleClient->disconnect(); return false; } if (bleChar->canNotify()) { bleChar->registerForNotify(notifyCallback); Serial.println("[BLE] Notifications aktiv."); delay(500); } // Direkt lesen (Erstwert) if (bleChar->canRead()) { String val = bleChar->readValue(); Serial.printf("[BLE] readValue() Länge: %d Bytes\n", val.length()); if (val.length() >= 22) { uint8_t buf[64]; size_t len = val.length() < sizeof(buf) ? val.length() : sizeof(buf); memcpy(buf, val.c_str(), len); if (decodeYC01(buf, len)) dataReady = true; } } // Auf Notification warten (nur falls readValue keinen Wert lieferte) if (!dataReady) { Serial.println("[BLE] Warte auf Notification..."); unsigned long t0 = millis(); while (!dataReady && (millis() - t0 < BLE_TIMEOUT_MS)) { delay(200); if ((millis() - t0) % 5000 < 200) { Serial.printf("[BLE] Warte... %lus\n", (millis() - t0) / 1000); } } } bleClient->disconnect(); Serial.printf("[BLE] Getrennt. Daten: %s\n", dataReady ? "OK" : "KEINE"); return dataReady; } // ============================================================ // Messwerte per MQTT publizieren // ============================================================ void publishValues() { // v1.6: nach BLE immer frisch verbinden (MQTT war bewusst getrennt) Serial.println("[MQTT] Verbinde nach BLE-Scan..."); if (!connectMQTT()) { Serial.println("[MQTT] Verbindung fehlgeschlagen – Daten verloren!"); return; } StaticJsonDocument<256> doc; doc["ph"] = serialized(String(v_ph, 2)); doc["temp_c"] = serialized(String(v_temp, 1)); doc["ec"] = serialized(String(v_ec, 0)); doc["tds"] = serialized(String(v_tds, 0)); doc["orp"] = serialized(String(v_orp, 0)); doc["battery"] = serialized(String(v_bat, 0)); if (isnan(v_cl)) doc["cl"] = nullptr; else doc["cl"] = serialized(String(v_cl, 1)); char payload[256]; serializeJson(doc, payload, sizeof(payload)); if (mqtt.publish(MQTT_STATE, payload, false)) { Serial.printf("[MQTT] ✓ Gesendet: %s\n", payload); lastMeasTime = currentTime(); } else { Serial.printf("[MQTT] ✗ Senden fehlgeschlagen! (rc=%d)\n", mqtt.state()); } } // ============================================================ // HTTP Statusseite // ============================================================ void handleStatus() { unsigned long nextMs = 0; if (lastPollMs > 0) { unsigned long elapsed = millis() - lastPollMs; nextMs = elapsed < POLL_MS ? (POLL_MS - elapsed) / 1000 : 0; } uint16_t nextMin = nextMs / 60; uint16_t nextSec = nextMs % 60; const char* phColor = (v_ph >= 7.2f && v_ph <= 7.6f) ? "#27ae60" : "#e74c3c"; const char* clColor = (!isnan(v_cl) && v_cl >= 0.5f && v_cl <= 1.5f) ? "#27ae60" : "#e74c3c"; const char* batColor = v_bat >= 30 ? "#27ae60" : "#e74c3c"; char clStr[10]; if (isnan(v_cl)) snprintf(clStr, sizeof(clStr), "n/a"); else snprintf(clStr, sizeof(clStr), "%.1f", v_cl); String html = "" "" "" "BLE-YC01 Pool Sensor" "" "

🏊 BLE-YC01 Pool Sensor

" "
Letzte Messung: " + lastMeasTime + "  |  " "Nächste in: " + String(nextMin) + "m " + String(nextSec) + "s
" "
"; html += "
pH-Wert
" "
" + String(v_ph, 2) + "
" "
pH
"; html += "
Temperatur
" "
" + String(v_temp, 1) + "
" "
°C
"; html += "
EC
" "
" + String((int)v_ec) + "
" "
µS/cm
"; html += "
TDS
" "
" + String((int)v_tds) + "
" "
ppm
"; html += "
ORP
" "
" + String((int)v_orp) + "
" "
mV
"; html += "
Chlor
" "
" + String(clStr) + "
" "
mg/L
"; html += "
Batterie
" "
" + String((int)v_bat) + "
" "
%
"; html += "
" "
" "Gerät: " DEVICE_NAME "  |  " "MAC: " YC01_MAC "  |  " "IP: " + WiFi.localIP().toString() + "

" "MQTT: " "" + String(mqtt.connected() ? "Verbunden" : "Getrennt") + "" "  |  WiFi RSSI: " + String(WiFi.RSSI()) + " dBm" "  |  Intervall: " + String(POLL_MINUTES) + " min" "
"; httpServer.send(200, "text/html; charset=utf-8", html); } void handleRoot() { httpServer.sendHeader("Location", "/status", true); httpServer.send(302, "text/plain", ""); } void setupHTTP() { httpServer.on("/", handleRoot); httpServer.on("/status", handleStatus); httpServer.begin(); Serial.printf("[HTTP] Statusseite: http://%s/status\n", WiFi.localIP().toString().c_str()); } // ============================================================ // SETUP // ============================================================ void setup() { Serial.begin(115200); delay(200); Serial.println("\n╔════════════════════════════════════╗"); Serial.println("║ BLE-YC01 MQTT Bridge v1.6 ║"); Serial.println("╚════════════════════════════════════╝"); connectWiFi(); syncNTP(); connectMQTT(); setupHTTP(); BLEDevice::init("ESP32_BLE_YC01"); Serial.println("[BLE] Initialisiert."); Serial.printf("[INFO] Aktiv: %02d:00 – %02d:00 Uhr, alle %d Minuten\n\n", ACTIVE_HOUR_FROM, ACTIVE_HOUR_TO, POLL_MINUTES); } // ============================================================ // LOOP // ============================================================ void loop() { if (WiFi.status() != WL_CONNECTED) { Serial.println("[WiFi] Verbindung verloren – reconnect..."); connectWiFi(); syncNTP(); } if (!mqtt.connected()) connectMQTT(); mqtt.loop(); httpServer.handleClient(); unsigned long now = millis(); bool first = (lastPollMs == 0); if (first || (now - lastPollMs >= POLL_MS)) { lastPollMs = now; if (!isActiveHour()) { Serial.printf("[Loop] %s – außerhalb Zeitfenster (%02d:00–%02d:00)\n", currentTime().c_str(), ACTIVE_HOUR_FROM, ACTIVE_HOUR_TO); return; } Serial.printf("[Loop] %s – Starte Messung...\n", currentTime().c_str()); dataReady = false; if (readBLE()) { publishValues(); } else { // Auch bei Fehler MQTT wieder verbinden Serial.println("[Loop] Keine BLE-Daten – reconnecte MQTT..."); connectMQTT(); } dataReady = false; } delay(100); }