Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature air quality sensor #96

Merged
merged 7 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ To allow automatic handling of all Bresser weather station variants, the decoder
| *7002531* | *3-in-1 Professional Wind Gauge / Anemometer* | *decodeBresser**6In1**Payload()* **1)** |
| 7002585 | Weather Station | decodeBresser**6In1**Payload() |
| 7009999 | Thermo-/Hygrometer Sensor | decodeBresser**6in1**Payload() |
| 7009970 | Air Quality Sensor PM 2.5 / PM 10 | decodeBresser**7In1**Payload() |
| 7009972 | Soil Moisture/Temperature Sensor | decodeBresser**6In1**Payload() |
| 7009973 | Pool / Spa Thermometer | decodeBresser**6In1**Payload() |
| 7009975 | Water Leakage Sensor | decodeBresser**Leakage**Payload() |
| 7009976 | Lightning Sensor | decodeBresser**Lightning**Payload() |
| 7009976 | Lightning Sensor | decodeBresser**Lightning**Payload() **3)** |
| 7003600 and WSX3001 | Weather Station | decodeBresser**7In1**Payload() **2)** |
| 7003210 | Weather Station | decodeBresser**7In1**Payload() |
| 7803200 | Weather Sensor | decodeBresser**7In1**Payload() |
Expand All @@ -50,6 +51,8 @@ Some guesswork:

**2)** The part number is specific to the actual variant, i.e. some more characters are appended

**3)** Work in progress

## Configuration

### Configuration by selecting a supported Board in the Arduino IDE
Expand Down
10 changes: 10 additions & 0 deletions examples/BresserWeatherSensorBasic/BresserWeatherSensorBasic.ino
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
// 20230624 Added Bresser Lightning Sensor decoder
// 20230804 Added Bresser Water Leakage Sensor decoder
// 20231023 Modified detection of Lightning Sensor
// 20231025 Added Bresser Air Quality (Particulate Matter) Sensor decoder
//
// ToDo:
// -
Expand Down Expand Up @@ -107,6 +108,15 @@ void loop()
);
Serial.printf("Leakage: [%-5s] ", (weatherSensor.sensor[i].water_leakage_alarm) ? "ALARM" : "OK");

} else if (weatherSensor.sensor[i].s_type == SENSOR_TYPE_AIR_PM) {
// Air Quality (Particle Matter) Sensor
Serial.printf("Id: [%8X] Typ: [%X] Battery: [%s] ",
(unsigned int)weatherSensor.sensor[i].sensor_id,
weatherSensor.sensor[i].s_type,
weatherSensor.sensor[i].battery_ok ? "OK " : "Low");
Serial.printf("Ch: [%d] ", weatherSensor.sensor[i].chan);
Serial.printf("PM2.5: [%uµg/m³] ", weatherSensor.sensor[i].aqs_pm_2_5);
Serial.printf("PM10: [%uµg/m³] ", weatherSensor.sensor[i].aqs_pm_10);
} else {
// Any other (weather-like) sensor is very similar
Serial.printf("Id: [%8X] Typ: [%X] Battery: [%s] ",
Expand Down
126 changes: 70 additions & 56 deletions examples/BresserWeatherSensorMQTTCustom/src/WeatherSensor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
// 20230814 Fixed receiver state handling in getMessage()
// 20231006 Added crc16() from https://github.com/merbanan/rtl_433/blob/master/src/util.c
// Added CRC check in decodeBresserLeakagePayload()
// 20231024 Added Pool / Spa Thermometer (P/N 7009973) to 6-in-1 decoder
// 20231026 Added Air Quality Sensor (Particulate Matter, P/N 7009970) to 7-in-1 decoder
// Modified decoding of sensor type (to raw, non de-whitened data)
//
// ToDo:
// -
Expand Down Expand Up @@ -644,6 +647,7 @@ DecodeStatus WeatherSensor::decodeBresser5In1Payload(const uint8_t *msg, uint8_t
// - also Froggit WH6000 sensors.
// - also rebranded as Ventus C8488A (W835)
// - also Bresser 3-in-1 Professional Wind Gauge / Anemometer PN 7002531
// - also Bresser Pool / Spa Thermometer PN 7009973 (s_type = 3)
//
// There are at least two different message types:
// - 24 seconds interval for temperature, hum, uv and rain (alternating messages)
Expand All @@ -655,7 +659,7 @@ DecodeStatus WeatherSensor::decodeBresser5In1Payload(const uint8_t *msg, uint8_t
// Moisture:
//
// f16e 187000e34 7 ffffff0000 252 2 16 fff 004 000 [25,2, 99%, CH 7]
// DIGEST:8h8h ID?8h8h8h8h :4h STARTUP:1b CH:3d 8h 8h8h 8h8h TEMP:12h ?2b BATT:1b ?1b MOIST:8h UV?~12h ?4h CHKSUM:8h
// DIGEST:8h8h ID?8h8h8h8h TYPE:4h STARTUP:1b CH:3d 8h 8h8h 8h8h TEMP:12h ?2b BATT:1b ?1b MOIST:8h UV?~12h ?4h CHKSUM:8h
//
// Moisture is transmitted in the humidity field as index 1-16: 0, 7, 13, 20, 27, 33, 40, 47, 53, 60, 67, 73, 80, 87, 93, 99.
// The Wind speed and direction fields decode to valid zero but we exclude them from the output.
Expand Down Expand Up @@ -693,8 +697,8 @@ DecodeStatus WeatherSensor::decodeBresser5In1Payload(const uint8_t *msg, uint8_t
//
// Wind and Temperature/Humidity or Rain:
//
// DIGEST:8h8h ID:8h8h8h8h :4h STARTUP:1b CH:3d WSPEED:~8h~4h ~4h~8h WDIR:12h ?4h TEMP:8h.4h ?2b BATT:1b ?1b HUM:8h UV?~12h ?4h CHKSUM:8h
// DIGEST:8h8h ID:8h8h8h8h :4h STARTUP:1b CH:3d WSPEED:~8h~4h ~4h~8h WDIR:12h ?4h RAINFLAG:8h RAIN:8h8h UV:8h8h CHKSUM:8h
// DIGEST:8h8h ID:8h8h8h8h TYPE:4h STARTUP:1b CH:3d WSPEED:~8h~4h ~4h~8h WDIR:12h ?4h TEMP:8h.4h ?2b BATT:1b ?1b HUM:8h UV?~12h ?4h CHKSUM:8h
// DIGEST:8h8h ID:8h8h8h8h TYPE:4h STARTUP:1b CH:3d WSPEED:~8h~4h ~4h~8h WDIR:12h ?4h RAINFLAG:8h RAIN:8h8h UV:8h8h CHKSUM:8h
//
// Digest is LFSR-16 gen 0x8810 key 0x5412, excluding the add-checksum and trailer.
// Checksum is 8-bit add (with carry) to 0xff.
Expand Down Expand Up @@ -835,13 +839,18 @@ DecodeStatus WeatherSensor::decodeBresser6In1Payload(const uint8_t *msg, uint8_t

moisture_ok = false;

// Pool / Spa thermometer
if (sensor[slot].s_type == SENSOR_TYPE_POOL_THERMO) {
humidity_ok = false;
}

// the moisture sensor might present valid readings but does not have the hardware
if (sensor[slot].s_type == 4) {
if (sensor[slot].s_type == SENSOR_TYPE_SOIL) {
wind_ok = 0;
uv_ok = 0;
}

if (sensor[slot].s_type == 4 && temp_ok && sensor[slot].humidity >= 1 && sensor[slot].humidity <= 16) {
if (sensor[slot].s_type == SENSOR_TYPE_SOIL && temp_ok && sensor[slot].humidity >= 1 && sensor[slot].humidity <= 16) {
moisture_ok = true;
humidity_ok = false;
sensor[slot].moisture = moisture_map[sensor[slot].humidity - 1];
Expand Down Expand Up @@ -888,8 +897,9 @@ Data layout:
{269}9a59b4a5a3da10aaaaaaaaaaaaaa8bdac8afea28a8caaaaaaa000000000000000000 [54.0 klx UV=2.6]
{230}fe15b4a5a3da10aaaaaaaaaaaaaa8bdacbba382aacdaaaaaaa00000000 [109.2klx UV=6.7]
{254}2544b4a5a32a10aaaaaaaaaaaaaa8bdac88aaaaabeaaaaaaaa00000000000000 [200.000 klx UV=14
DIGEST:8h8h ID?8h8h WDIR:8h4h 4h 8h WGUST:8h.4h WAVG:8h.4h RAIN:8h8h4h.4h RAIN?:8h TEMP:8h.4hC FLAGS?:4h HUM:8h% LIGHT:8h4h,8h4hKL UV:8h.4h TRAILER:8h8h8h4h
DIGEST:8h8h ID?8h8h WDIR:8h4h 4h STYPE:4h STARTUP:1b CH:3d WGUST:8h.4h WAVG:8h.4h RAIN:8h8h4h.4h RAIN?:8h TEMP:8h.4hC FLAGS?:4h HUM:8h% LIGHT:8h4h,8h4hKL UV:8h.4h TRAILER:8h8h8h4h
Unit of light is kLux (not W/m²).
STYPE, STARTUP and CH are not covered by whitening. Probably also ID.
First two bytes are an LFSR-16 digest, generator 0x8810 key 0xba95 with a final xor 0x6df1, which likely means we got that wrong.
*/
#ifdef BRESSER_7_IN_1
Expand All @@ -913,11 +923,13 @@ DecodeStatus WeatherSensor::decodeBresser7In1Payload(const uint8_t *msg, uint8_t
return DECODE_DIG_ERR;
}

#if CORE_DEBUG_LEVEL == ARDUHAL_LOG_LEVEL_VERBOSE
#if CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
log_message("De-whitened Data", msgw, msgSize);
#endif

int id_tmp = (msgw[2] << 8) | (msgw[3]);
int id_tmp = (msgw[2] << 8) | (msgw[3]);
int s_type = msg[6] >> 4; // raw data, no de-whitening

DecodeStatus status;

// Find appropriate slot in sensor data array and update <status>
Expand All @@ -944,41 +956,51 @@ DecodeStatus WeatherSensor::decodeBresser7In1Payload(const uint8_t *msg, uint8_t
float light_klx = lght_raw * 0.001f; // TODO: remove this
float light_lux = lght_raw;
float uv_index = uv_raw * 0.1f;

// The RTL_433 decoder does not include any field to verify that these data
// are ok, so we are assuming that they are ok if the decode status is ok.
sensor[slot].temp_ok = true;
sensor[slot].humidity_ok = true;
sensor[slot].wind_ok = true;
sensor[slot].rain_ok = true;
sensor[slot].light_ok = true;
sensor[slot].uv_ok = true;
uint16_t pm_2_5 = (msgw[10] & 0x0f) * 1000 + (msgw[11] >> 4) * 100 + (msgw[11] & 0x0f) * 10 + (msgw[12] >> 4);
uint16_t pm_10 = (msgw[12] & 0x0f) * 1000 + (msgw[13] >> 4) * 100 + (msgw[13] & 0x0f) * 10 + (msgw[14] >> 4);

sensor[slot].sensor_id = id_tmp;
sensor[slot].s_type = msgw[6] >> 4;
sensor[slot].startup = (msgw[6] & 0x08) == 0x08;
sensor[slot].temp_c = temp_c;
sensor[slot].humidity = humidity;
#ifdef WIND_DATA_FLOATINGPOINT
sensor[slot].wind_gust_meter_sec = wgst_raw * 0.1f;
sensor[slot].wind_avg_meter_sec = wavg_raw * 0.1f;
sensor[slot].wind_direction_deg = wdir * 1.0f;
#endif
#ifdef WIND_DATA_FIXEDPOINT
sensor[slot].wind_gust_meter_sec_fp1 = wgst_raw;
sensor[slot].wind_avg_meter_sec_fp1 = wavg_raw;
sensor[slot].wind_direction_deg_fp1 = wdir * 10;
#endif
sensor[slot].rain_mm = rain_mm;
sensor[slot].light_klx = light_klx;
sensor[slot].light_lux = light_lux;
sensor[slot].uv = uv_index;
sensor[slot].s_type = s_type;
sensor[slot].startup = (msg[6] & 0x08) == 0x08; // raw data, no de-whitening
sensor[slot].chan = msg[6] & 0x07; // raw data, no de-whitening


if (s_type == SENSOR_TYPE_WEATHER1) {
// The RTL_433 decoder does not include any field to verify that these data
// are ok, so we are assuming that they are ok if the decode status is ok.
sensor[slot].temp_ok = true;
sensor[slot].humidity_ok = true;
sensor[slot].wind_ok = true;
sensor[slot].rain_ok = true;
sensor[slot].light_ok = true;
sensor[slot].uv_ok = true;
sensor[slot].temp_c = temp_c;
sensor[slot].humidity = humidity;
#ifdef WIND_DATA_FLOATINGPOINT
sensor[slot].wind_gust_meter_sec = wgst_raw * 0.1f;
sensor[slot].wind_avg_meter_sec = wavg_raw * 0.1f;
sensor[slot].wind_direction_deg = wdir * 1.0f;
#endif
#ifdef WIND_DATA_FIXEDPOINT
sensor[slot].wind_gust_meter_sec_fp1 = wgst_raw;
sensor[slot].wind_avg_meter_sec_fp1 = wavg_raw;
sensor[slot].wind_direction_deg_fp1 = wdir * 10;
#endif
sensor[slot].rain_mm = rain_mm;
sensor[slot].light_klx = light_klx;
sensor[slot].light_lux = light_lux;
sensor[slot].uv = uv_index;
}
else if (s_type == SENSOR_TYPE_AIR_PM) {
sensor[slot].pm_ok = true;
sensor[slot].aqs_pm_2_5 = pm_2_5;
sensor[slot].aqs_pm_10 = pm_10;
}
sensor[slot].battery_ok = !battery_low;
sensor[slot].valid = true;
sensor[slot].complete = true;
sensor[slot].rssi = rssi;
sensor[slot].valid = true;
sensor[slot].complete = true;


/* clang-format off */
/* data = data_make(
Expand Down Expand Up @@ -1062,6 +1084,7 @@ DecodeStatus WeatherSensor::decodeBresserLightningPayload(const uint8_t *msg, ui
#endif

int id_tmp = (msgw[2] << 8) | (msgw[3]);
int s_type = msg[6] >> 4;

DecodeStatus status;

Expand All @@ -1075,13 +1098,12 @@ DecodeStatus WeatherSensor::decodeBresserLightningPayload(const uint8_t *msg, ui
log_v("--> CTR RAW: %d BCD: %d", ctr, ((((msgw[4] & 0xf0) >> 4) * 100) + (msgw[4] & 0x0f) * 10 + ((msgw[5] & 0xf0) >> 4)));
uint8_t battery_low = (msgw[5] & 0x08) == 0x00;
uint16_t unknown1 = ((msgw[5] & 0x0f) << 8) | msgw[6];
uint8_t type = msgw[6] >> 4;
uint8_t distance_km = msgw[7];
log_v("--> DST RAW: %d BCD: %d TAB: %d", msgw[7], ((((msgw[7] & 0xf0) >> 4) * 10) + (msgw[7] & 0x0f)), distance_map[ msgw[7] ]);
uint16_t unknown2 = (msgw[8] << 8) | msgw[9];

sensor[slot].sensor_id = id_tmp;
sensor[slot].s_type = type;
sensor[slot].s_type = s_type;
sensor[slot].lightning_count = ctr;
sensor[slot].lightning_distance_km = distance_km;
sensor[slot].lightning_unknown1 = unknown1;
Expand All @@ -1092,7 +1114,7 @@ DecodeStatus WeatherSensor::decodeBresserLightningPayload(const uint8_t *msg, ui
sensor[slot].valid = true;
sensor[slot].complete = true;

log_d("ID: 0x%04X TYPE: %d CTR: %d batt_low: %d distance_km: %d unknown1: 0x%x unknown2: 0x%04x", id_tmp, type, ctr, battery_low, distance_km, unknown1, unknown2);
log_d("ID: 0x%04X TYPE: %d CTR: %d batt_low: %d distance_km: %d unknown1: 0x%x unknown2: 0x%04x", id_tmp, s_type, ctr, battery_low, distance_km, unknown1, unknown2);


/* clang-format off */
Expand Down Expand Up @@ -1162,6 +1184,8 @@ DecodeStatus WeatherSensor::decodeBresserLeakagePayload(const uint8_t *msg, uint
{
#if CORE_DEBUG_LEVEL == ARDUHAL_LOG_LEVEL_VERBOSE
log_message("Data", msg, msgSize);
#else
(void)msgSize;
#endif

// Verify CRC (CRC16/XMODEM)
Expand All @@ -1178,26 +1202,16 @@ DecodeStatus WeatherSensor::decodeBresserLeakagePayload(const uint8_t *msg, uint
bool alarm = (msg[7] & 0x80) == 0x80;
bool no_alarm = (msg[7] & 0x40) == 0x40;

DecodeStatus status;

uint8_t null_bytes = msg[7] & 0xF;

for (int i=8; i<=15; i++) {
null_bytes |= msg[i];
}

// The checksum/digest algorithm is currently unknown.
// We apply some heuristics to validate that the message is realy from
// a water leakage sensor.
// Sanity checks
bool decode_ok = (type_tmp == SENSOR_TYPE_LEAKAGE) &&
(alarm != no_alarm) &&
(chan_tmp != 0) &&
(null_bytes == 0);
(chan_tmp != 0);

status = (decode_ok) ? DECODE_OK : DECODE_INVALID;
if (status != DECODE_OK)
return status;
if (!decode_ok)
return DECODE_INVALID;

DecodeStatus status = DECODE_OK;

// Find appropriate slot in sensor data array and update <status>
int slot = findSlot(id_tmp, &status);

Expand Down
Loading