3 * *** Fine Offset WH1080/WH3080 Weather Station ***
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.
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/ .
11 * See also Frank 'SevenW' page ( https://www.sevenwatt.com/main/wh1080-protocol-v2-fsk/ ) for some other useful info.
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).
16 *****************************************
18 *****************************************
20 * (aka Digitech XC0348 Weather Station)
22 * (aka Elecsa AstroTouch 6975)
23 * (aka Froggit WH1080)
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').
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).
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.
40 * Data are trasmitted in a 48 seconds cycle (data packet, then wait 48 seconds, then data packet...).
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.
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 :) ).
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
56 * The 'Total rainfall' field is a cumulative counter, increased by 0.3 millimeters of rain at once.
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! :)
65 *****************************************
67 *****************************************
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.
75 * The module is able to decode all kind of data coming from the WH3080: weather, datetime, UV and light plus some
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
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.
88 * 2016-2017 Nicola Quiriti ('ovrheat' - 'seven')
100 #define CRC_INIT 0xff
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",};
108 static unsigned short get_device_id(const uint8_t* br) {
109 return (br[1] << 4 & 0xf0 ) | (br[2] >> 4);
112 static char* get_battery(const uint8_t* br) {
113 if ((br[9] >> 4) != 1) {
120 // ------------ WEATHER SENSORS DECODING ----------------------------------------------------
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;
127 static int get_humidity(const uint8_t* br) {
131 static char* get_wind_direction_str(const uint8_t* br) {
132 return wind_dir_string[br[9] & 0x0f];
135 static char* get_wind_direction_deg(const uint8_t* br) {
136 return wind_dir_degr[br[9] & 0x0f];
139 static float get_wind_speed_raw(const uint8_t* br) {
143 static float get_wind_avg_ms(const uint8_t* br) {
144 return (br[5] * 34.0f) / 100; // Meters/sec.
147 static float get_wind_avg_mph(const uint8_t* br) {
148 return ((br[5] * 34.0f) / 100) * 2.23693629f; // Mph
151 static float get_wind_avg_kmh(const uint8_t* br) {
152 return ((br[5] * 34.0f) / 100) * 3.6f; // Km/h
155 static float get_wind_avg_knot(const uint8_t* br) {
156 return ((br[5] * 34.0f) / 100) * 1.94384f; // Knots
159 static float get_wind_gust_raw(const uint8_t* br) {
163 static float get_wind_gust_ms(const uint8_t* br) {
164 return (br[6] * 34.0f) / 100; // Meters/sec.
167 static float get_wind_gust_mph(const uint8_t* br) {
168 return ((br[6] * 34.0f) / 100) * 2.23693629f; // Mph
172 static float get_wind_gust_kmh(const uint8_t* br) {
173 return ((br[6] * 34.0f) / 100) * 3.6f; // Km/h
176 static float get_wind_gust_knot(const uint8_t* br) {
177 return ((br[6] * 34.0f) / 100) * 1.94384f; // Knots
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;
186 // ------------ WH3080 UV SENSOR DECODING ----------------------------------------------------
188 static unsigned short get_uv_sensor_id(const uint8_t* br) {
189 return (br[1] << 4 & 0xf0 ) | (br[2] >> 4);
192 static char* get_uvstatus(const uint8_t* br) {
200 static unsigned short wh3080_uvi(const uint8_t* br) {
201 return (br[2] & 0x0F );
205 // ------------ WH3080 LIGHT SENSOR DECODING -------------------------------------------------
207 static float get_rawlight(const uint8_t* br) {
208 return (((((br[4]) << 16) | ((br[5]) << 8) | br[6])));
212 //----------------- TIME DECODING ----------------------------------------------------
214 static char* get_signal(const uint8_t* br) {
215 if ((br[2] & 0x0F) == 10) {
222 static int get_hours(const uint8_t* br) {
223 return ((br[3] >> 4 & 0x03) * 10) + (br[3] & 0x0F);
226 static int get_minutes(const uint8_t* br) {
227 return (((br[4] & 0xF0) >> 4) * 10) + (br[4] & 0x0F);
230 static int get_seconds(const uint8_t* br) {
231 return (((br[5] & 0xF0) >> 4) * 10) + (br[5] & 0x0F);
234 static int get_year(const uint8_t* br) {
235 return (((br[6] & 0xF0) >> 4) * 10) + (br[6] & 0x0F);
238 static int get_month(const uint8_t* br) {
239 return ((br[7] >> 4 & 0x01) * 10) + (br[7] & 0x0F);
242 static int get_day(const uint8_t* br) {
243 return (((br[8] & 0xF0) >> 4) * 10) + (br[8] & 0x0F);
246 //-------------------------------------------------------------------------------------
247 //-------------------------------------------------------------------------------------
251 static int fineoffset_wh1080_callback(bitbuffer_t *bitbuffer) {
253 char time_str[LOCAL_TIME_BUFLEN];
255 int msg_type; // 0=Weather 1=Datetime 2=UV/Light
256 int sens_msg = 12; // 12=Weather/Time sensor 8=UV/Light sensor
258 uint8_t bbuf[sens_msg];
259 local_time_str(0, time_str);
261 if (bitbuffer->num_rows != 1) {
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)){
269 if(bitbuffer->bits_per_row[0] == 88) { // FineOffset WH1080/3080 Weather data msg
271 br = bitbuffer->bb[0];
272 } else if(bitbuffer->bits_per_row[0] == 87) { // FineOffset WH1080/3080 Weather data msg (different version (newest?))
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);
279 } else if(bitbuffer->bits_per_row[0] == 64) { // FineOffset WH3080 UV/Light data msg
281 br = bitbuffer->bb[0];
283 } else if(bitbuffer->bits_per_row[0] == 63) { // FineOffset WH3080 UV/Light data msg (different version (newest?))
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);
293 for (i=0 ; i<((sens_msg)-1) ; i++)
294 fprintf(stderr, "%02x ", bbuf[i]);
295 fprintf(stderr, "\n");
303 if (sens_msg == 12) {
304 if (br[10] != crc8(br, 10, CRC_POLY, CRC_INIT)) {
310 if (br[7] != crc8(br, 7, CRC_POLY, CRC_INIT)) {
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
327 //---------------------------------------------------------------------------------------
328 //-------- GETTING WEATHER SENSORS DATA -------------------------------------------------
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);
336 // Select which metric system for *wind avg speed* and *wind gust* :
338 // Wind average speed :
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
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
352 const float rain = get_rainfall(br);
353 const int device_id = get_device_id(br);
354 const char* battery = get_battery(br);
357 //---------------------------------------------------------------------------------------
358 //-------- GETTING UV DATA --------------------------------------------------------------
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);
365 //---------------------------------------------------------------------------------------
366 //-------- GETTING LIGHT DATA -----------------------------------------------------------
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);
375 //---------------------------------------------------------------------------------------
376 //-------- GETTING TIME DATA ------------------------------------------------------------
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);
387 //--------- PRESENTING DATA --------------------------------------------------------------
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,
405 data_acquired_handler(data);
408 } else if (msg_type == 1) {
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,
423 data_acquired_handler(data);
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,
439 data_acquired_handler(data);
444 static char *output_fields[] = {
473 r_device fineoffset_wh1080 = {
474 .name = "Fine Offset Electronics WH1080/WH3080 Weather Station",
475 .modulation = OOK_PULSE_PWM_RAW,
479 .json_callback = &fineoffset_wh1080_callback,
482 .fields = output_fields,
486 * http://www.jaycar.com.au/mini-lcd-display-weather-station/p/XC0400
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,
498 .fields = output_fields