Compare commits

...

16 Commits

Author SHA1 Message Date
J. Nick Koston
d59e323152 remove manual zero init 2026-01-23 21:52:10 -10:00
J. Nick Koston
b23f1b203f [tm1638] Use member array instead of heap allocation for display buffer 2026-01-23 21:31:32 -10:00
J. Nick Koston
30584e2e96 [sensirion_common] Use SmallBufferWithHeapFallback helper (#13496) 2026-01-23 22:53:44 -06:00
Jonathan Swoboda
468ae39a9e [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 23:13:03 -05:00
Big Mike
beb9c8d328 [sen5x] Fix missing this-> on class members and member functions (#13497) 2026-01-23 17:04:09 -10:00
Jonathan Swoboda
cdda3fb7cc [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 22:01:40 -05:00
Jas Strong
bba00a3906 [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-23 22:01:19 -05:00
dependabot[bot]
42e50ca178 Bump github/codeql-action from 4.31.10 to 4.31.11 (#13488)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 16:26:11 -10:00
Big Mike
165e362a1b [sensirion_common] Fix incorrect Big Endian conversion (#13492) 2026-01-23 16:19:41 -10:00
dependabot[bot]
e4763f8e71 Bump ruff from 0.14.13 to 0.14.14 (#13487)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-23 16:12:17 -10:00
Daniel Kent
9fddd0659e [bmp581] Split into bmp581_base and bmp581_i2c (#12485)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-01-23 19:28:14 -06:00
Keith Burzinski
faea546a0e [light] Fix cwww state restore (#13493) 2026-01-23 18:53:20 -06:00
Clyde Stubbs
069db2e128 [lvgl] Fix setting empty text (#13494) 2026-01-24 11:44:34 +11:00
Big Mike
5f2203b915 [sen5x] Fix store baseline functionality (#13469) 2026-01-23 18:03:23 -05:00
J. Nick Koston
5c67e04fef [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-23 12:37:06 -10:00
Clyde Stubbs
0cdcacc7fc [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-24 09:02:27 +11:00
30 changed files with 339 additions and 309 deletions

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
with:
category: "/language:${{matrix.language}}"

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.13
rev: v0.14.14
hooks:
# Run the linter.
- id: ruff

View File

@@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581/* @kahrendt
esphome/components/bmp581_base/* @danielkent-net @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi

View File

@@ -1,164 +1,5 @@
import math
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
CONFIG_SCHEMA = cv.invalid(
"The bmp581 sensor component has been renamed to bmp581_i2c."
)
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_(
"BMP581Component", cg.PollingComponent, i2c.I2CDevice
)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x46))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))

View File

@@ -0,0 +1,157 @@
import math
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
)
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_base")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
return var

View File

@@ -10,12 +10,11 @@
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
*/
#include "bmp581.h"
#include "bmp581_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace bmp581 {
namespace esphome::bmp581_base {
static const char *const TAG = "bmp581";
@@ -91,7 +90,6 @@ void BMP581Component::dump_config() {
break;
}
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
@@ -149,7 +147,7 @@ void BMP581Component::setup() {
uint8_t chip_id;
// read chip id from sensor
if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) {
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
ESP_LOGE(TAG, "Read chip ID failed");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -172,7 +170,7 @@ void BMP581Component::setup() {
// 3) Verify sensor status (check if NVM is okay) //
////////////////////////////////////////////////////
if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) {
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
ESP_LOGE(TAG, "Failed to read status register");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -359,7 +357,7 @@ bool BMP581Component::check_data_readiness_() {
uint8_t status;
if (!this->read_byte(BMP581_INT_STATUS, &status)) {
if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
}
@@ -400,7 +398,7 @@ bool BMP581Component::prime_iir_filter_() {
// flush the IIR filter with forced measurements (we will only flush once)
this->dsp_config_.bit.iir_flush_forced_en = true;
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -430,7 +428,7 @@ bool BMP581Component::prime_iir_filter_() {
// disable IIR filter flushings on future forced measurements
this->dsp_config_.bit.iir_flush_forced_en = false;
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -454,7 +452,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
}
uint8_t data[3];
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -483,7 +481,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
}
uint8_t data[6];
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -507,7 +505,7 @@ bool BMP581Component::reset_() {
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
// writes reset command to BMP's command register
if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) {
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
ESP_LOGE(TAG, "Failed to write reset command");
return false;
@@ -518,7 +516,7 @@ bool BMP581Component::reset_() {
delay(3);
// read interrupt status register
if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
@@ -562,7 +560,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p
// BMP581_DSP register and BMP581_DSP_IIR registers are successive
// - allows us to write the IIR configuration with one command to both registers
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data));
return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data));
}
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
@@ -572,7 +570,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
// write interrupt source register
return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
}
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
@@ -583,7 +581,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over
this->osr_config_.bit.osr_t = temperature_oversampling;
this->osr_config_.bit.osr_p = pressure_oversampling;
return this->write_byte(BMP581_OSR, this->osr_config_.reg);
return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg);
}
bool BMP581Component::write_power_mode_(OperationMode mode) {
@@ -593,8 +591,7 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
this->odr_config_.bit.pwr_mode = mode;
// write odr register
return this->write_byte(BMP581_ODR, this->odr_config_.reg);
return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg);
}
} // namespace bmp581
} // namespace esphome
} // namespace esphome::bmp581_base

View File

@@ -3,11 +3,9 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace bmp581 {
namespace esphome::bmp581_base {
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
@@ -59,7 +57,7 @@ enum IIRFilter {
IIR_FILTER_128 = 0x7
};
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
class BMP581Component : public PollingComponent {
public:
void dump_config() override;
@@ -84,6 +82,11 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
protected:
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
@@ -216,5 +219,4 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
} odr_config_ = {.reg = 0};
};
} // namespace bmp581
} // namespace esphome
} // namespace esphome::bmp581_base

View File

@@ -0,0 +1,12 @@
#include "bmp581_i2c.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
void BMP581I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BMP581Component::dump_config();
}
} // namespace esphome::bmp581_i2c

View File

@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
public:
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return read_bytes(a_register, data, len);
}
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return write_bytes(a_register, data, len);
}
void dump_config() override;
};
} // namespace esphome::bmp581_i2c

