mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 05:24:53 +01:00
[bme68x_bsec2_i2c] BME68X Temperature+Pressure+Humidity+Gas Sensor via BSEC2 (#4585)
* Added initial bme68x component * Initialize all child sensors to nullptr This was added to all other sensors in #3808 * Update BSEC2 and BME68x Libraries Current versions from Bosch Sensortec * Add myself to codeowners for bme68x_bsec * Move constants to const.py, according to ci-custom checks Move constants to const.py, according to ci-custom checks * Update library dependencies We'll stick with 1.4.2200 for now. 1.4.2200 is not on platform.io registry, use tag instead. Update to 1.5.2400 needs some work due to multi instance support. * Update BSEC2 to 1.6.2400 * Add consts to bme680x_bsec Enable inclusion with external_components * Update device class for pressure * Update to use multisensor API * Tidy up some constants * Add tests * Remove scd30 changes * Import CONF_SAMPLE_RATE * Pull BSEC config blob from repo based on config * Rename component to `bme68x_bsec_i2c` * Fix tests + codeowners * Cleanup for review * Rename using `bsec2` * Apply suggestions from code review Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Download file during validation stage, instead * Make `dump_config()` only dump stuff * Compile safely without sensor and text sensor headers * Use `intf_ptr` * Save state if measuring static IAQ, too * Update CODEOWNERS * Simplify esphome/components/bme68x_bsec2_i2c/__init__.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Remove extraneous colon & imports * Track & save the maximum accuracy value * Polish up accuracy sensor handling * Log static sensor, update `defines.h` * Walruses make it better * Add some logging of setup failures * Update esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp Co-authored-by: Trevor North <trevor@freedisc.co.uk> * Break out some things * Update CODEOWNERS * Update CODEOWNERS take 2 * Use `add_extra` in base schema * Another walrus in the sensor Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --------- Co-authored-by: Keith Burzinski <kbx81x@gmail.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Trevor North <trevor@freedisc.co.uk>
This commit is contained in:
parent
34d435c996
commit
f2e99fa319
16 changed files with 1217 additions and 1 deletions
|
@ -65,6 +65,8 @@ esphome/components/bluetooth_proxy/* @jesserockz
|
|||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
esphome/components/bme68x_bsec2/* @kbx81 @neffs
|
||||
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
||||
esphome/components/bmi160/* @flaviut
|
||||
esphome/components/bmp3xx/* @latonita
|
||||
esphome/components/bmp3xx_base/* @latonita @martgras
|
||||
|
|
196
esphome/components/bme68x_bsec2/__init__.py
Normal file
196
esphome/components/bme68x_bsec2/__init__.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from esphome import core, external_files
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MODEL,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_TEMPERATURE_OFFSET,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
|
||||
DOMAIN = "bme68x_bsec2"
|
||||
|
||||
BSEC2_LIBRARY_VERSION = "v1.7.2502"
|
||||
|
||||
CONF_ALGORITHM_OUTPUT = "algorithm_output"
|
||||
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
|
||||
CONF_IAQ_MODE = "iaq_mode"
|
||||
CONF_OPERATING_AGE = "operating_age"
|
||||
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
|
||||
CONF_SUPPLY_VOLTAGE = "supply_voltage"
|
||||
|
||||
bme68x_bsec2_ns = cg.esphome_ns.namespace("bme68x_bsec2")
|
||||
BME68xBSEC2Component = bme68x_bsec2_ns.class_("BME68xBSEC2Component", cg.Component)
|
||||
|
||||
|
||||
MODEL_OPTIONS = ["bme680", "bme688"]
|
||||
|
||||
AlgorithmOutput = bme68x_bsec2_ns.enum("AlgorithmOutput")
|
||||
ALGORITHM_OUTPUT_OPTIONS = {
|
||||
"classification": AlgorithmOutput.ALGORITHM_OUTPUT_CLASSIFICATION,
|
||||
"regression": AlgorithmOutput.ALGORITHM_OUTPUT_REGRESSION,
|
||||
}
|
||||
|
||||
OperatingAge = bme68x_bsec2_ns.enum("OperatingAge")
|
||||
OPERATING_AGE_OPTIONS = {
|
||||
"4d": OperatingAge.OPERATING_AGE_4D,
|
||||
"28d": OperatingAge.OPERATING_AGE_28D,
|
||||
}
|
||||
|
||||
SampleRate = bme68x_bsec2_ns.enum("SampleRate")
|
||||
SAMPLE_RATE_OPTIONS = {
|
||||
"LP": SampleRate.SAMPLE_RATE_LP,
|
||||
"ULP": SampleRate.SAMPLE_RATE_ULP,
|
||||
}
|
||||
|
||||
Voltage = bme68x_bsec2_ns.enum("Voltage")
|
||||
VOLTAGE_OPTIONS = {
|
||||
"1.8V": Voltage.VOLTAGE_1_8V,
|
||||
"3.3V": Voltage.VOLTAGE_3_3V,
|
||||
}
|
||||
|
||||
ALGORITHM_OUTPUT_FILE_NAME = {
|
||||
"classification": "sel",
|
||||
"regression": "reg",
|
||||
}
|
||||
|
||||
SAMPLE_RATE_FILE_NAME = {
|
||||
"LP": "3s",
|
||||
"ULP": "300s",
|
||||
}
|
||||
|
||||
VOLTAGE_FILE_NAME = {
|
||||
"1.8V": "18v",
|
||||
"3.3V": "33v",
|
||||
}
|
||||
|
||||
|
||||
def _compute_local_file_path(url: str) -> Path:
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def _compute_url(config: dict) -> str:
|
||||
model = config.get(CONF_MODEL)
|
||||
operating_age = config.get(CONF_OPERATING_AGE)
|
||||
sample_rate = SAMPLE_RATE_FILE_NAME[config.get(CONF_SAMPLE_RATE)]
|
||||
volts = VOLTAGE_FILE_NAME[config.get(CONF_SUPPLY_VOLTAGE)]
|
||||
if model == "bme688":
|
||||
algo = ALGORITHM_OUTPUT_FILE_NAME[
|
||||
config.get(CONF_ALGORITHM_OUTPUT, "classification")
|
||||
]
|
||||
filename = "bsec_selectivity"
|
||||
else:
|
||||
algo = "iaq"
|
||||
filename = "bsec_iaq"
|
||||
return f"https://raw.githubusercontent.com/boschsensortec/Bosch-BSEC2-Library/{BSEC2_LIBRARY_VERSION}/src/config/{model}/{model}_{algo}_{volts}_{sample_rate}_{operating_age}/{filename}.txt"
|
||||
|
||||
|
||||
def download_bme68x_blob(config):
|
||||
url = _compute_url(config)
|
||||
path = _compute_local_file_path(url)
|
||||
external_files.download_content(url, path)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def validate_bme68x(config):
|
||||
if CONF_ALGORITHM_OUTPUT not in config:
|
||||
return config
|
||||
|
||||
if config[CONF_MODEL] != "bme688":
|
||||
raise cv.Invalid(f"{CONF_ALGORITHM_OUTPUT} is only valid for BME688")
|
||||
|
||||
if config[CONF_ALGORITHM_OUTPUT] == "regression" and (
|
||||
config[CONF_OPERATING_AGE] != "4d"
|
||||
or config[CONF_SAMPLE_RATE] != "ULP"
|
||||
or config[CONF_SUPPLY_VOLTAGE] != "1.8V"
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f" To use '{CONF_ALGORITHM_OUTPUT}: regression', {CONF_OPERATING_AGE} must be '4d', {CONF_SAMPLE_RATE} must be 'ULP' and {CONF_SUPPLY_VOLTAGE} must be '1.8V'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA_BASE = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BME68xBSEC2Component),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODEL_OPTIONS, lower=True),
|
||||
cv.Optional(CONF_ALGORITHM_OUTPUT): cv.enum(
|
||||
ALGORITHM_OUTPUT_OPTIONS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_OPERATING_AGE, default="28d"): cv.enum(
|
||||
OPERATING_AGE_OPTIONS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum(
|
||||
SAMPLE_RATE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
|
||||
VOLTAGE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
|
||||
cv.Optional(
|
||||
CONF_STATE_SAVE_INTERVAL, default="6hours"
|
||||
): cv.positive_time_period_minutes,
|
||||
},
|
||||
)
|
||||
.add_extra(cv.only_with_arduino)
|
||||
.add_extra(validate_bme68x)
|
||||
.add_extra(download_bme68x_blob)
|
||||
)
|
||||
|
||||
|
||||
async def to_code_base(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if algo_output := config.get(CONF_ALGORITHM_OUTPUT):
|
||||
cg.add(var.set_algorithm_output(algo_output))
|
||||
cg.add(var.set_operating_age(config[CONF_OPERATING_AGE]))
|
||||
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
|
||||
cg.add(var.set_voltage(config[CONF_SUPPLY_VOLTAGE]))
|
||||
cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET]))
|
||||
cg.add(
|
||||
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
|
||||
)
|
||||
|
||||
path = _compute_local_file_path(_compute_url(config))
|
||||
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
bsec2_iaq_config = f.read()
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}")
|
||||
|
||||
# Convert retrieved BSEC2 config to an array of ints
|
||||
rhs = [int(x) for x in bsec2_iaq_config.split(",")]
|
||||
# Create an array which will reside in program memory and configure the sensor instance to use it
|
||||
bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs)))
|
||||
|
||||
# Although this component does not use SPI, the BSEC2 library requires the SPI library
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
"1.1.40407",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
|
||||
)
|
||||
|
||||
cg.add_define("USE_BSEC2")
|
||||
|
||||
return var
|
523
esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
Normal file
523
esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
Normal file
|
@ -0,0 +1,523 @@
|
|||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_BSEC2
|
||||
#include "bme68x_bsec2.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
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, "Setting up BME68X via BSEC2...");
|
||||
|
||||
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:");
|
||||
|
||||
ESP_LOGCONFIG(TAG, " BSEC2 version: %d.%d.%d.%d", this->version_.major, this->version_.minor,
|
||||
this->version_.major_bugfix, this->version_.minor_bugfix);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " BSEC2 configuration blob:");
|
||||
ESP_LOGCONFIG(TAG, " Configured: %s", 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", BME68X_BSEC2_OPERATING_AGE_LOG(this->operating_age_));
|
||||
ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->sample_rate_));
|
||||
ESP_LOGCONFIG(TAG, " Voltage: %s", BME68X_BSEC2_VOLTAGE_LOG(this->voltage_));
|
||||
ESP_LOGCONFIG(TAG, " State save interval: %ims", this->state_save_interval_ms_);
|
||||
ESP_LOGCONFIG(TAG, " Temperature offset: %.2f", 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_() {
|
||||
int64_t curr_time_ns = this->get_time_ns_();
|
||||
if (curr_time_ns < this->next_call_ns_) {
|
||||
return;
|
||||
}
|
||||
this->op_mode_ = this->bsec_settings_.op_mode;
|
||||
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;
|
||||
}
|
||||
this->next_call_ns_ = this->bsec_settings_.next_call;
|
||||
|
||||
if (this->bsec_settings_.trigger_measurement) {
|
||||
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_);
|
||||
|
||||
switch (this->bsec_settings_.op_mode) {
|
||||
case BME68X_FORCED_MODE:
|
||||
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;
|
||||
this->sleep_mode_ = false;
|
||||
ESP_LOGV(TAG, "Using forced mode");
|
||||
|
||||
break;
|
||||
case BME68X_PARALLEL_MODE:
|
||||
if (this->op_mode_ != this->bsec_settings_.op_mode) {
|
||||
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;
|
||||
this->sleep_mode_ = false;
|
||||
ESP_LOGV(TAG, "Using parallel mode");
|
||||
}
|
||||
break;
|
||||
case BME68X_SLEEP_MODE:
|
||||
if (!this->sleep_mode_) {
|
||||
bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_);
|
||||
this->sleep_mode_ = true;
|
||||
ESP_LOGV(TAG, "Using sleep mode");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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<uint8_t[BSEC_MAX_STATE_BLOB_SIZE]>(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
|
163
esphome/components/bme68x_bsec2/bme68x_bsec2.h
Normal file
163
esphome/components/bme68x_bsec2/bme68x_bsec2.h
Normal file
|
@ -0,0 +1,163 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifdef USE_BSEC2
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
#include <cinttypes>
|
||||
#include <queue>
|
||||
|
||||
#include <bsec2.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace bme68x_bsec2 {
|
||||
|
||||
enum AlgorithmOutput {
|
||||
ALGORITHM_OUTPUT_IAQ,
|
||||
ALGORITHM_OUTPUT_CLASSIFICATION,
|
||||
ALGORITHM_OUTPUT_REGRESSION,
|
||||
};
|
||||
|
||||
enum OperatingAge {
|
||||
OPERATING_AGE_4D,
|
||||
OPERATING_AGE_28D,
|
||||
};
|
||||
|
||||
enum SampleRate {
|
||||
SAMPLE_RATE_LP = 0,
|
||||
SAMPLE_RATE_ULP = 1,
|
||||
SAMPLE_RATE_DEFAULT = 2,
|
||||
};
|
||||
|
||||
enum Voltage {
|
||||
VOLTAGE_1_8V,
|
||||
VOLTAGE_3_3V,
|
||||
};
|
||||
|
||||
class BME68xBSEC2Component : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; }
|
||||
void set_operating_age(OperatingAge operating_age) { this->operating_age_ = operating_age; }
|
||||
void set_temperature_offset(float offset) { this->temperature_offset_ = offset; }
|
||||
void set_voltage(Voltage voltage) { this->voltage_ = voltage; }
|
||||
|
||||
void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; }
|
||||
void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; }
|
||||
void set_pressure_sample_rate(SampleRate sample_rate) { this->pressure_sample_rate_ = sample_rate; }
|
||||
void set_humidity_sample_rate(SampleRate sample_rate) { this->humidity_sample_rate_ = sample_rate; }
|
||||
|
||||
void set_bsec2_configuration(const uint8_t *data, const uint32_t len) {
|
||||
this->bsec2_configuration_ = data;
|
||||
this->bsec2_configuration_length_ = len;
|
||||
}
|
||||
|
||||
void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *sensor) { this->pressure_sensor_ = sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
|
||||
void set_gas_resistance_sensor(sensor::Sensor *sensor) { this->gas_resistance_sensor_ = sensor; }
|
||||
void set_iaq_sensor(sensor::Sensor *sensor) { this->iaq_sensor_ = sensor; }
|
||||
void set_iaq_static_sensor(sensor::Sensor *sensor) { this->iaq_static_sensor_ = sensor; }
|
||||
void set_iaq_accuracy_sensor(sensor::Sensor *sensor) { this->iaq_accuracy_sensor_ = sensor; }
|
||||
void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; }
|
||||
void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; }
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *sensor) { this->iaq_accuracy_text_sensor_ = sensor; }
|
||||
#endif
|
||||
virtual uint32_t get_hash() = 0;
|
||||
|
||||
protected:
|
||||
void set_config_(const uint8_t *config, u_int32_t len);
|
||||
float calc_sensor_sample_rate_(SampleRate sample_rate);
|
||||
void update_subscription_();
|
||||
|
||||
void run_();
|
||||
void read_(int64_t trigger_time_ns);
|
||||
void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
|
||||
int64_t get_time_ns_();
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
|
||||
#endif
|
||||
|
||||
void load_state_();
|
||||
void save_state_(uint8_t accuracy);
|
||||
|
||||
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
|
||||
|
||||
struct bme68x_dev bme68x_;
|
||||
bsec_bme_settings_t bsec_settings_;
|
||||
bsec_version_t version_;
|
||||
uint8_t bsec_instance_[BSEC_INSTANCE_SIZE];
|
||||
|
||||
struct bme68x_heatr_conf bme68x_heatr_conf_;
|
||||
uint8_t op_mode_; // operating mode of sensor
|
||||
bool sleep_mode_;
|
||||
bsec_library_return_t bsec_status_{BSEC_OK};
|
||||
int8_t bme68x_status_{BME68X_OK};
|
||||
|
||||
int64_t last_time_ms_{0};
|
||||
uint32_t millis_overflow_counter_{0};
|
||||
int64_t next_call_ns_{0};
|
||||
|
||||
std::queue<std::function<void()>> queue_;
|
||||
|
||||
uint8_t const *bsec2_configuration_{nullptr};
|
||||
uint32_t bsec2_configuration_length_{0};
|
||||
bool bsec2_blob_configured_{false};
|
||||
|
||||
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};
|
||||
|
||||
AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ};
|
||||
OperatingAge operating_age_{OPERATING_AGE_28D};
|
||||
Voltage voltage_{VOLTAGE_3_3V};
|
||||
|
||||
SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate
|
||||
SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT};
|
||||
SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT};
|
||||
SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT};
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *gas_resistance_sensor_{nullptr};
|
||||
sensor::Sensor *iaq_sensor_{nullptr};
|
||||
sensor::Sensor *iaq_static_sensor_{nullptr};
|
||||
sensor::Sensor *iaq_accuracy_sensor_{nullptr};
|
||||
sensor::Sensor *co2_equivalent_sensor_{nullptr};
|
||||
sensor::Sensor *breath_voc_equivalent_sensor_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *iaq_accuracy_text_sensor_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace bme68x_bsec2
|
||||
} // namespace esphome
|
||||
#endif
|
130
esphome/components/bme68x_bsec2/sensor.py
Normal file
130
esphome/components/bme68x_bsec2/sensor.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_GAS_RESISTANCE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_IAQ_ACCURACY,
|
||||
CONF_PRESSURE,
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_GAS_CYLINDER,
|
||||
ICON_GAUGE,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
UNIT_OHM,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
from . import CONF_BME68X_BSEC2_ID, SAMPLE_RATE_OPTIONS, BME68xBSEC2Component
|
||||
|
||||
DEPENDENCIES = ["bme68x_bsec2"]
|
||||
|
||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||
CONF_IAQ = "iaq"
|
||||
CONF_IAQ_STATIC = "iaq_static"
|
||||
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
|
||||
ICON_TEST_TUBE = "mdi:test-tube"
|
||||
UNIT_IAQ = "IAQ"
|
||||
|
||||
TYPES = [
|
||||
CONF_TEMPERATURE,
|
||||
CONF_PRESSURE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_GAS_RESISTANCE,
|
||||
CONF_IAQ,
|
||||
CONF_IAQ_STATIC,
|
||||
CONF_IAQ_ACCURACY,
|
||||
CONF_CO2_EQUIVALENT,
|
||||
CONF_BREATH_VOC_EQUIVALENT,
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
icon=ICON_GAUGE,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
|
||||
),
|
||||
cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_OHM,
|
||||
icon=ICON_GAS_CYLINDER,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_IAQ): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_IAQ,
|
||||
icon=ICON_GAUGE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_IAQ_STATIC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_IAQ,
|
||||
icon=ICON_GAUGE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema(
|
||||
icon=ICON_ACCURACY,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
icon=ICON_TEST_TUBE,
|
||||
accuracy_decimals=1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
icon=ICON_TEST_TUBE,
|
||||
accuracy_decimals=1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def setup_conf(config, key, hub):
|
||||
if conf := config.get(key):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
|
||||
if sample_rate := conf.get(CONF_SAMPLE_RATE):
|
||||
cg.add(getattr(hub, f"set_{key}_sample_rate")(sample_rate))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID])
|
||||
for key in TYPES:
|
||||
await setup_conf(config, key, hub)
|
33
esphome/components/bme68x_bsec2/text_sensor.py
Normal file
33
esphome/components/bme68x_bsec2/text_sensor.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_IAQ_ACCURACY
|
||||
|
||||
from . import CONF_BME68X_BSEC2_ID, BME68xBSEC2Component
|
||||
|
||||
DEPENDENCIES = ["bme68x_bsec2"]
|
||||
|
||||
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
|
||||
|
||||
TYPES = [CONF_IAQ_ACCURACY]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component),
|
||||
cv.Optional(CONF_IAQ_ACCURACY): text_sensor.text_sensor_schema(
|
||||
icon=ICON_ACCURACY
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def setup_conf(config, key, hub):
|
||||
if conf := config.get(key):
|
||||
sens = await text_sensor.new_text_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID])
|
||||
for key in TYPES:
|
||||
await setup_conf(config, key, hub)
|
28
esphome/components/bme68x_bsec2_i2c/__init__.py
Normal file
28
esphome/components/bme68x_bsec2_i2c/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.bme68x_bsec2 import (
|
||||
CONFIG_SCHEMA_BASE,
|
||||
BME68xBSEC2Component,
|
||||
to_code_base,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
|
||||
CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
|
||||
AUTO_LOAD = ["bme68x_bsec2"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c")
|
||||
BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_(
|
||||
"BME68xBSEC2I2CComponent", BME68xBSEC2Component, i2c.I2CDevice
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
|
||||
cv.Schema({cv.GenerateID(): cv.declare_id(BME68xBSEC2I2CComponent)})
|
||||
).extend(i2c.i2c_device_schema(0x76))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await to_code_base(config)
|
||||
await i2c.register_i2c_device(var, config)
|
53
esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp
Normal file
53
esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp
Normal file
|
@ -0,0 +1,53 @@
|
|||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_BSEC2
|
||||
#include "bme68x_bsec2_i2c.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace bme68x_bsec2_i2c {
|
||||
|
||||
static const char *const TAG = "bme68x_bsec2_i2c.sensor";
|
||||
|
||||
void BME68xBSEC2I2CComponent::setup() {
|
||||
// must set up our bme68x_dev instance before calling setup()
|
||||
this->bme68x_.intf_ptr = (void *) this;
|
||||
this->bme68x_.intf = BME68X_I2C_INTF;
|
||||
this->bme68x_.read = BME68xBSEC2I2CComponent::read_bytes_wrapper;
|
||||
this->bme68x_.write = BME68xBSEC2I2CComponent::write_bytes_wrapper;
|
||||
this->bme68x_.delay_us = BME68xBSEC2I2CComponent::delay_us;
|
||||
this->bme68x_.amb_temp = 25;
|
||||
|
||||
BME68xBSEC2Component::setup();
|
||||
}
|
||||
|
||||
void BME68xBSEC2I2CComponent::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
BME68xBSEC2Component::dump_config();
|
||||
}
|
||||
|
||||
uint32_t BME68xBSEC2I2CComponent::get_hash() { return fnv1_hash("bme68x_bsec_state_" + to_string(this->address_)); }
|
||||
|
||||
int8_t BME68xBSEC2I2CComponent::read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr) {
|
||||
ESP_LOGVV(TAG, "read_bytes_wrapper: reg = %u", a_register);
|
||||
return static_cast<BME68xBSEC2I2CComponent *>(intfPtr)->read_bytes(a_register, data, len) ? 0 : -1;
|
||||
}
|
||||
|
||||
int8_t BME68xBSEC2I2CComponent::write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len,
|
||||
void *intfPtr) {
|
||||
ESP_LOGVV(TAG, "write_bytes_wrapper: reg = %u", a_register);
|
||||
return static_cast<BME68xBSEC2I2CComponent *>(intfPtr)->write_bytes(a_register, data, len) ? 0 : -1;
|
||||
}
|
||||
|
||||
void BME68xBSEC2I2CComponent::delay_us(uint32_t period, void *intfPtr) {
|
||||
ESP_LOGVV(TAG, "Delaying for %" PRIu32 "us", period);
|
||||
delayMicroseconds(period);
|
||||
}
|
||||
|
||||
} // namespace bme68x_bsec2_i2c
|
||||
} // namespace esphome
|
||||
#endif
|
28
esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h
Normal file
28
esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifdef USE_BSEC2
|
||||
|
||||
#include "esphome/components/bme68x_bsec2/bme68x_bsec2.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bme68x_bsec2_i2c {
|
||||
|
||||
class BME68xBSEC2I2CComponent : public bme68x_bsec2::BME68xBSEC2Component, public i2c::I2CDevice {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
uint32_t get_hash() override;
|
||||
|
||||
static int8_t read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr);
|
||||
static int8_t write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len, void *intfPtr);
|
||||
static void delay_us(uint32_t period, void *intfPtr);
|
||||
};
|
||||
|
||||
} // namespace bme68x_bsec2_i2c
|
||||
} // namespace esphome
|
||||
#endif
|
|
@ -158,6 +158,7 @@
|
|||
#endif
|
||||
|
||||
// Disabled feature flags
|
||||
// #define USE_BSEC // Requires a library with proprietary license.
|
||||
// #define USE_BSEC // Requires a library with proprietary license
|
||||
// #define USE_BSEC2 // Requires a library with proprietary license
|
||||
|
||||
#define USE_DASHBOARD_IMPORT
|
||||
|
|
34
tests/components/bme68x_bsec2_i2c/common.yaml
Normal file
34
tests/components/bme68x_bsec2_i2c/common.yaml
Normal file
|
@ -0,0 +1,34 @@
|
|||
i2c:
|
||||
- id: i2c_bme68x
|
||||
scl: ${scl_pin}
|
||||
sda: ${sda_pin}
|
||||
|
||||
bme68x_bsec2_i2c:
|
||||
address: 0x76
|
||||
model: bme688
|
||||
algorithm_output: classification
|
||||
operating_age: 28d
|
||||
sample_rate: LP
|
||||
supply_voltage: 3.3V
|
||||
|
||||
sensor:
|
||||
- platform: bme68x_bsec2
|
||||
temperature:
|
||||
name: BME68X Temperature
|
||||
pressure:
|
||||
name: BME68X Pressure
|
||||
humidity:
|
||||
name: BME68X Humidity
|
||||
gas_resistance:
|
||||
name: BME68X Gas Sensor
|
||||
iaq:
|
||||
name: BME68X IAQ
|
||||
co2_equivalent:
|
||||
name: BME68X eCO2
|
||||
breath_voc_equivalent:
|
||||
name: BME68X Breath eVOC
|
||||
|
||||
text_sensor:
|
||||
- platform: bme68x_bsec2
|
||||
iaq_accuracy:
|
||||
name: BME68X Accuracy
|
5
tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml
Normal file
5
tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml
Normal file
5
tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
scl_pin: GPIO6
|
||||
sda_pin: GPIO7
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml
Normal file
5
tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml
Normal file
5
tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml
Normal file
5
tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
scl_pin: GPIO5
|
||||
sda_pin: GPIO4
|
||||
|
||||
<<: !include common.yaml
|
Loading…
Reference in a new issue