ihormelnyk / opentherm_library

Arduino/ESP8266/ESP32 OpenTherm Library for HVAC system control communication
MIT License
208 stars 92 forks source link

Querying/Controlling ventilation system #10

Open arnolde opened 5 years ago

arnolde commented 5 years ago

I'm trying to query/set my Wolf CWL300 (also known as Viessmann Vitovent etc.) home ventilation system via opentherm. Do you have any experience which parameters I can use, or any example code?

ihormelnyk commented 5 years ago

Hi @arnolde , This article contains link to OpenTherm specification http://ihormelnyk.com/opentherm_library I believe you can use mandatory commands, like read/write status (activate/deactivate ventilation) and set temperature(control setpoint). As sample you can use this script http://ihormelnyk.com/opentherm_adapter, use enableCooling parameter

arnolde commented 5 years ago

If I try this code (based on your example):

    Serial.println("Setting boiler status...");
    unsigned long response = ot.setBoilerStatus(enableCooling);
    Serial.println("Getting response...");
    OpenThermResponseStatus responseStatus = ot.getLastResponseStatus();

Then I get:

Setting boiler status...
Request: 80000100
Getting response...
Error: Invalid response f0000000
Request: 80190000
Boiler temperature is 0.00 degrees C

Is there any more example code anywhere, so I can see a few more functions?

ihormelnyk commented 5 years ago

enableCooling have to be third parameter:

bool enableCentralHeating = false;
bool enableHotWater = false;
bool enableCooling = true;
unsigned long response = ot.setBoilerStatus(enableCentralHeating, enableHotWater, enableCooling);

arnolde commented 5 years ago

That still returns: Invalid response f0000000 I'm not sure my opentherm communication is working at all, I have no opentherm master device to check. The only clue is, if I disconnect either the in or out pin, I get "timeout". Does the f0000000 prove that the slave is actually answering in a valid format? Are there other commands I can send for testing, i.e. query slave configuration? I'm not sure how to to that (send a packet with READ_DATA=0, id 3 = slave config flags, and data=0)? But how exactly? Do I need to use buildRequest? And then what? The actual parameter I probably want to read & write is "relative modulation level" (17). At least thats the only thing I can find in the docs that sounds to me like it could control a fan speed.

My complete code at the moment:

#include <Arduino.h>
#include <OpenTherm.h>

const int inPin = 12; // GPIO## pin number, grey
const int outPin = 13; // purple 
OpenTherm ot(inPin, outPin);

void handleInterrupt() {
    ot.handleInterrupt();
}

void setup()
{
    Serial.begin(115200);
    Serial.println("Start");

    ot.begin(handleInterrupt);
    Serial.println("Interrupt handler setup OK.");
}

void loop()
{   
    //Set/Get Boiler Status
    bool enableCentralHeating = false;
    bool enableHotWater = false;
    bool enableCooling = true;
    Serial.println("Setting boiler status...");
    unsigned long response = ot.setBoilerStatus(enableCentralHeating, enableHotWater, enableCooling);
    Serial.println("Getting response...");
    OpenThermResponseStatus responseStatus = ot.getLastResponseStatus();
    if (responseStatus == OpenThermResponseStatus::SUCCESS) {       
        Serial.println("Central Heating: " + String(ot.isCentralHeatingEnabled(response) ? "on" : "off"));
        Serial.println("Hot Water: " + String(ot.isHotWaterEnabled(response) ? "on" : "off"));
//      Serial.println("Flame: " + String(ot.isFlameOn(response) ? "on" : "off"));
        Serial.println("Cooling: " + String(ot.isCoolingEnabled(response) ? "on" : "off"));
    }
    if (responseStatus == OpenThermResponseStatus::NONE) {
        Serial.println("Error: OpenTherm is not initialized");
    }
    else if (responseStatus == OpenThermResponseStatus::INVALID) {
        Serial.println("Error: Invalid response " + String(response, HEX));
    }
    else if (responseStatus == OpenThermResponseStatus::TIMEOUT) {
        Serial.println("Error: Response timeout");
    }

    //Set Boiler Temperature to 64 degrees C
//  ot.readState();
//  req = ot.buildRequest(0, 17, 0); // READ-DATA, 
//  ot.sendRequestAync(req);

    //Get Boiler Temperature
    float temperature = ot.getBoilerTemperature();
    Serial.println("Boiler temperature is " + String(temperature) + " degrees C");  

    Serial.println();
    delay(1000);
}
phenotypic commented 5 years ago