View File

@@ -0,0 +1,23 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
AUTO_LOAD = ["bmp581_base"]
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c")
BMP581I2CComponent = bmp581_ns.class_(
"BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x46)
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
async def to_code(config):
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)

View File

@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
}
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;

View File

@@ -391,7 +391,10 @@ void LightCall::transform_parameters_() {
min_mireds > 0.0f && max_mireds > 0.0f) {
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
this->parent_->get_name().c_str());
if (this->has_color_temperature()) {
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
// This is important for state restoration, where both color_temperature and cold_white/warm_white
// are restored from flash - we want to preserve the saved cold_white/warm_white values.
if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) {
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
const float range = max_mireds - min_mireds;
const float ww_fraction = (color_temp - min_mireds) / range;

View File

@@ -32,7 +32,7 @@ class LabelType(WidgetType):
async def to_code(self, w: Widget, config):
"""For a text object, create and set text"""
if value := config.get(CONF_TEXT):
if (value := config.get(CONF_TEXT)) is not None:
await w.set_property(CONF_TEXT, await lv_text.process(value))
await w.set_property(CONF_LONG_MODE, config)
await w.set_property(CONF_RECOLOR, config)

View File

@@ -55,6 +55,7 @@ st7701s = ST7701S(
pclk_frequency="16MHz",
pclk_inverted=True,
initsequence=(
(0x01,), # Software Reset
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0
(0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05),
(0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,),

View File

@@ -279,7 +279,7 @@ def modbus_calc_properties(config):
if isinstance(value, str):
value = value.encode()
config[CONF_ADDRESS] = binascii.crc_hqx(value, 0)
config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM
config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom")
config[CONF_FORCE_NEW_RANGE] = True
return byte_offset, reg_count

View File

@@ -133,14 +133,17 @@ void RD03DComponent::process_frame_() {
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
// Extract raw bytes for this target
// Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution,
// actual radar output has Resolution before Speed (verified empirically -
// stationary targets were showing non-zero speed with original field order)
uint8_t x_low = this->buffer_[offset + 0];
uint8_t x_high = this->buffer_[offset + 1];
uint8_t y_low = this->buffer_[offset + 2];
uint8_t y_high = this->buffer_[offset + 3];
uint8_t speed_low = this->buffer_[offset + 4];
uint8_t speed_high = this->buffer_[offset + 5];
uint8_t res_low = this->buffer_[offset + 6];
uint8_t res_high = this->buffer_[offset + 7];
uint8_t res_low = this->buffer_[offset + 4];
uint8_t res_high = this->buffer_[offset + 5];
uint8_t speed_low = this->buffer_[offset + 6];
uint8_t speed_high = this->buffer_[offset + 7];
// Decode values per RD-03D format
int16_t x = decode_value(x_low, x_high);

View File

@@ -124,8 +124,8 @@ void SEN5XComponent::setup() {
sen5x_type = SEN55;
}
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
if (this->humidity_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
this->humidity_sensor_ = nullptr; // mark as not used
@@ -159,37 +159,23 @@ void SEN5XComponent::setup() {
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
uint16_t states[4];
states[0] = this->voc_baselines_storage_.state0 >> 16;
states[1] = this->voc_baselines_storage_.state0 & 0xFFFF;
states[2] = this->voc_baselines_storage_.state1 >> 16;
states[3] = this->voc_baselines_storage_.state1 & 0xFFFF;
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (this->pref_.load(&this->voc_baseline_state_)) {
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
} else {
ESP_LOGV(TAG, "VOC Baseline State loaded");
delay(20);
}
}
}
bool result;
if (this->auto_cleaning_interval_.has_value()) {
// override default value
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
} else {
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
}
if (result) {
delay(20);
@@ -288,6 +274,14 @@ void SEN5XComponent::dump_config() {
ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
}
if (this->voc_sensor_) {
char hex_buf[5 * 4];
format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
ESP_LOGCONFIG(TAG,
" Store Baseline: %s\n"
" State: %s\n",
TRUEFALSE(this->store_baseline_), hex_buf);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
@@ -304,36 +298,6 @@ void SEN5XComponent::update() {
return;
}
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
// run it a bit later to avoid adding a delay here
this->set_timeout(550, [this]() {
uint16_t states[4];
if (this->read_data(states, 4)) {
uint32_t state0 = states[0] << 16 | states[1];
uint32_t state1 = states[2] << 16 | states[3];
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
MAXIMUM_STORAGE_DIFF ||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = state0;
this->voc_baselines_storage_.state1 = state1;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
});
}
}
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
this->status_set_warning();
ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
@@ -402,7 +366,29 @@ void SEN5XComponent::update() {
if (this->nox_sensor_ != nullptr) {
this->nox_sensor_->publish_state(nox);
}
this->status_clear_warning();
if (!this->voc_sensor_ || !this->store_baseline_ ||
(App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
this->status_clear_warning();
} else {
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
this->set_timeout(20, [this]() {
if (!this->read_data(this->voc_baseline_state_, 4)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
if (this->pref_.save(&this->voc_baseline_state_)) {
ESP_LOGD(TAG, "VOC Baseline State saved");
}
this->status_clear_warning();
}
});
}
}
});
}

View File

@@ -24,11 +24,6 @@ enum RhtAccelerationMode : uint16_t {
HIGH_ACCELERATION = 2,
};
struct Sen5xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
@@ -44,11 +39,9 @@ struct TemperatureCompensation {
uint16_t time_constant;
};
// Shortest time interval of 3H for storing baseline values.
// Shortest time interval of 2H (in milliseconds) for storing baseline values.
// Prevents wear of the flash because of too many write operations
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
static const uint32_t MAXIMUM_STORAGE_DIFF = 50;
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000;
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public:
@@ -58,18 +51,20 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; }
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; }
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; }
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; }
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { this->pm_1_0_sensor_ = pm_1_0; }
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { this->pm_2_5_sensor_ = pm_2_5; }
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { this->pm_4_0_sensor_ = pm_4_0; }
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { this->pm_10_0_sensor_ = pm_10_0; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { this->voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { this->nox_sensor_ = nox_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_store_baseline(bool store_baseline) { this->store_baseline_ = store_baseline; }
void set_acceleration_mode(RhtAccelerationMode mode) { this->acceleration_mode_ = mode; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) {
this->auto_cleaning_interval_ = auto_cleaning_interval;
}
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
@@ -80,7 +75,7 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
tuning_params.std_initial = std_initial;
tuning_params.gain_factor = gain_factor;
voc_tuning_params_ = tuning_params;
this->voc_tuning_params_ = tuning_params;
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
@@ -92,14 +87,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
tuning_params.std_initial = 50;
tuning_params.gain_factor = gain_factor;
nox_tuning_params_ = tuning_params;
this->nox_tuning_params_ = tuning_params;
}
void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) {
TemperatureCompensation temp_comp;
temp_comp.offset = offset * 200;
temp_comp.normalized_offset_slope = normalized_offset_slope * 10000;
temp_comp.time_constant = time_constant;
temperature_compensation_ = temp_comp;
this->temperature_compensation_ = temp_comp;
}
bool start_fan_cleaning();
@@ -107,7 +102,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
uint32_t seconds_since_last_store_;
uint16_t voc_baseline_state_[4]{0};
uint32_t voc_baseline_time_;
uint16_t firmware_version_;
ERRORCODE error_code_;
uint8_t serial_number_[4];
@@ -132,7 +128,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
optional<TemperatureCompensation> temperature_compensation_;
ESPPreferenceObject pref_;
std::string product_name_;
Sen5xBaselines voc_baselines_storage_;
};
} // namespace sen5x

