Редизайн на основе текущей ветки мейнстрима + новые устройства.
[rtl-433.git] / src / devices / fineoffset_wh1080.c
diff --git a/src/devices/fineoffset_wh1080.c b/src/devices/fineoffset_wh1080.c
new file mode 100644 (file)
index 0000000..ba09033
--- /dev/null
@@ -0,0 +1,499 @@
+
+/*
+ * *** Fine Offset WH1080/WH3080 Weather Station ***
+ *
+ * This module is based on Stanisław Pitucha ('viraptor' https://github.com/viraptor ) code stub for the Digitech XC0348 
+ * Weather Station, which seems to be a rebranded Fine Offset WH1080 Weather Station. 
+ *
+ * Some info and code derived from Kevin Sangelee's page: 
+ * http://www.susa.net/wordpress/2012/08/raspberry-pi-reading-wh1081-weather-sensors-using-an-rfm01-and-rfm12b/ .
+ *
+ * See also Frank 'SevenW' page ( https://www.sevenwatt.com/main/wh1080-protocol-v2-fsk/ ) for some other useful info.
+ *
+ * For the WH1080 part I mostly have re-elaborated and merged their works. Credits (and kudos) should go to them all 
+ * (and to many others too).
+ *
+ *****************************************
+ * WH1080
+ *****************************************
+ * (aka Watson W-8681)
+ * (aka Digitech XC0348 Weather Station)
+ * (aka PCE-FWS 20) 
+ * (aka Elecsa AstroTouch 6975)
+ * (aka Froggit WH1080)
+ * (aka .....)
+ *
+ * This weather station is based on an indoor touchscreen receiver, and on a 5+1 outdoor wireless sensors group 
+ * (rain, wind speed, wind direction, temperature, humidity, plus a DCF77 time signal decoder, maybe capable to decode 
+ * some other time signal standard).
+ * See the product page here: http://www.foshk.com/weather_professional/wh1080.htm . 
+ * It's a very popular weather station, you can easily find it on eBay or Amazon (just do a search for 'WH1080').
+ *
+ * The module works fine, decoding all of the data as read into the original console (there is some minimal difference
+ * sometime on the decimals due to the different architecture of the console processor, which is a little less precise).
+ * 
+ * Please note that the pressure sensor (barometer) is enclosed in the indoor console unit, NOT in the outdoor 
+ * wireless sensors group. 
+ * That's why it's NOT possible to get pressure data by wireless communication. If you need pressure data you should try 
+ * an Arduino/Raspberry solution wired with a BMP180/280 or BMP085 sensor.
+ *
+ * Data are trasmitted in a 48 seconds cycle (data packet, then wait 48 seconds, then data packet...).
+ * 
+ * This module is also capable to decode the DCF77/WWVB time signal sent by the time signal decoder 
+ * (which is enclosed on the sensor tx): around the minute 59 of the even hours the sensor's TX stops sending weather data, 
+ * probably to receive (and sync with) DCF77/WWVB signals.
+ * After around 3-4 minutes of silence it starts to send just time data for some minute, then it starts again with 
+ * weather data as usual.
+ *
+ * By living in Europe I can only test DCF77 time decoding, so if you live outside Europe and you find garbage instead 
+ * of correct time, you should disable/ignore time decoding 
+ * (or, better, try to implement a more complete time decoding system :) ).
+ *
+ * To recognize message type (weather or time) you can use the 'msg_type' field on json output:
+ * msg_type 0 = weather data
+ * msg_type 1 = time data
+ *
+ * The 'Total rainfall' field is a cumulative counter, increased by 0.3 millimeters of rain at once.
+ *
+ * The station comes in three TX operating frequency versions: 433, 868.3 and 915 Mhz. 
+ * The module is tested with a 'Froggit WH1080' on 868.3 Mhz, using '-f 868140000' as frequency parameter and 
+ * it works fine (compiled in x86, RaspberryPi 1 (v2), Raspberry Pi2 and Pi3, and also on a BananaPi platform. Everything is OK). 
+ * I don't know if it works also with ALL of the rebranded versions/models of this weather station. 
+ * I guess it *should* do... Just give it a try! :)
+ *
+ *
+ *****************************************
+ * WH3080
+ *****************************************
+ *
+ * The WH3080 Weather Station seems to be basically a WH1080 with the addition of UV/Light sensors onboard. 
+ * The weather/datetime radio protocol used for both is identical, the only difference is for the addition in the WH3080
+ * of the UV/Light part.
+ * UV/Light radio messages are disjointed from (and shorter than) weather/datetime radio messages and are transmitted 
+ * in a 'once-every-60-seconds' cycle.
+ *
+ * The module is able to decode all kind of data coming from the WH3080: weather, datetime, UV and light plus some
+ * error/status code.
+ *
+ * To recognize message type (weather, datetime or UV/light) you can refer to the 'msg_type' field on json output:
+ * msg_type 0 = weather data
+ * msg_type 1 = datetime data
+ * msg_type 2 = UV/light data
+ * 
+ * While the LCD console seems to truncate/round values in order to best fit to its display, this module keeps entire values
+ * as received from externals sensors (exception made for some rounding while converting values from lux to watts/m and fc), 
+ * so you can see -sometimes- some little difference between module's output and LCD console's values.
+ *
+ *
+ * 2016-2017 Nicola Quiriti ('ovrheat' - 'seven')
+ *
+ *
+ */
+
+#include "data.h"
+#include "rtl_433.h"
+#include "util.h"
+#include "math.h"
+
+#define CRC_POLY 0x31
+#define CRC_INIT 0xff
+
+
+
+
+static char* wind_dir_string[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW",};
+static char* wind_dir_degr[]= {"0", "23", "45", "68", "90", "113", "135", "158", "180", "203", "225", "248", "270", "293", "315", "338",};
+
+static unsigned short get_device_id(const uint8_t* br) {
+       return (br[1] << 4 & 0xf0 ) | (br[2] >> 4);
+}
+
+static char* get_battery(const uint8_t* br) { 
+       if ((br[9] >> 4) != 1) {
+               return "OK";
+       } else {
+               return "LOW";
+       }       
+}
+       
+// ------------ WEATHER SENSORS DECODING ----------------------------------------------------
+
+static float get_temperature(const uint8_t* br) {
+    const int temp_raw = (br[2] << 8) + br[3];
+    return ((temp_raw & 0x0fff) - 0x190) / 10.0;
+}
+
+static int get_humidity(const uint8_t* br) {
+    return br[4];
+}
+
+static char* get_wind_direction_str(const uint8_t* br) {
+    return wind_dir_string[br[9] & 0x0f];
+}
+
+static char* get_wind_direction_deg(const uint8_t* br) {
+    return wind_dir_degr[br[9] & 0x0f];
+}
+
+static float get_wind_speed_raw(const uint8_t* br) {
+    return br[5]; // Raw
+}
+
+static float get_wind_avg_ms(const uint8_t* br) {
+    return (br[5] * 34.0f) / 100; // Meters/sec.
+}
+
+static float get_wind_avg_mph(const uint8_t* br) {
+    return ((br[5] * 34.0f) / 100) * 2.23693629f; // Mph
+}
+
+static float get_wind_avg_kmh(const uint8_t* br) {
+    return ((br[5] * 34.0f) / 100) * 3.6f; // Km/h
+}
+
+static float get_wind_avg_knot(const uint8_t* br) {
+    return ((br[5] * 34.0f) / 100) * 1.94384f; // Knots
+}
+
+static float get_wind_gust_raw(const uint8_t* br) {
+    return br[6]; // Raw
+}
+
+static float get_wind_gust_ms(const uint8_t* br) {
+    return (br[6] * 34.0f) / 100; // Meters/sec.
+}
+
+static float get_wind_gust_mph(const uint8_t* br) {
+    return ((br[6] * 34.0f) / 100) * 2.23693629f; // Mph
+       
+}
+
+static float get_wind_gust_kmh(const uint8_t* br) {
+    return ((br[6] * 34.0f) / 100) * 3.6f; // Km/h
+}
+
+static float get_wind_gust_knot(const uint8_t* br) {
+    return ((br[6] * 34.0f) / 100) * 1.94384f; // Knots
+}
+
+static float get_rainfall(const uint8_t* br) {
+    unsigned short rain_raw = (((unsigned short)br[7] & 0x0f) << 8) | br[8];
+    return (float)rain_raw * 0.3f;
+}
+
+
+// ------------ WH3080 UV SENSOR DECODING ----------------------------------------------------
+
+static unsigned short get_uv_sensor_id(const uint8_t* br) {
+    return (br[1] << 4 & 0xf0 ) | (br[2] >> 4);
+}
+
+static char* get_uvstatus(const uint8_t* br) { 
+    if (br[3] == 85) {
+    return "OK";
+    } else {
+    return "ERROR";
+    }
+}
+
+static unsigned short wh3080_uvi(const uint8_t* br) {
+    return (br[2] & 0x0F );
+}
+
+
+// ------------ WH3080 LIGHT SENSOR DECODING -------------------------------------------------
+
+static float get_rawlight(const uint8_t* br) {
+    return (((((br[4]) << 16) | ((br[5]) << 8) | br[6])));
+}
+
+
+//----------------- TIME DECODING ----------------------------------------------------
+
+static char* get_signal(const uint8_t* br) { 
+    if ((br[2] & 0x0F) == 10) {
+    return "DCF77";
+    } else {
+    return "WWVB/MSF";
+    }
+}
+
+static int get_hours(const uint8_t* br) {
+       return ((br[3] >> 4 & 0x03) * 10) + (br[3] & 0x0F);
+}
+
+static int get_minutes(const uint8_t* br) {
+       return (((br[4] & 0xF0) >> 4) * 10) + (br[4] & 0x0F);
+}
+
+static int get_seconds(const uint8_t* br) {
+       return (((br[5] & 0xF0) >> 4) * 10) + (br[5] & 0x0F);
+}
+
+static int get_year(const uint8_t* br) {
+       return (((br[6] & 0xF0) >> 4) * 10) + (br[6] & 0x0F);
+}
+       
+static int get_month(const uint8_t* br) {
+       return ((br[7] >> 4 & 0x01) * 10) + (br[7] & 0x0F);     
+}
+
+static int get_day(const uint8_t* br) {
+       return (((br[8] & 0xF0) >> 4) * 10) + (br[8] & 0x0F);
+}
+
+//-------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------
+
+
+
+static int fineoffset_wh1080_callback(bitbuffer_t *bitbuffer) {
+    data_t *data;
+    char time_str[LOCAL_TIME_BUFLEN];
+    const uint8_t *br;
+    int msg_type; // 0=Weather 1=Datetime 2=UV/Light
+    int sens_msg = 12; // 12=Weather/Time sensor  8=UV/Light sensor
+    int i;
+    uint8_t bbuf[sens_msg];
+    local_time_str(0, time_str);
+
+    if (bitbuffer->num_rows != 1) {
+        return 0;
+    }
+    if ((bitbuffer->bits_per_row[0] != 88) && (bitbuffer->bits_per_row[0] != 87) && 
+    (bitbuffer->bits_per_row[0] != 64) && (bitbuffer->bits_per_row[0] != 63)){
+        return 0;
+    }
+
+    if(bitbuffer->bits_per_row[0] == 88) { // FineOffset WH1080/3080 Weather data msg
+       sens_msg = 12;
+        br = bitbuffer->bb[0];
+    } else if(bitbuffer->bits_per_row[0] == 87) { // FineOffset WH1080/3080 Weather data msg (different version (newest?))
+       sens_msg = 12;
+        /* 7 bits of preamble, bit shift the whole buffer and fix the bytestream */
+        bitbuffer_extract_bytes(bitbuffer, 0, 7,
+        (uint8_t *)&bbuf+1, 10*8);
+        br = bbuf;
+        bbuf[0] = 0xFF;
+    } else if(bitbuffer->bits_per_row[0] == 64) {  // FineOffset WH3080 UV/Light data msg
+       sens_msg = 8;
+        br = bitbuffer->bb[0];
+
+    } else if(bitbuffer->bits_per_row[0] == 63) { // FineOffset WH3080 UV/Light data msg (different version (newest?))
+       sens_msg = 8;
+        /* 7 bits of preamble, bit shift the whole buffer and fix the bytestream */
+        bitbuffer_extract_bytes(bitbuffer, 0, 7,
+        (uint8_t *) & bbuf +1, 7*8);
+        br = bbuf;
+        bbuf[0] = 0xFF;
+    }
+
+    if (debug_output) {
+        for (i=0 ; i<((sens_msg)-1) ; i++)
+            fprintf(stderr, "%02x ", bbuf[i]);
+        fprintf(stderr, "\n");
+    }
+
+    if (br[0] != 0xff) {
+        // preamble missing
+        return 0;
+    }
+
+    if (sens_msg == 12) {
+       if (br[10] != crc8(br, 10, CRC_POLY, CRC_INIT)) {
+        // crc mismatch
+        return 0;
+    }
+
+       } else {
+       if (br[7] != crc8(br, 7, CRC_POLY, CRC_INIT)) {
+        // crc mismatch
+        return 0;
+    }
+       }
+
+    if (br[0] == 0xff && (br[1] >> 4) == 0x0a) {
+    msg_type = 0; // WH1080/3080 Weather msg
+    } else if (br[0] == 0xff && (br[1] >> 4) == 0x0b) {
+    msg_type = 1; // WH1080/3080 Datetime msg
+    } else if (br[0] == 0xff && (br[1] >> 4) == 0x07) {
+    msg_type = 2; // WH3080 UV/Light msg
+    }
+       
+
+
+       
+//---------------------------------------------------------------------------------------      
+//-------- GETTING WEATHER SENSORS DATA -------------------------------------------------
+       
+    const float temperature = get_temperature(br);
+    const int humidity = get_humidity(br);
+    const char* direction_str = get_wind_direction_str(br);
+       const char* direction_deg = get_wind_direction_deg(br); 
+       
+       
+       // Select which metric system for *wind avg speed* and *wind gust* :
+       
+       // Wind average speed :
+       
+       //const float speed = get_wind_avg_ms((br)   // <--- Data will be shown in Meters/sec.
+       //const float speed = get_wind_avg_mph((br)  // <--- Data will be shown in Mph
+       const float speed = get_wind_avg_kmh(br);  // <--- Data will be shown in Km/h
+       //const float speed = get_wind_avg_knot((br) // <--- Data will be shown in Knots
+       
+       // Wind gust speed :
+       
+    //const float gust = get_wind_gust_ms(br);   // <--- Data will be shown in Meters/sec.
+       //const float gust = get_wind_gust_mph(br);  // <--- Data will be shown in Mph
+       const float gust = get_wind_gust_kmh(br);  // <--- Data will be shown in km/h
+       //const float gust = get_wind_gust_knot(br); // <--- Data will be shown in Knots        
+       
+    const float rain = get_rainfall(br);
+    const int device_id = get_device_id(br);
+       const char* battery = get_battery(br);
+       
+       
+       //---------------------------------------------------------------------------------------       
+    //-------- GETTING UV DATA --------------------------------------------------------------
+
+    const int uv_sensor_id = get_uv_sensor_id(br);
+    const char* uv_status = get_uvstatus(br);
+    const int uv_index = wh3080_uvi(br);
+
+
+    //---------------------------------------------------------------------------------------  
+    //-------- GETTING LIGHT DATA -----------------------------------------------------------
+
+    const float light = get_rawlight(br);
+    const float lux = (get_rawlight(br)/10);
+    const float wm = (get_rawlight(br)/6830);
+    const float fc = ((get_rawlight(br)/10.76)/10.0);
+
+
+
+       //---------------------------------------------------------------------------------------       
+       //-------- GETTING TIME DATA ------------------------------------------------------------
+
+       char* signal = get_signal(br);
+       const int hours = get_hours(br);
+       const int minutes =     get_minutes(br);
+       const int seconds = get_seconds(br);
+       const int year = 2000 + get_year(br);
+       const int month = get_month(br);
+       const int day = get_day(br);
+       
+
+       //--------- PRESENTING DATA --------------------------------------------------------------
+       
+if (msg_type == 0) {
+
+    data = data_make(
+                       "time",         "",             DATA_STRING,                                    time_str,
+                       "model",        "",             DATA_STRING,    "Fine Offset Electronics WH1080/WH3080 Weather Station",
+                       "msg_type",     "Msg type",     DATA_INT,                                       msg_type,
+                       "id",           "Station ID",   DATA_FORMAT,    "%d",           DATA_INT,       device_id,
+                       "temperature_C","Temperature",  DATA_FORMAT,    "%.01f C",      DATA_DOUBLE,    temperature,
+                       "humidity",     "Humidity",     DATA_FORMAT,    "%u %%",        DATA_INT,       humidity,
+                       "direction_str","Wind string",  DATA_STRING,                                    direction_str,
+                       "direction_deg","Wind degrees", DATA_STRING,                                    direction_deg,
+                       "speed",        "Wind avg speed",DATA_FORMAT,   "%.02f",        DATA_DOUBLE,    speed,
+                       "gust",         "Wind gust",    DATA_FORMAT,    "%.02f",        DATA_DOUBLE,    gust,
+                       "rain",         "Total rainfall",DATA_FORMAT,   "%3.1f",        DATA_DOUBLE,    rain,
+                       "battery",      "Battery",      DATA_STRING,                                    battery,
+               NULL);
+    data_acquired_handler(data);
+    return 1;
+
+} else if (msg_type == 1) {
+
+    data = data_make(
+                       "time",         "",             DATA_STRING,            time_str,
+                       "model",        "",             DATA_STRING,    "Fine Offset Electronics WH1080/WH3080 Weather Station",
+                       "msg_type",     "Msg type",     DATA_INT,                               msg_type,
+                       "id",           "Station ID",   DATA_FORMAT,    "%d",   DATA_INT,       device_id,
+                       "signal",       "Signal Type",  DATA_STRING,                            signal,
+                       "hours",        "Hours\t",      DATA_FORMAT,    "%02d", DATA_INT,       hours,
+                       "minutes",      "Minutes",      DATA_FORMAT,    "%02d", DATA_INT,       minutes,
+                       "seconds",      "Seconds",      DATA_FORMAT,    "%02d", DATA_INT,       seconds,
+                       "year",         "Year\t",       DATA_FORMAT,    "%02d", DATA_INT,       year,
+                       "month",        "Month\t",      DATA_FORMAT,    "%02d", DATA_INT,       month,
+                       "day",          "Day\t",        DATA_FORMAT,    "%02d", DATA_INT,       day,
+               NULL);
+    data_acquired_handler(data);
+    return 1;
+
+} else {
+
+    data = data_make(
+                       "time",         "",             DATA_STRING,                            time_str,
+                       "model",        "",             DATA_STRING,    "Fine Offset Electronics WH3080 Weather Station",
+                       "msg_type",     "Msg type",     DATA_INT,                               msg_type,
+                       "uv_sensor_id", "UV Sensor ID", DATA_FORMAT,    "%d",   DATA_INT,       uv_sensor_id,
+                       "uv_status",    "Sensor Status",DATA_STRING,                            uv_status,
+                       "uv_index",     "UV Index",     DATA_INT,                               uv_index,
+                       "lux",          "Lux\t",        DATA_FORMAT,    "%.1f", DATA_DOUBLE,    lux,
+                       "wm",           "Watts/m\t",    DATA_FORMAT,    "%.2f", DATA_DOUBLE,    wm,
+                       "fc",           "Foot-candles", DATA_FORMAT,    "%.2f", DATA_DOUBLE,    fc,
+               NULL);
+    data_acquired_handler(data);
+    return 1; 
+    }
+}
+
+static char *output_fields[] = {
+    "time",
+    "model",
+    "id",
+    "temperature_C",
+    "humidity",
+    "direction_str",
+    "direction_deg",
+    "speed",
+    "gust",
+    "rain",
+    "msg_type",
+    "signal",
+    "hours",
+    "minutes",
+    "seconds",
+    "year",
+    "month",
+    "day",
+    "battery",
+    "sensor_code",
+    "uv_status",
+    "uv_index",
+    "lux",
+    "wm",
+    "fc",
+    NULL
+};
+
+r_device fineoffset_wh1080 = {
+    .name           = "Fine Offset Electronics WH1080/WH3080 Weather Station",
+    .modulation     = OOK_PULSE_PWM_RAW,
+    .short_limit    = 800,
+    .long_limit     = 2800,
+    .reset_limit    = 2800,
+    .json_callback  = &fineoffset_wh1080_callback,
+    .disabled       = 0,
+    .demod_arg      = 0,
+    .fields         = output_fields,
+};
+
+/**
+ * http://www.jaycar.com.au/mini-lcd-display-weather-station/p/XC0400
+ */
+
+r_device fineoffset_XC0400 = {
+    .name           = "Fine Offset Electronics, XC0400",
+    .modulation     = OOK_PULSE_PWM_RAW,
+    .short_limit    = 800,     // Short pulse 544µs, long pulse 1524µs, fixed gap 1036µs
+    .long_limit     = 2800,    // Maximum pulse period (long pulse + fixed gap)
+    .reset_limit    = 2800,    // We just want 1 package
+    .json_callback  = &fineoffset_wh1080_callback,
+    .disabled       = 0,
+    .demod_arg      = 0,
+    .fields         = output_fields
+};