Hi @arnolde ,

I'm not sure of how much help I can be but I do have recent experience writing my own 'smart' thermostat for my home using this library (linked here).

For me, it looks like your biggest problem at the moment is the fact that you are unable to interface correctly with the device. I was having this problem at first and it turned out that the problem was simply the fact that the pins I had allocated for communication were incapable of functioning for this purpose. What type of Arduino/Arduino-compatible do you have? Maybe you could try using different pins; maybe try 2 and 3? If this doesn't work, maybe the issue is with the Opentherm adapter you have. Indeed, there are many other reasons why the interfacing may not be working but this is at least a start.

For now, I would recommend trying to get your Arduino to interface correctly with OpenTherm before moving onwards; after you have an established connection, experimentation will be much easier. It looks like your current script above will do fine for testing your connection.

As for interfacing with your 'Wolf CWL300', I was wondering exactly what purpose this serves in your home as I have no experience with this device. I.e. does it function as a heater, cooler or both? Do you have a boiler in your home? If so, is this connected to your 'Wolf' device? What exactly is your utility set-up? The answers to these questions should help determine what exactly you need to achieve in terms of interfacing your Arduino with your OpenTherm instance.

As you know, the 4 main 'functions' of the library are: enableCentralHeating, enableHotWater, enableCooling and ot.setBoilerTemperature. I would hazard a guess that your hot water (for taps) is not controlled by this Wolf device and therefore we can basically ignore enableHotWater (leave it as true). If your device supports both heating and cooling, the other two enable functions are the relevant ones. When you want to heat your home, you will want to set enableCentralHeating to true and enableCooling to false; the same is true in reverse for when you want to cool your home. I saw you noted the relative modulation level feature of the Opentherm protocol; I feel this is not necessary for your use case and we can stick with the ot.setBoilerTemperature function of the library. Whilst the name may be misleading "setBoilerTemperature" I think this can be used to control the temperature of any OpenTherm device, whether it is a boiler or not. As a result, I think that your device should handle most things past you simply setting the desired temperature. When you want to heat your home, use a PID Controller algorithm to set the right temperature (my thermostat linked above includes one). However, as for setting the cooling temperature of your home, I have no experience doing so because my house only has a boiler but I would assume that you either simply set this to the desired final temperature or a sort of inverse boiler PID Controller.

Hopefully this helped you in some way. If you have any other questions, feel free to ask!

Kind regards, Tom

arnolde commented 5 years ago

Hi Tom,

The Wolf CWL300 is neither a heating nor a cooling device, its a home ventilation system with a heat exchanger, to constantly provide the home with fresh air and expel used air, while transferring most of the warmth from the exhaust to the fresh air, so you don't bring cold air inside in winter. So the only setting it has is fan rpm (actually m3/h ranging from 1 to 300). It does also have 2 temp sensors that might be queryable but it does not control any temperature. I am using an ESP8266 to control it, and can only use certain GPIO pins. I agree that maybe thats one of the problems, thats why I asked for the leanest way to get a valid response fron the slave, without trying to set any parameters. Anf if the f0000000 i'm getting is really coming from the slave or could that be a communications error? BTW I'm using Ihor's own opentherm interface, bought directly from him.

Regards, Ethan

phenotypic commented 5 years ago

