mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
Merge branch 'dev' into ci-ban-std-bind
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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_(); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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_() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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_(); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user