Stiebel Eltron WPM 3i - ISG Alternative Can Bus

WebUIs (DashUI, yahui, ...), Adapter (Hue, IRTrans, Sonos, ...), Logging, Scripting

Moderator: Co-Administratoren

g60vx
Beiträge: 195
Registriert: 16.04.2015, 21:48
Hat sich bedankt: 14 Mal
Danksagung erhalten: 1 Mal

Re: Stiebel Eltron WPM 3i - ISG Alternative Can Bus

Beitrag von g60vx » 02.09.2025, 08:32

Guten Morgen,
ich habe mir jetzt ein ESP32 Can sniffer gebaut, dieser lauscht erstmal nur den Can und pusht die Daten an MQTT.
Ich habe nun schon einige ElsterListen gesehen aber leider fehlen einige can_id´s und somit kann ich den Rohwert nicht interpretieren.

Code: Alles auswählen

can_id	hex	                index   raw_value
700	92 00 fa fd f4 01 3c	0xfdf4	316
700	92 00 fa fd f4 01 3b	0xfdf4	315
700	92 00 fa fd f4 01 3a	0xfdf4	314
700	92 00 fa fd f3 01 37	0xfdf3	311
700	92 00 fa fd f3 01 36	0xfdf3	310
700	92 00 fa fd f3 01 36	0xfdf3	310
700	92 00 fa fd f3 01 36	0xfdf3	310
700	92 00 fa fd f3 01 32	0xfdf3	306
700	92 00 fa fd f5 01 40	0xfdf5	320
700	92 00 fa fd f5 01 40	0xfdf5	320
700	92 00 fa fd f5 01 3f	0xfdf5	319
Hat jemand einen Hinweis, wie ich weiter vorgehen kann?

P.S. WP=WPC07 mit WPM3i

VG Rudi

g60vx
Beiträge: 195
Registriert: 16.04.2015, 21:48
Hat sich bedankt: 14 Mal
Danksagung erhalten: 1 Mal

Re: Stiebel Eltron WPM 3i - ISG Alternative Can Bus

Beitrag von g60vx » 25.10.2025, 00:26

Einen schönen guten Abend.

Ich und mein Freund ChatGPT haben es geschafft. Ich kann jetzt meine Wärmepumpe auslesen und steuern. Und das bequem von unterwegs mit der APP von openhab :D
openhab-WP.jpg
Screenshot 2025-10-25 at 00-11-18 Wärmepumpe - openHAB.png
Ich habe mir die ElsterListe schlussendlich selber zusammen kopiert und durch ausprobieren herausgefunden:
ElsterListe.h
(9.91 KiB) 87-mal heruntergeladen

Und hier der Code, der die Werte liest und über MQTT versendet und auch empfängt und anschließend auf den Can sendet.

Code: Alles auswählen

#include <AA_MCP2515.h>
#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoOTA.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ElsterListe.h>

const CANBitrate::Config CAN_BITRATE = CANBitrate::Config_16MHz_20kbps;
const uint8_t CAN_PIN_CS = 44;
const int8_t CAN_PIN_INT = 43;
bool otaActive = false;

// WiFi
const char* ssid = "change";
const char* password = "change";

// MQTT
const char* hostname = "change";
const char* mqtt_server = "192.168.1.40";
const int mqtt_port = 1883;
const char* mqtt_user = "change";
const char* mqtt_password = "change";
String AckMeldung = "";
bool AckSend = false;         
size_t listPos = 36; //Startposition in der ElsterListe.h für periodischen Aufruf  

unsigned long lastQueryMillis = 0;

WiFiClient espClient;
PubSubClient client(espClient); // MQTT

// MCP_CAN vorbereiten
const int FRAME_QUEUE_SIZE = 32;
volatile int frameWriteIdx = 0;
volatile int frameReadIdx = 0;
CANFrame frameQueue[FRAME_QUEUE_SIZE];
CANConfig config(CAN_BITRATE, CAN_PIN_CS, CAN_PIN_INT);
CANController CAN(config);

