mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-05-23 06:08:30 +02: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:
@@ -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
|
||||
Reference in New Issue
Block a user