From 877367677b14b397fa023430504791736d47a781 Mon Sep 17 00:00:00 2001 From: NMC Date: Mon, 4 Oct 2021 18:56:34 -0400 Subject: [PATCH] Add support for Airthing Wave Mini (#2440) --- CODEOWNERS | 1 + .../airthings_wave_mini/__init__.py | 1 + .../airthings_wave_mini.cpp | 120 ++++++++++++++++++ .../airthings_wave_mini/airthings_wave_mini.h | 65 ++++++++++ .../components/airthings_wave_mini/sensor.py | 88 +++++++++++++ tests/test2.yaml | 14 ++ 6 files changed, 289 insertions(+) create mode 100644 esphome/components/airthings_wave_mini/__init__.py create mode 100644 esphome/components/airthings_wave_mini/airthings_wave_mini.cpp create mode 100644 esphome/components/airthings_wave_mini/airthings_wave_mini.h create mode 100644 esphome/components/airthings_wave_mini/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2972b30b33..6576c78f0e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -15,6 +15,7 @@ esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/addressable_light/* @justfalter esphome/components/airthings_ble/* @jeromelaban +esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix diff --git a/esphome/components/airthings_wave_mini/__init__.py b/esphome/components/airthings_wave_mini/__init__.py new file mode 100644 index 0000000000..022f35b4cf --- /dev/null +++ b/esphome/components/airthings_wave_mini/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ncareau"] diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp new file mode 100644 index 0000000000..0ab0e65148 --- /dev/null +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp @@ -0,0 +1,120 @@ +#include "airthings_wave_mini.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +namespace esphome { +namespace airthings_wave_mini { + +static const char *const TAG = "airthings_wave_mini"; + +void AirthingsWaveMini::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->handle_ = 0; + auto chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), + sensors_data_characteristic_uuid_.to_string().c_str()); + break; + } + this->handle_ = chr->handle; + this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; + + 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->handle_) { + read_sensors_(param->read.value, param->read.value_len); + } + break; + } + + default: + break; + } +} + +void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) { + auto value = (WaveMiniReadings *) raw_value; + + if (sizeof(WaveMiniReadings) <= value_len) { + this->humidity_sensor_->publish_state(value->humidity / 100.0f); + this->pressure_sensor_->publish_state(value->pressure / 50.0f); + this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); + if (is_valid_voc_value_(value->voc)) { + this->tvoc_sensor_->publish_state(value->voc); + } + + // This instance must not stay connected + // so other clients can connect to it (e.g. the + // mobile app). + parent()->set_enabled(false); + } +} + +bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } + +void AirthingsWaveMini::loop() {} + +void AirthingsWaveMini::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 AirthingsWaveMini::request_read_values_() { + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + } +} + +void AirthingsWaveMini::dump_config() { + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); +} + +AirthingsWaveMini::AirthingsWaveMini() : PollingComponent(10000) { + auto service_bt = *BLEUUID::fromString(std::string("b42e3882-ade7-11e4-89d3-123b93f75cba")).getNative(); + auto characteristic_bt = *BLEUUID::fromString(std::string("b42e3b98-ade7-11e4-89d3-123b93f75cba")).getNative(); + + service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_uuid(service_bt); + sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_uuid(characteristic_bt); +} + +void AirthingsWaveMini::setup() {} + +} // namespace airthings_wave_mini +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.h b/esphome/components/airthings_wave_mini/airthings_wave_mini.h new file mode 100644 index 0000000000..5d1964d559 --- /dev/null +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.h @@ -0,0 +1,65 @@ +#pragma once + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include +#include +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace airthings_wave_mini { + +class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode { + public: + AirthingsWaveMini(); + + void setup() override; + void dump_config() override; + void update() override; + void loop() 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_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + + protected: + bool is_valid_voc_value_(uint16_t voc); + + void read_sensors_(uint8_t *value, uint16_t value_len); + void request_read_values_(); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + + uint16_t handle_; + esp32_ble_tracker::ESPBTUUID service_uuid_; + esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; + + struct WaveMiniReadings { + uint16_t unused01; + uint16_t temperature; + uint16_t pressure; + uint16_t humidity; + uint16_t voc; + uint16_t unused02; + uint32_t unused03; + uint32_t unused04; + }; +}; + +} // namespace airthings_wave_mini +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/airthings_wave_mini/sensor.py b/esphome/components/airthings_wave_mini/sensor.py new file mode 100644 index 0000000000..6a32cd8771 --- /dev/null +++ b/esphome/components/airthings_wave_mini/sensor.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client +from esphome.core import CORE + +from esphome.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + CONF_ID, + CONF_HUMIDITY, + CONF_TVOC, + CONF_PRESSURE, + CONF_TEMPERATURE, + UNIT_PARTS_PER_BILLION, + ICON_RADIATOR, +) + +DEPENDENCIES = ["ble_client"] + +airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") +AirthingsWaveMini = airthings_wave_mini_ns.class_( + "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AirthingsWaveMini), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("5mins")) + .extend(ble_client.BLE_CLIENT_SCHEMA), + # Until BLEUUID reference removed + cv.only_with_arduino, +) + + +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_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_PRESSURE in config: + sens = await sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure(sens)) + if CONF_TVOC in config: + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) + + if CORE.is_esp32: + cg.add_library("ESP32 BLE Arduino", None) diff --git a/tests/test2.yaml b/tests/test2.yaml index 4541fba616..364bcec28f 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -281,6 +281,17 @@ sensor: name: "Wave Plus CO2" tvoc: name: "Wave Plus VOC" + - platform: airthings_wave_mini + ble_client_id: airthingsmini01 + update_interval: 5min + temperature: + name: "Wave Mini Temperature" + humidity: + name: "Wave Mini Humidity" + pressure: + name: "Wave Mini Pressure" + tvoc: + name: "Wave Mini VOC" time: - platform: homeassistant @@ -378,6 +389,9 @@ esp32_ble_tracker: ble_client: - mac_address: 01:02:03:04:05:06 id: airthings01 + - mac_address: 01:02:03:04:05:06 + id: airthingsmini01 + airthings_ble: