mirror of
https://github.com/esphome/esphome.git
synced 2026-09-25 14:00:25 +00:00
Merge branch 'dev' into precompute-tag-forced-varint-fields
This commit is contained in:
@@ -23,9 +23,8 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) {
|
||||
|
||||
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
|
||||
bool new_state = arg->isr_pin_.digital_read();
|
||||
if (new_state != arg->last_state_) {
|
||||
if (new_state != arg->state_) {
|
||||
arg->state_ = new_state;
|
||||
arg->last_state_ = new_state;
|
||||
arg->changed_ = true;
|
||||
// Wake up the component from its disabled loop state
|
||||
if (arg->component_ != nullptr) {
|
||||
@@ -34,28 +33,27 @@ void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) {
|
||||
void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
|
||||
pin->setup();
|
||||
this->isr_pin_ = pin->to_isr();
|
||||
this->component_ = component;
|
||||
|
||||
// Read initial state
|
||||
this->last_state_ = pin->digital_read();
|
||||
this->state_ = this->last_state_;
|
||||
this->state_ = pin->digital_read();
|
||||
|
||||
// Attach interrupt - from this point on, any changes will be caught by the interrupt
|
||||
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type);
|
||||
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_);
|
||||
}
|
||||
|
||||
void GPIOBinarySensor::setup() {
|
||||
if (this->use_interrupt_ && !this->pin_->is_internal()) {
|
||||
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
|
||||
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
|
||||
this->use_interrupt_ = false;
|
||||
this->store_.use_interrupt_ = false;
|
||||
}
|
||||
|
||||
if (this->use_interrupt_) {
|
||||
if (this->store_.use_interrupt_) {
|
||||
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
|
||||
this->store_.setup(internal_pin, this->interrupt_type_, this);
|
||||
this->store_.setup(internal_pin, this);
|
||||
this->publish_initial_state(this->store_.get_state());
|
||||
} else {
|
||||
this->pin_->setup();
|
||||
@@ -66,14 +64,14 @@ void GPIOBinarySensor::setup() {
|
||||
void GPIOBinarySensor::dump_config() {
|
||||
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_)));
|
||||
if (this->use_interrupt_) {
|
||||
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_)));
|
||||
ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_)));
|
||||
if (this->store_.use_interrupt_) {
|
||||
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_)));
|
||||
}
|
||||
}
|
||||
|
||||
void GPIOBinarySensor::loop() {
|
||||
if (this->use_interrupt_) {
|
||||
if (this->store_.use_interrupt_) {
|
||||
if (this->store_.is_changed()) {
|
||||
// Clear the flag immediately to minimize the window where we might miss changes
|
||||
this->store_.clear_changed();
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
namespace esphome {
|
||||
namespace gpio {
|
||||
|
||||
// Store class for ISR data (no vtables, ISR-safe)
|
||||
// Store class for ISR data and configuration (no vtables, ISR-safe)
|
||||
class GPIOBinarySensorStore {
|
||||
public:
|
||||
void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component);
|
||||
void setup(InternalGPIOPin *pin, Component *component);
|
||||
|
||||
static void gpio_intr(GPIOBinarySensorStore *arg);
|
||||
|
||||
@@ -32,11 +32,13 @@ class GPIOBinarySensorStore {
|
||||
}
|
||||
|
||||
protected:
|
||||
friend class GPIOBinarySensor;
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
volatile bool state_{false};
|
||||
volatile bool last_state_{false};
|
||||
volatile bool changed_{false};
|
||||
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
|
||||
volatile bool state_{false};
|
||||
volatile bool changed_{false};
|
||||
bool use_interrupt_{true};
|
||||
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
|
||||
};
|
||||
|
||||
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
|
||||
@@ -44,9 +46,9 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
|
||||
// No destructor needed: ESPHome components are created at boot and live forever.
|
||||
// Interrupts are only detached on reboot when memory is cleared anyway.
|
||||
|
||||
void set_pin(GPIOPin *pin) { pin_ = pin; }
|
||||
void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; }
|
||||
void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; }
|
||||
void set_pin(GPIOPin *pin) { this->pin_ = pin; }
|
||||
void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; }
|
||||
void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; }
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
/// Setup pin
|
||||
@@ -59,8 +61,6 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
|
||||
|
||||
protected:
|
||||
GPIOPin *pin_;
|
||||
bool use_interrupt_{true};
|
||||
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
|
||||
GPIOBinarySensorStore store_;
|
||||
};
|
||||
|
||||
|
||||
@@ -748,7 +748,7 @@ void HonClimate::update_sub_sensor_(SubSensorType type, float value) {
|
||||
if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) {
|
||||
size_t index = (size_t) type;
|
||||
if ((this->sub_sensors_[index] != nullptr) &&
|
||||
((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value)))
|
||||
((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->get_raw_state() != value)))
|
||||
this->sub_sensors_[index]->publish_state(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,22 +322,6 @@ class LightState : public EntityBase, public Component {
|
||||
FixedVector<LightEffect *> effects_;
|
||||
/// Object used to store the persisted values of the light.
|
||||
ESPPreferenceObject rtc_;
|
||||
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
||||
uint32_t active_effect_index_{};
|
||||
/// Default transition length for all transitions in ms.
|
||||
uint32_t default_transition_length_{};
|
||||
/// Transition length to use for flash transitions.
|
||||
uint32_t flash_transition_length_{};
|
||||
/// Gamma correction factor for the light.
|
||||
float gamma_correct_{};
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
const uint16_t *gamma_table_{nullptr};
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
/// Whether the light value should be written in the next cycle.
|
||||
bool next_write_{true};
|
||||
// for effects, true if a transformer (transition) is active.
|
||||
bool is_transformer_active_ = false;
|
||||
|
||||
/** Listeners for remote values changes.
|
||||
*
|
||||
@@ -361,6 +345,22 @@ class LightState : public EntityBase, public Component {
|
||||
/// Initial state of the light.
|
||||
optional<LightStateRTCState> initial_state_{};
|
||||
|
||||
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
||||
uint32_t active_effect_index_{};
|
||||
/// Default transition length for all transitions in ms.
|
||||
uint32_t default_transition_length_{};
|
||||
/// Transition length to use for flash transitions.
|
||||
uint32_t flash_transition_length_{};
|
||||
/// Gamma correction factor for the light.
|
||||
float gamma_correct_{};
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
const uint16_t *gamma_table_{nullptr};
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
/// Whether the light value should be written in the next cycle.
|
||||
bool next_write_{true};
|
||||
// for effects, true if a transformer (transition) is active.
|
||||
bool is_transformer_active_{false};
|
||||
/// Restore mode of the light.
|
||||
LightRestoreMode restore_mode_;
|
||||
};
|
||||
|
||||
@@ -85,10 +85,6 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
|
||||
}
|
||||
|
||||
this->publish_state(published_state);
|
||||
} else {
|
||||
this->raw_state = state;
|
||||
this->state = state;
|
||||
this->set_has_state(true);
|
||||
}
|
||||
}
|
||||
this->update_component_settings();
|
||||
|
||||
@@ -300,9 +300,14 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict:
|
||||
context_vars = package_config.vars
|
||||
if CONF_PACKAGES in package_config or CONF_URL in package_config:
|
||||
# Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation.
|
||||
from esphome.components.substitutions import substitute_context_vars
|
||||
from esphome.components.substitutions import ContextVars, substitute
|
||||
|
||||
substitute_context_vars(package_config, context_vars)
|
||||
package_config = substitute(
|
||||
package_config,
|
||||
[],
|
||||
ContextVars(context_vars),
|
||||
strict_undefined=False,
|
||||
)
|
||||
package_config = PACKAGE_SCHEMA(package_config)
|
||||
if isinstance(package_config, str):
|
||||
return package_config # Jinja string, skip processing
|
||||
|
||||
@@ -40,7 +40,10 @@ const LogString *state_class_to_string(StateClass state_class) {
|
||||
return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
Sensor::Sensor() : state(NAN), raw_state(NAN) {}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
int8_t Sensor::get_accuracy_decimals() {
|
||||
if (this->sensor_flags_.has_accuracy_override)
|
||||
@@ -63,8 +66,13 @@ StateClass Sensor::get_state_class() {
|
||||
}
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->raw_state = state;
|
||||
#pragma GCC diagnostic pop
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
this->raw_callback_.call(state);
|
||||
#endif
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
|
||||
|
||||
@@ -110,8 +118,6 @@ void Sensor::clear_filters() {
|
||||
this->filter_list_ = nullptr;
|
||||
}
|
||||
#endif // USE_SENSOR_FILTER
|
||||
float Sensor::get_state() const { return this->state; }
|
||||
float Sensor::get_raw_state() const { return this->raw_state; }
|
||||
|
||||
void Sensor::internal_send_state_to_frontend(float state) {
|
||||
this->set_has_state(true);
|
||||
|
||||
@@ -95,9 +95,14 @@ class Sensor : public EntityBase {
|
||||
#endif
|
||||
|
||||
/// Getter-syntax for .state.
|
||||
float get_state() const;
|
||||
float get_state() const { return this->state; }
|
||||
/// Getter-syntax for .raw_state
|
||||
float get_raw_state() const;
|
||||
float get_raw_state() const {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return this->raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
/** Publish a new state to the front-end.
|
||||
*
|
||||
@@ -113,8 +118,14 @@ class Sensor : public EntityBase {
|
||||
/// Add a callback that will be called every time a filtered value arrives.
|
||||
template<typename F> void add_on_state_callback(F &&callback) { this->callback_.add(std::forward<F>(callback)); }
|
||||
/// Add a callback that will be called every time the sensor sends a raw value.
|
||||
/// When USE_SENSOR_FILTER is not enabled, delegates to the regular callback
|
||||
/// since raw state equals filtered state without filter support compiled in.
|
||||
template<typename F> void add_on_raw_state_callback(F &&callback) {
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
this->raw_callback_.add(std::forward<F>(callback));
|
||||
#else
|
||||
this->callback_.add(std::forward<F>(callback));
|
||||
#endif
|
||||
}
|
||||
|
||||
/** This member variable stores the last state that has passed through all filters.
|
||||
@@ -126,17 +137,20 @@ class Sensor : public EntityBase {
|
||||
*/
|
||||
float state;
|
||||
|
||||
/** This member variable stores the current raw state of the sensor, without any filters applied.
|
||||
*
|
||||
* Unlike .state,this will be updated immediately when publish_state is called.
|
||||
*/
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
/// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0.
|
||||
ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0")
|
||||
float raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void internal_send_state_to_frontend(float state);
|
||||
|
||||
protected:
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
#endif
|
||||
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
Filter *filter_list_{nullptr}; ///< Store all active filters.
|
||||
|
||||
@@ -81,12 +81,6 @@ def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBa
|
||||
return value
|
||||
|
||||
|
||||
def _try_substitute(value: Any, context: ContextVars) -> Any:
|
||||
"""Substitute variables in value, returning the result or the original if unchanged."""
|
||||
result = _substitute_item(value, [], context, strict_undefined=True)
|
||||
return result if result is not None else value
|
||||
|
||||
|
||||
def _resolve_var(name: str, context_vars: ContextVars) -> Any:
|
||||
"""Look up a substitution variable, falling back to the resolver callback."""
|
||||
sub = context_vars.get(name, Missing)
|
||||
@@ -253,7 +247,7 @@ def _push_context(
|
||||
if value is Missing:
|
||||
return Missing
|
||||
try:
|
||||
value = _try_substitute(value, resolver_context)
|
||||
value = substitute(value, [], resolver_context, True)
|
||||
except UndefinedError as err:
|
||||
unresolvables[key] = (value, err)
|
||||
return Missing
|
||||
@@ -297,68 +291,51 @@ def push_context(
|
||||
return parent_context
|
||||
|
||||
|
||||
def _substitute_item(
|
||||
def substitute(
|
||||
item: Any,
|
||||
path: SubstitutionPath,
|
||||
parent_context: ContextVars,
|
||||
strict_undefined: bool,
|
||||
errors: ErrList | None = None,
|
||||
) -> Any | None:
|
||||
"""Recursively substitute variables in a config item.
|
||||
) -> Any:
|
||||
"""Returns a recursively substituted version of `item`."""
|
||||
|
||||
Walks dicts, lists, strings, Lambdas, Extend, and Remove nodes,
|
||||
replacing variable references with values from context_vars.
|
||||
Mutates containers in-place; returns a replacement value for
|
||||
strings/scalars, or None if the item was unchanged.
|
||||
"""
|
||||
if isinstance(item, ESPLiteralValue):
|
||||
return item # do not substitute inside literal blocks
|
||||
|
||||
def _walk(item: Any, path: SubstitutionPath, parent_ctx: ContextVars) -> Any | None:
|
||||
if isinstance(item, ESPLiteralValue):
|
||||
return None # do not substitute inside literal blocks
|
||||
# Push the current item's context onto the context stack
|
||||
context_vars = push_context(item, parent_context, errors)
|
||||
|
||||
ctx = push_context(item, parent_ctx, errors)
|
||||
result = item
|
||||
|
||||
if isinstance(item, list):
|
||||
for idx, it in enumerate(item):
|
||||
sub = _walk(it, path + [idx], ctx)
|
||||
if sub is not None:
|
||||
item[idx] = sub
|
||||
elif isinstance(item, dict):
|
||||
replace_keys: list[tuple[str, Any]] = []
|
||||
for k, v in item.items():
|
||||
if path or k != CONF_SUBSTITUTIONS:
|
||||
sub = _walk(k, path + [k], ctx)
|
||||
if sub is not None:
|
||||
replace_keys.append((k, sub))
|
||||
sub = _walk(v, path + [k], ctx)
|
||||
if sub is not None:
|
||||
item[k] = sub
|
||||
for old, new in replace_keys:
|
||||
if str(new) == str(old):
|
||||
item[new] = item[old]
|
||||
else:
|
||||
item[new] = merge_config(item.get(new), item.get(old))
|
||||
del item[old]
|
||||
elif isinstance(item, str):
|
||||
sub = _expand_substitutions(item, path, ctx, strict_undefined, errors)
|
||||
if not isinstance(sub, str) or sub != item:
|
||||
return sub
|
||||
elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value:
|
||||
sub = _expand_substitutions(item.value, path, ctx, strict_undefined, errors)
|
||||
if sub != item.value:
|
||||
item.value = sub
|
||||
return None
|
||||
if isinstance(item, list):
|
||||
result = [
|
||||
substitute(it, path + [i], context_vars, strict_undefined, errors)
|
||||
for i, it in enumerate(item)
|
||||
]
|
||||
|
||||
return _walk(item, path, parent_context)
|
||||
elif isinstance(item, dict):
|
||||
result = OrderedDict()
|
||||
for k, v in item.items():
|
||||
v = substitute(v, path + [k], context_vars, strict_undefined, errors)
|
||||
k = substitute(k, path + [k], context_vars, strict_undefined, errors)
|
||||
result[k] = merge_config(result.get(k), v)
|
||||
|
||||
elif isinstance(item, str):
|
||||
result = _expand_substitutions(
|
||||
item, path, context_vars, strict_undefined, errors
|
||||
)
|
||||
|
||||
def substitute_context_vars(node: Any, context_vars: dict[str, Any]) -> None:
|
||||
"""Eagerly substitute context vars into a config node in-place.
|
||||
elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value:
|
||||
value = _expand_substitutions(
|
||||
item.value, path, context_vars, strict_undefined, errors
|
||||
)
|
||||
if item.value != value:
|
||||
result = type(item)(value)
|
||||
|
||||
Undefined variables are silently ignored — this is used before
|
||||
the main substitution pass when not all variables are visible yet.
|
||||
"""
|
||||
_substitute_item(node, [], ContextVars(context_vars), strict_undefined=False)
|
||||
if isinstance(item, ESPHomeDataBase):
|
||||
result = make_data_base(result, item)
|
||||
return result
|
||||
|
||||
|
||||
def _warn_unresolved_variables(errors: ErrList) -> None:
|
||||
@@ -387,7 +364,7 @@ def do_substitution_pass(
|
||||
Extracts the ``substitutions:`` block, merges in any command-line
|
||||
overrides, resolves inter-variable dependencies, then walks the
|
||||
config tree replacing all ``$var`` / ``${expr}`` references.
|
||||
Returns the (mutated) config dict with resolved substitutions
|
||||
Returns a new config dict with resolved substitutions
|
||||
restored at the front.
|
||||
"""
|
||||
# Extract substitutions from config, overriding with substitutions coming from command line:
|
||||
@@ -415,7 +392,7 @@ def do_substitution_pass(
|
||||
errors: ErrList = [] # Collect undefined errors during substitution
|
||||
parent_context, substitutions = _push_context(substitutions, ContextVars(), errors)
|
||||
|
||||
_substitute_item(config, [], parent_context, False, errors)
|
||||
config = substitute(config, [], parent_context, False, errors)
|
||||
|
||||
if errors:
|
||||
_warn_unresolved_variables(errors)
|
||||
|
||||
@@ -664,11 +664,22 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
config.show_hidden = 1;
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
|
||||
config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
|
||||
// Use shorter dwell times for roaming scans - we only need to detect strong
|
||||
// nearby APs, not do a thorough survey. This also reduces off-channel time
|
||||
// which can cause Beacon Timeout disconnects on some APs.
|
||||
// Roaming times match the ESP32 IDF scan defaults.
|
||||
static constexpr uint32_t SCAN_PASSIVE_DEFAULT_MS = 500;
|
||||
static constexpr uint32_t SCAN_PASSIVE_ROAMING_MS = 300;
|
||||
static constexpr uint32_t SCAN_ACTIVE_MIN_DEFAULT_MS = 400;
|
||||
static constexpr uint32_t SCAN_ACTIVE_MAX_DEFAULT_MS = 500;
|
||||
static constexpr uint32_t SCAN_ACTIVE_MIN_ROAMING_MS = 100;
|
||||
static constexpr uint32_t SCAN_ACTIVE_MAX_ROAMING_MS = 300;
|
||||
bool roaming = this->roaming_state_ == RoamingState::SCANNING;
|
||||
if (passive) {
|
||||
config.scan_time.passive = 500;
|
||||
config.scan_time.passive = roaming ? SCAN_PASSIVE_ROAMING_MS : SCAN_PASSIVE_DEFAULT_MS;
|
||||
} else {
|
||||
config.scan_time.active.min = 400;
|
||||
config.scan_time.active.max = 500;
|
||||
config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS;
|
||||
config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS;
|
||||
}
|
||||
#endif
|
||||
bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
|
||||
|
||||
@@ -550,8 +550,8 @@ def test_lambda_substitution() -> None:
|
||||
"lambda": lam,
|
||||
}
|
||||
)
|
||||
substitutions.do_substitution_pass(config)
|
||||
assert lam.value == "return 42;"
|
||||
config = substitutions.do_substitution_pass(config)
|
||||
assert config["lambda"].value == "return 42;"
|
||||
|
||||
|
||||
def test_lambda_no_substitution_unchanged() -> None:
|
||||
@@ -564,8 +564,8 @@ def test_lambda_no_substitution_unchanged() -> None:
|
||||
"lambda": lam,
|
||||
}
|
||||
)
|
||||
substitutions.do_substitution_pass(config)
|
||||
assert lam.value is original_value
|
||||
config = substitutions.do_substitution_pass(config)
|
||||
assert config["lambda"].value is original_value
|
||||
|
||||
|
||||
def test_extend_substitution() -> None:
|
||||
@@ -577,8 +577,42 @@ def test_extend_substitution() -> None:
|
||||
"sensor": ext,
|
||||
}
|
||||
)
|
||||
substitutions.do_substitution_pass(config)
|
||||
assert ext.value == "my_sensor"
|
||||
config = substitutions.do_substitution_pass(config)
|
||||
assert config["sensor"].value == "my_sensor"
|
||||
|
||||
|
||||
def test_substitute_does_not_mutate_input() -> None:
|
||||
"""substitute() must return a new tree without modifying the original."""
|
||||
inner_list = ["${var}", "static"]
|
||||
inner_dict = OrderedDict({"key": "${var}"})
|
||||
lam = Lambda("return ${var};")
|
||||
config = OrderedDict(
|
||||
{
|
||||
"a_list": inner_list,
|
||||
"a_dict": inner_dict,
|
||||
"a_lambda": lam,
|
||||
"plain": "${var}",
|
||||
}
|
||||
)
|
||||
context = substitutions.ContextVars({"var": "replaced"})
|
||||
result = substitutions.substitute(config, [], context, strict_undefined=True)
|
||||
|
||||
# Result has substitutions applied
|
||||
assert result["plain"] == "replaced"
|
||||
assert result["a_list"] == ["replaced", "static"]
|
||||
assert result["a_dict"]["key"] == "replaced"
|
||||
assert result["a_lambda"].value == "return replaced;"
|
||||
|
||||
# Original input is untouched
|
||||
assert config["plain"] == "${var}"
|
||||
assert inner_list == ["${var}", "static"]
|
||||
assert inner_dict["key"] == "${var}"
|
||||
assert lam.value == "return ${var};"
|
||||
|
||||
# Containers are new objects, not the originals
|
||||
assert result["a_list"] is not inner_list
|
||||
assert result["a_dict"] is not inner_dict
|
||||
assert result["a_lambda"] is not lam
|
||||
|
||||
|
||||
def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> None:
|
||||
|
||||
Reference in New Issue
Block a user