Merge branch 'dev' into ci-ban-std-bind

This commit is contained in:
J. Nick Koston
2026-03-19 08:50:53 -10:00
committed by GitHub
44 changed files with 1438 additions and 224 deletions
+1
View File
@@ -457,6 +457,7 @@ esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/spa06_base/* @danielkent-net
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
@@ -1,22 +1,29 @@
#include "esphome/core/log.h"
#include "absolute_humidity.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
static const char *const TAG = "absolute_humidity.sensor";
static const char *const TAG{"absolute_humidity.sensor"};
void AbsoluteHumidityComponent::setup() {
this->temperature_sensor_->add_on_state_callback([this](float state) {
this->temperature_ = state;
this->enable_loop();
});
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
// Get initial value
if (this->temperature_sensor_->has_state()) {
this->temperature_callback_(this->temperature_sensor_->get_state());
this->temperature_ = this->temperature_sensor_->get_state();
}
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->humidity_ = state;
this->enable_loop();
});
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
// Get initial value
if (this->humidity_sensor_->has_state()) {
this->humidity_callback_(this->humidity_sensor_->get_state());
this->humidity_ = this->humidity_sensor_->get_state();
}
}
@@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() {
}
void AbsoluteHumidityComponent::loop() {
if (!this->next_update_) {
return;
}
this->next_update_ = false;
// Only run once
this->disable_loop();
// Ensure we have source data
const bool no_temperature = std::isnan(this->temperature_);
const bool no_humidity = std::isnan(this->humidity_);
const bool no_temperature{std::isnan(this->temperature_)};
const bool no_humidity{std::isnan(this->humidity_)};
if (no_temperature || no_humidity) {
if (no_temperature) {
ESP_LOGW(TAG, "No valid state from temperature sensor!");
@@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() {
}
// Convert to desired units
const float temperature_c = this->temperature_;
const float temperature_k = temperature_c + 273.15;
const float hr = this->humidity_ / 100;
const float temperature_c{this->temperature_};
const float temperature_k{temperature_c + 273.15f};
const float hr{this->humidity_ / 100.0f};
// Calculate saturation vapor pressure
float es;
@@ -90,7 +95,7 @@ void AbsoluteHumidityComponent::loop() {
}
// Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k);
const float absolute_humidity{vapor_density(es, hr, temperature_k)};
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
@@ -103,16 +108,16 @@ void AbsoluteHumidityComponent::loop() {
// More accurate than Tetens in normal meteorologic conditions
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
float a, b, c, d;
if (temperature_c >= 0) {
a = 0.61121;
b = 18.678;
c = 234.5;
d = 257.14;
if (temperature_c >= 0.0f) {
a = 0.61121f;
b = 18.678f;
c = 234.5f;
d = 257.14f;
} else {
a = 0.61115;
b = 18.678;
c = 233.7;
d = 279.82;
a = 0.61115f;
b = 18.678f;
c = 233.7f;
d = 279.82f;
}
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
}
@@ -120,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) {
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
float a, b;
if (temperature_c >= 0) {
a = 17.27;
b = 237.3;
if (temperature_c >= 0.0f) {
a = 17.27f;
b = 237.3f;
} else {
a = 21.875;
b = 265.5;
a = 21.875f;
b = 265.5f;
}
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
return 0.61078f * expf((a * temperature_c) / (temperature_c + b));
}
// Wobus equation
@@ -146,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
//
// Baker, Schlatter 17-MAY-1982 Original version.
const float c0 = +0.99999683e00;
const float c1 = -0.90826951e-02;
const float c2 = +0.78736169e-04;
const float c3 = -0.61117958e-06;
const float c4 = +0.43884187e-08;
const float c5 = -0.29883885e-10;
const float c6 = +0.21874425e-12;
const float c7 = -0.17892321e-14;
const float c8 = +0.11112018e-16;
const float c9 = -0.30994571e-19;
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
return 0.61078 / pow(p, 8);
constexpr float c0{+0.99999683e+00f};
constexpr float c1{-0.90826951e-02f};
constexpr float c2{+0.78736169e-04f};
constexpr float c3{-0.61117958e-06f};
constexpr float c4{+0.43884187e-08f};
constexpr float c5{-0.29883885e-10f};
constexpr float c6{+0.21874425e-12f};
constexpr float c7{-0.17892321e-14f};
constexpr float c8{+0.11112018e-16f};
constexpr float c9{-0.30994571e-19f};
const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))};
return 0.61078f / powf(p, 8.0f);
}
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
@@ -168,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// hr = relative humidity [0-1]
// ta = absolute temperature (K)
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa)
const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹)
const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹)
return (ea * mw) / (r * ta);
}
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity
@@ -3,8 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
/// Enum listing all implemented saturation vapor pressure equations.
enum SaturationVaporPressureEquation {
@@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation {
/// This class implements calculation of absolute humidity from temperature and relative humidity.
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
public:
AbsoluteHumidityComponent() = default;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
@@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
void loop() override;
protected:
void temperature_callback_(float state) {
this->next_update_ = true;
this->temperature_ = state;
}
void humidity_callback_(float state) {
this->next_update_ = true;
this->humidity_ = state;
}
/** Buck equation for saturation vapor pressure in kPa.
*
* @param temperature_c Air temperature in °C.
@@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
* @param es Saturation vapor pressure in kPa.
* @param hr Relative humidity 0 to 1.
* @param ta Absolute temperature in K.
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
*/
static float vapor_density(float es, float hr, float ta);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
bool next_update_{false};
float temperature_{NAN};
float humidity_{NAN};
SaturationVaporPressureEquation equation_;
};
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity
+17 -10
View File
@@ -136,8 +136,9 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
[obj, callback](StringRef state) { (obj->*callback)(state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -148,10 +149,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback](const std::string &state) { (obj->*callback)(state); }));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
@@ -176,8 +179,10 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
[obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -188,10 +193,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); }));
}
#else
template<typename T>
@@ -96,8 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
void setup() override {
this->last_state_ = this->parent_->get_state_default(false);
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
this->parent_->add_on_state_callback(f);
this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); });
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -279,7 +279,8 @@ void BME68xBSEC2Component::run_() {
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); });
this->trigger_time_ns_ = curr_time_ns;
this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
} else {
ESP_LOGV(TAG, "Measurement not required");
this->read_(curr_time_ns);
@@ -116,6 +116,8 @@ class BME68xBSEC2Component : public Component {
int8_t bme68x_status_{BME68X_OK};
int64_t last_time_ms_{0};
int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit
// toolchains with small std::function SBO
uint32_t millis_overflow_counter_{0};
std::queue<std::function<void()>> queue_;
+1 -2
View File
@@ -127,8 +127,7 @@ void DPS310Component::read_() {
this->update_in_progress_ = false;
this->status_clear_warning();
} else {
auto f = std::bind(&DPS310Component::read_, this);
this->set_timeout("dps310", 10, f);
this->set_timeout("dps310", 10, [this]() { this->read_(); });
}
}
+240 -47
View File
@@ -28,6 +28,7 @@ from esphome.const import (
CONF_PLATFORMIO_OPTIONS,
CONF_REF,
CONF_SAFE_MODE,
CONF_SIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_VARIANT,
@@ -96,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_RELEASE = "release"
CONF_SUBTYPE = "subtype"
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
@@ -1258,6 +1260,43 @@ def _set_default_framework(config):
return config
RESERVED_PARTITION_NAMES = {
"nvs",
"app0",
"app1",
"otadata",
"eeprom",
"spiffs",
"phy_init",
}
VALID_APP_SUBTYPES = {"factory", "test"}
VALID_DATA_SUBTYPES = {
"nvs",
"nvs_keys",
"spiffs",
"coredump",
"efuse",
"fat",
"undefined",
"littlefs",
}
def _validate_custom_partition(config: ConfigType) -> ConfigType:
"""Voluptuous validator for custom partition schema."""
try:
_validate_partition(
config[CONF_NAME],
config[CONF_TYPE],
config[CONF_SUBTYPE],
config[CONF_SIZE],
)
except ValueError as e:
raise cv.Invalid(str(e)) from e
return config
FLASH_SIZES = [
"2MB",
"4MB",
@@ -1280,7 +1319,28 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True
),
cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_PARTITIONS): cv.Any(
cv.file_,
cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Required(CONF_TYPE): cv.All(
cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)),
cv.int_to_hex_string,
),
cv.Required(CONF_SUBTYPE): cv.All(
cv.Any(cv.string_strict, cv.int_range(0, 0xFE)),
cv.int_to_hex_string,
),
cv.Required(CONF_SIZE): cv.int_range(min=0x1000),
}
),
_validate_custom_partition,
),
),
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
}
@@ -1749,9 +1809,18 @@ async def to_code(config):
if use_platformio:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if isinstance(config[CONF_PARTITIONS], list):
for partition in config[CONF_PARTITIONS]:
add_partition(
partition[CONF_NAME],
partition[CONF_TYPE],
partition[CONF_SUBTYPE],
partition[CONF_SIZE],
)
else:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
for key, flag in ASSERTION_LEVELS.items():
@@ -1885,45 +1954,175 @@ async def to_code(config):
CORE.add_job(_write_arduino_libraries_sdkconfig)
APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB
"4MB": 0x1C0000, # 1792 KB
"8MB": 0x3C0000, # 3840 KB
"16MB": 0x7C0000, # 7936 KB
"32MB": 0xFC0000, # 16128 KB
KEY_CUSTOM_PARTITIONS = "custom_partitions"
@dataclass
class PartitionEntry:
name: str
type: str
subtype: str
size: int
# Partition sizes (offsets auto-placed by gen_esp32part.py).
# These constants are the single source of truth — used in both
# the CSV generation and the overhead calculation.
BOOTLOADER_SIZE = 0x8000
PARTITION_TABLE_SIZE = 0x1000
FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE
OTADATA_SIZE = 0x2000
PHY_INIT_SIZE = 0x1000
EEPROM_SIZE = 0x1000 # Arduino only
SPIFFS_SIZE = 0xF000 # Arduino only
ARDUINO_NVS_SIZE = 0x60000
IDF_NVS_SIZE = 0x70000
def _get_partition_overhead() -> int:
"""Total non-app partition budget (system partitions + nvs + padding).
Custom partitions are appended at the end and steal from app.
"""
# otadata + phy_init are followed by app0 which requires 64KB alignment,
# so pad up to the next 64KB boundary.
overhead = (
FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF
) & ~0xFFFF
if CORE.using_arduino:
overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE
else:
overhead += IDF_NVS_SIZE
return overhead
VALID_SUBTYPES: dict[str, set[str]] = {
"app": VALID_APP_SUBTYPES,
"data": VALID_DATA_SUBTYPES,
}
def get_arduino_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
eeprom_partition_size = 0x1000 # 4 KB
spiffs_partition_size = 0xF000 # 60 KB
app0_partition_start = 0x010000 # 64 KB
app1_partition_start = app0_partition_start + app_partition_size
eeprom_partition_start = app1_partition_start + app_partition_size
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
return f"""\
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
"""
def _validate_partition(
name: str, p_type: str | int, subtype: str | int, size: int
) -> None:
"""Validate partition parameters. Raises ValueError on invalid input."""
if name in RESERVED_PARTITION_NAMES:
raise ValueError(f"Partition name '{name}' is reserved.")
if size % 0x1000 != 0:
raise ValueError("Partition size must be 4KB (0x1000) aligned.")
# Numeric or already-normalized hex types/subtypes skip string validation
if not isinstance(p_type, str) or p_type.startswith("0x"):
return
if p_type not in VALID_SUBTYPES:
raise ValueError(
f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed."
" Use numbers for custom types."
)
if not isinstance(subtype, str) or subtype.startswith("0x"):
return
valid = VALID_SUBTYPES[p_type]
if subtype not in valid:
raise ValueError(
f"Subtype '{subtype}' is invalid for {p_type} type."
f" Only {', '.join(sorted(valid))} are allowed."
" Use numbers for custom subtypes."
)
def get_idf_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None:
"""Register a custom partition to be appended to the partition table.
return f"""\
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
app0, app, ota_0, , 0x{app_partition_size:X},
app1, app, ota_1, , 0x{app_partition_size:X},
nvs, data, nvs, , 0x6D000,
"""
Called from component to_code() to request additional flash partitions.
Size must be 4KB aligned. Integer types/subtypes are converted to hex strings.
"""
if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}):
raise ValueError(f"Partition name '{name}' is already defined.")
_validate_partition(name, p_type, subtype, size)
p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type
subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype
custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {})
custom_partitions[name] = PartitionEntry(
name=name, type=p_type_str, subtype=subtype_str, size=size
)
def _flash_size_to_bytes(flash_size_mb: str) -> int:
"""Convert flash size string (e.g. '4MB') to bytes."""
return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024
def _get_custom_partitions_total_size() -> int:
"""Total size of custom partitions including alignment padding."""
size = 0
for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values():
if partition.type == "app":
size = (size + 0xFFFF) & ~0xFFFF # align to 64KB
size += partition.size
return size
def _get_app_partition_size(flash_size_mb: str) -> int:
flash_bytes = _flash_size_to_bytes(flash_size_mb)
custom_total = _get_custom_partitions_total_size()
# Align down to 64KB — app partitions require 64KB-aligned offsets,
# so the size must also be aligned to avoid unbudgeted padding.
raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2
app_size = raw_size & ~0xFFFF
wasted = (raw_size - app_size) * 2
if wasted:
_LOGGER.info(
"Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.",
wasted // 1024,
)
if app_size <= 0x10000: # 64 KB
raise ValueError(
"Custom partitions are too large to fit in the available flash size. "
"Reduce custom partition sizes."
)
if app_size <= 0x80000: # 512 KB
_LOGGER.warning(
"App partition size is only %dKB. This may be too small for firmware with "
"many components. Consider reducing custom partition sizes or using a "
"larger flash chip.",
app_size // 1024,
)
return app_size
def get_partition_csv(flash_size_mb: str) -> str:
app_size = _get_app_partition_size(flash_size_mb)
partitions: list[PartitionEntry] = [
PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE),
PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE),
PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size),
PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size),
]
if CORE.using_arduino:
partitions.append(
PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE)
)
partitions.append(
PartitionEntry(
name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE
)
)
partitions.append(
PartitionEntry(
name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE
)
)
else:
partitions.append(
PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE)
)
partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values())
csv = "".join(
f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions
)
_LOGGER.debug("Partition table:\n%s", csv)
return csv
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
@@ -2030,16 +2229,10 @@ def copy_files():
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(flash_size),
)
else:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(flash_size),
)
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_partition_csv(flash_size),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,
# and no version.txt file exists, the CMake script fails for some setups.
@@ -350,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
this->set_timeout("wifi-connect-timeout", 30000, f);
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
this->incoming_data_.clear();
break;
}
+1 -1
View File
@@ -242,7 +242,7 @@ void HaierClimateBase::setup() {
this->last_request_timestamp_ = std::chrono::steady_clock::now();
this->set_phase(ProtocolPhases::SENDING_INIT_1);
this->haier_protocol_.set_default_timeout_handler(
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
[this](haier_protocol::FrameType type) { return this->timeout_default_handler_(type); });
this->set_handlers();
this->initialization();
}
+22 -16
View File
@@ -301,32 +301,38 @@ void HonClimate::set_handlers() {
// Set handlers
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_device_version_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_DEVICE_ID,
std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_device_id_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::CONTROL,
std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->status_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_management_information_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_ALARM_STATUS,
std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_alarm_status_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_message_handler(
haier_protocol::FrameType::ALARM_STATUS,
std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->report_network_status_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_message_handler(haier_protocol::FrameType::ALARM_STATUS,
[this](haier_protocol::FrameType type, const uint8_t *data, size_t size) {
return this->alarm_status_message_handler_(type, data, size);
});
}
void HonClimate::dump_config() {
+10 -7
View File
@@ -106,18 +106,21 @@ void Smartair2Climate::set_handlers() {
// Set handlers
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_device_version_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::CONTROL,
std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->status_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->report_network_status_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_default_timeout_handler(
std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1));
[this](haier_protocol::FrameType type) { return this->messages_timeout_handler_with_cycle_for_init_(type); });
}
void Smartair2Climate::dump_config() {
@@ -55,15 +55,15 @@ void HomeassistantNumber::step_retrieved_(StringRef step) {
}
void HomeassistantNumber::setup() {
api::global_api_server->subscribe_home_assistant_state(
this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1));
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr,
[this](StringRef state) { this->state_changed_(state); });
api::global_api_server->get_home_assistant_state(
this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(this->entity_id_, "min",
[this](StringRef min) { this->min_retrieved_(min); });
api::global_api_server->get_home_assistant_state(this->entity_id_, "max",
[this](StringRef max) { this->max_retrieved_(max); });
api::global_api_server->get_home_assistant_state(this->entity_id_, "step",
[this](StringRef step) { this->step_retrieved_(step); });
}
void HomeassistantNumber::dump_config() {
@@ -487,12 +487,10 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
body = this->body_.value(x...);
}
if (!this->json_.empty()) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1);
body = json::build_json(f);
body = json::build_json([this, x...](JsonObject root) { this->encode_json_(x..., root); });
}
if (this->json_func_ != nullptr) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
body = json::build_json(f);
body = json::build_json([this, x...](JsonObject root) { this->json_func_(x..., root); });
}
std::vector<Header> request_headers;
request_headers.reserve(this->request_headers_.size());
@@ -561,7 +559,6 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
root[item.first] = val.value(x...);
}
}
void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); }
HttpRequestComponent *parent_;
FixedVector<std::pair<const char *, TemplatableValue<const char *, Ts...>>> request_headers_{};
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
@@ -245,8 +245,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this);
this->set_timeout("wifi-connect-timeout", 30000, f);
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
return true;
}
case improv::GET_CURRENT_STATE:
+1 -2
View File
@@ -15,8 +15,7 @@ void MAX31855Sensor::update() {
this->disable();
// Conversion time typ: 170ms, max: 220ms
auto f = std::bind(&MAX31855Sensor::read_data_, this);
this->set_timeout("value", 220, f);
this->set_timeout("value", 220, [this]() { this->read_data_(); });
}
void MAX31855Sensor::setup() { this->spi_setup(); }
+2 -2
View File
@@ -41,8 +41,8 @@ void MAX31856Sensor::update() {
this->one_shot_temperature_();
// Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz
auto f = std::bind(&MAX31856Sensor::read_thermocouple_temperature_, this);
this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, f);
this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185,
[this]() { this->read_thermocouple_temperature_(); });
}
void MAX31856Sensor::read_thermocouple_temperature_() {
+1 -2
View File
@@ -60,8 +60,7 @@ void MAX31865Sensor::update() {
this->write_config_(0b11100000, 0b10100000);
// Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz
auto f = std::bind(&MAX31865Sensor::read_data_, this);
this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, f);
this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); });
}
void MAX31865Sensor::setup() {
+1 -2
View File
@@ -13,8 +13,7 @@ void MAX6675Sensor::update() {
this->disable();
// Conversion time typ: 170ms, max: 220ms
auto f = std::bind(&MAX6675Sensor::read_data_, this);
this->set_timeout("value", 250, f);
this->set_timeout("value", 250, [this]() { this->read_data_(); });
}
void MAX6675Sensor::setup() { this->spi_setup(); }
+1 -1
View File
@@ -59,7 +59,7 @@ template<typename T> class ApplianceBase : public Component {
public:
ApplianceBase() {
this->base_.setStream(&this->stream_);
this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this));
this->base_.addOnStateCallback([this]() { this->on_status_change(); });
dudanov::midea::ApplianceBase::setLogger(
[](int level, const char *tag, int line, const String &format, va_list args) {
esp_log_vprintf_(level, tag, line, format.c_str(), args);
+15 -10
View File
@@ -189,28 +189,33 @@ class CustomMQTTDevice {
template<typename T>
void CustomMQTTDevice::subscribe(const std::string &topic,
void (T::*callback)(const std::string &, const std::string &), uint8_t qos) {
auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2);
global_mqtt_client->subscribe(topic, f, qos);
auto *obj = static_cast<T *>(this);
global_mqtt_client->subscribe(
topic, [obj, callback](const std::string &t, const std::string &payload) { (obj->*callback)(t, payload); }, qos);
}
template<typename T>
void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &), uint8_t qos) {
auto f = std::bind(callback, (T *) this, std::placeholders::_2);
global_mqtt_client->subscribe(topic, f, qos);
auto *obj = static_cast<T *>(this);
global_mqtt_client->subscribe(
topic, [obj, callback](const std::string &, const std::string &payload) { (obj->*callback)(payload); }, qos);
}
template<typename T> void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(), uint8_t qos) {
auto f = std::bind(callback, (T *) this);
global_mqtt_client->subscribe(topic, f, qos);
auto *obj = static_cast<T *>(this);
global_mqtt_client->subscribe(
topic, [obj, callback](const std::string &, const std::string &) { (obj->*callback)(); }, qos);
}
template<typename T>
void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject),
uint8_t qos) {
auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2);
global_mqtt_client->subscribe_json(topic, f, qos);
auto *obj = static_cast<T *>(this);
global_mqtt_client->subscribe_json(
topic, [obj, callback](const std::string &t, JsonObject root) { (obj->*callback)(t, root); }, qos);
}
template<typename T>
void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos) {
auto f = std::bind(callback, (T *) this, std::placeholders::_2);
global_mqtt_client->subscribe_json(topic, f, qos);
auto *obj = static_cast<T *>(this);
global_mqtt_client->subscribe_json(
topic, [obj, callback](const std::string &, JsonObject root) { (obj->*callback)(root); }, qos);
}
} // namespace esphome::mqtt
+2 -3
View File
@@ -405,15 +405,14 @@ template<typename... Ts> class MQTTPublishJsonAction : public Action<Ts...> {
void set_payload(std::function<void(Ts..., JsonObject)> payload) { this->payload_ = payload; }
void play(const Ts &...x) override {
auto f = std::bind(&MQTTPublishJsonAction<Ts...>::encode_, this, x..., std::placeholders::_1);
auto topic = this->topic_.value(x...);
auto qos = this->qos_.value(x...);
auto retain = this->retain_.value(x...);
this->parent_->publish_json(topic, f, qos, retain);
this->parent_->publish_json(
topic, [this, x...](JsonObject root) { this->payload_(x..., root); }, qos, retain);
}
protected:
void encode_(Ts... x, JsonObject root) { this->payload_(x..., root); }
std::function<void(Ts..., JsonObject)> payload_;
MQTTClientComponent *parent_;
};
+2 -4
View File
@@ -45,8 +45,7 @@ void MS5611Component::update() {
return;
}
auto f = std::bind(&MS5611Component::read_temperature_, this);
this->set_timeout("temperature", 10, f);
this->set_timeout("temperature", 10, [this]() { this->read_temperature_(); });
}
void MS5611Component::read_temperature_() {
uint8_t bytes[3];
@@ -62,8 +61,7 @@ void MS5611Component::read_temperature_() {
return;
}
auto f = std::bind(&MS5611Component::read_pressure_, this, raw_temperature);
this->set_timeout("pressure", 10, f);
this->set_timeout("pressure", 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); });
}
void MS5611Component::read_pressure_(uint32_t raw_temperature) {
uint8_t bytes[3];
+3 -6
View File
@@ -281,9 +281,8 @@ void MS8607Component::request_read_temperature_() {
return;
}
auto f = std::bind(&MS8607Component::read_temperature_, this);
// datasheet says 17.2ms max conversion time at OSR 8192
this->set_timeout("temperature", 20, f);
this->set_timeout("temperature", 20, [this]() { this->read_temperature_(); });
}
void MS8607Component::read_temperature_() {
@@ -303,9 +302,8 @@ void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) {
return;
}
auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature);
// datasheet says 17.2ms max conversion time at OSR 8192
this->set_timeout("pressure", 20, f);
this->set_timeout("pressure", 20, [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); });
}
void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) {
@@ -325,9 +323,8 @@ void MS8607Component::request_read_humidity_(float temperature_float) {
return;
}
auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float);
// datasheet says 15.89ms max conversion time at OSR 8192
this->set_timeout("humidity", 20, f);
this->set_timeout("humidity", 20, [this, temperature_float]() { this->read_humidity_(temperature_float); });
}
void MS8607Component::read_humidity_(float temperature_float) {
+1 -2
View File
@@ -83,8 +83,7 @@ void NAU7802Sensor::setup() {
// turn on AFE
pu_ctrl |= PU_CTRL_POWERUP_ANALOG;
auto f = std::bind(&NAU7802Sensor::complete_setup_, this);
this->set_timeout(600, f);
this->set_timeout(600, [this]() { this->complete_setup_(); });
}
void NAU7802Sensor::complete_setup_() {
@@ -310,6 +310,50 @@ async def beo4_action(var, config, args):
cg.add(var.set_repeats(template_))
# Brennenstuhl
(
BrennenstuhlData,
BrennenstuhlBinarySensor,
BrennenstuhlTrigger,
BrennenstuhlAction,
BrennenstuhlDumper,
) = declare_protocol("Brennenstuhl")
BRENNENSTUHL_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): cv.hex_uint32_t,
}
)
@register_binary_sensor("brennenstuhl", BrennenstuhlBinarySensor, BRENNENSTUHL_SCHEMA)
def brennenstuhl_binary_sensor(var, config):
cg.add(
var.set_data(
cg.StructInitializer(
BrennenstuhlData,
("code", config[CONF_CODE]),
)
)
)
@register_trigger("brennenstuhl", BrennenstuhlTrigger, BrennenstuhlData)
def brennenstuhl_trigger(var, config):
pass
@register_dumper("brennenstuhl", BrennenstuhlDumper)
def brennenstuhl_dumper(var, config):
pass
@register_action("brennenstuhl", BrennenstuhlAction, BRENNENSTUHL_SCHEMA)
async def brennenstuhl_action(var, config, args):
template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32)
cg.add(var.set_code(template_))
# ByronSX
(
ByronSXData,
@@ -0,0 +1,144 @@
#include "brennenstuhl_protocol.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome::remote_base {
static const char *const TAG = "remote.brennenstuhl";
// receiver timing ranges [µs]
constexpr uint32_t START_PULSE_MIN = 200;
constexpr uint32_t START_PULSE_MAX = 500;
constexpr uint32_t START_SYMBOL_MIN = 2600;
constexpr uint32_t START_SYMBOL_MAX = 2700;
constexpr uint32_t DATA_SYMBOL_MIN = 1500;
constexpr uint32_t DATA_SYMBOL_MAX = 1600;
// transmitter timings [µs]
constexpr uint32_t PW_SHORT_US = 390;
constexpr uint32_t PW_LONG_US = 1160;
constexpr uint32_t PW_START_US = 2300;
// number of data bits
constexpr uint32_t N_BITS = 24;
// number of required symbols = 2 x (start + N_BITS) = 50
constexpr uint32_t N_SYMBOLS_REQ = 2u * (N_BITS + 1);
// number of bs codes within received frame
constexpr int32_t N_FRAME_CODES = 4;
// decoder finite-state-machine
enum class RxSt { START_PULSE, START_SYMBOL, PULSE, DATA_SYMBOL };
// The encode() member function reserves and fills a complete frame, to be send. The Brennenstuhl
// RC receivers demand a frame with a start-symbol followed by 4 repeated codes.
void BrennenstuhlProtocol::encode(RemoteTransmitData *dst, const BrennenstuhlData &data) {
uint32_t code = data.code;
dst->reserve((N_SYMBOLS_REQ * N_FRAME_CODES) + 1);
for (int32_t kc = 0; kc != N_FRAME_CODES; kc++) {
dst->item(PW_SHORT_US, PW_START_US);
for (int32_t ic = (N_BITS - 1); ic != -1; ic--) {
if ((code >> ic) & 1) {
dst->item(PW_LONG_US, PW_SHORT_US);
} else {
dst->item(PW_SHORT_US, PW_LONG_US);
}
}
}
}
// The decode() member function extracts Brennenstuhl codes from the received frame. Instead
// of validating the pulse width of the carriers and pauses individually, it is more accurate
// to validate the symbols (symbol=carrier+pause) The symbol pulsewidth is around 1550µs, but
// the pulse with of the carrier and the pauses vary greatly. Once the symbol pulsewidth is
// valid, a code bit becomes "1" if the carrier is longer then the pause and "0" else. A total
// frame consists of a start symbol and up to four codes. The decoder decodes all codes and
// returns the best code (the one with the most identical codes)
optional<BrennenstuhlData> BrennenstuhlProtocol::decode(RemoteReceiveData src) {
uint32_t n_received = static_cast<uint32_t>(src.size());
BrennenstuhlData data{
.code = 0,
};
// suppress noisy frames, at least a complete bs_code should be available
if (n_received > N_SYMBOLS_REQ) {
uint32_t bs_codes[4] = {0, 0, 0, 0}; // internal codes
int32_t bs_cnt = 0; // number of bs codes found within frame
int32_t bs_idx = -1; // index to best bs code
uint32_t bit_cnt = 0; // bit counter [0..23]
uint32_t pw_pre = 0; // pulsewidth of previous carrier (abs value)
RxSt fsm = RxSt::START_PULSE;
for (uint32_t ic = 0; (ic != n_received) && (bs_cnt != N_FRAME_CODES); ic++) {
uint32_t pw_cur = (uint32_t) (src[ic] < 0 ? -src[ic] : src[ic]); // current pulsewidth
uint32_t pw_sym = pw_cur + pw_pre; // symbol=pulse+pause
switch (fsm) {
case RxSt::START_PULSE: { // check if start pulse is valid
if ((src[ic] > 0) && (pw_cur >= START_PULSE_MIN) && (pw_cur <= START_PULSE_MAX)) {
bs_codes[bs_cnt] = 0;
bit_cnt = 0;
pw_pre = pw_cur;
fsm = RxSt::START_SYMBOL;
}
break;
}
case RxSt::START_SYMBOL: { // check if start symbol is valid
if ((src[ic] < 0) && (pw_sym >= START_SYMBOL_MIN) && (pw_sym <= START_SYMBOL_MAX)) {
fsm = RxSt::PULSE;
} else {
fsm = RxSt::START_PULSE;
}
break;
}
case RxSt::PULSE: { // just grab pulse, validation is done in DATA_SYMBOL state
if (src[ic] > 0) {
pw_pre = pw_cur;
fsm = RxSt::DATA_SYMBOL;
} else {
fsm = RxSt::START_PULSE;
}
break;
}
case RxSt::DATA_SYMBOL: { // check if data symbol is valid and append bit to data
if ((src[ic] < 0) && (pw_sym >= DATA_SYMBOL_MIN) && (pw_sym <= DATA_SYMBOL_MAX)) {
bs_codes[bs_cnt] <<= 1;
bs_codes[bs_cnt] += (pw_cur < pw_pre) ? 1 : 0;
if (++bit_cnt < N_BITS) {
fsm = RxSt::PULSE;
} else {
bs_cnt++; // complete code found
fsm = RxSt::START_PULSE; // start over for further codes in frame
}
} else {
fsm = RxSt::START_PULSE; // decoding failed, start over for further codes
}
break;
}
}
}
if (bs_cnt > 0) { // complete codes found, find best code in list now
int32_t identical_max = 0;
for (int32_t ic = 0; ic != bs_cnt; ic++) {
int32_t identical_cnt = 0;
for (int32_t jc = 0; jc != bs_cnt; jc++) {
identical_cnt += (bs_codes[ic] == bs_codes[jc]) ? 1 : 0;
}
if (identical_cnt > identical_max) {
identical_max = identical_cnt;
bs_idx = ic; // save index to best code
}
}
if (bs_idx > -1) {
data.code = bs_codes[bs_idx];
return data; // return best bs code of list
}
}
}
return {};
}
void BrennenstuhlProtocol::dump(const BrennenstuhlData &data) {
ESP_LOGI(TAG, "Brennenstuhl: code=0x%06" PRIx32, data.code);
}
} // namespace esphome::remote_base
@@ -0,0 +1,34 @@
#pragma once
#include "remote_base.h"
#include <cinttypes>
namespace esphome::remote_base {
struct BrennenstuhlData {
uint32_t code;
bool operator==(const BrennenstuhlData &rhs) const { return code == rhs.code; }
};
class BrennenstuhlProtocol : public RemoteProtocol<BrennenstuhlData> {
public:
void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override;
optional<BrennenstuhlData> decode(RemoteReceiveData src) override;
void dump(const BrennenstuhlData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Brennenstuhl)
template<typename... Ts> class BrennenstuhlAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint32_t, code)
void encode(RemoteTransmitData *dst, Ts... x) override {
BrennenstuhlData data{};
data.code = this->code_.value(x...);
BrennenstuhlProtocol().encode(dst, data);
}
};
} // namespace esphome::remote_base
+1 -1
View File
@@ -63,7 +63,7 @@ void SHT4XComponent::setup() {
}
ESP_LOGD(TAG, "Heater command: %x", this->heater_command_);
this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this));
this->set_interval(heater_interval, [this]() { this->start_heater_(); });
}
}
+201
View File
@@ -0,0 +1,201 @@
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_OVERSAMPLING,
CONF_PRESSURE,
CONF_SAMPLE_RATE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
)
CODEOWNERS = ["@danielkent-net"]
spa06_ns = cg.esphome_ns.namespace("spa06_base")
SampleRate = spa06_ns.enum("SampleRate")
SAMPLE_RATE_OPTIONS = {
"1": SampleRate.SAMPLE_RATE_1,
"2": SampleRate.SAMPLE_RATE_2,
"4": SampleRate.SAMPLE_RATE_4,
"8": SampleRate.SAMPLE_RATE_8,
"16": SampleRate.SAMPLE_RATE_16,
"32": SampleRate.SAMPLE_RATE_32,
"64": SampleRate.SAMPLE_RATE_64,
"128": SampleRate.SAMPLE_RATE_128,
"25p16": SampleRate.SAMPLE_RATE_25P16,
"25p8": SampleRate.SAMPLE_RATE_25P8,
"25p4": SampleRate.SAMPLE_RATE_25P4,
"25p2": SampleRate.SAMPLE_RATE_25P2,
"25": SampleRate.SAMPLE_RATE_25,
"50": SampleRate.SAMPLE_RATE_50,
"100": SampleRate.SAMPLE_RATE_100,
"200": SampleRate.SAMPLE_RATE_200,
}
Oversampling = spa06_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,
}
SPA06Component = spa06_ns.class_("SPA06Component", cg.PollingComponent)
def spa_oversample_time(oversample):
# Pressure oversampling conversion times are listed on datasheet Pg. 26
# Datasheet does not have a table for temperature oversampling;
# assumption is that it is the same as pressure
OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 3.6,
"2X": 5.2,
"4X": 8.4,
"8X": 14.8,
"16X": 27.6,
"32X": 53.2,
"64X": 104.4,
"128X": 206.8,
}
return OVERSAMPLING_CONVERSION_TIMES[oversample]
def spa_sample_rate(rate):
SAMPLE_RATE_OPTIONS_HZ = {
"1": 1.0,
"2": 2.0,
"4": 4.0,
"8": 8.0,
"16": 16.0,
"32": 32.0,
"64": 64.0,
"128": 128.0,
"25p16": 25.0 / 16.0,
"25p8": 25.0 / 8.0,
"25p4": 25.0 / 4.0,
"25p2": 25.0 / 2.0,
"25": 25.0,
"50": 50.0,
"100": 100.0,
"200": 200.0,
}
return SAMPLE_RATE_OPTIONS_HZ[rate]
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
# No conversion time necessary without a pressure sensor
pressure_conversion_time = 0.0
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = spa_oversample_time(
pressure_config.get(CONF_OVERSAMPLING)
)
# Temperature required in all cases, default to minimum sample time
temperature_conversion_time = 3.6
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = spa_oversample_time(
temperature_config.get(CONF_OVERSAMPLING)
)
# TODO: Read datasheet to find conversion time error
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
def measurement_timing_check(config):
temp_time = 0.0
if temperature_config := config.get(CONF_TEMPERATURE):
temp_oss = (
spa_oversample_time(temperature_config.get(CONF_OVERSAMPLING)) / 1000.0
)
temp_hz = spa_sample_rate(temperature_config.get(CONF_SAMPLE_RATE))
temp_time = temp_oss * temp_hz
pres_time = 0.0
if pressure_config := config.get(CONF_PRESSURE):
pres_oss = spa_oversample_time(pressure_config.get(CONF_OVERSAMPLING)) / 1000.0
pres_hz = spa_sample_rate(pressure_config.get(CONF_SAMPLE_RATE))
pres_time = pres_oss * pres_hz
if temp_time + pres_time >= 1:
raise cv.Invalid(
"Combined sample_rate and oversampling for temperature and pressure is too high"
)
return config
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SPA06Component),
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_SAMPLE_RATE, default="1"): cv.enum(
SAMPLE_RATE_OPTIONS, lower=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_SAMPLE_RATE, default="1"): cv.enum(
SAMPLE_RATE_OPTIONS, lower=True
),
}
),
},
).extend(cv.polling_component_schema("60s"))
CONFIG_SCHEMA_BASE.add_extra(measurement_timing_check)
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_sample_rate_config(temperature_config[CONF_SAMPLE_RATE])
)
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_sample_rate_config(pressure_config[CONF_SAMPLE_RATE]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
return var
@@ -0,0 +1,320 @@
#include "spa06_base.h"
#include "esphome/core/helpers.h"
namespace esphome::spa06_base {
static const char *const TAG = "spa06";
// Sign extension function for <=16 bit types
inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) {
return static_cast<int16_t>(encode_uint16(msb, lsb) << head) >> (16 - bits);
}
// Sign extension function for <=32 bit types
inline int32_t decode32(uint8_t xmsb, uint8_t msb, uint8_t lsb, uint8_t xlsb, size_t bits, size_t head = 0) {
return static_cast<int32_t>(encode_uint32(xmsb, msb, lsb, xlsb) << head) >> (32 - bits);
}
void SPA06Component::dump_config() {
ESP_LOGCONFIG(TAG, "SPA06:");
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
if (this->temperature_sensor_) {
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
ESP_LOGCONFIG(TAG,
" Oversampling: %s\n"
" Rate: %s",
LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)),
LOG_STR_ARG(meas_rate_to_str(this->temperature_rate_)));
}
if (this->pressure_sensor_) {
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
ESP_LOGCONFIG(TAG,
" Oversampling: %s\n"
" Rate: %s",
LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)),
LOG_STR_ARG(meas_rate_to_str(this->pressure_rate_)));
}
}
void SPA06Component::setup() {
// Startup sequence for SPA06 (Pg. 16, Figure 4.6.4):
// 1. Perform a soft reset
// 2. Verify sensor chip ID matches
// 3. Verify coefficients are ready
// 4. Read coefficients
// 5. Configure temperature and pressure sensors
// 6. Write communication settings
// 7. Write measurement settings (background measurement mode)
// 1. Soft reset
if (!this->soft_reset_()) {
this->mark_failed(LOG_STR("Reset failed"));
return;
}
// soft_reset_() internally delays by 3ms to make sure that
// the sensor is in a ready state and coefficients are ready.
// 2. Read chip ID
// TODO: check ID for consistency?
if (!spa_read_byte(SPA06_ID, &this->prod_id_.reg)) {
this->mark_failed(LOG_STR("Chip ID read failure"));
return;
}
ESP_LOGV(TAG,
"Product Info:\n"
" Prod ID: %u\n"
" Rev ID: %u",
this->prod_id_.bit.prod_id, this->prod_id_.bit.rev_id);
// 3. Read chip readiness from MEAS_CFG
// First check if the sensor reports ready
if (!spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) {
this->mark_failed(LOG_STR("Sensor status read failure"));
return;
}
// Check if the sensor reports coefficients are ready
if (!meas_.bit.coef_ready) {
this->mark_failed(LOG_STR("Coefficients not ready"));
return;
}
// 4. Read coefficients
if (!this->read_coefficients_()) {
this->mark_failed(LOG_STR("Coefficients read error"));
return;
}
// 5. Configure temperature and pressure sensors
// Default to measuring both temperature and pressure
// Temperature must be read regardless of configuration to compute pressure
// If temperature is not configured in config:
// - No oversampling is used
// - Lowest possible rate is configured
if (!this->temperature_sensor_) {
this->temperature_rate_ = SAMPLE_RATE_1;
this->temperature_oversampling_ = OVERSAMPLING_NONE;
this->kt_ = oversampling_to_scale_factor(OVERSAMPLING_NONE);
}
// If pressure is not configured in config
// - No oversampling is used
// - Lowest possible rate is configured
if (!this->pressure_sensor_) {
this->pressure_rate_ = SAMPLE_RATE_1;
this->pressure_oversampling_ = OVERSAMPLING_NONE;
this->kp_ = oversampling_to_scale_factor(OVERSAMPLING_NONE);
}
// Write temperature settings
if (!write_temperature_settings_(this->temperature_oversampling_, this->temperature_rate_)) {
this->mark_failed(LOG_STR("Temperature settings write fail"));
return;
}
// Write pressure settings
if (!write_pressure_settings_(this->pressure_oversampling_, this->pressure_rate_)) {
this->mark_failed(LOG_STR("Pressure settings write fail"));
return;
}
// 6. Write communication settings
// This call sets the bit shifts for pressure and temperature if
// their respective oversampling config is > X8
// This call also disables interrupts, FIFO, and specifies SPI 4-wire
if (!write_communication_settings_(this->pressure_oversampling_ > OVERSAMPLING_X8,
this->temperature_oversampling_ > OVERSAMPLING_X8)) {
this->mark_failed(LOG_STR("Comm settings write fail"));
return;
}
// 7. Write measurement settings
// This function sets background measurement mode without FIFO
if (!write_measurement_settings_(this->pressure_sensor_ ? MeasCrtl::MEASCRTL_BG_BOTH : MeasCrtl::MEASCRTL_BG_TEMP)) {
this->mark_failed(LOG_STR("Measurement settings write fail"));
return;
}
}
bool SPA06Component::write_temperature_settings_(Oversampling oversampling, SampleRate rate) {
return this->write_sensor_settings_(oversampling, rate, SPA06_TMP_CFG);
}
bool SPA06Component::write_pressure_settings_(Oversampling oversampling, SampleRate rate) {
return this->write_sensor_settings_(oversampling, rate, SPA06_PSR_CFG);
}
bool SPA06Component::write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg) {
if (reg != SPA06_PSR_CFG && reg != SPA06_TMP_CFG) {
return false;
}
this->pt_meas_cfg_.bit.rate = rate;
this->pt_meas_cfg_.bit.prc = oversampling;
ESP_LOGD(TAG, "Config write: %02x", this->pt_meas_cfg_.reg);
return spa_write_byte(reg, this->pt_meas_cfg_.reg);
}
bool SPA06Component::write_measurement_settings_(MeasCrtl crtl) {
this->meas_.bit.meas_crtl = crtl;
return spa_write_byte(SPA06_MEAS_CFG, this->meas_.reg);
}
bool SPA06Component::write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl,
bool interrupt_fifo, bool interrupt_tmp, bool interrupt_prs,
bool enable_fifo, bool spi_3wire) {
this->cfg_.bit.p_shift = pressure_shift;
this->cfg_.bit.t_shift = temperature_shift;
this->cfg_.bit.int_hl = interrupt_hl;
this->cfg_.bit.int_fifo = interrupt_fifo;
this->cfg_.bit.int_tmp = interrupt_tmp;
this->cfg_.bit.int_prs = interrupt_prs;
this->cfg_.bit.fifo_en = enable_fifo;
this->cfg_.bit.spi_3wire = spi_3wire;
return spa_write_byte(SPA06_CFG_REG, this->cfg_.reg);
}
bool SPA06Component::read_coefficients_() {
uint8_t coef[SPA06_COEF_LEN];
if (!spa_read_bytes(SPA06_COEF, coef, SPA06_COEF_LEN)) {
return false;
}
this->c0_ = decode16(coef[0], coef[1], 12);
this->c1_ = decode16(coef[1], coef[2], 12, 4);
this->c00_ = decode32(coef[3], coef[4], coef[5], 0, 20);
this->c10_ = decode32(coef[5], coef[6], coef[7], 0, 20, 4);
this->c01_ = decode16(coef[8], coef[9], 16);
this->c11_ = decode16(coef[10], coef[11], 16);
this->c20_ = decode16(coef[12], coef[13], 16);
this->c21_ = decode16(coef[14], coef[15], 16);
this->c30_ = decode16(coef[16], coef[17], 16);
this->c31_ = decode16(coef[18], coef[19], 12);
this->c40_ = decode16(coef[19], coef[20], 12, 4);
ESP_LOGV(TAG,
"Coefficients:\n"
" c0: %i, c1: %i,\n"
" c00: %i, c10: %i, c20: %i, c30: %i, c40: %i,\n"
" c01: %i, c11: %i, c21: %i, c31: %i",
this->c0_, this->c1_, this->c00_, this->c10_, this->c20_, this->c30_, this->c40_, this->c01_, this->c11_,
this->c21_, this->c31_);
return true;
}
bool SPA06Component::soft_reset_() {
// Setup steps for SPA06:
// 1. Perform a protocol reset (required to write command for SPI code, noop for I2C)
this->protocol_reset();
// 2. Perform the actual reset
this->reset_.bit.fifo_flush = true;
this->reset_.bit.soft_rst = SPA06_SOFT_RESET;
if (!this->spa_write_byte(SPA06_RESET, this->reset_.reg)) {
return false;
}
// 3. Wait for chip to become ready. Datasheet specifies 2 ms; wait 3
delay(3);
// 4. Perform another protocol reset (required for SPI code, noop for I2C)
this->protocol_reset();
return true;
}
// Temperature conversion formula. See datasheet pg. 14
float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5 + this->c1_ * t_raw_sc; }
// Pressure conversion formula. See datasheet pg. 14
float SPA06Component::convert_pressure_(const float &p_raw_sc, const float &t_raw_sc) {
float p2_raw_sc = p_raw_sc * p_raw_sc;
float p3_raw_sc = p2_raw_sc * p_raw_sc;
float p4_raw_sc = p3_raw_sc * p_raw_sc;
return this->c00_ + (float) this->c10_ * p_raw_sc + (float) this->c20_ * p2_raw_sc + (float) this->c30_ * p3_raw_sc +
(float) this->c40_ * p4_raw_sc +
t_raw_sc * ((float) this->c01_ + (float) this->c11_ * p_raw_sc + (float) this->c21_ * p2_raw_sc +
(float) this->c31_ * p3_raw_sc);
}
void SPA06Component::update() {
// Verify either a temperature or pressure sensor is defined before proceeding
if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) {
return;
}
// Queue a background task for retrieving the measurement
this->set_timeout("measurement", this->conversion_time_, [this]() {
float raw_temperature;
float temperature = 0.0;
float pressure = 0.0;
// Check measurement register for readiness
if (!this->spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) {
ESP_LOGW(TAG, "Cannot read meas config");
this->status_set_warning();
return;
}
if (this->pressure_sensor_) {
if (!this->meas_.bit.prs_ready || !this->meas_.bit.tmp_ready) {
ESP_LOGW(TAG, "Temperature and pressure not ready");
this->status_set_warning();
return;
}
if (!this->read_temperature_and_pressure_(temperature, pressure, raw_temperature)) {
ESP_LOGW(TAG, "Temperature and pressure read failure");
this->status_set_warning();
return;
}
} else {
if (!this->meas_.bit.tmp_ready) {
ESP_LOGW(TAG, "Temperature not ready");
this->status_set_warning();
return;
}
if (!this->read_temperature_(temperature, raw_temperature)) {
ESP_LOGW(TAG, "Temperature read fail");
this->status_set_warning();
return;
}
}
if (this->temperature_sensor_) {
this->temperature_sensor_->publish_state(temperature);
} else {
ESP_LOGV(TAG, "No temperature sensor configured");
}
if (this->pressure_sensor_) {
this->pressure_sensor_->publish_state(pressure);
} else {
ESP_LOGV(TAG, "No pressure sensor configured");
}
this->status_clear_warning();
});
}
bool SPA06Component::read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc) {
// Temperature read and decode
if (!this->read_temperature_(temperature, t_raw_sc)) {
return false;
}
// Read raw pressure from device
uint8_t buf[3];
if (!this->spa_read_bytes(SPA06_PSR, buf, 3)) {
return false;
}
// Calculate raw scaled pressure value
float p_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kp_;
// Calculate full pressure values
pressure = this->convert_pressure_(p_raw_sc, t_raw_sc);
return true;
}
bool SPA06Component::read_temperature_(float &temperature, float &t_raw_sc) {
uint8_t buf[3];
if (!this->spa_read_bytes(SPA06_TMP, buf, 3)) {
return false;
}
t_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kt_;
temperature = this->convert_temperature_(t_raw_sc);
return true;
}
} // namespace esphome::spa06_base
+257
View File
@@ -0,0 +1,257 @@
// SPA06 interface code for ESPHome
// All datasheet page references refer to Goermicro SPA06-003 datasheet version 2.0
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/progmem.h"
namespace esphome::spa06_base {
// Read sizes. All other registers are size 1
constexpr size_t SPA06_MEAS_LEN = 3;
constexpr size_t SPA06_COEF_LEN = 21;
// Soft reset command (0b1001, 0x9)
constexpr uint8_t SPA06_SOFT_RESET = 0x9;
// SPA06 Register Addresses
enum Register : uint8_t {
SPA06_PSR = 0x00, // Pressure Reading MSB (or all 3)
SPA06_PSR_B1 = 0x01, // Pressure Reading LSB
SPA06_PSR_B0 = 0x02, // Pressure Reading XLSB (LSB: Pressure flag in FIFO)
SPA06_TMP = 0x03, // Temperature Reading MSB (or all 3)
SPA06_TMP_B1 = 0x04, // Temperature Reading LSB
SPA06_TMP_B0 = 0x05, // Temperature Reading XLSB
SPA06_PSR_CFG = 0x06, // Pressure Configuration
SPA06_TMP_CFG = 0x07, // Temperature Configuration
SPA06_MEAS_CFG = 0x08, // Measurement Configuration (includes readiness)
SPA06_CFG_REG = 0x09, // Configuration Register
SPA06_INT_STS = 0x0A, // Interrupt Status
SPA06_FIFO_STS = 0x0B, // FIFO Status
SPA06_RESET = 0x0C, // Reset + FIFO Flush
SPA06_ID = 0x0D, // Product ID and revision
SPA06_COEF = 0x10, // Coefficients (0x10-0x24)
SPA06_INVALID_CMD = 0x25, // End of enum command
};
// Oversampling config.
enum Oversampling : uint8_t {
OVERSAMPLING_NONE = 0x0,
OVERSAMPLING_X2 = 0x1,
OVERSAMPLING_X4 = 0x2,
OVERSAMPLING_X8 = 0x3,
OVERSAMPLING_X16 = 0x4,
OVERSAMPLING_X32 = 0x5,
OVERSAMPLING_X64 = 0x6,
OVERSAMPLING_X128 = 0x7,
OVERSAMPLING_COUNT = 0x8,
};
// Measuring rate config
enum SampleRate : uint8_t {
SAMPLE_RATE_1 = 0x0,
SAMPLE_RATE_2 = 0x1,
SAMPLE_RATE_4 = 0x2,
SAMPLE_RATE_8 = 0x3,
SAMPLE_RATE_16 = 0x4,
SAMPLE_RATE_32 = 0x5,
SAMPLE_RATE_64 = 0x6,
SAMPLE_RATE_128 = 0x7,
SAMPLE_RATE_25P16 = 0x8,
SAMPLE_RATE_25P8 = 0x9,
SAMPLE_RATE_25P4 = 0xA,
SAMPLE_RATE_25P2 = 0xB,
SAMPLE_RATE_25 = 0xC,
SAMPLE_RATE_50 = 0xD,
SAMPLE_RATE_100 = 0xE,
SAMPLE_RATE_200 = 0xF,
};
// Measuring control config, set in MEAS_CFG register.
// See datasheet pages 28-29
enum MeasCrtl : uint8_t {
MEASCRTL_IDLE = 0x0,
MEASCRTL_PRES = 0x1,
MEASCRTL_TEMP = 0x2,
MEASCRTL_BG_PRES = 0x5,
MEASCRTL_BG_TEMP = 0x6,
MEASCRTL_BG_BOTH = 0x7,
};
// Oversampling scale factors. See datasheet page 15.
constexpr uint32_t OVERSAMPLING_K_LUT[8] = {524288, 1572864, 3670016, 7864320, 253952, 516096, 1040384, 2088960};
PROGMEM_STRING_TABLE(MeasRateStrings, "1Hz", "2Hz", "4Hz", "8Hz", "16Hz", "32Hz", "64Hz", "128Hz", "1.5625Hz",
"3.125Hz", "6.25Hz", "12.5Hz", "25Hz", "50Hz", "100Hz", "200Hz");
PROGMEM_STRING_TABLE(OversamplingStrings, "X1", "X2", "X4", "X8", "X16", "X32", "X64", "X128");
inline static const LogString *oversampling_to_str(const Oversampling oversampling) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
}
inline static const LogString *meas_rate_to_str(SampleRate rate) {
return MeasRateStrings::get_log_str(static_cast<uint8_t>(rate), MeasRateStrings::LAST_INDEX);
}
inline uint32_t oversampling_to_scale_factor(const Oversampling oversampling) {
return OVERSAMPLING_K_LUT[static_cast<uint8_t>(oversampling)];
};
class SPA06Component : public PollingComponent {
public:
//// Standard ESPHome component class functions
void setup() override;
void update() override;
void dump_config() override;
//// ESPHome-side settings
void set_conversion_time(uint16_t conversion_time) { this->conversion_time_ = conversion_time; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
void set_temperature_oversampling_config(Oversampling temperature_oversampling) {
this->temperature_oversampling_ = temperature_oversampling;
this->kt_ = oversampling_to_scale_factor(temperature_oversampling);
}
void set_pressure_oversampling_config(Oversampling pressure_oversampling) {
this->pressure_oversampling_ = pressure_oversampling;
this->kp_ = oversampling_to_scale_factor(pressure_oversampling);
}
void set_pressure_sample_rate_config(SampleRate rate) { this->pressure_rate_ = rate; }
void set_temperature_sample_rate_config(SampleRate rate) { this->temperature_rate_ = rate; }
protected:
// Virtual read functions. Implemented in SPI/I2C components
virtual bool spa_read_byte(uint8_t reg, uint8_t *data) = 0;
virtual bool spa_write_byte(uint8_t reg, uint8_t data) = 0;
virtual bool spa_read_bytes(uint8_t reg, uint8_t *data, size_t len) = 0;
virtual bool spa_write_bytes(uint8_t reg, uint8_t *data, size_t len) = 0;
//// Protocol-specific read functions
// Soft reset
bool soft_reset_();
// Protocol-specific reset (used for SPI only, implemented as noop for I2C)
virtual void protocol_reset() {}
// Read temperature and calculate Celsius and scaled raw temperatures
bool read_temperature_(float &temperature, float &t_raw_sc);
// No pressure only read! Pressure calculation depends on scaled temperature value
// Read temperature and calculate Celsius temperature, Pascal pressure, and scaled raw temperature
bool read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc);
// Read coefficients. Stores in class variables.
bool read_coefficients_();
//// Protocol-specific write functions
// Write temperature settings to TMP_CFG register
bool write_temperature_settings_(Oversampling oversampling, SampleRate rate);
// Write pressure settings to PRS_CFG register
bool write_pressure_settings_(Oversampling oversampling, SampleRate rate);
// Write measurement settings to MEAS_CRTL register
bool write_measurement_settings_(MeasCrtl crtl);
// Write communication settings to CFG_REG register
// Set pressure_shift to true if pressure oversampling >X8
// Set temperature_shift to true if temperature oversampling >X8
bool write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl = false,
bool interrupt_fifo = false, bool interrupt_tmp = false,
bool interrupt_prs = false, bool enable_fifo = false, bool spi_3wire = false);
//// Protocol helper functions
// Write function for both temperature and pressure (deduplicates code)
bool write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg);
// Convert raw temperature reading into Celsius
float convert_temperature_(const float &t_raw_sc);
// Convert raw pressure and scaled raw temperature into Pascals
float convert_pressure_(const float &p_raw_sc, const float &t_raw_sc);
//// Protocol-related variables
// Oversampling scale factors. Defaults are for X16 (pressure) and X1 (temp)
uint32_t kp_{253952}, kt_{524288};
// Coefficients for calculating pressure and temperature from raw values
// Obtained from IC during setup
int32_t c00_{0}, c10_{0};
int16_t c0_{0}, c1_{0}, c01_{0}, c11_{0}, c20_{0}, c21_{0}, c30_{0}, c31_{0}, c40_{0};
//// ESPHome class objects and configuration
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
Oversampling temperature_oversampling_{Oversampling::OVERSAMPLING_NONE};
Oversampling pressure_oversampling_{Oversampling::OVERSAMPLING_X16};
SampleRate temperature_rate_{SampleRate::SAMPLE_RATE_1};
SampleRate pressure_rate_{SampleRate::SAMPLE_RATE_1};
// Default conversion time: 27.6ms (16x pres) + 3.6ms (1x temp) ~ 32ms
uint16_t conversion_time_{32};
union {
struct {
Oversampling prc : 4;
SampleRate rate : 4;
} bit;
uint8_t reg;
} pt_meas_cfg_ = {.reg = 0}; // PRS_CFG and TMP_CFG
union {
struct {
uint8_t meas_crtl : 3;
bool tmp_ext : 1;
bool prs_ready : 1;
bool tmp_ready : 1;
bool sensor_ready : 1;
bool coef_ready : 1;
} bit;
uint8_t reg;
} meas_ = {.reg = 0}; // MEAS_REG
union {
struct {
uint8_t _reserved : 5;
bool int_prs : 1;
bool int_tmp : 1;
bool int_fifo_full : 1;
} bit;
uint8_t reg;
} int_status_ = {.reg = 0}; // INT_STS
union {
struct {
bool spi_3wire : 1;
bool fifo_en : 1;
bool p_shift : 1;
bool t_shift : 1;
bool int_prs : 1;
bool int_tmp : 1;
bool int_fifo : 1;
bool int_hl : 1;
} bit;
uint8_t reg;
} cfg_ = {.reg = 0}; // CFG_REG
union {
struct {
bool fifo_empty : 1;
bool fifo_full : 1;
uint8_t _reserved : 6;
} bit;
uint8_t reg;
} fifo_sts_ = {.reg = 0}; // FIFO_STS
union {
struct {
// Set to true to flush FIFO
bool fifo_flush : 1;
// Reserved bits
uint8_t _reserved : 3;
// Soft reset. Set to 1001 (0x9) to perform reset.
uint8_t soft_rst : 4;
} bit;
uint8_t reg = 0;
} reset_ = {.reg = 0}; // RESET
union {
struct {
uint8_t prod_id : 4;
uint8_t rev_id : 4;
} bit;
uint8_t reg = 0;
} prod_id_ = {.reg = 0}; // ID
}; // class SPA06Component
} // namespace esphome::spa06_base
+2 -2
View File
@@ -335,8 +335,8 @@ Sprinkler::Sprinkler(const char *name) : name_(name) {
// The `name` is stored for dump_config logging
this->timer_.init(2);
// Timer names only need to be unique within this component instance
this->timer_.push_back({"sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)});
this->timer_.push_back({"vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)});
this->timer_.push_back({"sm", false, 0, 0, [this]() { this->sm_timer_callback_(); }});
this->timer_.push_back({"vs", false, 0, 0, [this]() { this->valve_selection_callback_(); }});
}
void Sprinkler::setup() {
+8 -3
View File
@@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = {
def _load_tzdata(iana_key: str) -> bytes | None:
# From https://tzdata.readthedocs.io/en/latest/#examples
if not iana_key:
return None
try:
package_loc, resource = iana_key.rsplit("/", 1)
except ValueError:
return None
package = "tzdata.zoneinfo." + package_loc.replace("/", ".")
# Handle top-level timezone entries like "UTC", "GMT"
package = "tzdata.zoneinfo"
resource = iana_key
else:
package = "tzdata.zoneinfo." + package_loc.replace("/", ".")
try:
return (resources.files(package) / resource).read_bytes()
except (FileNotFoundError, ModuleNotFoundError):
except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError):
return None
@@ -629,8 +629,8 @@ void WiFiComponent::wifi_pre_setup_() {
return;
}
auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
WiFi.onEvent(f);
WiFi.onEvent(
[this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); });
// Make sure WiFi is in clean state before anything starts
this->wifi_mode_(false, false);
}
+3 -4
View File
@@ -2,7 +2,6 @@
#ifdef USE_WIREGUARD
#include <cinttypes>
#include <ctime>
#include <functional>
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -48,8 +47,8 @@ void Wireguard::setup() {
if (this->wg_initialized_ == ESP_OK) {
ESP_LOGI(TAG, "Initialized");
this->wg_peer_offline_time_ = millis();
this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this));
this->defer(std::bind(&Wireguard::start_connection_, this)); // defer to avoid blocking setup
this->srctime_->add_on_time_sync_callback([this]() { this->start_connection_(); });
this->defer([this]() { this->start_connection_(); }); // defer to avoid blocking setup
#ifdef USE_TEXT_SENSOR
if (this->address_sensor_ != nullptr) {
@@ -206,7 +205,7 @@ void Wireguard::enable() {
void Wireguard::disable() {
this->enabled_ = false;
this->defer(std::bind(&Wireguard::stop_connection_, this)); // defer to avoid blocking running loop
this->defer([this]() { this->stop_connection_(); }); // defer to avoid blocking running loop
ESP_LOGI(TAG, "Disabled");
this->publish_enabled_state();
}
@@ -58,15 +58,13 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
#ifdef USE_BINARY_SENSOR
if (res->has_motion.has_value() && this->motion_ != nullptr) {
this->motion_->publish_state(*res->has_motion);
this->set_timeout("motion_timeout", this->motion_timeout_,
[this, res]() { this->motion_->publish_state(false); });
this->set_timeout("motion_timeout", this->motion_timeout_, [this]() { this->motion_->publish_state(false); });
}
if (res->is_light.has_value() && this->light_ != nullptr)
this->light_->publish_state(*res->is_light);
if (res->button_press.has_value() && this->button_ != nullptr) {
this->button_->publish_state(*res->button_press);
this->set_timeout("button_timeout", this->button_timeout_,
[this, res]() { this->button_->publish_state(false); });
this->set_timeout("button_timeout", this->button_timeout_, [this]() { this->button_->publish_state(false); });
}
#endif
#ifdef USE_SENSOR
+7
View File
@@ -494,6 +494,13 @@ def hex_int(value):
return HexInt(int_(value))
def int_to_hex_string(value: int | str) -> str:
"""Convert an integer to a hex string (e.g. 64 -> '0x40'). Pass-through strings."""
if isinstance(value, int):
return f"0x{value:X}"
return value
def int_(value):
"""Validate that the config option is an integer.
+3 -3
View File
@@ -188,7 +188,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
// Issue #10264: This is a workaround for parallel script delays interfering with each other.
// Optimization: For no-argument delays (most common case), use direct lambda
// instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution)
// to avoid overhead from capturing arguments by value
if constexpr (sizeof...(Ts) == 0) {
App.scheduler.set_timer_common_(
this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr,
@@ -196,9 +196,9 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
[this]() { this->play_next_(); },
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
} else {
// For delays with arguments, use std::bind to preserve argument values
// For delays with arguments, capture by value to preserve argument values
// Arguments must be copied because original references may be invalid after delay
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
auto f = [this, x...]() { this->play_next_(x...); };
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
this->delay_.value(x...), std::move(f),
+1 -1
View File
@@ -12,7 +12,7 @@ platformio==6.1.19
esptool==5.2.0
click==8.3.1
esphome-dashboard==20260210.0
aioesphomeapi==44.6.0
aioesphomeapi==44.6.1
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
@@ -1,5 +1,10 @@
esp32:
variant: esp32s3
partitions:
- name: my_data
type: data
subtype: spiffs
size: 0x1000
framework:
type: esp-idf
advanced:
@@ -8,6 +8,11 @@ on_beo4:
- logger.log:
format: "on_beo4: %u %u"
args: ["x.source", "x.command"]
on_brennenstuhl:
then:
- logger.log:
format: "on_brennenstuhl: %u"
args: ["x.code"]
on_aeha:
then:
- logger.log:
@@ -14,6 +14,12 @@ button:
remote_transmitter.transmit_beo4:
source: 0x01
command: 0x0C
- platform: template
name: brennenstuhl
id: button_a_on
on_press:
remote_transmitter.transmit_brennenstuhl:
code: 0xBD2E2C
- platform: template
name: Dyson fan up
id: dyson_fan_up