Bugfixes
[rtl-433.git] / src / devices / fineoffset_wh1080.c
1
2 /*
3  * *** Fine Offset WH1080/WH3080 Weather Station ***
4  *
5  * This module is based on Stanisław Pitucha ('viraptor' https://github.com/viraptor ) code stub for the Digitech XC0348 
6  * Weather Station, which seems to be a rebranded Fine Offset WH1080 Weather Station. 
7  *
8  * Some info and code derived from Kevin Sangelee's page: 
9  * http://www.susa.net/wordpress/2012/08/raspberry-pi-reading-wh1081-weather-sensors-using-an-rfm01-and-rfm12b/ .
10  *
11  * See also Frank 'SevenW' page ( https://www.sevenwatt.com/main/wh1080-protocol-v2-fsk/ ) for some other useful info.
12  *
13  * For the WH1080 part I mostly have re-elaborated and merged their works. Credits (and kudos) should go to them all 
14  * (and to many others too).
15  *
16  *****************************************
17  * WH1080
18  *****************************************
19  * (aka Watson W-8681)
20  * (aka Digitech XC0348 Weather Station)
21  * (aka PCE-FWS 20) 
22  * (aka Elecsa AstroTouch 6975)
23  * (aka Froggit WH1080)
24  * (aka .....)
25  *
26  * This weather station is based on an indoor touchscreen receiver, and on a 5+1 outdoor wireless sensors group 
27  * (rain, wind speed, wind direction, temperature, humidity, plus a DCF77 time signal decoder, maybe capable to decode 
28  * some other time signal standard).
29  * See the product page here: http://www.foshk.com/weather_professional/wh1080.htm . 
30  * It's a very popular weather station, you can easily find it on eBay or Amazon (just do a search for 'WH1080').
31  *
32  * The module works fine, decoding all of the data as read into the original console (there is some minimal difference
33  * sometime on the decimals due to the different architecture of the console processor, which is a little less precise).
34  * 
35  * Please note that the pressure sensor (barometer) is enclosed in the indoor console unit, NOT in the outdoor 
36  * wireless sensors group. 
37  * That's why it's NOT possible to get pressure data by wireless communication. If you need pressure data you should try 
38  * an Arduino/Raspberry solution wired with a BMP180/280 or BMP085 sensor.
39  *
40  * Data are trasmitted in a 48 seconds cycle (data packet, then wait 48 seconds, then data packet...).
41  * 
42  * This module is also capable to decode the DCF77/WWVB time signal sent by the time signal decoder 
43  * (which is enclosed on the sensor tx): around the minute 59 of the even hours the sensor's TX stops sending weather data, 
44  * probably to receive (and sync with) DCF77/WWVB signals.
45  * After around 3-4 minutes of silence it starts to send just time data for some minute, then it starts again with 
46  * weather data as usual.
47  *
48  * By living in Europe I can only test DCF77 time decoding, so if you live outside Europe and you find garbage instead 
49  * of correct time, you should disable/ignore time decoding 
50  * (or, better, try to implement a more complete time decoding system :) ).
51  *
52  * To recognize message type (weather or time) you can use the 'msg_type' field on json output:
53  * msg_type 0 = weather data
54  * msg_type 1 = time data
55  *
56  * The 'Total rainfall' field is a cumulative counter, increased by 0.3 millimeters of rain at once.
57  *
58  * The station comes in three TX operating frequency versions: 433, 868.3 and 915 Mhz. 
59  * The module is tested with a 'Froggit WH1080' on 868.3 Mhz, using '-f 868140000' as frequency parameter and 
60  * it works fine (compiled in x86, RaspberryPi 1 (v2), Raspberry Pi2 and Pi3, and also on a BananaPi platform. Everything is OK). 
61  * I don't know if it works also with ALL of the rebranded versions/models of this weather station. 
62  * I guess it *should* do... Just give it a try! :)
63  *
64  *
65  *****************************************
66  * WH3080
67  *****************************************
68  *
69  * The WH3080 Weather Station seems to be basically a WH1080 with the addition of UV/Light sensors onboard. 
70  * The weather/datetime radio protocol used for both is identical, the only difference is for the addition in the WH3080
71  * of the UV/Light part.
72  * UV/Light radio messages are disjointed from (and shorter than) weather/datetime radio messages and are transmitted 
73  * in a 'once-every-60-seconds' cycle.
74  *
75  * The module is able to decode all kind of data coming from the WH3080: weather, datetime, UV and light plus some
76  * error/status code.
77  *
78  * To recognize message type (weather, datetime or UV/light) you can refer to the 'msg_type' field on json output:
79  * msg_type 0 = weather data
80  * msg_type 1 = datetime data
81  * msg_type 2 = UV/light data
82  * 
83  * While the LCD console seems to truncate/round values in order to best fit to its display, this module keeps entire values
84  * as received from externals sensors (exception made for some rounding while converting values from lux to watts/m and fc), 
85  * so you can see -sometimes- some little difference between module's output and LCD console's values.
86  *
87  *
88  * 2016-2017 Nicola Quiriti ('ovrheat' - 'seven')
89  *
90  *
91  */
92  
93
94 #include "data.h"
95 #include "rtl_433.h"
96 #include "util.h"
97 #include "math.h"
98
99 #define CRC_POLY 0x31
100 #define CRC_INIT 0xff
101
102
103
104
105 static char* wind_dir_string[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW",};
106 static char* wind_dir_degr[]= {"0", "23", "45", "68", "90", "113", "135", "158", "180", "203", "225", "248", "270", "293", "315", "338",};
107
108 static unsigned short get_device_id(const uint8_t* br) {
109         return (br[1] << 4 & 0xf0 ) | (br[2] >> 4);
110 }
111
112 static char* get_battery(const uint8_t* br) { 
113         if ((br[9] >> 4) != 1) {
114                 return "OK";
115         } else {
116                 return "LOW";
117         }       
118 }
119         
120 // ------------ WEATHER SENSORS DECODING ----------------------------------------------------
121
122 static float get_temperature(const uint8_t* br) {
123     const int temp_raw = (br[2] << 8) + br[3];
124     return ((temp_raw & 0x0fff) - 0x190) / 10.0;
125 }
126
127 static int get_humidity(const uint8_t* br) {
128     return br[4];
129 }
130
131 static char* get_wind_direction_str(const uint8_t* br) {
132     return wind_dir_string[br[9] & 0x0f];
133 }
134
135 static char* get_wind_direction_deg(const uint8_t* br) {
136     return wind_dir_degr[br[9] & 0x0f];
137 }
138
139 static float get_wind_speed_raw(const uint8_t* br) {
140     return br[5]; // Raw
141 }
142
143 static float get_wind_avg_ms(const uint8_t* br) {
144     return (br[5] * 34.0f) / 100; // Meters/sec.
145 }
146
147 static float get_wind_avg_mph(const uint8_t* br) {
148     return ((br[5] * 34.0f) / 100) * 2.23693629f; // Mph
149 }
150
151 static float get_wind_avg_kmh(const uint8_t* br) {
152     return ((br[5] * 34.0f) / 100) * 3.6f; // Km/h
153 }
154
155 static float get_wind_avg_knot(const uint8_t* br) {
156     return ((br[5] * 34.0f) / 100) * 1.94384f; // Knots
157 }
158
159 static float get_wind_gust_raw(const uint8_t* br) {
160     return br[6]; // Raw
161 }
162
163 static float get_wind_gust_ms(const uint8_t* br) {
164     return (br[6] * 34.0f) / 100; // Meters/sec.
165 }
166
167 static float get_wind_gust_mph(const uint8_t* br) {
168     return ((br[6] * 34.0f) / 100) * 2.23693629f; // Mph
169         
170 }
171
172 static float get_wind_gust_kmh(const uint8_t* br) {
173     return ((br[6] * 34.0f) / 100) * 3.6f; // Km/h
174 }
175
176 static float get_wind_gust_knot(const uint8_t* br) {
177     return ((br[6] * 34.0f) / 100) * 1.94384f; // Knots
178 }
179
180 static float get_rainfall(const uint8_t* br) {
181     unsigned short rain_raw = (((unsigned short)br[7] & 0x0f) << 8) | br[8];
182     return (float)rain_raw * 0.3f;
183 }
184
185
186 // ------------ WH3080 UV SENSOR DECODING ----------------------------------------------------
187
188 static unsigned short get_uv_sensor_id(const uint8_t* br) {
189     return (br[1] << 4 & 0xf0 ) | (br[2] >> 4);
190 }
191
192 static char* get_uvstatus(const uint8_t* br) { 
193     if (br[3] == 85) {
194     return "OK";
195     } else {
196     return "ERROR";
197     }
198 }
199
200 static unsigned short wh3080_uvi(const uint8_t* br) {
201     return (br[2] & 0x0F );
202 }
203
204
205 // ------------ WH3080 LIGHT SENSOR DECODING -------------------------------------------------
206
207 static float get_rawlight(const uint8_t* br) {
208     return (((((br[4]) << 16) | ((br[5]) << 8) | br[6])));
209 }
210
211
212 //----------------- TIME DECODING ----------------------------------------------------
213
214 static char* get_signal(const uint8_t* br) { 
215     if ((br[2] & 0x0F) == 10) {
216     return "DCF77";
217     } else {
218     return "WWVB/MSF";
219     }
220 }
221
222 static int get_hours(const uint8_t* br) {
223         return ((br[3] >> 4 & 0x03) * 10) + (br[3] & 0x0F);
224 }
225
226 static int get_minutes(const uint8_t* br) {
227         return (((br[4] & 0xF0) >> 4) * 10) + (br[4] & 0x0F);
228 }
229
230 static int get_seconds(const uint8_t* br) {
231         return (((br[5] & 0xF0) >> 4) * 10) + (br[5] & 0x0F);
232 }
233
234 static int get_year(const uint8_t* br) {
235         return (((br[6] & 0xF0) >> 4) * 10) + (br[6] & 0x0F);
236 }
237         
238 static int get_month(const uint8_t* br) {
239         return ((br[7] >> 4 & 0x01) * 10) + (br[7] & 0x0F);     
240 }
241
242 static int get_day(const uint8_t* br) {
243         return (((br[8] & 0xF0) >> 4) * 10) + (br[8] & 0x0F);
244 }
245
246 //-------------------------------------------------------------------------------------
247 //-------------------------------------------------------------------------------------
248
249
250
251 static int fineoffset_wh1080_callback(bitbuffer_t *bitbuffer) {
252     data_t *data;
253     char time_str[LOCAL_TIME_BUFLEN];
254     const uint8_t *br;
255     int msg_type; // 0=Weather 1=Datetime 2=UV/Light
256     int sens_msg = 12; // 12=Weather/Time sensor  8=UV/Light sensor
257     int i;
258     uint8_t bbuf[sens_msg];
259     local_time_str(0, time_str);
260
261     if (bitbuffer->num_rows != 1) {
262         return 0;
263     }
264     if ((bitbuffer->bits_per_row[0] != 88) && (bitbuffer->bits_per_row[0] != 87) && 
265     (bitbuffer->bits_per_row[0] != 64) && (bitbuffer->bits_per_row[0] != 63)){
266         return 0;
267     }
268
269     if(bitbuffer->bits_per_row[0] == 88) { // FineOffset WH1080/3080 Weather data msg
270         sens_msg = 12;
271         br = bitbuffer->bb[0];
272     } else if(bitbuffer->bits_per_row[0] == 87) { // FineOffset WH1080/3080 Weather data msg (different version (newest?))
273         sens_msg = 12;
274         /* 7 bits of preamble, bit shift the whole buffer and fix the bytestream */
275         bitbuffer_extract_bytes(bitbuffer, 0, 7,
276         (uint8_t *)&bbuf+1, 10*8);
277         br = bbuf;
278         bbuf[0] = 0xFF;
279     } else if(bitbuffer->bits_per_row[0] == 64) {  // FineOffset WH3080 UV/Light data msg
280         sens_msg = 8;
281         br = bitbuffer->bb[0];
282
283     } else if(bitbuffer->bits_per_row[0] == 63) { // FineOffset WH3080 UV/Light data msg (different version (newest?))
284         sens_msg = 8;
285         /* 7 bits of preamble, bit shift the whole buffer and fix the bytestream */
286         bitbuffer_extract_bytes(bitbuffer, 0, 7,
287         (uint8_t *) & bbuf +1, 7*8);
288         br = bbuf;
289         bbuf[0] = 0xFF;
290     }
291
292     if (debug_output) {
293         for (i=0 ; i<((sens_msg)-1) ; i++)
294             fprintf(stderr, "%02x ", bbuf[i]);
295         fprintf(stderr, "\n");
296     }
297
298     if (br[0] != 0xff) {
299         // preamble missing
300         return 0;
301     }
302
303     if (sens_msg == 12) {
304         if (br[10] != crc8(br, 10, CRC_POLY, CRC_INIT)) {
305         // crc mismatch
306         return 0;
307     }
308
309         } else {
310         if (br[7] != crc8(br, 7, CRC_POLY, CRC_INIT)) {
311         // crc mismatch
312         return 0;
313     }
314         }
315
316     if (br[0] == 0xff && (br[1] >> 4) == 0x0a) {
317     msg_type = 0; // WH1080/3080 Weather msg
318     } else if (br[0] == 0xff && (br[1] >> 4) == 0x0b) {
319     msg_type = 1; // WH1080/3080 Datetime msg
320     } else if (br[0] == 0xff && (br[1] >> 4) == 0x07) {
321     msg_type = 2; // WH3080 UV/Light msg
322     }
323         
324
325
326         
327 //---------------------------------------------------------------------------------------       
328 //-------- GETTING WEATHER SENSORS DATA -------------------------------------------------
329         
330     const float temperature = get_temperature(br);
331     const int humidity = get_humidity(br);
332     const char* direction_str = get_wind_direction_str(br);
333         const char* direction_deg = get_wind_direction_deg(br); 
334         
335         
336         // Select which metric system for *wind avg speed* and *wind gust* :
337         
338         // Wind average speed :
339         
340         //const float speed = get_wind_avg_ms((br)   // <--- Data will be shown in Meters/sec.
341         //const float speed = get_wind_avg_mph((br)  // <--- Data will be shown in Mph
342         const float speed = get_wind_avg_kmh(br);  // <--- Data will be shown in Km/h
343         //const float speed = get_wind_avg_knot((br) // <--- Data will be shown in Knots
344         
345         // Wind gust speed :
346         
347     //const float gust = get_wind_gust_ms(br);   // <--- Data will be shown in Meters/sec.
348         //const float gust = get_wind_gust_mph(br);  // <--- Data will be shown in Mph
349         const float gust = get_wind_gust_kmh(br);  // <--- Data will be shown in km/h
350         //const float gust = get_wind_gust_knot(br); // <--- Data will be shown in Knots        
351         
352     const float rain = get_rainfall(br);
353     const int device_id = get_device_id(br);
354         const char* battery = get_battery(br);
355         
356         
357         //---------------------------------------------------------------------------------------       
358     //-------- GETTING UV DATA --------------------------------------------------------------
359
360     const int uv_sensor_id = get_uv_sensor_id(br);
361     const char* uv_status = get_uvstatus(br);
362     const int uv_index = wh3080_uvi(br);
363
364
365     //---------------------------------------------------------------------------------------   
366     //-------- GETTING LIGHT DATA -----------------------------------------------------------
367
368     const float light = get_rawlight(br);
369     const float lux = (get_rawlight(br)/10);
370     const float wm = (get_rawlight(br)/6830);
371     const float fc = ((get_rawlight(br)/10.76)/10.0);
372
373
374
375         //---------------------------------------------------------------------------------------       
376         //-------- GETTING TIME DATA ------------------------------------------------------------
377
378         char* signal = get_signal(br);
379         const int hours = get_hours(br);
380         const int minutes =     get_minutes(br);
381         const int seconds = get_seconds(br);
382         const int year = 2000 + get_year(br);
383         const int month = get_month(br);
384         const int day = get_day(br);
385         
386
387         //--------- PRESENTING DATA --------------------------------------------------------------
388         
389 if (msg_type == 0) {
390
391     data = data_make(
392                         "time",         "",             DATA_STRING,                                    time_str,
393                         "model",        "",             DATA_STRING,    "Fine Offset Electronics WH1080/WH3080 Weather Station",
394                         "msg_type",     "Msg type",     DATA_INT,                                       msg_type,
395                         "id",           "Station ID",   DATA_FORMAT,    "%d",           DATA_INT,       device_id,
396                         "temperature_C","Temperature",  DATA_FORMAT,    "%.01f C",      DATA_DOUBLE,    temperature,
397                         "humidity",     "Humidity",     DATA_FORMAT,    "%u %%",        DATA_INT,       humidity,
398                         "direction_str","Wind string",  DATA_STRING,                                    direction_str,
399                         "direction_deg","Wind degrees", DATA_STRING,                                    direction_deg,
400                         "speed",        "Wind avg speed",DATA_FORMAT,   "%.02f",        DATA_DOUBLE,    speed,
401                         "gust",         "Wind gust",    DATA_FORMAT,    "%.02f",        DATA_DOUBLE,    gust,
402                         "rain",         "Total rainfall",DATA_FORMAT,   "%3.1f",        DATA_DOUBLE,    rain,
403                         "battery",      "Battery",      DATA_STRING,                                    battery,
404                 NULL);
405     data_acquired_handler(data);
406     return 1;
407
408 } else if (msg_type == 1) {
409
410     data = data_make(
411                         "time",         "",             DATA_STRING,            time_str,
412                         "model",        "",             DATA_STRING,    "Fine Offset Electronics WH1080/WH3080 Weather Station",
413                         "msg_type",     "Msg type",     DATA_INT,                               msg_type,
414                         "id",           "Station ID",   DATA_FORMAT,    "%d",   DATA_INT,       device_id,
415                         "signal",       "Signal Type",  DATA_STRING,                            signal,
416                         "hours",        "Hours\t",      DATA_FORMAT,    "%02d", DATA_INT,       hours,
417                         "minutes",      "Minutes",      DATA_FORMAT,    "%02d", DATA_INT,       minutes,
418                         "seconds",      "Seconds",      DATA_FORMAT,    "%02d", DATA_INT,       seconds,
419                         "year",         "Year\t",       DATA_FORMAT,    "%02d", DATA_INT,       year,
420                         "month",        "Month\t",      DATA_FORMAT,    "%02d", DATA_INT,       month,
421                         "day",          "Day\t",        DATA_FORMAT,    "%02d", DATA_INT,       day,
422                 NULL);
423     data_acquired_handler(data);
424     return 1;
425
426 } else {
427
428     data = data_make(
429                         "time",         "",             DATA_STRING,                            time_str,
430                         "model",        "",             DATA_STRING,    "Fine Offset Electronics WH3080 Weather Station",
431                         "msg_type",     "Msg type",     DATA_INT,                               msg_type,
432                         "uv_sensor_id", "UV Sensor ID", DATA_FORMAT,    "%d",   DATA_INT,       uv_sensor_id,
433                         "uv_status",    "Sensor Status",DATA_STRING,                            uv_status,
434                         "uv_index",     "UV Index",     DATA_INT,                               uv_index,
435                         "lux",          "Lux\t",        DATA_FORMAT,    "%.1f", DATA_DOUBLE,    lux,
436                         "wm",           "Watts/m\t",    DATA_FORMAT,    "%.2f", DATA_DOUBLE,    wm,
437                         "fc",           "Foot-candles", DATA_FORMAT,    "%.2f", DATA_DOUBLE,    fc,
438                 NULL);
439     data_acquired_handler(data);
440     return 1; 
441     }
442 }
443
444 static char *output_fields[] = {
445     "time",
446     "model",
447     "id",
448     "temperature_C",
449     "humidity",
450     "direction_str",
451     "direction_deg",
452     "speed",
453     "gust",
454     "rain",
455     "msg_type",
456     "signal",
457     "hours",
458     "minutes",
459     "seconds",
460     "year",
461     "month",
462     "day",
463     "battery",
464     "sensor_code",
465     "uv_status",
466     "uv_index",
467     "lux",
468     "wm",
469     "fc",
470     NULL
471 };
472
473 r_device fineoffset_wh1080 = {
474     .name           = "Fine Offset Electronics WH1080/WH3080 Weather Station",
475     .modulation     = OOK_PULSE_PWM_RAW,
476     .short_limit    = 800,
477     .long_limit     = 2800,
478     .reset_limit    = 2800,
479     .json_callback  = &fineoffset_wh1080_callback,
480     .disabled       = 0,
481     .demod_arg      = 0,
482     .fields         = output_fields,
483 };
484
485 /**
486  * http://www.jaycar.com.au/mini-lcd-display-weather-station/p/XC0400
487  */
488
489 r_device fineoffset_XC0400 = {
490     .name           = "Fine Offset Electronics, XC0400",
491     .modulation     = OOK_PULSE_PWM_RAW,
492     .short_limit    = 800,      // Short pulse 544µs, long pulse 1524µs, fixed gap 1036µs
493     .long_limit     = 2800,     // Maximum pulse period (long pulse + fixed gap)
494     .reset_limit    = 2800,     // We just want 1 package
495     .json_callback  = &fineoffset_wh1080_callback,
496     .disabled       = 0,
497     .demod_arg      = 0,
498     .fields         = output_fields
499 };