From e2f5c8e4985c44439ca5a6640456dfadc0866c5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=85dne=20Hovda?= <aadne@hovda.no>
Date: Fri, 1 Mar 2024 12:12:51 +0100
Subject: [PATCH] Add support for Watts WFHT-RF thermostat (#2648)

---
 README.md                      |   1 +
 conf/rtl_433.example.conf      |   1 +
 include/rtl_433_devices.h      |   1 +
 src/CMakeLists.txt             |   1 +
 src/devices/watts_thermostat.c | 190 +++++++++++++++++++++++++++++++++
 5 files changed, 194 insertions(+)
 create mode 100644 src/devices/watts_thermostat.c

diff --git a/README.md b/README.md
index 89665dfc..802bb22b 100644
--- a/README.md
+++ b/README.md
@@ -338,6 +338,7 @@ See [CONTRIBUTING.md](./docs/CONTRIBUTING.md).
     [250]  Schou 72543 Day Rain Gauge, Motonet MTX Rain, MarQuant Rain Gauge
     [251]  Fine Offset / Ecowitt WH55 water leak sensor
     [252]  BMW Gen5 TPMS, multi-brand HUF, Continental, Schrader/Sensata
+    [253]  Watts WFHT-RF Thermostat
 
 * Disabled by default, use -R n or a conf file to enable
 
diff --git a/conf/rtl_433.example.conf b/conf/rtl_433.example.conf
index 0461bea2..7ef082f0 100644
--- a/conf/rtl_433.example.conf
+++ b/conf/rtl_433.example.conf
@@ -479,6 +479,7 @@ convert si
   protocol 250 # Schou 72543 Day Rain Gauge, Motonet MTX Rain, MarQuant Rain Gauge
   protocol 251 # Fine Offset / Ecowitt WH55 water leak sensor
   protocol 252 # BMW Gen5 TPMS, multi-brand HUF, Continental, Schrader/Sensata
+  protocol 253 # Watts WFHT-RF Thermostat
 
 ## Flex devices (command line option "-X")
 
diff --git a/include/rtl_433_devices.h b/include/rtl_433_devices.h
index abb5157a..80419954 100644
--- a/include/rtl_433_devices.h
+++ b/include/rtl_433_devices.h
@@ -260,6 +260,7 @@
     DECL(schou_72543_rain) \
     DECL(fineoffset_wh55) \
     DECL(tpms_bmw) \
+    DECL(watts_thermostat) \
 
     /* Add new decoders here. */
 
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5fed8604..d7e81d5a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -254,6 +254,7 @@ add_library(r_433 STATIC
     devices/vaillant_vrt340f.c
     devices/vauno_en8822c.c
     devices/visonic_powercode.c
+    devices/watts_thermostat.c
     devices/waveman.c
     devices/wec2103.c
     devices/wg_pb12v1.c
diff --git a/src/devices/watts_thermostat.c b/src/devices/watts_thermostat.c
new file mode 100644
index 00000000..02283561
--- /dev/null
+++ b/src/devices/watts_thermostat.c
@@ -0,0 +1,190 @@
+/** @file
+    Watts WFHT-RF Thermostat.
+
+    Copyright (C) 2022 Ådne Hovda <aadne@hovda.no>
+    based on protocol decoding by Christian W. Zuckschwerdt <zany@triq.net>
+    and Ådne Hovda <aadne@hovda.no>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+*/
+
+#include "decoder.h"
+
+/** @fn int watts_thermostat_decode(r_device *decoder, bitbuffer_t *bitbuffer)
+Watts WFHT-RF Thermostat.
+
+This code is based on a slightly older OEM system created by ADEV in France which
+later merged with Watts. The closest thing currently available seems to be
+https://wattswater.eu/catalog/regulation-and-control/radio-wfht-thermostats/electronic-room-thermostat-with-rf-control-wfht-rf-basic/,
+but it is not known whether they are protocol compatible.
+
+Modulation is PWM with preceeding gap. There is a very long lead-in pulse.
+Symbols are ~260 us gap + ~600 us pulse and ~600 us gap + ~260 us pulse.
+Bits are inverted and reflected.
+
+Example Data:
+
+    10100101   1011010001110110   1000   100100001   000011000   10101011
+    preamble   id                 flags  temp         setpoint   chksum
+
+Data Layout:
+
+    PP II II F .TT .SS XX
+
+- P: (8-bit reflected) Preamble
+- I: (16-bit reflected) ID
+- F: (4-bit reflected) Flags
+- T: (9-bit reflected) Temperature
+- S: (9-bit reflected) Set-Point
+- X: (8-bit reflected) Checksum (8-bit sum)
+
+    The only flag found is PAIRING (0b0001). Chksum is calculated by summing all
+    high and low bytes the for ID, Flags, Temperature and Set-Point.
+
+    Temperature and Set-Point values are in 0.1°C steps with an observed Set-Point
+    range of ~4°C to ~30°C.
+
+Raw data:
+
+    {54}5ab24971f79994
+    {54}5ab24971f79994
+    {54}5ab249f1f79b94
+    {54}5ab249f1f79b94
+    {54}5ab249f9f79854
+    {54}5ab249f5f79a54
+    {54}5ab249f68f998c
+    {54}5ab249f98f9a4c
+    {54}5ab249f58b9a4c
+    {54}5ab249fb8f9acc
+
+    https://tinyurl.com/wattsthermobitbench
+
+    Format string:
+    PRE:^8h ID:^16d FLAGS:^4b TEMP:^9d SETP:^9d CHK:^8d
+
+Decoded example:
+
+    PRE:a5 ID:28082 FLAGS:0001 TEMP:271 SETP:304 CHK:097
+    PRE:a5 ID:28252 FLAGS:0000 TEMP:019 SETP:303 CHK:013
+
+*/
+
+#define WATTSTHERMO_BITLEN             54
+#define WATTSTHERMO_PREAMBLE_BITLEN    8
+#define WATTSTHERMO_ID_BITLEN          16
+#define WATTSTHERMO_FLAGS_BITLEN       4
+#define WATTSTHERMO_TEMPERATURE_BITLEN 9
+#define WATTSTHERMO_SETPOINT_BITLEN    9
+#define WATTSTHERMO_CHKSUM_BITLEN      8
+
+enum WATTSTHERMO_FLAGS {
+    WF_NONE     = 0,
+    WF_PAIRING  = 1,
+    WF_UNKNOWN1 = 2,
+    WF_UNKNOWN2 = 4,
+    WF_UNKNOWN3 = 8,
+};
+
+static int watts_thermostat_decode(r_device *decoder, bitbuffer_t *bitbuffer)
+{
+    uint8_t const preamble_pattern[] = {0xa5}; // inverted and reflected, raw value is 0x5a
+
+    bitbuffer_invert(bitbuffer);
+
+    // We're expecting a single row
+    for (uint16_t row = 0; row < bitbuffer->num_rows; ++row) {
+
+        uint16_t row_len = bitbuffer->bits_per_row[row];
+        unsigned bitpos  = 0;
+
+        bitpos = bitbuffer_search(bitbuffer, row, 0, preamble_pattern, WATTSTHERMO_PREAMBLE_BITLEN);
+        if (bitpos >= row_len) {
+            decoder_log(decoder, 2, __func__, "Preamble not found");
+            return DECODE_ABORT_EARLY;
+        }
+
+        if (row_len < WATTSTHERMO_BITLEN) {
+            decoder_log(decoder, 2, __func__, "Message too short");
+            return DECODE_ABORT_LENGTH;
+        }
+        bitpos += WATTSTHERMO_PREAMBLE_BITLEN;
+
+        uint8_t id_raw[2];
+        bitbuffer_extract_bytes(bitbuffer, row, bitpos, id_raw, WATTSTHERMO_ID_BITLEN);
+        reflect_bytes(id_raw, 2);
+        int id  = (id_raw[1] << 8) | id_raw[0];
+        bitpos += WATTSTHERMO_ID_BITLEN;
+
+        uint8_t flags[1];
+        bitbuffer_extract_bytes(bitbuffer, row, bitpos, flags, WATTSTHERMO_FLAGS_BITLEN);
+        reflect_bytes(flags, 1);
+        int pairing = flags[0] & WF_PAIRING;
+        bitpos += WATTSTHERMO_FLAGS_BITLEN;
+
+        uint8_t temp_raw[2];
+        bitbuffer_extract_bytes(bitbuffer, row, bitpos, temp_raw, WATTSTHERMO_TEMPERATURE_BITLEN);
+        reflect_bytes(temp_raw, 2);
+        int temp = (temp_raw[1] << 8) | temp_raw[0];
+        bitpos += WATTSTHERMO_TEMPERATURE_BITLEN;
+
+        uint8_t setp_raw[2];
+        bitbuffer_extract_bytes(bitbuffer, row, bitpos, setp_raw, WATTSTHERMO_SETPOINT_BITLEN);
+        reflect_bytes(setp_raw, 2);
+        int setp = (setp_raw[1] << 8) | setp_raw[0];
+        bitpos += WATTSTHERMO_SETPOINT_BITLEN;
+
+        uint8_t chksum = add_bytes(id_raw, 2)
+                + add_bytes(flags, 1)
+                + add_bytes(temp_raw, 2)
+                + add_bytes(setp_raw, 2);
+
+        uint8_t chk[1];
+        bitbuffer_extract_bytes(bitbuffer, row, bitpos, chk, WATTSTHERMO_CHKSUM_BITLEN);
+        reflect_bytes(chk, 1);
+        if (chk[0] != chksum) {
+            decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "Checksum fail");
+            return DECODE_FAIL_MIC;
+        }
+
+        /* clang-format off */
+        data_t *data = data_make(
+                "model",            "Model",            DATA_STRING, "Watts-WFHTRF",
+                "id",               "ID",               DATA_INT,    id,
+                "pairing",          "Pairing",          DATA_INT,    pairing,
+                "temperature_C",    "Temperature",      DATA_FORMAT, "%.1f C",      DATA_DOUBLE,  temp * 0.1f,
+                "setpoint_C",       "Setpoint",         DATA_FORMAT, "%.1f C",      DATA_DOUBLE,  setp * 0.1f,
+                "flags",            "Flags",            DATA_INT,    flags[0],
+                "mic",              "Integrity",        DATA_STRING, "CHECKSUM",
+                NULL);
+        /* clang-format on */
+
+        decoder_output_data(decoder, data);
+        return 1;
+    }
+    return 0;
+}
+
+static char const *const output_fields[] = {
+        "model",
+        "id",
+        "pairing",
+        "temperature_C",
+        "setpoint_C",
+        "flags",
+        "mic",
+        NULL,
+};
+
+r_device const watts_thermostat = {
+        .name        = "Watts WFHT-RF Thermostat",
+        .modulation  = OOK_PULSE_PWM,
+        .short_width = 260,
+        .long_width  = 600,
+        .sync_width  = 6000,
+        .reset_limit = 900,
+        .decode_fn   = &watts_thermostat_decode,
+        .fields      = output_fields,
+};