View File

@@ -210,6 +210,7 @@ SENSOR_MAP = {
SETTING_MAP = {
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
CONF_ACCELERATION_MODE: "set_acceleration_mode",
CONF_STORE_BASELINE: "set_store_baseline",
}

View File

@@ -39,42 +39,23 @@ bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) {
*/
bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data,
const uint8_t data_len) {
uint8_t temp_stack[BUFFER_STACK_SIZE];
std::unique_ptr<uint8_t[]> temp_heap;
uint8_t *temp;
size_t required_buffer_len = data_len * 3 + 2;
// Is a dynamic allocation required ?
if (required_buffer_len >= BUFFER_STACK_SIZE) {
temp_heap = std::unique_ptr<uint8_t[]>(new uint8_t[required_buffer_len]);
temp = temp_heap.get();
} else {
temp = temp_stack;
}
SmallBufferWithHeapFallback<BUFFER_STACK_SIZE> buffer(required_buffer_len);
uint8_t *temp = buffer.get();
// First byte or word is the command
uint8_t raw_idx = 0;
if (command_len == 1) {
temp[raw_idx++] = command & 0xFF;
} else {
// command is 2 bytes
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = command >> 8;
temp[raw_idx++] = command & 0xFF;
#else
temp[raw_idx++] = command & 0xFF;
temp[raw_idx++] = command >> 8;
#endif
}
// add parameters followed by crc
// skipped if len == 0
for (size_t i = 0; i < data_len; i++) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = data[i] >> 8;
temp[raw_idx++] = data[i] & 0xFF;
#else
temp[raw_idx++] = data[i] & 0xFF;
temp[raw_idx++] = data[i] >> 8;
#endif
// Use MSB first since Sensirion devices use CRC-8 with MSB first
uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true);
temp[raw_idx++] = crc;