Ah I see, unfortunately I'm not aware of how you'd interface with such a device using the library. Indeed, whilst I'm sure it is possible with the OpenTherm protocol, you can see at the bottom of this file that the functions are rather limited in this library and I see no mention of fan speed control. It's most likely something you'll have to code in yourself.

As for the ESP8266, I would recommend using pin 4 (D2) for input and pin 5 (D1) for output as this worked for me using an ESP8266-based board. Also just check that the ESP in goes to the interface out and vice versa. Again, I understand that you're trying to find the simplest way of checking for a valid connection but I think that the steps listed in your original script will give you the most information because whilst using float temperature = ot.getBoilerTemperature(); would be the shortest method, it may not work on your system nor may it provide much insight about the connection status.

Best, Tom

ihormelnyk commented 5 years ago

Hi @arnolde , Here is an article how to build custom request: http://ihormelnyk.com/opentherm_library Also there is link to protocol specification

Use buildRequest, sendRequest and getLastResponseStatus functions. Try to check some commands related to the cooling.

arnolde commented 5 years ago

Hi @ihormelnyk , thank you again for pointing me to the custom request example. I hope I understand it now, but unfortunately I am apparently still not getting a reasonable result back from my ventilation.

Here is the output of my loop:

Reading slave status: Request: ffff
Response: f0000000
Setting boiler status...
Request: 80000400
Response: f0000000
Error: Invalid response f0000000
Setting cooling level to max: Request: 90076400
Response: f0076400
Error: Invalid response f0076400
Reading mod level: Request: 11ffff
Response: f0110000
Getting response: Error: Invalid response f0110000

