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

Added RadonEye RD200 Component #3119

Merged
merged 12 commits into from
Feb 8, 2022
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @stevebaxter
esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/qr_code/* @wjtje
esphome/components/radon_eye_ble/* @jeffeb3
esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet
Expand Down
23 changes: 23 additions & 0 deletions esphome/components/radon_eye_ble/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import esp32_ble_tracker
from esphome.const import CONF_ID

DEPENDENCIES = ["esp32_ble_tracker"]
CODEOWNERS = ["@jeffeb3"]

radon_eye_ble_ns = cg.esphome_ns.namespace("radon_eye_ble")
RadonEyeListener = radon_eye_ble_ns.class_(
"RadonEyeListener", esp32_ble_tracker.ESPBTDeviceListener
)

CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(RadonEyeListener),
}
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)


def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield esp32_ble_tracker.register_ble_device(var, config)
25 changes: 25 additions & 0 deletions esphome/components/radon_eye_ble/radon_eye_listener.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "radon_eye_listener.h"
#include "esphome/core/log.h"

#ifdef USE_ESP32

namespace esphome {
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()) {
if (device.get_name().rfind("FR:R20:SN", 0) == 0) {
// This is an RD200, I think
ESP_LOGD(TAG, "Found Radon Eye RD200 device Name: %s (MAC: %s)", device.get_name().c_str(),
device.address_str().c_str());
}
}
return false;
}

} // namespace radon_eye_ble
} // namespace esphome

#endif
19 changes: 19 additions & 0 deletions esphome/components/radon_eye_ble/radon_eye_listener.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#ifdef USE_ESP32

#include "esphome/core/component.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"

namespace esphome {
namespace radon_eye_ble {

class RadonEyeListener : public esp32_ble_tracker::ESPBTDeviceListener {
public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
};

} // namespace radon_eye_ble
} // namespace esphome

#endif
1 change: 1 addition & 0 deletions esphome/components/radon_eye_rd200/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CODEOWNERS = ["@jeffeb3"]
179 changes: 179 additions & 0 deletions esphome/components/radon_eye_rd200/radon_eye_rd200.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#include "radon_eye_rd200.h"

#ifdef USE_ESP32

namespace esphome {
namespace radon_eye_rd200 {

static const char *const TAG = "radon_eye_rd200";

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) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}

case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}

case ESP_GATTC_SEARCH_CMPL_EVT: {
this->read_handle_ = 0;
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_read_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor read characteristic found at service %s char %s", service_uuid_.to_string().c_str(),
sensors_read_characteristic_uuid_.to_string().c_str());
break;
}
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());
break;
}
this->write_handle_ = write_chr->handle;

this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;

write_query_message_();

request_read_values_();
break;
}

case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->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);
break;
}
if (param->read.handle == this->read_handle_) {
read_sensors_(param->read.value, param->read.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");
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]);

if (value[0] != 0x50) {
// This isn't a sensor reading.
return;
}

// 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);
}

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;

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);
}

ESP_LOGV(TAG, " Measurements (Bq/m³) now: %0.03f, day: %0.03f, month: %0.03f", radon_now, radon_day, radon_month);

ESP_LOGV(TAG, " Measurements (pCi/L) now: %0.03f, day: %0.03f, month: %0.03f", radon_now / convert_to_bwpm3,
radon_day / convert_to_bwpm3, radon_month / convert_to_bwpm3);

// This instance must not stay connected
// so other clients can connect to it (e.g. the
// mobile app).
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()->gattc_if, this->parent()->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()->gattc_if, this->parent()->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)) {}

} // namespace radon_eye_rd200
} // namespace esphome

#endif // USE_ESP32
59 changes: 59 additions & 0 deletions esphome/components/radon_eye_rd200/radon_eye_rd200.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#ifdef USE_ESP32

#include <esp_gattc_api.h>
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"

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();

void dump_config() override;
void update() override;

void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;

void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
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};

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
} // namespace esphome

#endif // USE_ESP32
55 changes: 55 additions & 0 deletions esphome/components/radon_eye_rd200/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client

from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_BECQUEREL_PER_CUBIC_METER,
CONF_ID,
CONF_RADON,
CONF_RADON_LONG_TERM,
ICON_RADIOACTIVE,
)

DEPENDENCIES = ["ble_client"]

radon_eye_rd200_ns = cg.esphome_ns.namespace("radon_eye_rd200")
RadonEyeRD200 = radon_eye_rd200_ns.class_(
"RadonEyeRD200", cg.PollingComponent, ble_client.BLEClientNode
)

CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(RadonEyeRD200),
cv.Optional(CONF_RADON): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("5min"))
.extend(ble_client.BLE_CLIENT_SCHEMA),
)


async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

await ble_client.register_ble_node(var, config)

if CONF_RADON in config:
sens = await sensor.new_sensor(config[CONF_RADON])
cg.add(var.set_radon(sens))
if CONF_RADON_LONG_TERM in config:
sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
cg.add(var.set_radon_long_term(sens))