Skip to content

Commit e2f5c8e

Browse files
authored
Add support for Watts WFHT-RF thermostat (merbanan#2648)
1 parent 8d1541b commit e2f5c8e

File tree

5 files changed

+194
-0
lines changed

5 files changed

+194
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ See [CONTRIBUTING.md](./docs/CONTRIBUTING.md).
338338
[250] Schou 72543 Day Rain Gauge, Motonet MTX Rain, MarQuant Rain Gauge
339339
[251] Fine Offset / Ecowitt WH55 water leak sensor
340340
[252] BMW Gen5 TPMS, multi-brand HUF, Continental, Schrader/Sensata
341+
[253] Watts WFHT-RF Thermostat
341342
342343
* Disabled by default, use -R n or a conf file to enable
343344

conf/rtl_433.example.conf

+1
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ convert si
479479
protocol 250 # Schou 72543 Day Rain Gauge, Motonet MTX Rain, MarQuant Rain Gauge
480480
protocol 251 # Fine Offset / Ecowitt WH55 water leak sensor
481481
protocol 252 # BMW Gen5 TPMS, multi-brand HUF, Continental, Schrader/Sensata
482+
protocol 253 # Watts WFHT-RF Thermostat
482483

483484
## Flex devices (command line option "-X")
484485

include/rtl_433_devices.h

+1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@
260260
DECL(schou_72543_rain) \
261261
DECL(fineoffset_wh55) \
262262
DECL(tpms_bmw) \
263+
DECL(watts_thermostat) \
263264

264265
/* Add new decoders here. */
265266

src/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ add_library(r_433 STATIC
254254
devices/vaillant_vrt340f.c
255255
devices/vauno_en8822c.c
256256
devices/visonic_powercode.c
257+
devices/watts_thermostat.c
257258
devices/waveman.c
258259
devices/wec2103.c
259260
devices/wg_pb12v1.c

src/devices/watts_thermostat.c

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/** @file
2+
Watts WFHT-RF Thermostat.
3+
4+
Copyright (C) 2022 Ådne Hovda <[email protected]>
5+
based on protocol decoding by Christian W. Zuckschwerdt <[email protected]>
6+
and Ådne Hovda <[email protected]>
7+
8+
This program is free software; you can redistribute it and/or modify
9+
it under the terms of the GNU General Public License as published by
10+
the Free Software Foundation; either version 2 of the License, or
11+
(at your option) any later version.
12+
*/
13+
14+
#include "decoder.h"
15+
16+
/** @fn int watts_thermostat_decode(r_device *decoder, bitbuffer_t *bitbuffer)
17+
Watts WFHT-RF Thermostat.
18+
19+
This code is based on a slightly older OEM system created by ADEV in France which
20+
later merged with Watts. The closest thing currently available seems to be
21+
https://wattswater.eu/catalog/regulation-and-control/radio-wfht-thermostats/electronic-room-thermostat-with-rf-control-wfht-rf-basic/,
22+
but it is not known whether they are protocol compatible.
23+
24+
Modulation is PWM with preceeding gap. There is a very long lead-in pulse.
25+
Symbols are ~260 us gap + ~600 us pulse and ~600 us gap + ~260 us pulse.
26+
Bits are inverted and reflected.
27+
28+
Example Data:
29+
30+
10100101 1011010001110110 1000 100100001 000011000 10101011
31+
preamble id flags temp setpoint chksum
32+
33+
Data Layout:
34+
35+
PP II II F .TT .SS XX
36+
37+
- P: (8-bit reflected) Preamble
38+
- I: (16-bit reflected) ID
39+
- F: (4-bit reflected) Flags
40+
- T: (9-bit reflected) Temperature
41+
- S: (9-bit reflected) Set-Point
42+
- X: (8-bit reflected) Checksum (8-bit sum)
43+
44+
The only flag found is PAIRING (0b0001). Chksum is calculated by summing all
45+
high and low bytes the for ID, Flags, Temperature and Set-Point.
46+
47+
Temperature and Set-Point values are in 0.1°C steps with an observed Set-Point
48+
range of ~4°C to ~30°C.
49+
50+
Raw data:
51+
52+
{54}5ab24971f79994
53+
{54}5ab24971f79994
54+
{54}5ab249f1f79b94
55+
{54}5ab249f1f79b94
56+
{54}5ab249f9f79854
57+
{54}5ab249f5f79a54
58+
{54}5ab249f68f998c
59+
{54}5ab249f98f9a4c
60+
{54}5ab249f58b9a4c
61+
{54}5ab249fb8f9acc
62+
63+
https://tinyurl.com/wattsthermobitbench
64+
65+
Format string:
66+
PRE:^8h ID:^16d FLAGS:^4b TEMP:^9d SETP:^9d CHK:^8d
67+
68+
Decoded example:
69+
70+
PRE:a5 ID:28082 FLAGS:0001 TEMP:271 SETP:304 CHK:097
71+
PRE:a5 ID:28252 FLAGS:0000 TEMP:019 SETP:303 CHK:013
72+
73+
*/
74+
75+
#define WATTSTHERMO_BITLEN 54
76+
#define WATTSTHERMO_PREAMBLE_BITLEN 8
77+
#define WATTSTHERMO_ID_BITLEN 16
78+
#define WATTSTHERMO_FLAGS_BITLEN 4
79+
#define WATTSTHERMO_TEMPERATURE_BITLEN 9
80+
#define WATTSTHERMO_SETPOINT_BITLEN 9
81+
#define WATTSTHERMO_CHKSUM_BITLEN 8
82+
83+
enum WATTSTHERMO_FLAGS {
84+
WF_NONE = 0,
85+
WF_PAIRING = 1,
86+
WF_UNKNOWN1 = 2,
87+
WF_UNKNOWN2 = 4,
88+
WF_UNKNOWN3 = 8,
89+
};
90+
91+
static int watts_thermostat_decode(r_device *decoder, bitbuffer_t *bitbuffer)
92+
{
93+
uint8_t const preamble_pattern[] = {0xa5}; // inverted and reflected, raw value is 0x5a
94+
95+
bitbuffer_invert(bitbuffer);
96+
97+
// We're expecting a single row
98+
for (uint16_t row = 0; row < bitbuffer->num_rows; ++row) {
99+
100+
uint16_t row_len = bitbuffer->bits_per_row[row];
101+
unsigned bitpos = 0;
102+
103+
bitpos = bitbuffer_search(bitbuffer, row, 0, preamble_pattern, WATTSTHERMO_PREAMBLE_BITLEN);
104+
if (bitpos >= row_len) {
105+
decoder_log(decoder, 2, __func__, "Preamble not found");
106+
return DECODE_ABORT_EARLY;
107+
}
108+
109+
if (row_len < WATTSTHERMO_BITLEN) {
110+
decoder_log(decoder, 2, __func__, "Message too short");
111+
return DECODE_ABORT_LENGTH;
112+
}
113+
bitpos += WATTSTHERMO_PREAMBLE_BITLEN;
114+
115+
uint8_t id_raw[2];
116+
bitbuffer_extract_bytes(bitbuffer, row, bitpos, id_raw, WATTSTHERMO_ID_BITLEN);
117+
reflect_bytes(id_raw, 2);
118+
int id = (id_raw[1] << 8) | id_raw[0];
119+
bitpos += WATTSTHERMO_ID_BITLEN;
120+
121+
uint8_t flags[1];
122+
bitbuffer_extract_bytes(bitbuffer, row, bitpos, flags, WATTSTHERMO_FLAGS_BITLEN);
123+
reflect_bytes(flags, 1);
124+
int pairing = flags[0] & WF_PAIRING;
125+
bitpos += WATTSTHERMO_FLAGS_BITLEN;
126+
127+
uint8_t temp_raw[2];
128+
bitbuffer_extract_bytes(bitbuffer, row, bitpos, temp_raw, WATTSTHERMO_TEMPERATURE_BITLEN);
129+
reflect_bytes(temp_raw, 2);
130+
int temp = (temp_raw[1] << 8) | temp_raw[0];
131+
bitpos += WATTSTHERMO_TEMPERATURE_BITLEN;
132+
133+
uint8_t setp_raw[2];
134+
bitbuffer_extract_bytes(bitbuffer, row, bitpos, setp_raw, WATTSTHERMO_SETPOINT_BITLEN);
135+
reflect_bytes(setp_raw, 2);
136+
int setp = (setp_raw[1] << 8) | setp_raw[0];
137+
bitpos += WATTSTHERMO_SETPOINT_BITLEN;
138+
139+
uint8_t chksum = add_bytes(id_raw, 2)
140+
+ add_bytes(flags, 1)
141+
+ add_bytes(temp_raw, 2)
142+
+ add_bytes(setp_raw, 2);
143+
144+
uint8_t chk[1];
145+
bitbuffer_extract_bytes(bitbuffer, row, bitpos, chk, WATTSTHERMO_CHKSUM_BITLEN);
146+
reflect_bytes(chk, 1);
147+
if (chk[0] != chksum) {
148+
decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "Checksum fail");
149+
return DECODE_FAIL_MIC;
150+
}
151+
152+
/* clang-format off */
153+
data_t *data = data_make(
154+
"model", "Model", DATA_STRING, "Watts-WFHTRF",
155+
"id", "ID", DATA_INT, id,
156+
"pairing", "Pairing", DATA_INT, pairing,
157+
"temperature_C", "Temperature", DATA_FORMAT, "%.1f C", DATA_DOUBLE, temp * 0.1f,
158+
"setpoint_C", "Setpoint", DATA_FORMAT, "%.1f C", DATA_DOUBLE, setp * 0.1f,
159+
"flags", "Flags", DATA_INT, flags[0],
160+
"mic", "Integrity", DATA_STRING, "CHECKSUM",
161+
NULL);
162+
/* clang-format on */
163+
164+
decoder_output_data(decoder, data);
165+
return 1;
166+
}
167+
return 0;
168+
}
169+
170+
static char const *const output_fields[] = {
171+
"model",
172+
"id",
173+
"pairing",
174+
"temperature_C",
175+
"setpoint_C",
176+
"flags",
177+
"mic",
178+
NULL,
179+
};
180+
181+
r_device const watts_thermostat = {
182+
.name = "Watts WFHT-RF Thermostat",
183+
.modulation = OOK_PULSE_PWM,
184+
.short_width = 260,
185+
.long_width = 600,
186+
.sync_width = 6000,
187+
.reset_limit = 900,
188+
.decode_fn = &watts_thermostat_decode,
189+
.fields = output_fields,
190+
};

0 commit comments

Comments
 (0)