Skip to content

Commit 805f113

Browse files
authored
Conv schedule
- add conversion of schedules - cleanup readme a bit
1 parent de9d7b6 commit 805f113

File tree

9 files changed

+241
-43
lines changed

9 files changed

+241
-43
lines changed

README.md

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# VitoWiFi
22

3-
Library for ESP8266 to communicate with Viessmann systems using a (DIY) serial optolink.
3+
Library for ESP32, ESP8266 and Linux to communicate with Viessmann systems using a (DIY) serial optolink.
44

55
Based on the fantastic work on [openv](https://github.com/openv/openv/wiki).
66

@@ -10,7 +10,7 @@ Based on the fantastic work on [openv](https://github.com/openv/openv/wiki).
1010

1111
## Features
1212

13-
- VS1 (KW) and VS2 (P300) support. Older systems using the GWG protocol are not supported
13+
- VS1 (KW) and VS2 (P300) support. The older GWG protocol is also supported.
1414
- Non-blocking API calls
1515
- For the Arduino framework and POSIX systems (Linux, tested on a Raspberry Pi 1B)
1616
- Possible to use `SoftwareSerial` on ESP8266
@@ -32,7 +32,7 @@ Based on the fantastic work on [openv](https://github.com/openv/openv/wiki).
3232

3333
## Hardware
3434

35-
The optolink hardware for ESP8266 and ESP32 is simple. Using the circuit below you can build your own optolink.
35+
The optolink hardware can be really simple. Using the circuit below you can build your own optolink.
3636
Please also check the [openv wiki, in German](https://github.com/openv/openv/wiki/Die-Optolink-Schnittstelle) for more implementations.
3737

3838
```
@@ -75,7 +75,7 @@ The canonical way to use this library is simple and straightforward. A few steps
7575
- attach the callbacks
7676
- start VitoWiFi
7777
5. in `void loop()`:
78-
- call `loop()` regularly. It keeps VitoWiFi running.
78+
- call `loop()` regularly. It keeps VitoWiFi running. Ideally it is called more than once every 10ms.
7979

8080
A simple program for ESP32 to test and query your devicetype looks like this:
8181

@@ -160,11 +160,11 @@ if (currentIndex > 0) {
160160

161161
### More examples
162162

163-
you can find more examples in the `examples` directory in this repo.
163+
You can find more examples in the `examples` directory in this repo.
164164

165165
## Datapoints
166166

167-
When defining your datapoints, you need to specify the name, address, length and converion type. Datapoints in C++ looks like this:
167+
When defining your datapoints, you need to specify the name, address, length and conversion type. Datapoints in C++ looks like this:
168168

169169
```cpp
170170
VitoWiFi::Datapoint datapoint1("outside temp", 0x5525, 2, VitoWiFi::div10);
@@ -178,24 +178,26 @@ While name, address and length are self-explanatory, conversion type is a bit mo
178178
179179
### Conversion types
180180
181-
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.
181+
Data is stored in binary and often needs a conversion function to transform into a more usable type. This is specified by the conversion type, the last argument in the datapoint definition.
182182
183-
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.
183+
C++ is a strongly typed programming language so using the right type is important (read: mandatory). Each conversion type corresponds to 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.
184184
185185
In the table below you can find how to define your datapoints:
186186
187187
|name|size|converter|return type|remarks|
188188
|---|---|---|---|---|
189189
|Temperature|2|div10|float||
190-
|Temperature short|1|noconv|uint8_t|equivalent to Mode|
191-
|Power|1|div2|float|also used for temperature in GWG|
192-
|Status|1|noconv|bool|this is the same as 'Temperature short' and 'Mode'. The `uint8_t` value will be implicitely converted to bool.|
193-
|Hours|4|div3600|float|this is in fact a `Count` datapoint (seconds) converted to hours.|
190+
|Temperature short|1|noconv|uint8_t|Equivalent to Mode|
191+
|Power|1|div2|float|Also used for temperature in GWG|
192+
|Status|1|noconv|bool|This is the same as 'Temperature short' and 'Mode'. The `uint8_t` value will be implicitely converted to bool.|
193+
|Hours|4|div3600|float|This is in fact a `Count` datapoint (seconds) converted to hours.|
194194
|Count|4|noconv|uint32_t||
195195
|Count short|2|noconv|uint16_t||
196-
|Mode|1|noconv|uint8_t|possibly castable to ENUM|
196+
|Mode|1|noconv|uint8_t|Possibly castable to ENUM|
197197
|CoP|1|div10|float|Also used for heating curve slope|
198198
199+
Mind that the converters are declared within the `VitoWiFi` namespace.
200+
199201
## Bugs and feature requests
200202
201203
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.
@@ -204,9 +206,9 @@ Please use Github's facilities to get in touch. While the issue template is not
204206
205207
Below is an overview of all commonly used methods. For extra functions you can consult the source code.
206208
207-
### Datapoint
209+
### `VitoWiFi::Datapoint`
208210
209-
##### `Datapoint(const char* name, uint16_t address, uint8_t length, const Converter& converter)`
211+
##### `VitoWiFi::Datapoint(const char* name, uint16_t address, uint8_t length, const Converter& converter)`
210212
211213
Constructor for datapoints.
212214
@@ -227,16 +229,16 @@ Returns `VariantValue` which is implicitely castable to the correct datatype. Co
227229
228230
##### `VariantValue decode(const uint8_t* data, uint8_t length) const`
229231
230-
Decodes the data in the supplied `data`-buffer using the converter class attached.
232+
Decodes the data in the supplied `data`-buffer using the Converter class attached.
231233
Returns `VariantValue` which is implicitely castable to the correct datatype. Consult the table above.
232234
233235
##### `void encode(uint8_t* buf, uint8_t len, const VariantValue& value) const`
234236
235-
Encodes `value` into the supplied `buf` with maximum size `len`. The size should obviously be at least the length of the datapoint.
237+
Encodes `value` into the supplied `buf` with maximum size `len`. The size must be at least the length of the datapoint.
236238
237239
`VariantValue` is a type to implicitely convert datatypes for use in VitoWiFi. Make sure to use the type that matches your Converter type.
238240
239-
### `PacketVS2`
241+
### `VitoWiFi::PacketVS2`
240242
241243
Only used in VS2. This type is used in the onResponse callback and contains the returned data.
242244
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`.
@@ -249,25 +251,27 @@ Returns the number of bytes in the payload.
249251
250252
Returns a pointer to the payload.
251253
252-
### VitoWiFi
254+
### `VitoWiFi::VitoWiFi`
253255
254-
##### `VitoWiFi<PROTOCOL_VERSION>(IFACE* interface)`
256+
##### `VitoWiFi::VitoWiFi<PROTOCOL_VERSION>(IFACE* interface)`
255257
256-
Constructor of the VitoWiFi class. `PROTOCOL_VERSION` can be `GWG`, `VS1` or `VS2`. If your Viessmann device is somewhat modern, you should use `VS2`.
258+
Constructor of the VitoWiFi class. `PROTOCOL_VERSION` can be `VitoWiFi::GWG`, `VitoWiFi::VS1` or `VitoWiFi::VS2`. If your Viessmann device is somewhat modern, you should use `VitoWiFi::VS2`.
257259
`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"`).
258260
259261
##### `void onResponse(typename PROTOCOLVERSION::OnResponseCallback callback)`
260262
261-
Attach an onResponse callback. You can only attack one and will overwrite the previously attached callback.
263+
Attach an onResponse callback. You can only attach one and will overwrite the previously attached callback.
262264
The callback has the following signature:
263-
`VS1`: `void (const uint8_t*, uint8_t, const VitoWiFi::Datapoint&)`
264-
`VS2`: `void (const VitoWiFi::PacketVS2&, const VitoWiFi::Datapoint&)`
265+
266+
- `VitoWiFi::VS1`: `void (const uint8_t*, uint8_t, const VitoWiFi::Datapoint&)`
267+
- `VitoWiFi::VS2`: `void (const VitoWiFi::PacketVS2&, const VitoWiFi::Datapoint&)`
265268
266269
##### `void onError(typename PROTOCOLVERSION::OnErrorCallback callback)`
267270
268271
Attach an onError callback. You can only attack one and will overwrite the previously attached callback.
269272
The callback has the following signature:
270-
`void (VitoWiFi::OptolinkResult, const VitoWiFi::Datapoint&)`
273+
274+
- `void (VitoWiFi::OptolinkResult, const VitoWiFi::Datapoint&)`
271275
272276
##### `bool begin()`
273277
@@ -287,7 +291,7 @@ Read `datapoint`. Returns `true` on success.
287291
288292
##### `bool write(Datapoint datapoint, T value)`
289293
290-
Write `value` with type `T` to `datapoint`. Make sure to use the correct type. consult the table with types in the "Datapoints" section.
294+
Write `value` with type `T` to `datapoint`. Make sure to use the correct type. Consult the table with types in the "Datapoints" section.
291295
292296
##### `write(Datapoint datapoint, const uint8_t* data, uint8_t length)`
293297
@@ -308,11 +312,11 @@ Used in the onError callback. Possible returned values are:
308312
309313
##### `VW_START_PAYLOAD_LENGTH`
310314
311-
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.
315+
This macro sets the initial payload (data) length for incoming packets. VitoWiFi will increased the buffer if needed. If you know the maximum data length you are going to request beforehand, use this set to prevent dynamic memory reallocation. The default is 10 bytes.
312316
313317
## Bugs and feature requests
314318
315-
Please use Github's facilities, issues and discussions, to get in touch.
319+
Please use Githubs facilities, issues and discussions, to get in touch.
316320
When creating a bug report, please use the provided template. In any case, better to include too much info than too little.
317321
318322
## License
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
Copyright (c) 2023 Bert Melis. All rights reserved.
3+
4+
This work is licensed under the terms of the MIT license.
5+
For a copy, see <https://opensource.org/licenses/MIT> or
6+
the LICENSE file.
7+
*/
8+
9+
#include "ConversionHelpers.h"
10+
11+
bool isDigit(const char c) {
12+
return c >= '0' && c <= '9';
13+
}
14+
15+
namespace VitoWiFi {
16+
17+
std::size_t encodeSchedule(const char* schedule, std::size_t len, uint8_t* output) {
18+
enum ScheduleParserStep {
19+
Hours1,
20+
Hours2,
21+
Minutes1,
22+
Minutes2,
23+
Space,
24+
};
25+
26+
std::memset(output, 0x00, 8);
27+
28+
std::size_t i = 0 - 1; // first operation in iteration is ++i
29+
std::size_t scheduleIndex = 0;
30+
ScheduleParserStep step = ScheduleParserStep::Hours1;
31+
unsigned int hour = 0;
32+
unsigned int minutes = 0;
33+
34+
while (++i < len) {
35+
if (step == ScheduleParserStep::Hours1) {
36+
if (isDigit(schedule[i])) {
37+
hour = schedule[i] - '0';
38+
step = ScheduleParserStep::Hours2;
39+
continue;
40+
}
41+
} else if (step == ScheduleParserStep::Hours2) {
42+
if (isDigit(schedule[i])) {
43+
hour = hour * 10 + (schedule[i] - '0');
44+
continue;
45+
} else if (schedule[i] == ':') {
46+
step = ScheduleParserStep::Minutes1;
47+
continue;
48+
}
49+
} else if (step == ScheduleParserStep::Minutes1) {
50+
if (isDigit(schedule[i])) {
51+
minutes = schedule[i] - '0';
52+
step = ScheduleParserStep::Minutes2;
53+
continue;
54+
}
55+
} else if (step == ScheduleParserStep::Minutes2) {
56+
if (schedule[i] == '0') {
57+
minutes = minutes * 10 + (schedule[i] - '0');
58+
// parsing is possibly complete
59+
if (hour <= 23 || minutes <= 59) {
60+
output[scheduleIndex] = (0xF8 & hour << 3) | minutes / 10;
61+
++scheduleIndex;
62+
step = ScheduleParserStep::Space;
63+
continue;
64+
}
65+
}
66+
} else { // step == ScheduleParserStep::Space
67+
if (schedule[i] == ' ') {
68+
step = ScheduleParserStep::Hours1;
69+
continue;
70+
}
71+
}
72+
return 0;
73+
}
74+
if (scheduleIndex % 2 == 0 && step == ScheduleParserStep::Space) {
75+
// TODO(bertmelis): hours have to be ordered
76+
return (scheduleIndex + 1) / 2;
77+
}
78+
return 0;
79+
}
80+
81+
std::size_t encodeSchedule(const char* schedule, uint8_t* output) {
82+
return encodeSchedule(schedule, strlen(schedule), output);
83+
}
84+
85+
std::size_t decodeSchedule(const uint8_t* data, std::size_t len, char* output, std::size_t maxLen) {
86+
assert(len == 8);
87+
assert(maxLen >= 48); // 8 times 07:30, 7 spaces and 0-terminator --> 8 * 5 + 7 * 1 + 1
88+
89+
std::size_t pos = 0;
90+
for (std::size_t i = 0; i < 8; ++i) {
91+
unsigned int hour = data[i] >> 3;
92+
unsigned int minutes = (data[i] & 0x07) * 10;
93+
if (hour > 23 || minutes > 59) {
94+
hour = 0;
95+
minutes = 0;
96+
}
97+
int result = snprintf(&output[pos], maxLen - pos, "%.2u:%.2u", hour, minutes);
98+
if (result < 0) return 0;
99+
pos += result;
100+
if (i < 7) {
101+
output[pos++] = ' ';
102+
}
103+
}
104+
return pos + 1; // include 0-terminator
105+
}
106+
107+
} // end namespace VitoWiFi

src/Datapoint/ConversionHelpers.h

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright (c) 2025 Bert Melis. All rights reserved.
3+
4+
This work is licensed under the terms of the MIT license.
5+
For a copy, see <https://opensource.org/licenses/MIT> or
6+
the LICENSE file.
7+
*/
8+
9+
#pragma once
10+
11+
#include <cassert>
12+
#include <cstdint>
13+
#include <cstddef>
14+
#include <cstring> // strlen
15+
#include <cstdio> // snprintf
16+
17+
namespace VitoWiFi {
18+
19+
/*
20+
Encodes a c-string containing a schedule to an 8-byte schedule sequence.
21+
Returns the number of pairs encoded
22+
Per byte:
23+
- b7-b3 hour
24+
- b2-b1 minute * 10
25+
26+
Output needs to be at least 8 bytes
27+
28+
A scedule consists of time pairs with minutes rounded to a multiple of 10min.
29+
Hours can be with leading zero or not.
30+
31+
Valid:
32+
- 7:30 8:30 15:00 23:50
33+
- 07:30 8:30 15:00 23:50
34+
35+
Invalid:
36+
- time not specified in pairs
37+
- (hours not ordered earliest to latest)
38+
- other formatting than colons or spaces
39+
- minutes not rounded to multiples of 10
40+
- whitespace not trimmed
41+
*/
42+
std::size_t encodeSchedule(const char* schedule, std::size_t len, uint8_t* output);
43+
std::size_t encodeSchedule(const char* schedule, uint8_t* output);
44+
45+
/*
46+
Decodes a byte series to a human-readable schedule consisting of time pairs.
47+
Returns the number of characters written including 0-terminator
48+
(will always result in 48 or 0 in case of an error)
49+
50+
Although passed as variables, the function fails when
51+
- len != 8
52+
- maxLen < 48
53+
*/
54+
std::size_t decodeSchedule(const uint8_t* data, std::size_t len, char* output, std::size_t maxLen);
55+
56+
}; // end namespace VitoWiFi

src/Datapoint/Converter.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ the LICENSE file.
99
#pragma once
1010

1111
#include <cassert>
12+
#include <cstdint>
1213
#include <cmath>
1314
#include <cstring>
1415

1516
#include "../Logging.h"
17+
#include "ConversionHelpers.h"
1618

1719
namespace VitoWiFi {
1820

src/GWG/GWG.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ void GWG::_setState(State state) {
199199
void GWG::_init() {
200200
if (_interface->available()) {
201201
if (_interface->read() == VitoWiFiInternals::ProtocolBytes.ENQ && _currentDatapoint) {
202+
_bytesTransferred = 0;
202203
_setState(State::SEND);
203204
}
204205
}
@@ -220,7 +221,6 @@ void GWG::_receive() {
220221
_lastMillis = _currentMillis;
221222
}
222223
if (_bytesTransferred == _currentRequest.length()) {
223-
_bytesTransferred = 0;
224224
_setState(State::INIT);
225225
_tryOnResponse();
226226
}

0 commit comments

Comments
 (0)