Merge remote-tracking branch 'origin/dev' into integration

This commit is contained in:
J. Nick Koston
2026-04-21 15:14:45 +02:00
10 changed files with 141 additions and 30 deletions
+24 -18
View File
@@ -766,32 +766,38 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
void LD2412Component::set_basic_config() {
uint8_t min_gate = 1;
uint8_t max_gate = TOTAL_GATES;
uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
uint8_t out_pin_level = 0x01;
#ifdef USE_NUMBER
if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() ||
!this->timeout_number_->has_state()) {
return;
if (this->min_distance_gate_number_ != nullptr) {
if (!this->min_distance_gate_number_->has_state())
return;
min_gate = static_cast<int>(this->min_distance_gate_number_->state);
}
if (this->max_distance_gate_number_ != nullptr) {
if (!this->max_distance_gate_number_->has_state())
return;
max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1;
}
if (this->timeout_number_ != nullptr) {
if (!this->timeout_number_->has_state())
return;
timeout = static_cast<int>(this->timeout_number_->state);
}
#endif
#ifdef USE_SELECT
if (!this->out_pin_level_select_->has_state()) {
return;
if (this->out_pin_level_select_ != nullptr) {
if (!this->out_pin_level_select_->has_state())
return;
out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
}
#endif
uint8_t value[5] = {
#ifdef USE_NUMBER
lowbyte(static_cast<int>(this->min_distance_gate_number_->state)),
lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1),
lowbyte(static_cast<int>(this->timeout_number_->state)),
highbyte(static_cast<int>(this->timeout_number_->state)),
#else
1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
#endif
#ifdef USE_SELECT
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()),
#else
0x01, // Default value if not using select
#endif
lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
};
this->set_config_mode_(true);
this->send_command_(CMD_BASIC_CONF, value, sizeof(value));
+2
View File
@@ -35,9 +35,11 @@ LockStateForwarder = lock_ns.class_("LockStateForwarder")
LockState = lock_ns.enum("LockState")
LOCK_STATES = {
"OPEN": LockState.LOCK_STATE_OPEN,
"LOCKED": LockState.LOCK_STATE_LOCKED,
"UNLOCKED": LockState.LOCK_STATE_UNLOCKED,
"JAMMED": LockState.LOCK_STATE_JAMMED,
"OPENING": LockState.LOCK_STATE_OPENING,
"LOCKING": LockState.LOCK_STATE_LOCKING,
"UNLOCKING": LockState.LOCK_STATE_UNLOCKING,
}
+8 -3
View File
@@ -8,9 +8,10 @@ namespace esphome::lock {
static const char *const TAG = "lock";
// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING
// Lock state strings indexed by LockState enum.
// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING");
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING", "OPENING",
"OPEN");
const LogString *lock_state_to_string(LockState state) {
return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0);
@@ -74,12 +75,16 @@ LockCall &LockCall::set_state(optional<LockState> state) {
return *this;
}
LockCall &LockCall::set_state(const char *state) {
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPEN")) == 0) {
this->set_state(LOCK_STATE_OPEN);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
this->set_state(LOCK_STATE_LOCKED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) {
this->set_state(LOCK_STATE_UNLOCKED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) {
this->set_state(LOCK_STATE_JAMMED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPENING")) == 0) {
this->set_state(LOCK_STATE_OPENING);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) {
this->set_state(LOCK_STATE_LOCKING);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) {
+3 -1
View File
@@ -26,7 +26,9 @@ enum LockState : uint8_t {
LOCK_STATE_UNLOCKED = 2,
LOCK_STATE_JAMMED = 3,
LOCK_STATE_LOCKING = 4,
LOCK_STATE_UNLOCKING = 5
LOCK_STATE_UNLOCKING = 5,
LOCK_STATE_OPENING = 6,
LOCK_STATE_OPEN = 7,
};
const LogString *lock_state_to_string(LockState state);
+30 -6
View File
@@ -24,6 +24,8 @@ static const uint8_t QMC5883L_REGISTER_CONTROL_1 = 0x09;
static const uint8_t QMC5883L_REGISTER_CONTROL_2 = 0x0A;
static const uint8_t QMC5883L_REGISTER_PERIOD = 0x0B;
void IRAM_ATTR QMC5883LComponent::gpio_intr(QMC5883LComponent *arg) { arg->enable_loop_soon_any_context(); }
void QMC5883LComponent::setup() {
// Soft Reset
if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, 1 << 7)) {
@@ -35,6 +37,12 @@ void QMC5883LComponent::setup() {
if (this->drdy_pin_) {
this->drdy_pin_->setup();
if (this->drdy_pin_->is_internal()) {
static_cast<InternalGPIOPin *>(this->drdy_pin_)
->attach_interrupt(&QMC5883LComponent::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
this->drdy_use_isr_ = true;
this->stop_poller();
}
}
uint8_t control_1 = 0;
@@ -65,8 +73,8 @@ void QMC5883LComponent::setup() {
return;
}
if (this->get_update_interval() < App.get_loop_interval()) {
high_freq_.start();
if (!this->drdy_use_isr_ && this->get_update_interval() < App.get_loop_interval()) {
this->high_freq_.start();
}
}
@@ -84,16 +92,32 @@ void QMC5883LComponent::dump_config() {
LOG_SENSOR(" ", "Heading", this->heading_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_PIN(" DRDY Pin: ", this->drdy_pin_);
if (this->drdy_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " DRDY mode: %s",
this->drdy_use_isr_ ? LOG_STR_LITERAL("interrupt") : LOG_STR_LITERAL("polling"));
}
}
void QMC5883LComponent::update() {
i2c::ErrorCode err;
uint8_t status = false;
// If DRDY pin is configured and the data is not ready return.
// If DRDY is on an external expander we keep the polling path and early-return
// if data is not ready yet. Internal DRDY pins take the ISR path via loop().
if (this->drdy_pin_ && !this->drdy_pin_->digital_read()) {
return;
}
this->read_sensor_();
}
void QMC5883LComponent::loop() {
this->disable_loop();
if (!this->drdy_use_isr_ || !this->drdy_pin_->digital_read()) {
return;
}
this->read_sensor_();
}
void QMC5883LComponent::read_sensor_() {
i2c::ErrorCode err;
uint8_t status = false;
// Status byte gets cleared when data is read, so we have to read this first.
// If status and two axes are desired, it's possible to save one byte of traffic by enabling
+5
View File
@@ -32,6 +32,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; }
void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; }
@@ -44,6 +45,9 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
protected:
static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg);
void read_sensor_();
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
QMC5883LRange range_{QMC5883L_RANGE_200_UT};
QMC5883LOversampling oversampling_{QMC5883L_SAMPLING_512};
@@ -53,6 +57,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *heading_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
GPIOPin *drdy_pin_{nullptr};
bool drdy_use_isr_{false};
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
+11 -2
View File
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import text
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INITIAL_VALUE,
CONF_LAMBDA,
CONF_MAX_LENGTH,
@@ -12,6 +13,7 @@ from esphome.const import (
CONF_RESTORE_VALUE,
CONF_SET_ACTION,
)
from esphome.core import ID
from .. import template_ns
@@ -84,8 +86,15 @@ async def to_code(config):
if initial_value_config := config.get(CONF_INITIAL_VALUE):
cg.add(var.set_initial_value(initial_value_config))
if config[CONF_RESTORE_VALUE]:
args = cg.TemplateArguments(config[CONF_MAX_LENGTH])
saver = TextSaverTemplate.template(args).new()
saver_id = ID(
f"{config[CONF_ID].id}_value_saver",
is_declaration=True,
type=TextSaverBase,
)
saver_type = TextSaverTemplate.template(
cg.TemplateArguments(config[CONF_MAX_LENGTH])
)
saver = cg.Pvariable(saver_id, saver_type.new())
cg.add(var.set_value_saver(saver))
if CONF_SET_ACTION in config:
@@ -0,0 +1,14 @@
esphome:
name: test
host:
text:
- platform: template
name: "Test Text Restore"
id: test_text_restore
optimistic: true
max_length: 10
mode: text
initial_value: "hello"
restore_value: true
@@ -0,0 +1,44 @@
"""Tests for the template text component."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
def test_template_text_saver_uses_placement_new_with_templated_subclass(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Regression test for template text restore saver using placement new.
When ``restore_value: true``, the saver is its own Pvariable with
placement new: storage is sized for ``TextSaver<MAX_LENGTH>``, the
declared pointer stays at ``TemplateTextSaverBase *`` for polymorphism,
and the templated subclass constructor runs. A regression would either
reintroduce the heap ``new TextSaver<...>()`` expression or size the
storage for the base class and silently skip the subclass ctor.
"""
main_cpp = generate_main(component_config_path("template_text_restore.yaml"))
# Storage is sized and aligned for the templated subclass.
assert "sizeof(template_::TextSaver<10>)" in main_cpp
assert "alignas(template_::TextSaver<10>)" in main_cpp
# Pointer declared as base type for polymorphism.
assert (
"static template_::TemplateTextSaverBase *const test_text_restore_value_saver"
in main_cpp
)
# Placement new runs the templated subclass constructor.
assert "new(test_text_restore_value_saver) template_::TextSaver<10>()" in main_cpp
# Base-class default ctor must NOT be used.
assert (
"new(test_text_restore_value_saver) template_::TemplateTextSaverBase()"
not in main_cpp
)
# No heap `new TextSaver<...>()` left over — the pre-fix pattern.
assert "new template_::TextSaver<" not in main_cpp
# Saver is wired into the text component.
assert (
"test_text_restore->set_value_saver(test_text_restore_value_saver)" in main_cpp
)