// Index suchen
const ElsterIndex* GetElsterIndex(uint16_t index) {
    for(auto &ei : ElsterListe) {
        if(ei.Index == index) return &ei;
    }
    return nullptr;
}

// ======================== ElsterIndex nach Name suchen ========================
const ElsterIndex* GetElsterIndexByName(const char* name) {
    for (size_t i = 0; i < elsterListeSize; ++i) {
        if (strcasecmp(ElsterListe[i].Name, name) == 0) {
            return &ElsterListe[i];
        }
    }
    return nullptr;
}

// ======================== CAN-Frame zum Senden vorbereiten ========================
bool prepareCanFrame(const ElsterIndex* ei, double value, CANFrame &frameOut, uint32_t can_id = 1664) {
    if (!ei) return false;

    uint16_t index = ei->Index;
    uint8_t data[8] = {0};
    data[0] = 0x30;
    data[1] = 0x00;

    bool extended = (index >= 0xFA);
    size_t frameLen = 0;
    int16_t raw_value = 0;

    // === Skalierung gemäß ElsterType ===
    switch (ei->Type) {
        case et_uint16_val:   raw_value = (int16_t)round(value); break;
        case et_dec_signed_10: raw_value = (int16_t)round(value * 10.0); break;
        case et_dec_signed_100: raw_value = (int16_t)round(value * 100.0); break;
        case et_status:        raw_value = (int16_t)round(value); break;
    }

    // === Telegrammaufbau ===
    if (ei->Type == et_status) {
        if (!extended) {
            // kurzer Index, 1 Byte Wert
            data[2] = (uint8_t)(index & 0xFF);
            data[3] = (uint8_t)(raw_value & 0xFF);
            frameLen = 4;
        } else {
            // erweiterter Index, 1 Byte Wert
            data[2] = 0xFA;
            data[3] = (uint8_t)((index >> 8) & 0xFF);
            data[4] = (uint8_t)(index & 0xFF);
            data[5] = (uint8_t)(raw_value & 0xFF);
            frameLen = 6;
        }
    } else {
        uint16_t uval = (uint16_t)raw_value;
        if (!extended) {
            // kurzer Index, 2-Byte Wert
            data[2] = (uint8_t)(index & 0xFF);
            data[3] = (uint8_t)((uval >> 8) & 0xFF);
            data[4] = (uint8_t)(uval & 0xFF);
            frameLen = 5;
        } else {
            // erweiterter Index, 2-Byte Wert
            data[2] = 0xFA;
            data[3] = (uint8_t)((index >> 8) & 0xFF);
            data[4] = (uint8_t)(index & 0xFF);
            data[5] = (uint8_t)((uval >> 8) & 0xFF);
            data[6] = (uint8_t)(uval & 0xFF);
            frameLen = 7;
        }
    }

    frameOut = CANFrame(can_id, data, (uint8_t)frameLen);
    return true;
}

// ======================== CAN Callback ========================
void onReceive(CANController&, CANFrame frame) {
    int next = (frameWriteIdx + 1) % FRAME_QUEUE_SIZE;
    if(next != frameReadIdx) {
        frameQueue[frameWriteIdx] = frame;
        frameWriteIdx = next;
    }
}

void onWakeup(CANController& controller) {
    controller.setMode(CANController::Mode::Normal);
}

// ======================== MQTT ========================
void connectMQTT() {
    while (!client.connected()) {
        client.setServer(mqtt_server, mqtt_port);

        char lwtTopic[64];
        snprintf(lwtTopic, sizeof(lwtTopic), "%s/statusESP", hostname);

        if(client.connect("CAN_MQTT_Client", mqtt_user, mqtt_password, lwtTopic, 0, true, "offline")) {

            client.setCallback(callback);

            // LWT / Online-Status setzen
            client.publish(lwtTopic, "online", true);

            // CAN-Daten Topic abonnieren
            char canSendTopic[64];
            snprintf(canSendTopic, sizeof(canSendTopic), "%s/canDataSend", hostname);
            client.subscribe(canSendTopic);

            // RSSI senden
            int32_t rssi = WiFi.RSSI();
            char rssiTopic[64];
            snprintf(rssiTopic, sizeof(rssiTopic), "%s/espRSSI", hostname);
            char rssiStr[8];
            snprintf(rssiStr, sizeof(rssiStr), "%ld", rssi);
            client.publish(rssiTopic, rssiStr, true);
        }
    }
}