View File

@@ -1,6 +1,7 @@
#include "slow_pwm_output.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome {
namespace slow_pwm {
@@ -20,7 +21,9 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
}
if (new_state != current_state_) {
if (this->pin_) {
ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state));
char pin_summary[GPIO_SUMMARY_MAX_LEN];
this->pin_->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state));
} else {
ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state));
}

View File

@@ -35,9 +35,6 @@ void TM1638Component::setup() {
this->set_intensity(intensity_);
this->reset_(); // all LEDs off
for (uint8_t i = 0; i < 8; i++) // zero fill print buffer
this->buffer_[i] = 0;
}
void TM1638Component::dump_config() {

View File

@@ -70,7 +70,7 @@ class TM1638Component : public PollingComponent {
GPIOPin *clk_pin_;
GPIOPin *stb_pin_;
GPIOPin *dio_pin_;
uint8_t *buffer_ = new uint8_t[8];
uint8_t buffer_[8]{};
tm1638_writer_t writer_{};
std::vector<KeyListener *> listeners_{};
};

View File

@@ -1,6 +1,6 @@
pylint==4.0.4
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.13 # also change in .pre-commit-config.yaml when updating
ruff==0.14.14 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -1,5 +1,5 @@
sensor:
- platform: bmp581
- platform: bmp581_i2c
i2c_id: i2c_bus
temperature:
name: BMP581 Temperature

View File

@@ -197,6 +197,9 @@ lvgl:
- lvgl.label.update:
id: msgbox_label
text: Unloaded
- lvgl.label.update:
id: msgbox_label
text: "" # Empty text
on_all_events:
logger.log:
format: "Event %s"