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

Support for the AirThings Wave Plus #1656

Merged
merged 26 commits into from Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c3063d6
feat: Add support for radon related classes, unit and icon
jeromelaban Apr 3, 2021
0b8ec47
feat: Add airthings ble listener and sensors
jeromelaban Apr 3, 2021
ea19a53
chore: Move listener to its own component
jeromelaban Apr 3, 2021
8bdf307
test: Add airthings_ble, airthings_wave_plus tests
jeromelaban Apr 3, 2021
406eeb7
chore: adjust naming, formatting
jeromelaban Apr 3, 2021
9efbd7a
fix: airwave things plus tests typo
jeromelaban Jun 26, 2021
9f2e6d2
fix: Validate for out-of-range radon, voc and co2
jeromelaban Jun 27, 2021
7b4ce1b
chore: add missing update interval support
jeromelaban Jun 27, 2021
8c77054
chore: Update airthings sensor.py update interval
jeromelaban Aug 1, 2021
0c19756
chore: Remove invalid file
jeromelaban Aug 1, 2021
d555c85
chore: Apply c++ linting suggestions
jeromelaban Aug 2, 2021
c41c690
chore: Apply python linting suggestions
jeromelaban Aug 2, 2021
8eccf46
chore: add set_address suggestion
jeromelaban Aug 8, 2021
f08418f
chore: Adjustments from code review
jeromelaban Aug 8, 2021
b825cfb
chore: Refactor to polling component
jeromelaban Aug 8, 2021
0f3159d
chore: linter adjustments
jeromelaban Aug 8, 2021
8bba439
chore: additional linter fixes
jeromelaban Aug 8, 2021
a2a7976
refactor: Move to BLEClientNode
jeromelaban Aug 29, 2021
27bb03d
chore: Adjust for linter suggestions
jeromelaban Aug 29, 2021
fbf1933
chore: Adjust test for airthings
jeromelaban Aug 29, 2021
1a81dad
chore: Adjust for more linter suggestions
jeromelaban Aug 29, 2021
4bf3c9a
chore: Fix test2 validation
jeromelaban Aug 29, 2021
d2f5789
Make some config changes
jesserockz Aug 30, 2021
961386e
chore: Additional removes of DEVICE_CLASS_RADON
jeromelaban Aug 31, 2021
f140207
chore: remove invalid added class to sensor
jeromelaban Aug 31, 2021
84169f9
Add state classes
jesserockz Aug 31, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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