// ======================== MQTT Callback ========================
void callback(char* topic, byte* payload, unsigned int length) {
    String message;
    message.reserve(length + 1);
    for (unsigned int i = 0; i < length; i++) message += (char)payload[i];

    StaticJsonDocument<384> doc;
    DeserializationError err = deserializeJson(doc, message);
    if (err) return;

    // Erwartetes Format: { "name": "ECOSOLLTEMP", "value": 20.6 }
    if (!doc.containsKey("name") || !doc.containsKey("value")) return;

    const char* name = doc["name"];
    double value = doc["value"].as<double>();

    const ElsterIndex* ei = GetElsterIndexByName(name);
    if (!ei) {
        AckMeldung = "unknown name";
        AckSend = true;
        return;
    }

    CANFrame frame;
    if (!prepareCanFrame(ei, value, frame)) {
        AckMeldung = "frame build failed";
        AckSend = true;
        return;
    }

    CANController::IOResult result = CAN.write(frame);
    AckMeldung = (result == CANController::IOResult::OK) ? "write OK" : "write Failed";
    AckSend = true;
}


// ======================== CAN-Frame auswerten ========================
void processCanFrame(CANFrame &frame) {
    if(frame.getDlc() < 3) return; // mindestens 3 Bytes notwendig

    const uint8_t* data = frame.getData();
    uint16_t index = 0;
    uint16_t raw_value = 0;
    uint8_t pos = 0;

    // Index bestimmen
    if(data[2] == 0xFA) { // erweiterter Index
        if(frame.getDlc() < 5) return;
        index = ((uint16_t)data[3] << 8) | data[4];
        pos = 5; // Nutzdaten beginnen hier
    } else { // normaler Index
        index = data[2];
        pos = 3; // Nutzdaten beginnen hier
    }

    const ElsterIndex* ei = GetElsterIndex(index);

    // Raw Value auslesen (für 2-Byte Werte), sonst nur 1 Byte
    if (ei && ei->Type == et_status) {
        if (frame.getDlc() > pos) raw_value = data[pos];
        else return;
    } else {
        if (frame.getDlc() > pos + 1) raw_value = (data[pos] << 8) | data[pos + 1];
        else return;
    }

    // JSON vorbereiten
    StaticJsonDocument<512> doc;
    doc["can_id"] = String(frame.getId(), HEX);
    char indexStr[7]; sprintf(indexStr, "0x%04x", index);
    doc["index"] = indexStr;
    doc["raw_value"] = raw_value;

    String hexStr;
    for(int i=0;i<frame.getDlc();i++){
        if(i>0) hexStr+=" ";
        if(data[i]<0x10) hexStr+="0";
        hexStr += String(data[i], HEX);
    }
    doc["hex"] = hexStr;

    char buffer[256]; 
    char topic[64];

    if(ei) {
        double value = 0.0;
        switch(ei->Type){
            case et_uint16_val: value = raw_value; break;
            case et_dec_signed_10: value = ((int16_t)raw_value)/10.0; break;
            case et_dec_signed_100: value = ((int16_t)raw_value)/100.0; break;
            case et_status: value = raw_value; break; // 1 Byte direkt
            default: value = raw_value; break;
        }
        doc["name"] = ei->Name;
        doc["value"] = value;
        if(ei->Unit) doc["unit"] = ei->Unit;

        serializeJson(doc, buffer);
        snprintf(topic, sizeof(topic), "%s/canData/%lX", hostname, frame.getId());
        client.publish(topic, buffer, false);
    } else {
        // unbekannter Index
        serializeJson(doc, buffer);
        snprintf(topic, sizeof(topic), "%s/canUnbekannt", hostname);
        client.publish(topic, buffer, false);
    }
}

// ======================== Abfragen ========================
void sendElsterListe(uint16_t idx) {
        uint8_t data[6]; // max 5 Bytes Index + 1 optionales Statusbyte
        uint8_t dlc = 0;

        if(idx <= 0xFF){
            data[0] = 0x31;
            data[1] = 0x00;
            data[2] = (uint8_t)(idx & 0xFF);
            dlc = 3;
        } else {
            data[0] = 0x31;
            data[1] = 0x00;
            data[2] = 0xFA;
            data[3] = (uint8_t)(idx >> 8);   // IndexHigh
            data[4] = (uint8_t)(idx & 0xFF); // IndexLow
            dlc = 5;
        }
        CANFrame frame(0x680, data, dlc); // CAN-ID 0x680 für Abfrage
        CAN.write(frame);
}

// ======================== Setup ========================
void setup() {
    Serial.begin(115200);
    delay(500);

    // WiFi-Verbindung
    WiFi.setHostname(hostname);
    WiFi.begin(ssid, password);
    while(WiFi.status() != WL_CONNECTED) { 
        delay(500); 
    }
 
    // MQTT-Verbindung aufbauen
    connectMQTT();

    // CAN-Controller initialisieren
    while(CAN.begin(CANController::Mode::Config) != CANController::OK) { 
        delay(1000); 
    }

    // CAN-Filter setzen
    CAN.setFiltersRxb0(0x180, 0x180, 0x07ff, false);
    CAN.setFiltersRxb1(0x180, 0x180, 0x180, 0x180, 0x07ff, false);
    CAN.setFilters(true);
	
    // CAN-Modus und Interrupts
    CAN.setMode(CANController::Mode::Normal);
    CAN.setInterruptCallbacks(&onReceive, &onWakeup);

    // OTA-Setup
    ArduinoOTA.setHostname(hostname);
    ArduinoOTA.setPassword(mqtt_password);
    ArduinoOTA.onStart([]() {
        otaActive = true;   // OTA läuft
    });
    ArduinoOTA.onEnd([]() {
        otaActive = false;  // OTA beendet
    });
    ArduinoOTA.onError([](ota_error_t error) {
        otaActive = false;
    });
    ArduinoOTA.begin();
}

// ======================== Loop ========================
void loop() {
    // MQTT-Verbindung prüfen und Loop aufrufen
    connectMQTT();
    client.loop();

    // OTA prüfen
    ArduinoOTA.handle();

    if (!otaActive) {

        // CAN-Frames verarbeiten
        while(frameReadIdx != frameWriteIdx){
            CANFrame frame = frameQueue[frameReadIdx];
            frameReadIdx = (frameReadIdx + 1) % FRAME_QUEUE_SIZE;
            processCanFrame(frame);
        }

        // Gestaffelte Abfrage alle 735ms
        if (millis() - lastQueryMillis >= 735) {
			sendElsterListe(ElsterListe[listPos].Index);
			listPos++;
			if (listPos >= elsterListeSize) {
				listPos = 36;
			}
			lastQueryMillis = millis();
		}

        // MQTT Ack senden
        if(AckSend){
            char ackTopic[64];
            snprintf(ackTopic,sizeof(ackTopic),"%s/canACK",hostname);
            client.publish(ackTopic, AckMeldung.c_str(), false);
            AckSend = false;
        }
    } else {
        // OTA läuft
        static unsigned long lastOTAPrint = 0;
        if(millis() - lastOTAPrint > 2000) { // alle 2 Sekunden
            lastOTAPrint = millis();
        }
    }
}
VG Rudi

Antworten

Zurück zu „CCU.IO“