Skip to content

Commit

Permalink
Support for the AirThings Wave Plus (#1656)
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
  • Loading branch information
jeromelaban and jesserockz committed Aug 31, 2021
1 parent 58350b6 commit 140ef79
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -14,6 +14,8 @@ esphome/core/* @esphome/core
esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/addressable_light/* @justfalter
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix
esphome/components/animation/* @syndlex
Expand Down
23 changes: 23 additions & 0 deletions esphome/components/airthings_ble/__init__.py
@@ -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 = ["@jeromelaban"]

airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble")
AirthingsListener = airthings_ble_ns.class_(
"AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener
)

CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(AirthingsListener),
}
).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)
33 changes: 33 additions & 0 deletions esphome/components/airthings_ble/airthings_listener.cpp
@@ -0,0 +1,33 @@
#include "airthings_listener.h"
#include "esphome/core/log.h"

#ifdef ARDUINO_ARCH_ESP32

namespace esphome {
namespace airthings_ble {

static const char *TAG = "airthings_ble";

bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
for (auto &it : device.get_manufacturer_datas()) {
if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) {
if (it.data.size() < 4)
continue;

uint32_t sn = it.data[0];
sn |= ((uint32_t) it.data[1] << 8);
sn |= ((uint32_t) it.data[2] << 16);
sn |= ((uint32_t) it.data[3] << 24);

ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str());
return true;
}
}

return false;
}

} // namespace airthings_ble
} // namespace esphome

#endif
20 changes: 20 additions & 0 deletions esphome/components/airthings_ble/airthings_listener.h
@@ -0,0 +1,20 @@
#pragma once

#ifdef ARDUINO_ARCH_ESP32

#include "esphome/core/component.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include <BLEDevice.h>

namespace esphome {
namespace airthings_ble {

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

} // namespace airthings_ble
} // namespace esphome

#endif
1 change: 1 addition & 0 deletions esphome/components/airthings_wave_plus/__init__.py
@@ -0,0 +1 @@
CODEOWNERS = ["@jeromelaban"]
142 changes: 142 additions & 0 deletions esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
@@ -0,0 +1,142 @@
#include "airthings_wave_plus.h"

#ifdef ARDUINO_ARCH_ESP32

namespace esphome {
namespace airthings_wave_plus {

void AirthingsWavePlus::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 = espbt::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 AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
auto value = (WavePlusReadings *) raw_value;

if (sizeof(WavePlusReadings) <= value_len) {
ESP_LOGD(TAG, "version = %d", value->version);

if (value->version == 1) {
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);

this->humidity_sensor_->publish_state(value->humidity / 2.0f);
if (is_valid_radon_value_(value->radon)) {
this->radon_sensor_->publish_state(value->radon);
}
if (is_valid_radon_value_(value->radon_lt)) {
this->radon_long_term_sensor_->publish_state(value->radon_lt);
}
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
if (is_valid_co2_value_(value->co2)) {
this->co2_sensor_->publish_state(value->co2);
}
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);
} else {
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
}
}
}

bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; }

bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; }

bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; }

void AirthingsWavePlus::loop() {}

void AirthingsWavePlus::update() {
if (this->node_state != espbt::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 AirthingsWavePlus::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 AirthingsWavePlus::dump_config() {
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
}

AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) {
auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative();
auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative();

service_uuid = espbt::ESPBTUUID::from_uuid(service_bt);
sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt);
}

void AirthingsWavePlus::setup() {}

} // namespace airthings_wave_plus
} // namespace esphome

#endif // ARDUINO_ARCH_ESP32
79 changes: 79 additions & 0 deletions esphome/components/airthings_wave_plus/airthings_wave_plus.h
@@ -0,0 +1,79 @@
#pragma once

#include "esphome/core/component.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"
#include "esphome/core/log.h"
#include <algorithm>
#include <iterator>

#ifdef ARDUINO_ARCH_ESP32
#include <esp_gattc_api.h>
#include <BLEDevice.h>

using namespace esphome::ble_client;

namespace esphome {
namespace airthings_wave_plus {

static const char *TAG = "airthings_wave_plus";

class AirthingsWavePlus : public PollingComponent, public BLEClientNode {
public:
AirthingsWavePlus();

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_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }

protected:
bool is_valid_radon_value_(short radon);
bool is_valid_voc_value_(short voc);
bool is_valid_co2_value_(short co2);

void read_sensors_(uint8_t *value, uint16_t value_len);
void request_read_values_();

sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};

uint16_t handle;
espbt::ESPBTUUID service_uuid;
espbt::ESPBTUUID sensors_data_characteristic_uuid;

struct WavePlusReadings {
uint8_t version;
uint8_t humidity;
uint8_t ambientLight;
uint8_t unused01;
uint16_t radon;
uint16_t radon_lt;
uint16_t temperature;
uint16_t pressure;
uint16_t co2;
uint16_t voc;
};
};

} // namespace airthings_wave_plus
} // namespace esphome

#endif // ARDUINO_ARCH_ESP32

0 comments on commit 140ef79

Please sign in to comment.