From b402e403a04b3daf3662d7bb233c010f41a14984 Mon Sep 17 00:00:00 2001 From: Evaldas Auryla Date: Tue, 6 Jan 2026 03:34:23 +0100 Subject: [PATCH] [radon_eye_rd200] update Radon Eye RD200 with v2/v3 support (#7962) Co-authored-by: Artem Butusov Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../radon_eye_ble/radon_eye_listener.cpp | 19 +- .../radon_eye_rd200/radon_eye_rd200.cpp | 184 ++++++++++-------- .../radon_eye_rd200/radon_eye_rd200.h | 14 +- 3 files changed, 115 insertions(+), 102 deletions(-) diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index 0c6165c691..2c3ef77add 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -1,7 +1,6 @@ #include "radon_eye_listener.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include #ifdef USE_ESP32 @@ -11,17 +10,11 @@ namespace radon_eye_ble { static const char *const TAG = "radon_eye_ble"; bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - if (not device.get_name().empty()) { - // Vector containing the prefixes to search for - std::vector prefixes = {"FR:R", "FR:I", "FR:H"}; - - // Check if the device name starts with any of the prefixes - if (std::any_of(prefixes.begin(), prefixes.end(), - [&](const std::string &prefix) { return device.get_name().starts_with(prefix); })) { - // Device found - ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), - device.address_str().c_str()); - } + // Radon Eye devices have names starting with "FR:" + if (device.get_name().starts_with("FR:")) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), + device.address_str_to(addr_buf)); } return false; } diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp index 3ccb7bf082..1bd0b842fe 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp @@ -1,5 +1,7 @@ #include "radon_eye_rd200.h" +#include + #ifdef USE_ESP32 namespace esphome { @@ -7,6 +9,22 @@ namespace radon_eye_rd200 { static const char *const TAG = "radon_eye_rd200"; +static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V1 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001523-1212-efde-1523-785feabcd123"); +static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V1 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001524-1212-efde-1523-785feabcd123"); +static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V1 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001525-1212-efde-1523-785feabcd123"); +static const uint8_t WRITE_COMMAND_V1 = 0x50; + +static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V2 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001523-0000-1000-8000-00805f9b34fb"); +static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V2 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001524-0000-1000-8000-00805f9b34fb"); +static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V2 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001525-0000-1000-8000-00805f9b34fb"); +static const uint8_t WRITE_COMMAND_V2 = 0x40; + void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { @@ -23,6 +41,22 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_SEARCH_CMPL_EVT: { + if (this->parent()->get_service(SERVICE_UUID_V1) != nullptr) { + service_uuid_ = SERVICE_UUID_V1; + sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V1; + sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V1; + write_command_ = WRITE_COMMAND_V1; + } else if (this->parent()->get_service(SERVICE_UUID_V2) != nullptr) { + service_uuid_ = SERVICE_UUID_V2; + sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V2; + sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V2; + write_command_ = WRITE_COMMAND_V2; + } else { + ESP_LOGW(TAG, "No supported device has been found, disconnecting"); + parent()->set_enabled(false); + break; + } + this->read_handle_ = 0; auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_read_characteristic_uuid_); if (chr == nullptr) { @@ -32,90 +66,114 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } this->read_handle_ = chr->handle; - // Write a 0x50 to the write characteristic. auto *write_chr = this->parent()->get_characteristic(service_uuid_, sensors_write_characteristic_uuid_); if (write_chr == nullptr) { ESP_LOGW(TAG, "No sensor write characteristic found at service %s char %s", service_uuid_.to_string().c_str(), - sensors_read_characteristic_uuid_.to_string().c_str()); + sensors_write_characteristic_uuid_.to_string().c_str()); break; } this->write_handle_ = write_chr->handle; - this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; - - write_query_message_(); - - request_read_values_(); + esp_err_t status = + esp_ble_gattc_register_for_notify(gattc_if, this->parent()->get_remote_bda(), this->read_handle_); + if (status) { + ESP_LOGW(TAG, "Error registering for sensor notify, status=%d", status); + } break; } - case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->get_conn_id()) - break; - if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + case ESP_GATTC_WRITE_DESCR_EVT: { + if (param->write.status != ESP_GATT_OK) { + ESP_LOGE(TAG, "write descr failed, error status = %x", param->write.status); break; } - if (param->read.handle == this->read_handle_) { - read_sensors_(param->read.value, param->read.value_len); + ESP_LOGV(TAG, "Write descr success, writing 0x%02X at write_handle=%d", this->write_command_, + this->write_handle_); + esp_err_t status = + esp_ble_gattc_write_char(gattc_if, this->parent()->get_conn_id(), this->write_handle_, sizeof(write_command_), + (uint8_t *) &write_command_, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error writing 0x%02x command, status=%d", write_command_, status); } break; } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.is_notify) { + ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive notify value, %d bytes", param->notify.value_len); + } else { + ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive indicate value, %d bytes", param->notify.value_len); + } + read_sensors_(param->notify.value, param->notify.value_len); + break; + } + default: break; } } void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) { - if (value_len < 20) { - ESP_LOGD(TAG, "Invalid read"); + if (value_len < 1) { + ESP_LOGW(TAG, "Unexpected empty message"); return; } - // Example data - // [13:08:47][D][radon_eye_rd200:107]: result bytes: 5010 85EBB940 00000000 00000000 2200 2500 0000 - ESP_LOGV(TAG, "result bytes: %02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X %02X%02X %02X%02X", - value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7], value[8], value[9], - value[10], value[11], value[12], value[13], value[14], value[15], value[16], value[17], value[18], - value[19]); + uint8_t command = value[0]; - if (value[0] != 0x50) { - // This isn't a sensor reading. + if ((command == WRITE_COMMAND_V1 && value_len < 20) || (command == WRITE_COMMAND_V2 && value_len < 68)) { + ESP_LOGW(TAG, "Unexpected command 0x%02X message length %d", command, value_len); return; } + // Example data V1: + // 501085EBB9400000000000000000220025000000 + // Example data V2: + // 4042323230313033525532303338330652443230304e56322e302e3200014a00060a00080000000300010079300000e01108001c00020000003822005c8f423fa4709d3f + ESP_LOGV(TAG, "radon sensors raw bytes"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, value, value_len, ESP_LOG_VERBOSE); + // Convert from pCi/L to Bq/m³ constexpr float convert_to_bwpm3 = 37.0; - RadonValue radon_value; - radon_value.chars[0] = value[2]; - radon_value.chars[1] = value[3]; - radon_value.chars[2] = value[4]; - radon_value.chars[3] = value[5]; - float radon_now = radon_value.number * convert_to_bwpm3; - if (is_valid_radon_value_(radon_now)) { - radon_sensor_->publish_state(radon_now); + float radon_now; // in Bq/m³ + float radon_day; // in Bq/m³ + float radon_month; // in Bq/m³ + if (command == WRITE_COMMAND_V1) { + // Use memcpy to avoid unaligned memory access + float temp; + memcpy(&temp, value + 2, sizeof(float)); + radon_now = temp * convert_to_bwpm3; + memcpy(&temp, value + 6, sizeof(float)); + radon_day = temp * convert_to_bwpm3; + memcpy(&temp, value + 10, sizeof(float)); + radon_month = temp * convert_to_bwpm3; + } else if (command == WRITE_COMMAND_V2) { + // Use memcpy to avoid unaligned memory access + uint16_t temp; + memcpy(&temp, value + 33, sizeof(uint16_t)); + radon_now = temp; + memcpy(&temp, value + 35, sizeof(uint16_t)); + radon_day = temp; + memcpy(&temp, value + 37, sizeof(uint16_t)); + radon_month = temp; + } else { + ESP_LOGW(TAG, "Unexpected command value: 0x%02X", command); + return; } - radon_value.chars[0] = value[6]; - radon_value.chars[1] = value[7]; - radon_value.chars[2] = value[8]; - radon_value.chars[3] = value[9]; - float radon_day = radon_value.number * convert_to_bwpm3; + if (this->radon_sensor_ != nullptr) { + this->radon_sensor_->publish_state(radon_now); + } - radon_value.chars[0] = value[10]; - radon_value.chars[1] = value[11]; - radon_value.chars[2] = value[12]; - radon_value.chars[3] = value[13]; - float radon_month = radon_value.number * convert_to_bwpm3; - - if (is_valid_radon_value_(radon_month)) { - ESP_LOGV(TAG, "Radon Long Term based on month"); - radon_long_term_sensor_->publish_state(radon_month); - } else if (is_valid_radon_value_(radon_day)) { - ESP_LOGV(TAG, "Radon Long Term based on day"); - radon_long_term_sensor_->publish_state(radon_day); + if (this->radon_long_term_sensor_ != nullptr) { + if (radon_month > 0) { + ESP_LOGV(TAG, "Radon Long Term based on month"); + this->radon_long_term_sensor_->publish_state(radon_month); + } else { + ESP_LOGV(TAG, "Radon Long Term based on day"); + this->radon_long_term_sensor_->publish_state(radon_day); + } } ESP_LOGV(TAG, @@ -130,49 +188,23 @@ void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) { parent()->set_enabled(false); } -bool RadonEyeRD200::is_valid_radon_value_(float radon) { return radon > 0.0 and radon < 37000; } - void RadonEyeRD200::update() { if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { if (!parent()->enabled) { ESP_LOGW(TAG, "Reconnecting to device"); parent()->set_enabled(true); - parent()->connect(); } else { ESP_LOGW(TAG, "Connection in progress"); } } } -void RadonEyeRD200::write_query_message_() { - ESP_LOGV(TAG, "writing 0x50 to write service"); - int request = 0x50; - auto status = esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->write_handle_, sizeof(request), (uint8_t *) &request, - ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "Error sending write request for sensor, status=%d", status); - } -} - -void RadonEyeRD200::request_read_values_() { - auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->read_handle_, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); - } -} - void RadonEyeRD200::dump_config() { LOG_SENSOR(" ", "Radon", this->radon_sensor_); LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); } -RadonEyeRD200::RadonEyeRD200() - : PollingComponent(10000), - service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), - sensors_write_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(WRITE_CHARACTERISTIC_UUID)), - sensors_read_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(READ_CHARACTERISTIC_UUID)) {} +RadonEyeRD200::RadonEyeRD200() : PollingComponent(10000) {} } // namespace radon_eye_rd200 } // namespace esphome diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.h b/esphome/components/radon_eye_rd200/radon_eye_rd200.h index 7b29be7bd8..f874c815f8 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.h +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.h @@ -14,10 +14,6 @@ namespace esphome { namespace radon_eye_rd200 { -static const char *const SERVICE_UUID = "00001523-1212-efde-1523-785feabcd123"; -static const char *const WRITE_CHARACTERISTIC_UUID = "00001524-1212-efde-1523-785feabcd123"; -static const char *const READ_CHARACTERISTIC_UUID = "00001525-1212-efde-1523-785feabcd123"; - class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode { public: RadonEyeRD200(); @@ -32,25 +28,17 @@ class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } protected: - bool is_valid_radon_value_(float radon); - void read_sensors_(uint8_t *value, uint16_t value_len); - void write_query_message_(); - void request_read_values_(); sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; + uint8_t write_command_; uint16_t read_handle_; uint16_t write_handle_; esp32_ble_tracker::ESPBTUUID service_uuid_; esp32_ble_tracker::ESPBTUUID sensors_write_characteristic_uuid_; esp32_ble_tracker::ESPBTUUID sensors_read_characteristic_uuid_; - - union RadonValue { - char chars[4]; - float number; - }; }; } // namespace radon_eye_rd200