#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_BSEC2 #include "bme68x_bsec2.h" #include namespace esphome { namespace bme68x_bsec2 { #define BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(a) (a == ALGORITHM_OUTPUT_CLASSIFICATION ? "Classification" : "Regression") #define BME68X_BSEC2_OPERATING_AGE_LOG(o) (o == OPERATING_AGE_4D ? "4 days" : "28 days") #define BME68X_BSEC2_SAMPLE_RATE_LOG(r) (r == SAMPLE_RATE_DEFAULT ? "Default" : (r == SAMPLE_RATE_ULP ? "ULP" : "LP")) #define BME68X_BSEC2_VOLTAGE_LOG(v) (v == VOLTAGE_3_3V ? "3.3V" : "1.8V") static const char *const TAG = "bme68x_bsec2.sensor"; static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; void BME68xBSEC2Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); this->bsec_status_ = bsec_init_m(&this->bsec_instance_); if (this->bsec_status_ != BSEC_OK) { this->mark_failed(); ESP_LOGE(TAG, "bsec_init_m failed: status %d", this->bsec_status_); return; } bsec_get_version_m(&this->bsec_instance_, &this->version_); this->bme68x_status_ = bme68x_init(&this->bme68x_); if (this->bme68x_status_ != BME68X_OK) { this->mark_failed(); ESP_LOGE(TAG, "bme68x_init failed: status %d", this->bme68x_status_); return; } if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { this->set_config_(this->bsec2_configuration_, this->bsec2_configuration_length_); if (this->bsec_status_ != BSEC_OK) { this->mark_failed(); ESP_LOGE(TAG, "bsec_set_configuration_m failed: status %d", this->bsec_status_); return; } } this->update_subscription_(); if (this->bsec_status_ != BSEC_OK) { this->mark_failed(); ESP_LOGE(TAG, "bsec_update_subscription_m failed: status %d", this->bsec_status_); return; } this->load_state_(); } void BME68xBSEC2Component::dump_config() { ESP_LOGCONFIG(TAG, "BME68X via BSEC2:\n" " BSEC2 version: %d.%d.%d.%d\n" " BSEC2 configuration blob:\n" " Configured: %s", this->version_.major, this->version_.minor, this->version_.major_bugfix, this->version_.minor_bugfix, YESNO(this->bsec2_blob_configured_)); if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { ESP_LOGCONFIG(TAG, " Size: %" PRIu32, this->bsec2_configuration_length_); } if (this->is_failed()) { ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, this->bme68x_status_); } if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { ESP_LOGCONFIG(TAG, " Algorithm output: %s", BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(this->algorithm_output_)); } ESP_LOGCONFIG(TAG, " Operating age: %s\n" " Sample rate: %s\n" " Voltage: %s\n" " State save interval: %ims\n" " Temperature offset: %.2f", BME68X_BSEC2_OPERATING_AGE_LOG(this->operating_age_), BME68X_BSEC2_SAMPLE_RATE_LOG(this->sample_rate_), BME68X_BSEC2_VOLTAGE_LOG(this->voltage_), this->state_save_interval_ms_, this->temperature_offset_); #ifdef USE_SENSOR LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->temperature_sample_rate_)); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->pressure_sample_rate_)); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->humidity_sample_rate_)); LOG_SENSOR(" ", "Gas resistance", this->gas_resistance_sensor_); LOG_SENSOR(" ", "CO2 equivalent", this->co2_equivalent_sensor_); LOG_SENSOR(" ", "Breath VOC equivalent", this->breath_voc_equivalent_sensor_); LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); LOG_SENSOR(" ", "IAQ static", this->iaq_static_sensor_); LOG_SENSOR(" ", "Numeric IAQ accuracy", this->iaq_accuracy_sensor_); #endif #ifdef USE_TEXT_SENSOR LOG_TEXT_SENSOR(" ", "IAQ accuracy", this->iaq_accuracy_text_sensor_); #endif } float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } void BME68xBSEC2Component::loop() { this->run_(); if (this->bsec_status_ < BSEC_OK || this->bme68x_status_ < BME68X_OK) { this->status_set_error(); } else { this->status_clear_error(); } if (this->bsec_status_ > BSEC_OK || this->bme68x_status_ > BME68X_OK) { this->status_set_warning(); } else { this->status_clear_warning(); } // Process a single action from the queue. These are primarily sensor state publishes // that in totality take too long to send in a single call. if (this->queue_.size()) { auto action = std::move(this->queue_.front()); this->queue_.pop(); action(); } } void BME68xBSEC2Component::set_config_(const uint8_t *config, uint32_t len) { if (len > BSEC_MAX_PROPERTY_BLOB_SIZE) { ESP_LOGE(TAG, "Configuration is larger than BSEC_MAX_PROPERTY_BLOB_SIZE"); this->mark_failed(); return; } uint8_t work_buffer[BSEC_MAX_PROPERTY_BLOB_SIZE]; this->bsec_status_ = bsec_set_configuration_m(&this->bsec_instance_, config, len, work_buffer, sizeof(work_buffer)); if (this->bsec_status_ == BSEC_OK) { this->bsec2_blob_configured_ = true; } } float BME68xBSEC2Component::calc_sensor_sample_rate_(SampleRate sample_rate) { if (sample_rate == SAMPLE_RATE_DEFAULT) { sample_rate = this->sample_rate_; } return sample_rate == SAMPLE_RATE_ULP ? BSEC_SAMPLE_RATE_ULP : BSEC_SAMPLE_RATE_LP; } void BME68xBSEC2Component::update_subscription_() { bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; uint8_t num_virtual_sensors = 0; #ifdef USE_SENSOR if (this->iaq_sensor_) { virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_IAQ; virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); num_virtual_sensors++; } if (this->iaq_static_sensor_) { virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_STATIC_IAQ; virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 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 = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 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 = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 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 = this->calc_sensor_sample_rate_(this->pressure_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 = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 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 = this->calc_sensor_sample_rate_(this->temperature_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 = this->calc_sensor_sample_rate_(this->humidity_sample_rate_); num_virtual_sensors++; } #endif 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_m(&this->bsec_instance_, virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings); } void BME68xBSEC2Component::run_() { this->op_mode_ = this->bsec_settings_.op_mode; int64_t curr_time_ns = this->get_time_ns_(); if (curr_time_ns < this->bsec_settings_.next_call) { return; } uint8_t status; ESP_LOGV(TAG, "Performing sensor run"); struct bme68x_conf bme68x_conf; this->bsec_status_ = bsec_sensor_control_m(&this->bsec_instance_, curr_time_ns, &this->bsec_settings_); if (this->bsec_status_ < BSEC_OK) { ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_); return; } switch (this->bsec_settings_.op_mode) { case BME68X_FORCED_MODE: bme68x_get_conf(&bme68x_conf, &this->bme68x_); bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; bme68x_set_conf(&bme68x_conf, &this->bme68x_); this->bme68x_heatr_conf_.enable = BME68X_ENABLE; this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; // status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); this->op_mode_ = BME68X_FORCED_MODE; ESP_LOGV(TAG, "Using forced mode"); break; case BME68X_PARALLEL_MODE: if (this->op_mode_ != this->bsec_settings_.op_mode) { bme68x_get_conf(&bme68x_conf, &this->bme68x_); bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; bme68x_set_conf(&bme68x_conf, &this->bme68x_); this->bme68x_heatr_conf_.enable = BME68X_ENABLE; this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile; this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile; this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len; this->bme68x_heatr_conf_.shared_heatr_dur = BSEC_TOTAL_HEAT_DUR - (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); this->op_mode_ = BME68X_PARALLEL_MODE; ESP_LOGV(TAG, "Using parallel mode"); } break; case BME68X_SLEEP_MODE: if (this->op_mode_ != this->bsec_settings_.op_mode) { bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_); this->op_mode_ = BME68X_SLEEP_MODE; ESP_LOGV(TAG, "Using sleep mode"); } break; } if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { uint32_t meas_dur = 0; meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); } else { ESP_LOGV(TAG, "Measurement not required"); this->read_(curr_time_ns); } } void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { ESP_LOGV(TAG, "Reading data"); if (this->bsec_settings_.trigger_measurement) { uint8_t current_op_mode; this->bme68x_status_ = bme68x_get_op_mode(¤t_op_mode, &this->bme68x_); if (current_op_mode == BME68X_SLEEP_MODE) { ESP_LOGV(TAG, "Still in sleep mode, doing nothing"); return; } } if (!this->bsec_settings_.process_data) { ESP_LOGV(TAG, "Data processing not required"); return; } struct bme68x_data data[3]; uint8_t nFields = 0; this->bme68x_status_ = bme68x_get_data(this->op_mode_, &data[0], &nFields, &this->bme68x_); if (this->bme68x_status_ != BME68X_OK) { ESP_LOGW(TAG, "Failed to get sensor data (BME68X error code %d)", this->bme68x_status_); return; } if (nFields < 1) { ESP_LOGD(TAG, "BME68X did not provide new data"); return; } for (uint8_t i = 0; i < nFields; i++) { bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance uint8_t num_inputs = 0; if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_TEMPERATURE)) { inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; inputs[num_inputs].signal = data[i].temperature; inputs[num_inputs].time_stamp = trigger_time_ns; num_inputs++; } if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HEATSOURCE)) { inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; inputs[num_inputs].signal = this->temperature_offset_; inputs[num_inputs].time_stamp = trigger_time_ns; num_inputs++; } if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HUMIDITY)) { inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; inputs[num_inputs].signal = data[i].humidity; inputs[num_inputs].time_stamp = trigger_time_ns; num_inputs++; } if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PRESSURE)) { inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; inputs[num_inputs].signal = data[i].pressure; inputs[num_inputs].time_stamp = trigger_time_ns; num_inputs++; } if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_GASRESISTOR)) { if (data[i].status & BME68X_GASM_VALID_MSK) { inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; inputs[num_inputs].signal = data[i].gas_resistance; inputs[num_inputs].time_stamp = trigger_time_ns; num_inputs++; } else { ESP_LOGD(TAG, "BME68X did not report gas data"); } } if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PROFILE_PART) && (data[i].status & BME68X_GASM_VALID_MSK)) { inputs[num_inputs].sensor_id = BSEC_INPUT_PROFILE_PART; inputs[num_inputs].signal = (this->op_mode_ == BME68X_FORCED_MODE) ? 0 : data[i].gas_index; inputs[num_inputs].time_stamp = trigger_time_ns; num_inputs++; } if (num_inputs < 1) { ESP_LOGD(TAG, "No signal inputs available for BSEC2"); return; } bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; this->bsec_status_ = bsec_do_steps_m(&this->bsec_instance_, inputs, num_inputs, outputs, &num_outputs); if (this->bsec_status_ != BSEC_OK) { ESP_LOGW(TAG, "BSEC2 failed to process signals (BSEC2 error code %d)", this->bsec_status_); return; } if (num_outputs < 1) { ESP_LOGD(TAG, "No signal outputs provided by BSEC2"); return; } this->publish_(outputs, num_outputs); } } void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { ESP_LOGV(TAG, "Publishing sensor states"); bool update_accuracy = false; uint8_t max_accuracy = 0; for (uint8_t i = 0; i < num_outputs; i++) { float signal = outputs[i].signal; switch (outputs[i].sensor_id) { case BSEC_OUTPUT_IAQ: max_accuracy = std::max(outputs[i].accuracy, max_accuracy); update_accuracy = true; #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); #endif break; case BSEC_OUTPUT_STATIC_IAQ: max_accuracy = std::max(outputs[i].accuracy, max_accuracy); update_accuracy = true; #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_static_sensor_, signal); }); #endif break; case BSEC_OUTPUT_CO2_EQUIVALENT: #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); }); #endif break; case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); }); #endif break; case BSEC_OUTPUT_RAW_PRESSURE: #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); }); #endif break; case BSEC_OUTPUT_RAW_GAS: #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); }); #endif break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); }); #endif break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: #ifdef USE_SENSOR this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); }); #endif break; } } if (update_accuracy) { #ifdef USE_SENSOR this->queue_push_( [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); #endif #ifdef USE_TEXT_SENSOR this->queue_push_([this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[max_accuracy]); }); #endif // Queue up an opportunity to save state this->queue_push_([this, max_accuracy]() { this->save_state_(max_accuracy); }); } } int64_t BME68xBSEC2Component::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); } #ifdef USE_SENSOR void BME68xBSEC2Component::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) { if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { return; } sensor->publish_state(value); } #endif #ifdef USE_TEXT_SENSOR void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } sensor->publish_state(value); } #endif void BME68xBSEC2Component::load_state_() { uint32_t hash = this->get_hash(); 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_m(&this->bsec_instance_, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); if (this->bsec_status_ != BSEC_OK) { ESP_LOGW(TAG, "Failed to load state (BSEC2 error code %d)", this->bsec_status_); } ESP_LOGI(TAG, "Loaded state"); } } void BME68xBSEC2Component::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_m(&this->bsec_instance_, 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 (BSEC2 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 bme68x_bsec2 } // namespace esphome #endif