From 2d618768d53bf6ae52e494341e3c19d86cbf930f Mon Sep 17 00:00:00 2001 From: Trevor North Date: Tue, 6 Apr 2021 11:19:56 +0100 Subject: [PATCH] Add BME680 via BSEC integration (#1313) --- CODEOWNERS | 1 + esphome/components/bme680_bsec/__init__.py | 64 +++ .../components/bme680_bsec/bme680_bsec.cpp | 398 ++++++++++++++++++ esphome/components/bme680_bsec/bme680_bsec.h | 106 +++++ esphome/components/bme680_bsec/sensor.py | 91 ++++ esphome/components/bme680_bsec/text_sensor.py | 41 ++ 6 files changed, 701 insertions(+) create mode 100644 esphome/components/bme680_bsec/__init__.py create mode 100644 esphome/components/bme680_bsec/bme680_bsec.cpp create mode 100644 esphome/components/bme680_bsec/bme680_bsec.h create mode 100644 esphome/components/bme680_bsec/sensor.py create mode 100644 esphome/components/bme680_bsec/text_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a726f85cf1..ef2af83fa7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -20,6 +20,7 @@ esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter esphome/components/climate/* @esphome/core diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py new file mode 100644 index 0000000000..f5028a90a3 --- /dev/null +++ b/esphome/components/bme680_bsec/__init__.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@trvrnrth"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_BME680_BSEC_ID = "bme680_bsec_id" +CONF_TEMPERATURE_OFFSET = "temperature_offset" +CONF_IAQ_MODE = "iaq_mode" +CONF_SAMPLE_RATE = "sample_rate" +CONF_STATE_SAVE_INTERVAL = "state_save_interval" + +bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec") + +IAQMode = bme680_bsec_ns.enum("IAQMode") +IAQ_MODE_OPTIONS = { + "STATIC": IAQMode.IAQ_MODE_STATIC, + "MOBILE": IAQMode.IAQ_MODE_MOBILE, +} + +SampleRate = bme680_bsec_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "LP": SampleRate.SAMPLE_RATE_LP, + "ULP": SampleRate.SAMPLE_RATE_ULP, +} + +BME680BSECComponent = bme680_bsec_ns.class_( + "BME680BSECComponent", cg.Component, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( + IAQ_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + } +).extend(i2c.i2c_device_schema(0x76)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add( + var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) + ) + + cg.add_build_flag("-DUSING_BSEC") + cg.add_library("BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp new file mode 100644 index 0000000000..0efe4083ef --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -0,0 +1,398 @@ +#ifdef USING_BSEC + +#include "bme680_bsec.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome { +namespace bme680_bsec { + +static const char *TAG = "bme680_bsec.sensor"; + +static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +BME680BSECComponent *BME680BSECComponent::instance; + +void BME680BSECComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); + BME680BSECComponent::instance = this; + + this->bsec_status_ = bsec_init(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->bme680_.dev_id = this->address_; + this->bme680_.intf = BME680_I2C_INTF; + this->bme680_.read = BME680BSECComponent::read_bytes_wrapper; + this->bme680_.write = BME680BSECComponent::write_bytes_wrapper; + this->bme680_.delay_ms = BME680BSECComponent::delay_ms; + this->bme680_.amb_temp = 25; + this->bme680_.power_mode = BME680_FORCED_MODE; + + this->bme680_status_ = bme680_init(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + this->mark_failed(); + return; + } + + if (this->sample_rate_ == SAMPLE_RATE_ULP) { + const uint8_t bsec_config[] = { +#include "config/generic_33v_300s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + this->update_subscription_(BSEC_SAMPLE_RATE_ULP); + } else { + const uint8_t bsec_config[] = { +#include "config/generic_33v_3s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + this->update_subscription_(BSEC_SAMPLE_RATE_LP); + } + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->load_state_(); +} + +void BME680BSECComponent::set_config_(const uint8_t *config) { + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer)); +} + +void BME680BSECComponent::update_subscription_(float sample_rate) { + bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; + int num_virtual_sensors = 0; + + if (this->iaq_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = + this->iaq_mode_ == IAQ_MODE_STATIC ? BSEC_OUTPUT_STATIC_IAQ : BSEC_OUTPUT_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->co2_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->breath_voc_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->pressure_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->gas_resistance_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->temperature_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->humidity_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; + uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; + this->bsec_status_ = + bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings); +} + +void BME680BSECComponent::dump_config() { + ESP_LOGCONFIG(TAG, "BME680 via BSEC:"); + + bsec_version_t version; + bsec_get_version(&version); + ESP_LOGCONFIG(TAG, " BSEC Version: %d.%d.%d.%d", version.major, version.minor, version.major_bugfix, + version.minor_bugfix); + + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed (BSEC Status: %d, BME680 Status: %d)", this->bsec_status_, + this->bme680_status_); + } + + ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_); + ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile"); + ESP_LOGCONFIG(TAG, " Sample Rate: %s", this->sample_rate_ == SAMPLE_RATE_ULP ? "ULP" : "LP"); + ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Gas Resistance", this->gas_resistance_sensor_); + LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); + LOG_SENSOR(" ", "Numeric IAQ Accuracy", this->iaq_accuracy_sensor_); + LOG_TEXT_SENSOR(" ", "IAQ Accuracy", this->iaq_accuracy_text_sensor_); + LOG_SENSOR(" ", "CO2 Equivalent", this->co2_equivalent_sensor_); + LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); +} + +float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } + +void BME680BSECComponent::loop() { + this->run_(); + + if (this->bsec_status_ < BSEC_OK || this->bme680_status_ < BME680_OK) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + if (this->bsec_status_ > BSEC_OK || this->bme680_status_ > BME680_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } +} + +void BME680BSECComponent::run_() { + int64_t curr_time_ns = this->get_time_ns_(); + if (curr_time_ns < this->next_call_ns_) { + return; + } + + ESP_LOGV(TAG, "Performing sensor run"); + + bsec_bme_settings_t bme680_settings; + this->bsec_status_ = bsec_sensor_control(curr_time_ns, &bme680_settings); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_); + return; + } + this->next_call_ns_ = bme680_settings.next_call; + + this->bme680_.gas_sett.run_gas = bme680_settings.run_gas; + this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling; + this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling; + this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling; + this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature; + this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration; + uint16_t desired_settings = + BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_FILTER_SEL | BME680_GAS_SENSOR_SEL; + this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_); + return; + } + + this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; + } + + uint16_t meas_dur = 0; + bme680_get_profile_dur(&meas_dur, &this->bme680_); + ESP_LOGV(TAG, "Queueing read in %ums", meas_dur); + this->set_timeout("read", meas_dur, [this, bme680_settings]() { this->read_(bme680_settings); }); +} + +void BME680BSECComponent::read_(bsec_bme_settings_t bme680_settings) { + ESP_LOGV(TAG, "Reading data"); + struct bme680_field_data data; + this->bme680_status_ = bme680_get_sensor_data(&data, &this->bme680_); + + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to get sensor data (BME680 Error Code %d)", this->bme680_status_); + return; + } + if (!(data.status & BME680_NEW_DATA_MSK)) { + ESP_LOGD(TAG, "BME680 did not report new data"); + return; + } + + bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance + uint8_t num_inputs = 0; + int64_t curr_time_ns = this->get_time_ns_(); + + if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; + inputs[num_inputs].signal = data.temperature / 100.0f; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + + // Temperature offset from the real temperature due to external heat sources + inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; + inputs[num_inputs].signal = this->temperature_offset_; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; + inputs[num_inputs].signal = data.humidity / 1000.0f; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; + inputs[num_inputs].signal = data.pressure; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_GAS) { + inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; + inputs[num_inputs].signal = data.gas_resistance; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (num_inputs < 1) { + ESP_LOGD(TAG, "No signal inputs available for BSEC"); + return; + } + + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; + uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; + this->bsec_status_ = bsec_do_steps(inputs, num_inputs, outputs, &num_outputs); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_); + return; + } + if (num_outputs < 1) { + ESP_LOGD(TAG, "No signal outputs provided by BSEC"); + return; + } + + this->publish_(outputs, num_outputs); +} + +void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { + ESP_LOGV(TAG, "Publishing sensor states"); + for (uint8_t i = 0; i < num_outputs; i++) { + switch (outputs[i].sensor_id) { + case BSEC_OUTPUT_IAQ: + case BSEC_OUTPUT_STATIC_IAQ: + uint8_t accuracy; + accuracy = outputs[i].accuracy; + this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); + this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); + this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); + + // Queue up an opportunity to save state + this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); + break; + case BSEC_OUTPUT_CO2_EQUIVALENT: + this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: + this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_RAW_PRESSURE: + this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); + break; + case BSEC_OUTPUT_RAW_GAS: + this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: + this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: + this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); + break; + } + } +} + +int64_t BME680BSECComponent::get_time_ns_() { + int64_t time_ms = millis(); + if (this->last_time_ms_ > time_ms) { + this->millis_overflow_counter_++; + } + this->last_time_ms_ = time_ms; + + return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); +} + +void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { + if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { + if (!sensor || (sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->read_bytes(a_register, data, len) ? 0 : -1; +} + +int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->write_bytes(a_register, data, len) ? 0 : -1; +} + +void BME680BSECComponent::delay_ms(uint32_t period) { + ESP_LOGV(TAG, "Delaying for %ums", period); + delay(period); +} + +void BME680BSECComponent::load_state_() { + uint32_t hash = fnv1_hash("bme680_bsec_state_" + to_string(this->address_)); + this->bsec_state_ = global_preferences.make_preference(hash, true); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + if (this->bsec_state_.load(&state)) { + ESP_LOGV(TAG, "Loading state"); + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_state(state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to load state (BSEC Error Code %d)", this->bsec_status_); + } + ESP_LOGI(TAG, "Loaded state"); + } +} + +void BME680BSECComponent::save_state_(uint8_t accuracy) { + if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { + return; + } + + ESP_LOGV(TAG, "Saving state"); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + + this->bsec_status_ = + bsec_get_state(0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed fetch state for save (BSEC Error Code %d)", this->bsec_status_); + return; + } + + if (!this->bsec_state_.save(&state)) { + ESP_LOGW(TAG, "Failed to save state"); + return; + } + this->last_state_save_ms_ = millis(); + + ESP_LOGI(TAG, "Saved state"); +} + +} // namespace bme680_bsec +} // namespace esphome + +#endif diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h new file mode 100644 index 0000000000..ce35e21c9a --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -0,0 +1,106 @@ +#ifdef USING_BSEC + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/preferences.h" +#include +#include + +namespace esphome { +namespace bme680_bsec { + +enum IAQMode { + IAQ_MODE_STATIC = 0, + IAQ_MODE_MOBILE = 1, +}; + +enum SampleRate { + SAMPLE_RATE_LP = 0, + SAMPLE_RATE_ULP = 1, +}; + +class BME680BSECComponent : public Component, public i2c::I2CDevice { + public: + void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } + void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } + void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } + void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_gas_resistance_sensor(sensor::Sensor *gas_resistance_sensor) { + gas_resistance_sensor_ = gas_resistance_sensor; + } + void set_iaq_sensor(sensor::Sensor *iaq_sensor) { iaq_sensor_ = iaq_sensor; } + void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *iaq_accuracy_text_sensor) { + iaq_accuracy_text_sensor_ = iaq_accuracy_text_sensor; + } + void set_iaq_accuracy_sensor(sensor::Sensor *iaq_accuracy_sensor) { iaq_accuracy_sensor_ = iaq_accuracy_sensor; } + void set_co2_equivalent_sensor(sensor::Sensor *co2_equivalent_sensor) { + co2_equivalent_sensor_ = co2_equivalent_sensor; + } + void set_breath_voc_equivalent_sensor(sensor::Sensor *breath_voc_equivalent_sensor) { + breath_voc_equivalent_sensor_ = breath_voc_equivalent_sensor; + } + + static BME680BSECComponent *instance; + static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static int8_t write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static void delay_ms(uint32_t period); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + protected: + void set_config_(const uint8_t *config); + void update_subscription_(float sample_rate); + + void run_(); + void read_(bsec_bme_settings_t bme680_settings); + void publish_(const bsec_output_t *outputs, uint8_t num_outputs); + int64_t get_time_ns_(); + + void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); + void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); + + void load_state_(); + void save_state_(uint8_t accuracy); + + struct bme680_dev bme680_; + bsec_library_return_t bsec_status_{BSEC_OK}; + int8_t bme680_status_{BME680_OK}; + + int64_t last_time_ms_{0}; + uint32_t millis_overflow_counter_{0}; + int64_t next_call_ns_{0}; + + ESPPreferenceObject bsec_state_; + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_ = 0; + + float temperature_offset_{0}; + IAQMode iaq_mode_{IAQ_MODE_STATIC}; + SampleRate sample_rate_{SAMPLE_RATE_LP}; + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + sensor::Sensor *humidity_sensor_; + sensor::Sensor *gas_resistance_sensor_; + sensor::Sensor *iaq_sensor_; + text_sensor::TextSensor *iaq_accuracy_text_sensor_; + sensor::Sensor *iaq_accuracy_sensor_; + sensor::Sensor *co2_equivalent_sensor_; + sensor::Sensor *breath_voc_equivalent_sensor_; +}; + +} // namespace bme680_bsec +} // namespace esphome + +#endif diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py new file mode 100644 index 0000000000..b27b267c11 --- /dev/null +++ b/esphome/components/bme680_bsec/sensor.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_GAS_RESISTANCE, + CONF_HUMIDITY, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_HECTOPASCAL, + UNIT_OHM, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, + ICON_GAS_CYLINDER, + ICON_GAUGE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, +) +from esphome.core import coroutine +from . import BME680BSECComponent, CONF_BME680_BSEC_ID + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ = "iaq" +CONF_IAQ_ACCURACY = "iaq_accuracy" +CONF_CO2_EQUIVALENT = "co2_equivalent" +CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" +UNIT_IAQ = "IAQ" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" +ICON_TEST_TUBE = "mdi:test-tube" + +TYPES = { + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_PRESSURE: "set_pressure_sensor", + CONF_HUMIDITY: "set_humidity_sensor", + CONF_GAS_RESISTANCE: "set_gas_resistance_sensor", + CONF_IAQ: "set_iaq_sensor", + CONF_IAQ_ACCURACY: "set_iaq_accuracy_sensor", + CONF_CO2_EQUIVALENT: "set_co2_equivalent_sensor", + CONF_BREATH_VOC_EQUIVALENT: "set_breath_voc_equivalent_sensor", +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + UNIT_HECTOPASCAL, ICON_GAUGE, 1, DEVICE_CLASS_PRESSURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_WATER_PERCENT, 1, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_IAQ): sensor.sensor_schema( + UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( + UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( + UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( + UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY + ), + } +) + + +@coroutine +def setup_conf(config, key, hub, funcName): + if key in config: + conf = config[key] + var = yield sensor.new_sensor(conf) + func = getattr(hub, funcName) + cg.add(func(var)) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) + for key, funcName in TYPES.items(): + yield setup_conf(config, key, hub, funcName) diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py new file mode 100644 index 0000000000..992e2989c9 --- /dev/null +++ b/esphome/components/bme680_bsec/text_sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID, CONF_ICON +from esphome.core import coroutine +from . import BME680BSECComponent, CONF_BME680_BSEC_ID + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ_ACCURACY = "iaq_accuracy" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" + +TYPES = {CONF_IAQ_ACCURACY: "set_iaq_accuracy_text_sensor"} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_ACCURACY): cv.icon, + } + ), + } +) + + +@coroutine +def setup_conf(config, key, hub, funcName): + if key in config: + conf = config[key] + var = cg.new_Pvariable(conf[CONF_ID]) + yield text_sensor.register_text_sensor(var, conf) + func = getattr(hub, funcName) + cg.add(func(var)) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) + for key, funcName in TYPES.items(): + yield setup_conf(config, key, hub, funcName)