And here the code: (sorry it's so messy, but I got very frustrated at one point, it really needs cleaning up):

#include <Arduino.h>
#include <OpenTherm.h>

const int inPin = 4; // 12; // GPIO## pin number, grey
const int outPin = 5; // 13; // purple 
OpenTherm ot(inPin, outPin);

void handleInterrupt() {
    ot.handleInterrupt();
}

void setup()
{
    Serial.begin(115200);
    Serial.println("Start");

    ot.begin(handleInterrupt);
    Serial.println("Interrupt handler setup OK.");
}

unsigned int data = 0xFFFF;

void loop()
{   

    // Get Slave Config
    unsigned long getSlaveConfig = ot.buildRequest(
      OpenThermRequestType::READ,
      OpenThermMessageID::Status,
      data);
    Serial.print("Reading slave status: ");
    unsigned long response3 = ot.sendRequest(getSlaveConfig);
    Serial.println("Response: " + String(response3, HEX));

    //Set/Get Boiler Status
    bool enableCentralHeating = false;
    bool enableHotWater = false;
    bool enableCooling = true;
    Serial.println("Setting boiler status...");
    unsigned long response = ot.setBoilerStatus(enableCentralHeating, enableHotWater, enableCooling);
    Serial.println("Response: " + String(response, HEX));
    OpenThermResponseStatus responseStatus = ot.getLastResponseStatus();
    if (responseStatus == OpenThermResponseStatus::SUCCESS) {       
        Serial.println("Central Heating: " + String(ot.isCentralHeatingEnabled(response) ? "on" : "off"));
        Serial.println("Hot Water: " + String(ot.isHotWaterEnabled(response) ? "on" : "off"));
//      Serial.println("Flame: " + String(ot.isFlameOn(response) ? "on" : "off"));
        Serial.println("Cooling: " + String(ot.isCoolingEnabled(response) ? "on" : "off"));
    }
    if (responseStatus == OpenThermResponseStatus::NONE) {
        Serial.println("Error: OpenTherm is not initialized");
    }
    else if (responseStatus == OpenThermResponseStatus::INVALID) {
        Serial.println("Error: Invalid response " + String(response, HEX));
    }
    else if (responseStatus == OpenThermResponseStatus::TIMEOUT) {
        Serial.println("Error: Response timeout");
    }

// Set 100% cooling level
   unsigned int cool100 = 0x6400;
   unsigned long request4 = ot.buildRequest(
      OpenThermRequestType::WRITE,
      OpenThermMessageID::CoolingControl,
      cool100);
    Serial.print("Setting cooling level to max: ");
    unsigned long response4 = ot.sendRequest(request4);
    Serial.println("Response: " + String(response4, HEX));

    OpenThermResponseStatus responseStatus4 = ot.getLastResponseStatus();
    if (responseStatus4 == OpenThermResponseStatus::SUCCESS) {      
        Serial.println("Success.");
    }
    if (responseStatus4 == OpenThermResponseStatus::NONE) {
        Serial.println("Error: OpenTherm is not initialized");
    }
    else if (responseStatus4 == OpenThermResponseStatus::INVALID) {
        Serial.println("Error: Invalid response " + String(response4, HEX));
    }
    else if (responseStatus4 == OpenThermResponseStatus::TIMEOUT) {
        Serial.println("Error: Response timeout");
    }

  //Read Relative Modulation Level:

    unsigned long request = ot.buildRequest(
      OpenThermRequestType::READ,
      OpenThermMessageID::RelModLevel,
      data);
    Serial.print("Reading mod level: ");
    unsigned long response2 = ot.sendRequest(request);
    Serial.println("Response: " + String(response2, HEX));
        Serial.print("Getting response: ");
    OpenThermResponseStatus responseStatus2 = ot.getLastResponseStatus();
    if (responseStatus2 == OpenThermResponseStatus::SUCCESS) {      
        Serial.println("Success.");
    }
    if (responseStatus2 == OpenThermResponseStatus::NONE) {
        Serial.println("Error: OpenTherm is not initialized");
    }
    else if (responseStatus2 == OpenThermResponseStatus::INVALID) {
        Serial.println("Error: Invalid response " + String(response2, HEX));
    }
    else if (responseStatus2 == OpenThermResponseStatus::TIMEOUT) {
        Serial.println("Error: Response timeout");
    }
    Serial.println();
    delay(1000);
}

I tried moving to pins 4+5 and I get basically the same result. If I disconnect either one or switch them around, I get timeouts instaead of "invalid response" so I assume the slave is communicating OK with the master.

I just don't understand why I keep getting an "invalid response", no mattery what request I try.

But I also see the slave reacting: when the interface is connected and running, the ventilation shuts down. If I disconnect the OT interface, it starts up again after a few seconds. So at least it seems to be detecting that there is a master attached, but not recieving the right commands to start ventilating.

EDIT: Even if I just disconnect the "out" pin from ESP to OT-interface, the fans start up. That seems to indicate the ventilator is recognizing some (attempted) communication via OT. But I'm still stumped why it won't acknowledge any of my requests, not even request slave config.

arnolde commented 5 years ago

I just found another little puzzle piece: According to http://otgw.tclcode.com/matrix.cgi#boilers the Viessmann Vitovent 300, which is another label of my Wolf CWL300, only supports the message id's 2,70-72,74,77,80,82 and 89. Unfortunately only #2 is included in the Opentherm 2.2 protocol spec. So that may explain why I'm getting NAK's on most of my messages. I sure wish I could find a newer protocol somewhere...

Aha, on GitHub.com/apdlv72/VitoWifi I found some more clues: 71 = control setpoint, 77=rel ventilation, 80+82 are inlet temperatures, etc... I will continue experimenting...

More info from https://forum.fhem.de/index.php?topic=29762.115;wap2

; New ID for ventilation/heat-recovery applications 70,"STATUS V/H",READ,FLAG,00000000,FLAG,00000000,Yes 71,"CONTROL SETPOINT V/H",WRITE,U8,0,100,0,Yes 72,"FAULT FLAGS/CODE V/H",READ,FLAG,00000000,U8,0,255,0,Yes 73,"DIAGNOSTIC CODE V/H",READ,U16,0,65000,0,Yes 74,"CONFIG/MEMBERID V/H",READ,FLAG,00000000,U8,0,255,0,Yes 75,"OPENTHERM VERSION V/H",READ,F8.8,0,127,"2,32",Yes 76,"VERSION & TYPE V/H",READ,U8,0,255,1,U8,0,255,0,Yes 77,"RELATIVE VENTILATION",READ,U8,0,255,0,Yes 78,"RELATIVE HUMIDITY",READ,U8,0,255,0,Yes 78,"RELATIVE HUMIDITY",WRITE,U8,0,255,0,No 79,"CO2 LEVEL",READ,U16,0,10000,0,Yes 79,"CO2 LEVEL",WRITE,U16,0,10000,0,No 80,"SUPPLY INLET TEMPERATURE",READ,F8.8,0,127,"0,00",Yes 81,"SUPPLY OUTLET TEMPERATURE",READ,F8.8,0,127,"0,00",Yes 82,"EXHAUST INLET TEMPERATURE",READ,F8.8,0,127,"0,00",Yes 83,"EXHAUST OUTLET TEMPERATURE",READ,F8.8,0,127,"0,00",Yes 84,"ACTUAL EXHAUST FAN SPEED",READ,U16,0,10000,0,Yes 85,"ACTUAL INLET FAN SPEED",READ,U16,0,10000,0,Yes 86,"REMOTE PARAMETER SETTINGS V/H",READ,FLAG,00000000,FLAG,0000000,Yes 87,"NOMINAL VENTIALTION VALUE",READ,U8,0,255,0,Yes 87,"NOMINAL VENTIALTION VALUE",WRITE,U8,0,255,0,No 88,"TSP NUMBER V/H",READ,U8,0,255,0,U8,0,0,0,Yes 89,"TSP ENTRY V/H",READ,U8,0,255,0,U8,0,255,0,Yes 89,"TSP ENTRY V/H",WRITE,U8,0,255,0,U8,0,255,0,No 90,"FAULT BUFFER SIZE V/H",READ,U8,0,255,0,U8,0,0,0,Yes 91,"FAULT BUFFER ENTRY V/H",READ,U8,0,255,0,U8,0,255,0,Yes

After adding these values to Ihor's OpenTherm.h I can now write a value for 71 and read a value for 77, although the results don't make much sense yet, also I can read the temperatures 80+82 and they do make sense. So the interface seems to work :-) now I just need to experiment with setting the fan RPM sensibly. Then I can finally complete my goal: Use a DHT humidity sensor in the exhaust pipe to measure the average humidity in the home and adjust the ventilation speed so that it does not go below 40%. Thats my main problem in winter: The heat exchanger makes the winter fresh air pretty dry so when the ventilation runs all day, even on the lowest step (50m3/h) the house dries out too much.

arnolde commented 5 years ago

Almost solved. Reading the slave status seems to trigger some kind of "init" routine on the slave, it blinks "init" in the display with a countdown of 120 seconds. So I removed that altogether and now simply write either 0 or 25...100 to id 71, which turns the fans off (0) or sets them to 20%...95%. Haven't acheived 100% yet but I'm OK for now. I plan to publish the final result on gitlab.org/c2h6 so look there soon if you're interested. Now I just need to add humidity+temp tracked control and I'm all set. Thanks again for this great library, circuit and the help!

EDIT: Here is the link to my project, it's not very nice yet but I will update it now and then. https://gitlab.com/c2h6/esp8266-opentherm

I plan to add wifi, cloud logging to an influxDB instance, and control of fan rpm depending on outside temp and inside humidity.

ihormelnyk commented 5 years ago

Cool! Thanks.

CJNE commented 4 years ago

Hi @arnolde! I have another clone of the Wolf CWL300 called Fresh Renovent M300 that i'd like to integrate with Home Assistant. Your project looks like a good starting point, however it doesn't include the additional message id's that are needed in opentherm.h, any chance you could share your version of that file?