Library for ESP8266 to communicate with Viessmann systems using a (DIY) serial optolink.
Based on the fantastic work on openv.
SoftwareSerial
on ESP8266The optolink hardware for ESP8266 and ESP32 is simple. Using the circuit below you can build your own optolink. Please also check the openv wiki, in German for more implementations.
3.3V
O
|
+-----+-----+
| |
--- ---
| | | |
| | 180Ohm | | 10kOhm
| | | |
--- ---
| |
--- |
SFH487-2 \ / -> |
V -> |
--- |
| |
TX O-------+ |
RX O-------------------+
|
|/ c
-> | SFH309FA
-> |> e
|
-----
---
-
The canonical way to use this library is simple and straightforward. A few steps are involved:
void setup()
void loop()
:
loop()
regularly. It keeps VitoWiFi running.A simple program for ESP32 to test and query your devicetype looks like this:
#include <Arduino.h>
#include <VitoWiFi.h>
VitoWiFi::VitoWiFi<VitoWiFi::VS2> myHeater(&Serial1);
VitoWiFi::Datapoint deviceId("device id", 0x00F8, 2, VitoWiFi::noconv);
void onResponse(const VitoWiFi::PacketVS2& response, const VitoWiFi::Datapoint& request) {
Serial.print("Raw data received:");
const uint8_t* data = response.data();
for (uint8_t i = 0; i < response.dataLength(); ++i) {
Serial.printf(" %02x", data[i]);
}
Serial.print("\n");
}
void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) {
Serial.printf("Datapoint \"%s\" error: ", request.name());
if (error == VitoWiFi::OptolinkResult::TIMEOUT) {
Serial.print("timeout\n");
} else if (error == VitoWiFi::OptolinkResult::LENGTH) {
Serial.print("length\n");
} else if (error == VitoWiFi::OptolinkResult::NACK) {
Serial.print("nack\n");
} else if (error == VitoWiFi::OptolinkResult::CRC) {
Serial.print("crc\n");
} else if (error == VitoWiFi::OptolinkResult::ERROR) {
Serial.print("error\n");
}
}
void setup() {
delay(1000);
Serial.begin(115200);
Serial.print("Setting up vitoWiFi\n");
myHeater.onResponse(onResponse);
myHeater.onError(onError);
myHeater.begin();
Serial.print("Setup finished\n");
}
void loop() {
static uint32_t lastReadTime = 0;
if (millis() - lastReadTime > 30000) {
lastReadTime = millis();
if (myHeater.read(deviceId)) {
Serial.printf("reading \"%s\"\n", deviceId.name());
} else {
Serial.printf("error reading \"%s\"\n", deviceId.name());
}
}
myHeater.loop();
}
Most users will have a collection of datapoints thay want to read. A possible technique to query a large number of datapoints is by simply iterating over them:
// create a collection (array) of datapoints:
VitoWiFi::Datapoint datapoints[] = {
VitoWiFi::Datapoint("outside temp", 0x5525, 2, VitoWiFi::div10),
VitoWiFi::Datapoint("boiler temp", 0x0810, 2, VitoWiFi::div10),
VitoWiFi::Datapoint("pump status", 0x2906, 1, VitoWiFi::noconv)
};
int numberDatapoints = 3;
int currentIndex = -1;
// to start reading, set currentIndex to 0
if (currentIndex > 0) {
// reading will return `true` when successful.
// as long as VitoWiFi is busy it will return `false`
if (myVitoWiFi.read(datapoints[currentIndex])) {
++currentIndex;
if (currentIndex == numberDatapoints) currentIndex = -1;
}
}
you can find more examples in the examples
directory in this repo.
When defining your datapoints, you need to specify the name, address, length and converion type. Datapoints in C++ looks like this:
VitoWiFi::Datapoint datapoint1("outside temp", 0x5525, 2, VitoWiFi::div10);
VitoWiFi::Datapoint datapoint2("boiler temp", 0x0810, 2, VitoWiFi::div10);
VitoWiFi::Datapoint datapoint3("pump status", 0x2906, 1, VitoWiFi::noconv);
It is not possible for me to give you a list of available datapoints for your device. Please consult the openv wiki or the InsideViessmannVitosoft repo.
While name, address and length are self-explanatory, conversion type is a bit more complicated.
Data is stored in binary and often needs a conversion function to transform into a more readable type. This is specified by the conversion type, the last argument in the datapoint definition.
Since C++ is a strongly typed programming language so using the right type is important (read: mandatory). Each conversion type corresponds with a certain type. Reading or writing has to be done using this specific type and failure to do so will not work or will lead to undefined results.
In the table below you can find how to define your datapoints:
name | size | converter | return type | remarks |
---|---|---|---|---|
Temperature | 2 | div10 | float | |
Temperature short | 1 | noconv | uint8_t | equivalent to Mode |
Power | 1 | div2 | float | also used for temperature in GWG |
Status | 1 | noconv | bool | this is the same as 'Temperature short' and 'Mode'. The uint8_t value will be implicitely converted to bool. |
Hours | 4 | div3600 | float | this is in fact a Count datapoint (seconds) converted to hours. |
Count | 4 | noconv | uint32_t | |
Count short | 2 | noconv | uint16_t | |
Mode | 1 | noconv | uint8_t | possibly castable to ENUM |
CoP | 1 | div10 | float | Also used for heating curve slope |
Please use Github's facilities to get in touch. While the issue template is not mandatory to use, please use it at least as a starting point to supply the needed info for bughunting.
Below is an overview of all commonly used methods. For extra functions you can consult the source code.
Datapoint(const char* name, uint16_t address, uint8_t length, const Converter& converter)
Constructor for datapoints.
const char* name() const
uint16_t address() const
uint8_t length() const
Self-explanatory
const Converter& converter() const
Returns the associated converter class. Can be used to select code flow in the callbacks.
VariantValue decode(const PacketVS2& packet) const
Decodes the data in packet
using the converter class attached.
Returns VariantValue
which is implicitely castable to the correct datatype. Consult the table above.
VariantValue decode(const uint8_t* data, uint8_t length) const
Decodes the data in the supplied data
-buffer using the converter class attached.
Returns VariantValue
which is implicitely castable to the correct datatype. Consult the table above.
void encode(uint8_t* buf, uint8_t len, const VariantValue& value) const
Encodes value
into the supplied buf
with maximum size len
. The size should obviously be at least the length of the datapoint.
VariantValue
is a type to implicitely convert datatypes for use in VitoWiFi. Make sure to use the type that matches your Converter type.
PacketVS2
Only used in VS2. This type is used in the onResponse callback and contains the returned data.
Most users will only use the following two methods and only if they want to access the raw data. Otherwise, the data can be decoded using the corresponding Datapoint
.
uint8_t dataLength() const
Returns the number of bytes in the payload.
const uint8_t* data() const
Returns a pointer to the payload.
VitoWiFi<PROTOCOL_VERSION>(IFACE* interface)
Constructor of the VitoWiFi class. PROTOCOL_VERSION
can be GWG
, VS1
or VS2
. If your Viessmann device is somewhat modern, you should use VS2
.
interface
can be any of the HardwareSerial
interfaces (Serial
, Serial1
...) on Arduino boards, SoftwareSerial
(on ESP8266) or if you are on Linux, pass the c-string depicting your device (for example "/dev/ttyUSB0"
).
void onResponse(typename PROTOCOLVERSION::OnResponseCallback callback)
Attach an onResponse callback. You can only attack one and will overwrite the previously attached callback.
The callback has the following signature:
VS1
: void (const uint8_t*, uint8_t, const VitoWiFi::Datapoint&)
VS2
: void (const VitoWiFi::PacketVS2&, const VitoWiFi::Datapoint&)
void onError(typename PROTOCOLVERSION::OnErrorCallback callback)
Attach an onError callback. You can only attack one and will overwrite the previously attached callback.
The callback has the following signature:
void (VitoWiFi::OptolinkResult, const VitoWiFi::Datapoint&)
bool begin()
Start the optolink serial interface. Returns bool on success.
void end()
Stop the optolink serial interface
void loop()
Worker function, must be called regularly. This is the method that polls the serial interface. Callbacks are dispatched from this method so they will run in the same thread/task.
bool read(Datapoint datapoint)
Read datapoint
. Returns true
on success.
bool write(Datapoint datapoint, T value)
Write value
with type T
to datapoint
. Make sure to use the correct type. consult the table with types in the "Datapoints" section.
write(Datapoint datapoint, const uint8_t* data, uint8_t length)
Write the raw data
with length
to datapoint
. Returns true
on success. length
has to match the length of the datapoint.
VitoWiFi::OptolinkResult
Used in the onError callback. Possible returned values are:
VW_START_PAYLOAD_LENGTH
This macro sets the initial payload (data) length VitoWiFi allocates for incoming packets. If you know beforehand the maximum data length you are going to request, you can set this to that value to prevent reallocation of dynamic memory. The default is 10 bytes.
Please use Github's facilities, issues and discussions, to get in touch. When creating a bug report, please use the provided template. In any case, better to include too much info than too little.
Copyright (c) 2017, 2023 Bert Melis
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.