├── .gitignore ├── README.md ├── components └── bme680_bsec │ ├── __init__.py │ ├── bme680_bsec.cpp │ ├── bme680_bsec.h │ ├── sensor.py │ └── text_sensor.py └── ha-screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :gift: Component now [included in the core distribution](https://esphome.io/components/sensor/bme680_bsec.html) :gift: 2 | :warning: **Warning:** You should only use this repository if you are specifically testing a new release or experimental feature. 3 | 4 | ## Introduction 5 | This component facilitates use of the Bosch BME680 sensor with [ESPHome](https://esphome.io) via the closed source [Bosch BSEC library](https://github.com/BoschSensortec/BSEC-Arduino-library) providing additional calculated indoor air quality measurements not available with the [core component](https://esphome.io/components/sensor/bme680.html): 6 | 7 | ![Home Assistant Entities](ha-screenshot.png) 8 | 9 | ## Installation 10 | As of ESPHome 1.18 this component is [included in the core distribution](https://esphome.io/components/sensor/bme680_bsec.html), however you may still wish to use this repository if a new release or experimental feature is available. 11 | 12 | ⚠️ **Note**: The Bosch BSEC library is only available for use after accepting its software license agreement. By enabling this component, you are explicitly agreeing to the terms of the [BSEC license agreement](https://www.bosch-sensortec.com/media/boschsensortec/downloads/bsec/2017-07-17_clickthrough_license_terms_environmentalib_sw_clean.pdf). 13 | 14 | ### As an external component 15 | In ESPHome 1.18 and later you can use the [external components](https://esphome.io/components/external_components.html) system: 16 | ```yaml 17 | external_components: 18 | - source: github://trvrnrth/esphome-bsec-bme680 19 | ``` 20 | 21 | ### ESPHome 1.17 and earlier 22 | Copy the `components/bme680_bsec` directory into your ESPHome `custom_components` directory (creating it if it does not exist). If you are running via Home Assistant this will be `/config/esphome/custom_components/bme680_bsec`. 23 | 24 | ## Dependencies 25 | The [I2C Bus](https://esphome.io/components/i2c.html#i2c) must be set up in order for this component to work. 26 | 27 | ## Minimal configuration 28 | The following configuration shows the minimal set up to read temperature and humidity from the sensor. 29 | ```yaml 30 | i2c: 31 | 32 | bme680_bsec: 33 | 34 | sensor: 35 | - platform: bme680_bsec 36 | temperature: 37 | name: "BME680 Temperature" 38 | humidity: 39 | name: "BME680 Humidity" 40 | ``` 41 | 42 | ## Advanced configuration 43 | The following configuration shows all the available sensors and optional settings for the component. It also includes an example of filtering to guard against 44 | outliers, limit the number of updates sent to home assistant and reduce storage requirements in other systems such as influxdb used to store historical data. 45 | 46 | For each sensor all other options from [Sensor](https://esphome.io/components/sensor/index.html#config-sensor) and [TextSensor](https://esphome.io/components/text_sensor/index.html#base-text-sensor-configuration) are also available for filtering, automation and so on. 47 | 48 | ```yaml 49 | bme680_bsec: 50 | # i2c address 51 | # ----------- 52 | # Common values are: 53 | # - 0x76 54 | # - 0x77 55 | # Default: 0x76 56 | address: 0x76 57 | 58 | # Temperature offset 59 | # ------------------ 60 | # Useful if device is in enclosure and reads too high 61 | # Default: 0 62 | temperature_offset: 0 63 | 64 | # IAQ calculation mode 65 | # -------------------- 66 | # Available options: 67 | # - static (for fixed position devices) 68 | # - mobile (for on person or other moveable devices) 69 | # Default: static 70 | iaq_mode: static 71 | 72 | # Sample rate 73 | # ----------- 74 | # This controls the sampling rate for gas-dependant sensors and will govern the interval 75 | # at which the sensor heater is operated. 76 | # By default this rate will also be used for temperature, pressure and humidity sensors 77 | # but these can be overridden on a per-sensor level if required. 78 | # 79 | # Available options: 80 | # - lp (low power - samples every 3 seconds) 81 | # - ulp (ultra low power - samples every 5 minutes) 82 | # Default: lp 83 | sample_rate: ulp 84 | 85 | # Interval at which to save BSEC state 86 | # ------------------------------------ 87 | # Default: 6h 88 | state_save_interval: 6h 89 | 90 | sensor: 91 | - platform: bme680_bsec 92 | temperature: 93 | # Temperature in °C 94 | name: "BME680 Temperature" 95 | sample_rate: lp 96 | filters: 97 | - median 98 | pressure: 99 | # Pressure in hPa 100 | name: "BME680 Pressure" 101 | sample_rate: lp 102 | filters: 103 | - median 104 | humidity: 105 | # Relative humidity % 106 | name: "BME680 Humidity" 107 | sample_rate: lp 108 | filters: 109 | - median 110 | gas_resistance: 111 | # Gas resistance in Ω 112 | name: "BME680 Gas Resistance" 113 | filters: 114 | - median 115 | iaq: 116 | # Indoor air quality value 117 | name: "BME680 IAQ" 118 | filters: 119 | - median 120 | iaq_accuracy: 121 | # IAQ accuracy as a numeric value of 0, 1, 2, 3 122 | name: "BME680 Numeric IAQ Accuracy" 123 | co2_equivalent: 124 | # CO2 equivalent estimate in ppm 125 | name: "BME680 CO2 Equivalent" 126 | filters: 127 | - median 128 | breath_voc_equivalent: 129 | # Volatile organic compounds equivalent estimate in ppm 130 | name: "BME680 Breath VOC Equivalent" 131 | filters: 132 | - median 133 | 134 | text_sensor: 135 | - platform: bme680_bsec 136 | iaq_accuracy: 137 | # IAQ accuracy as a text value of Stabilizing, Uncertain, Calibrating, Calibrated 138 | name: "BME680 IAQ Accuracy" 139 | ``` 140 | 141 | ## Indoor Air Quality (IAQ) Measurement 142 | Indoor Air Quality measurements are expressed in the IAQ index scale with 25IAQ corresponding to typical good air and 250IAQ 143 | indicating typical polluted air after calibration. 144 | 145 | ## IAQ Accuracy and Calibration 146 | The BSEC algorithm automatically gathers data in order to calibrate the IAQ measurements. The IAQ Accuracy sensor will give one 147 | of the following values: 148 | 149 | - `Stabilizing`: The device has just started, and the sensor is stabilizing (this typically lasts 5 minutes) 150 | - `Uncertain`: The background history of BSEC is uncertain. This typically means the gas sensor data was too 151 | stable for BSEC to clearly define its reference. 152 | - `Calibrating`: BSEC found new calibration data and is currently calibrating. 153 | - `Calibrated`: BSEC calibrated successfully. 154 | 155 | Every `state_save_interval`, or as soon thereafter as full calibration is reached, the current algorithm state is saved to flash 156 | so that the process does not have to start from zero on device restart. 157 | -------------------------------------------------------------------------------- /components/bme680_bsec/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import i2c 4 | from esphome.const import CONF_ID 5 | 6 | CODEOWNERS = ["@trvrnrth"] 7 | DEPENDENCIES = ["i2c"] 8 | AUTO_LOAD = ["sensor", "text_sensor"] 9 | 10 | CONF_BME680_BSEC_ID = "bme680_bsec_id" 11 | CONF_TEMPERATURE_OFFSET = "temperature_offset" 12 | CONF_IAQ_MODE = "iaq_mode" 13 | CONF_SAMPLE_RATE = "sample_rate" 14 | CONF_STATE_SAVE_INTERVAL = "state_save_interval" 15 | 16 | bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec") 17 | 18 | IAQMode = bme680_bsec_ns.enum("IAQMode") 19 | IAQ_MODE_OPTIONS = { 20 | "STATIC": IAQMode.IAQ_MODE_STATIC, 21 | "MOBILE": IAQMode.IAQ_MODE_MOBILE, 22 | } 23 | 24 | SampleRate = bme680_bsec_ns.enum("SampleRate") 25 | SAMPLE_RATE_OPTIONS = { 26 | "LP": SampleRate.SAMPLE_RATE_LP, 27 | "ULP": SampleRate.SAMPLE_RATE_ULP, 28 | } 29 | 30 | BME680BSECComponent = bme680_bsec_ns.class_( 31 | "BME680BSECComponent", cg.Component, i2c.I2CDevice 32 | ) 33 | 34 | CONFIG_SCHEMA = cv.Schema( 35 | { 36 | cv.GenerateID(): cv.declare_id(BME680BSECComponent), 37 | cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, 38 | cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( 39 | IAQ_MODE_OPTIONS, upper=True 40 | ), 41 | cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( 42 | SAMPLE_RATE_OPTIONS, upper=True 43 | ), 44 | cv.Optional( 45 | CONF_STATE_SAVE_INTERVAL, default="6hours" 46 | ): cv.positive_time_period_minutes, 47 | } 48 | ).extend(i2c.i2c_device_schema(0x76)) 49 | 50 | 51 | def to_code(config): 52 | var = cg.new_Pvariable(config[CONF_ID]) 53 | yield cg.register_component(var, config) 54 | yield i2c.register_i2c_device(var, config) 55 | 56 | cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) 57 | cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) 58 | cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) 59 | cg.add( 60 | var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) 61 | ) 62 | 63 | cg.add_define("USE_BSEC") 64 | cg.add_library("BSEC Software Library", "1.6.1480") 65 | -------------------------------------------------------------------------------- /components/bme680_bsec/bme680_bsec.cpp: -------------------------------------------------------------------------------- 1 | #include "bme680_bsec.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/helpers.h" 4 | #include 5 | 6 | namespace esphome { 7 | namespace bme680_bsec { 8 | #ifdef USE_BSEC 9 | static const char *TAG = "bme680_bsec.sensor"; 10 | 11 | static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; 12 | 13 | BME680BSECComponent *BME680BSECComponent::instance; 14 | 15 | void BME680BSECComponent::setup() { 16 | ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); 17 | BME680BSECComponent::instance = this; 18 | 19 | this->bsec_status_ = bsec_init(); 20 | if (this->bsec_status_ != BSEC_OK) { 21 | this->mark_failed(); 22 | return; 23 | } 24 | 25 | this->bme680_.dev_id = this->address_; 26 | this->bme680_.intf = BME680_I2C_INTF; 27 | this->bme680_.read = BME680BSECComponent::read_bytes_wrapper; 28 | this->bme680_.write = BME680BSECComponent::write_bytes_wrapper; 29 | this->bme680_.delay_ms = BME680BSECComponent::delay_ms; 30 | this->bme680_.amb_temp = 25; 31 | 32 | this->bme680_status_ = bme680_init(&this->bme680_); 33 | if (this->bme680_status_ != BME680_OK) { 34 | this->mark_failed(); 35 | return; 36 | } 37 | 38 | if (this->sample_rate_ == SAMPLE_RATE_ULP) { 39 | const uint8_t bsec_config[] = { 40 | #include "config/generic_33v_300s_28d/bsec_iaq.txt" 41 | }; 42 | this->set_config_(bsec_config); 43 | } else { 44 | const uint8_t bsec_config[] = { 45 | #include "config/generic_33v_3s_28d/bsec_iaq.txt" 46 | }; 47 | this->set_config_(bsec_config); 48 | } 49 | this->update_subscription_(); 50 | if (this->bsec_status_ != BSEC_OK) { 51 | this->mark_failed(); 52 | return; 53 | } 54 | 55 | this->load_state_(); 56 | } 57 | 58 | void BME680BSECComponent::set_config_(const uint8_t *config) { 59 | uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; 60 | this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer)); 61 | } 62 | 63 | float BME680BSECComponent::calc_sensor_sample_rate_(SampleRate sample_rate) { 64 | if (sample_rate == SAMPLE_RATE_DEFAULT) { 65 | sample_rate = this->sample_rate_; 66 | } 67 | return sample_rate == SAMPLE_RATE_ULP ? BSEC_SAMPLE_RATE_ULP : BSEC_SAMPLE_RATE_LP; 68 | } 69 | 70 | void BME680BSECComponent::update_subscription_() { 71 | bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; 72 | int num_virtual_sensors = 0; 73 | 74 | if (this->iaq_sensor_) { 75 | virtual_sensors[num_virtual_sensors].sensor_id = 76 | this->iaq_mode_ == IAQ_MODE_STATIC ? BSEC_OUTPUT_STATIC_IAQ : BSEC_OUTPUT_IAQ; 77 | virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 78 | num_virtual_sensors++; 79 | } 80 | 81 | if (this->co2_equivalent_sensor_) { 82 | virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; 83 | virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 84 | num_virtual_sensors++; 85 | } 86 | 87 | if (this->breath_voc_equivalent_sensor_) { 88 | virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; 89 | virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 90 | num_virtual_sensors++; 91 | } 92 | 93 | if (this->pressure_sensor_) { 94 | virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; 95 | virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->pressure_sample_rate_); 96 | num_virtual_sensors++; 97 | } 98 | 99 | if (this->gas_resistance_sensor_) { 100 | virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; 101 | virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); 102 | num_virtual_sensors++; 103 | } 104 | 105 | if (this->temperature_sensor_) { 106 | virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; 107 | virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->temperature_sample_rate_); 108 | num_virtual_sensors++; 109 | } 110 | 111 | if (this->humidity_sensor_) { 112 | virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; 113 | virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->humidity_sample_rate_); 114 | num_virtual_sensors++; 115 | } 116 | 117 | bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; 118 | uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; 119 | this->bsec_status_ = 120 | bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings); 121 | } 122 | 123 | void BME680BSECComponent::dump_config() { 124 | ESP_LOGCONFIG(TAG, "BME680 via BSEC:"); 125 | 126 | bsec_version_t version; 127 | bsec_get_version(&version); 128 | ESP_LOGCONFIG(TAG, " BSEC Version: %d.%d.%d.%d", version.major, version.minor, version.major_bugfix, 129 | version.minor_bugfix); 130 | 131 | LOG_I2C_DEVICE(this); 132 | 133 | if (this->is_failed()) { 134 | ESP_LOGE(TAG, "Communication failed (BSEC Status: %d, BME680 Status: %d)", this->bsec_status_, 135 | this->bme680_status_); 136 | } 137 | 138 | ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_); 139 | ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile"); 140 | ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_)); 141 | ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_); 142 | 143 | LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); 144 | ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->temperature_sample_rate_)); 145 | LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); 146 | ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->pressure_sample_rate_)); 147 | LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); 148 | ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->humidity_sample_rate_)); 149 | LOG_SENSOR(" ", "Gas Resistance", this->gas_resistance_sensor_); 150 | LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); 151 | LOG_SENSOR(" ", "Numeric IAQ Accuracy", this->iaq_accuracy_sensor_); 152 | LOG_TEXT_SENSOR(" ", "IAQ Accuracy", this->iaq_accuracy_text_sensor_); 153 | LOG_SENSOR(" ", "CO2 Equivalent", this->co2_equivalent_sensor_); 154 | LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); 155 | } 156 | 157 | float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } 158 | 159 | void BME680BSECComponent::loop() { 160 | this->run_(); 161 | 162 | if (this->bsec_status_ < BSEC_OK || this->bme680_status_ < BME680_OK) { 163 | this->status_set_error(); 164 | } else { 165 | this->status_clear_error(); 166 | } 167 | if (this->bsec_status_ > BSEC_OK || this->bme680_status_ > BME680_OK) { 168 | this->status_set_warning(); 169 | } else { 170 | this->status_clear_warning(); 171 | } 172 | } 173 | 174 | void BME680BSECComponent::run_() { 175 | int64_t curr_time_ns = this->get_time_ns_(); 176 | if (curr_time_ns < this->next_call_ns_) { 177 | return; 178 | } 179 | 180 | ESP_LOGV(TAG, "Performing sensor run"); 181 | 182 | bsec_bme_settings_t bme680_settings; 183 | this->bsec_status_ = bsec_sensor_control(curr_time_ns, &bme680_settings); 184 | if (this->bsec_status_ < BSEC_OK) { 185 | ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_); 186 | return; 187 | } 188 | this->next_call_ns_ = bme680_settings.next_call; 189 | 190 | if (bme680_settings.trigger_measurement) { 191 | this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling; 192 | this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling; 193 | this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling; 194 | this->bme680_.gas_sett.run_gas = bme680_settings.run_gas; 195 | this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature; 196 | this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration; 197 | this->bme680_.power_mode = BME680_FORCED_MODE; 198 | uint16_t desired_settings = BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_GAS_SENSOR_SEL; 199 | this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_); 200 | if (this->bme680_status_ != BME680_OK) { 201 | ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_); 202 | return; 203 | } 204 | 205 | this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_); 206 | if (this->bme680_status_ != BME680_OK) { 207 | ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_); 208 | return; 209 | } 210 | 211 | uint16_t meas_dur = 0; 212 | bme680_get_profile_dur(&meas_dur, &this->bme680_); 213 | ESP_LOGV(TAG, "Queueing read in %ums", meas_dur); 214 | this->set_timeout("read", meas_dur, 215 | [this, curr_time_ns, bme680_settings]() { this->read_(curr_time_ns, bme680_settings); }); 216 | } else { 217 | ESP_LOGV(TAG, "Measurement not required"); 218 | this->read_(curr_time_ns, bme680_settings); 219 | } 220 | } 221 | 222 | void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings) { 223 | ESP_LOGV(TAG, "Reading data"); 224 | 225 | if (bme680_settings.trigger_measurement) { 226 | while (this->bme680_.power_mode != BME680_SLEEP_MODE) { 227 | this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_); 228 | if (this->bme680_status_ != BME680_OK) { 229 | ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); 230 | } 231 | } 232 | } 233 | 234 | if (!bme680_settings.process_data) { 235 | ESP_LOGV(TAG, "Data processing not required"); 236 | return; 237 | } 238 | 239 | struct bme680_field_data data; 240 | this->bme680_status_ = bme680_get_sensor_data(&data, &this->bme680_); 241 | 242 | if (this->bme680_status_ != BME680_OK) { 243 | ESP_LOGW(TAG, "Failed to get sensor data (BME680 Error Code %d)", this->bme680_status_); 244 | return; 245 | } 246 | if (!(data.status & BME680_NEW_DATA_MSK)) { 247 | ESP_LOGD(TAG, "BME680 did not report new data"); 248 | return; 249 | } 250 | 251 | bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance 252 | uint8_t num_inputs = 0; 253 | 254 | if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) { 255 | inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; 256 | inputs[num_inputs].signal = data.temperature / 100.0f; 257 | inputs[num_inputs].time_stamp = trigger_time_ns; 258 | num_inputs++; 259 | 260 | // Temperature offset from the real temperature due to external heat sources 261 | inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; 262 | inputs[num_inputs].signal = this->temperature_offset_; 263 | inputs[num_inputs].time_stamp = trigger_time_ns; 264 | num_inputs++; 265 | } 266 | if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) { 267 | inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; 268 | inputs[num_inputs].signal = data.humidity / 1000.0f; 269 | inputs[num_inputs].time_stamp = trigger_time_ns; 270 | num_inputs++; 271 | } 272 | if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) { 273 | inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; 274 | inputs[num_inputs].signal = data.pressure; 275 | inputs[num_inputs].time_stamp = trigger_time_ns; 276 | num_inputs++; 277 | } 278 | if (bme680_settings.process_data & BSEC_PROCESS_GAS) { 279 | if (data.status & BME680_GASM_VALID_MSK) { 280 | inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; 281 | inputs[num_inputs].signal = data.gas_resistance; 282 | inputs[num_inputs].time_stamp = trigger_time_ns; 283 | num_inputs++; 284 | } else { 285 | ESP_LOGD(TAG, "BME680 did not report gas data"); 286 | } 287 | } 288 | if (num_inputs < 1) { 289 | ESP_LOGD(TAG, "No signal inputs available for BSEC"); 290 | return; 291 | } 292 | 293 | bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; 294 | uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; 295 | this->bsec_status_ = bsec_do_steps(inputs, num_inputs, outputs, &num_outputs); 296 | if (this->bsec_status_ != BSEC_OK) { 297 | ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_); 298 | return; 299 | } 300 | if (num_outputs < 1) { 301 | ESP_LOGD(TAG, "No signal outputs provided by BSEC"); 302 | return; 303 | } 304 | 305 | this->publish_(outputs, num_outputs); 306 | } 307 | 308 | void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { 309 | ESP_LOGV(TAG, "Publishing sensor states"); 310 | for (uint8_t i = 0; i < num_outputs; i++) { 311 | switch (outputs[i].sensor_id) { 312 | case BSEC_OUTPUT_IAQ: 313 | case BSEC_OUTPUT_STATIC_IAQ: 314 | uint8_t accuracy; 315 | accuracy = outputs[i].accuracy; 316 | this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); 317 | this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); 318 | this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); 319 | 320 | // Queue up an opportunity to save state 321 | this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); 322 | break; 323 | case BSEC_OUTPUT_CO2_EQUIVALENT: 324 | this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); 325 | break; 326 | case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: 327 | this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); 328 | break; 329 | case BSEC_OUTPUT_RAW_PRESSURE: 330 | this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); 331 | break; 332 | case BSEC_OUTPUT_RAW_GAS: 333 | this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); 334 | break; 335 | case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: 336 | this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); 337 | break; 338 | case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: 339 | this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); 340 | break; 341 | } 342 | } 343 | } 344 | 345 | int64_t BME680BSECComponent::get_time_ns_() { 346 | int64_t time_ms = millis(); 347 | if (this->last_time_ms_ > time_ms) { 348 | this->millis_overflow_counter_++; 349 | } 350 | this->last_time_ms_ = time_ms; 351 | 352 | return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); 353 | } 354 | 355 | void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { 356 | if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { 357 | return; 358 | } 359 | sensor->publish_state(value); 360 | } 361 | 362 | void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { 363 | if (!sensor || (sensor->has_state() && sensor->state == value)) { 364 | return; 365 | } 366 | sensor->publish_state(value); 367 | } 368 | 369 | int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { 370 | return BME680BSECComponent::instance->read_bytes(a_register, data, len) ? 0 : -1; 371 | } 372 | 373 | int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { 374 | return BME680BSECComponent::instance->write_bytes(a_register, data, len) ? 0 : -1; 375 | } 376 | 377 | void BME680BSECComponent::delay_ms(uint32_t period) { 378 | ESP_LOGV(TAG, "Delaying for %ums", period); 379 | delay(period); 380 | } 381 | 382 | void BME680BSECComponent::load_state_() { 383 | uint32_t hash = fnv1_hash("bme680_bsec_state_" + to_string(this->address_)); 384 | this->bsec_state_ = global_preferences.make_preference(hash, true); 385 | 386 | uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; 387 | if (this->bsec_state_.load(&state)) { 388 | ESP_LOGV(TAG, "Loading state"); 389 | uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; 390 | this->bsec_status_ = bsec_set_state(state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); 391 | if (this->bsec_status_ != BSEC_OK) { 392 | ESP_LOGW(TAG, "Failed to load state (BSEC Error Code %d)", this->bsec_status_); 393 | } 394 | ESP_LOGI(TAG, "Loaded state"); 395 | } 396 | } 397 | 398 | void BME680BSECComponent::save_state_(uint8_t accuracy) { 399 | if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { 400 | return; 401 | } 402 | 403 | ESP_LOGV(TAG, "Saving state"); 404 | 405 | uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; 406 | uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; 407 | uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; 408 | 409 | this->bsec_status_ = 410 | bsec_get_state(0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); 411 | if (this->bsec_status_ != BSEC_OK) { 412 | ESP_LOGW(TAG, "Failed fetch state for save (BSEC Error Code %d)", this->bsec_status_); 413 | return; 414 | } 415 | 416 | if (!this->bsec_state_.save(&state)) { 417 | ESP_LOGW(TAG, "Failed to save state"); 418 | return; 419 | } 420 | this->last_state_save_ms_ = millis(); 421 | 422 | ESP_LOGI(TAG, "Saved state"); 423 | } 424 | #endif 425 | } // namespace bme680_bsec 426 | } // namespace esphome 427 | -------------------------------------------------------------------------------- /components/bme680_bsec/bme680_bsec.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/sensor/sensor.h" 5 | #include "esphome/components/text_sensor/text_sensor.h" 6 | #include "esphome/components/i2c/i2c.h" 7 | #include "esphome/core/preferences.h" 8 | #include 9 | 10 | #ifdef USE_BSEC 11 | #include 12 | #endif 13 | 14 | namespace esphome { 15 | namespace bme680_bsec { 16 | #ifdef USE_BSEC 17 | 18 | enum IAQMode { 19 | IAQ_MODE_STATIC = 0, 20 | IAQ_MODE_MOBILE = 1, 21 | }; 22 | 23 | enum SampleRate { 24 | SAMPLE_RATE_LP = 0, 25 | SAMPLE_RATE_ULP = 1, 26 | SAMPLE_RATE_DEFAULT = 2, 27 | }; 28 | 29 | #define BME680_BSEC_SAMPLE_RATE_LOG(r) (r == SAMPLE_RATE_DEFAULT ? "Default" : (r == SAMPLE_RATE_ULP ? "ULP" : "LP")) 30 | 31 | class BME680BSECComponent : public Component, public i2c::I2CDevice { 32 | public: 33 | void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } 34 | void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } 35 | void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } 36 | 37 | void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } 38 | void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; } 39 | void set_pressure_sample_rate(SampleRate sample_rate) { this->pressure_sample_rate_ = sample_rate; } 40 | void set_humidity_sample_rate(SampleRate sample_rate) { this->humidity_sample_rate_ = sample_rate; } 41 | 42 | void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; } 43 | void set_pressure_sensor(sensor::Sensor *sensor) { this->pressure_sensor_ = sensor; } 44 | void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } 45 | void set_gas_resistance_sensor(sensor::Sensor *sensor) { this->gas_resistance_sensor_ = sensor; } 46 | void set_iaq_sensor(sensor::Sensor *sensor) { this->iaq_sensor_ = sensor; } 47 | void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *sensor) { this->iaq_accuracy_text_sensor_ = sensor; } 48 | void set_iaq_accuracy_sensor(sensor::Sensor *sensor) { this->iaq_accuracy_sensor_ = sensor; } 49 | void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; } 50 | void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; } 51 | 52 | static BME680BSECComponent *instance; 53 | static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); 54 | static int8_t write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); 55 | static void delay_ms(uint32_t period); 56 | 57 | void setup() override; 58 | void dump_config() override; 59 | float get_setup_priority() const override; 60 | void loop() override; 61 | 62 | protected: 63 | void set_config_(const uint8_t *config); 64 | float calc_sensor_sample_rate_(SampleRate sample_rate); 65 | void update_subscription_(); 66 | 67 | void run_(); 68 | void read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings); 69 | void publish_(const bsec_output_t *outputs, uint8_t num_outputs); 70 | int64_t get_time_ns_(); 71 | 72 | void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); 73 | void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); 74 | 75 | void load_state_(); 76 | void save_state_(uint8_t accuracy); 77 | 78 | struct bme680_dev bme680_; 79 | bsec_library_return_t bsec_status_{BSEC_OK}; 80 | int8_t bme680_status_{BME680_OK}; 81 | 82 | int64_t last_time_ms_{0}; 83 | uint32_t millis_overflow_counter_{0}; 84 | int64_t next_call_ns_{0}; 85 | 86 | ESPPreferenceObject bsec_state_; 87 | uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day 88 | uint32_t last_state_save_ms_ = 0; 89 | 90 | float temperature_offset_{0}; 91 | IAQMode iaq_mode_{IAQ_MODE_STATIC}; 92 | 93 | SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate 94 | SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; 95 | SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; 96 | SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; 97 | 98 | sensor::Sensor *temperature_sensor_; 99 | sensor::Sensor *pressure_sensor_; 100 | sensor::Sensor *humidity_sensor_; 101 | sensor::Sensor *gas_resistance_sensor_; 102 | sensor::Sensor *iaq_sensor_; 103 | text_sensor::TextSensor *iaq_accuracy_text_sensor_; 104 | sensor::Sensor *iaq_accuracy_sensor_; 105 | sensor::Sensor *co2_equivalent_sensor_; 106 | sensor::Sensor *breath_voc_equivalent_sensor_; 107 | }; 108 | #endif 109 | } // namespace bme680_bsec 110 | } // namespace esphome 111 | -------------------------------------------------------------------------------- /components/bme680_bsec/sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import sensor 4 | from esphome.const import ( 5 | CONF_GAS_RESISTANCE, 6 | CONF_HUMIDITY, 7 | CONF_PRESSURE, 8 | CONF_TEMPERATURE, 9 | DEVICE_CLASS_EMPTY, 10 | DEVICE_CLASS_HUMIDITY, 11 | DEVICE_CLASS_PRESSURE, 12 | DEVICE_CLASS_TEMPERATURE, 13 | UNIT_CELSIUS, 14 | UNIT_EMPTY, 15 | UNIT_HECTOPASCAL, 16 | UNIT_OHM, 17 | UNIT_PARTS_PER_MILLION, 18 | UNIT_PERCENT, 19 | ICON_GAS_CYLINDER, 20 | ICON_GAUGE, 21 | ICON_THERMOMETER, 22 | ICON_WATER_PERCENT, 23 | ) 24 | from esphome.core import coroutine 25 | from . import ( 26 | BME680BSECComponent, 27 | CONF_BME680_BSEC_ID, 28 | CONF_SAMPLE_RATE, 29 | SAMPLE_RATE_OPTIONS, 30 | ) 31 | 32 | DEPENDENCIES = ["bme680_bsec"] 33 | 34 | CONF_IAQ = "iaq" 35 | CONF_IAQ_ACCURACY = "iaq_accuracy" 36 | CONF_CO2_EQUIVALENT = "co2_equivalent" 37 | CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" 38 | UNIT_IAQ = "IAQ" 39 | ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" 40 | ICON_TEST_TUBE = "mdi:test-tube" 41 | 42 | TYPES = [ 43 | CONF_TEMPERATURE, 44 | CONF_PRESSURE, 45 | CONF_HUMIDITY, 46 | CONF_GAS_RESISTANCE, 47 | CONF_IAQ, 48 | CONF_IAQ_ACCURACY, 49 | CONF_CO2_EQUIVALENT, 50 | CONF_BREATH_VOC_EQUIVALENT, 51 | ] 52 | 53 | CONFIG_SCHEMA = cv.Schema( 54 | { 55 | cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), 56 | cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( 57 | UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE 58 | ).extend( 59 | {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} 60 | ), 61 | cv.Optional(CONF_PRESSURE): sensor.sensor_schema( 62 | UNIT_HECTOPASCAL, ICON_GAUGE, 1, DEVICE_CLASS_PRESSURE 63 | ).extend( 64 | {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} 65 | ), 66 | cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( 67 | UNIT_PERCENT, ICON_WATER_PERCENT, 1, DEVICE_CLASS_HUMIDITY 68 | ).extend( 69 | {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} 70 | ), 71 | cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( 72 | UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY 73 | ), 74 | cv.Optional(CONF_IAQ): sensor.sensor_schema( 75 | UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY 76 | ), 77 | cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( 78 | UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY 79 | ), 80 | cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( 81 | UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY 82 | ), 83 | cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( 84 | UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY 85 | ), 86 | } 87 | ) 88 | 89 | 90 | @coroutine 91 | def setup_conf(config, key, hub): 92 | if key in config: 93 | conf = config[key] 94 | sens = yield sensor.new_sensor(conf) 95 | cg.add(getattr(hub, f"set_{key}_sensor")(sens)) 96 | if CONF_SAMPLE_RATE in conf: 97 | cg.add(getattr(hub, f"set_{key}_sample_rate")(conf[CONF_SAMPLE_RATE])) 98 | 99 | 100 | def to_code(config): 101 | hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) 102 | for key in TYPES: 103 | yield setup_conf(config, key, hub) 104 | -------------------------------------------------------------------------------- /components/bme680_bsec/text_sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import text_sensor 4 | from esphome.const import CONF_ID, CONF_ICON 5 | from esphome.core import coroutine 6 | from . import BME680BSECComponent, CONF_BME680_BSEC_ID 7 | 8 | DEPENDENCIES = ["bme680_bsec"] 9 | 10 | CONF_IAQ_ACCURACY = "iaq_accuracy" 11 | ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" 12 | 13 | TYPES = [CONF_IAQ_ACCURACY] 14 | 15 | CONFIG_SCHEMA = cv.Schema( 16 | { 17 | cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), 18 | cv.Optional(CONF_IAQ_ACCURACY): text_sensor.TEXT_SENSOR_SCHEMA.extend( 19 | { 20 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 21 | cv.Optional(CONF_ICON, default=ICON_ACCURACY): cv.icon, 22 | } 23 | ), 24 | } 25 | ) 26 | 27 | 28 | @coroutine 29 | def setup_conf(config, key, hub): 30 | if key in config: 31 | conf = config[key] 32 | sens = cg.new_Pvariable(conf[CONF_ID]) 33 | yield text_sensor.register_text_sensor(sens, conf) 34 | cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) 35 | 36 | 37 | def to_code(config): 38 | hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) 39 | for key in TYPES: 40 | yield setup_conf(config, key, hub) 41 | -------------------------------------------------------------------------------- /ha-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trvrnrth/esphome-bsec-bme680/13c1f936056695430f91f6effe3e44deba0cf21c/ha-screenshot.png --------------------------------------------------------------------------------