mirror of
https://github.com/esphome/esphome.git
synced 2025-10-25 05:03:52 +01:00
Merge branch 'dev' into integration
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/helpers.h"
|
|
||||||
#include "climate_mode.h"
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include "climate_mode.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
@@ -109,44 +109,12 @@ class ClimateTraits {
|
|||||||
|
|
||||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
||||||
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
|
||||||
void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
|
||||||
void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
|
||||||
void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
|
||||||
void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_only_mode(bool supports_fan_only_mode) {
|
|
||||||
set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode);
|
|
||||||
}
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
|
||||||
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
|
|
||||||
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
||||||
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
||||||
|
|
||||||
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
||||||
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
||||||
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
|
||||||
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
|
|
||||||
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
|
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
|
||||||
bool get_supports_fan_modes() const {
|
bool get_supports_fan_modes() const {
|
||||||
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
|
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
|
||||||
@@ -178,16 +146,6 @@ class ClimateTraits {
|
|||||||
|
|
||||||
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
|
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
|
||||||
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
|
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
|
||||||
void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
|
||||||
void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
|
||||||
void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); }
|
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
|
||||||
void set_supports_swing_mode_horizontal(bool supported) {
|
|
||||||
set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
|
|
||||||
}
|
|
||||||
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
|
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
|
||||||
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
|
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
|
||||||
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
|
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
|||||||
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
|
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
|
||||||
|
|
||||||
this->waiting_ = false;
|
this->waiting_ = false;
|
||||||
ESP_LOGV(TAG, "Data: %s", hexencode(data).c_str());
|
ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str());
|
||||||
|
|
||||||
float value = (float) get_16bit(0);
|
float value = (float) get_16bit(0);
|
||||||
for (int i = 0; i < data[3]; i++)
|
for (int i = 0; i < data[3]; i++)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "esphome/core/defines.h"
|
|
||||||
#include "esphome/core/color.h"
|
|
||||||
#include "esp_color_correction.h"
|
#include "esp_color_correction.h"
|
||||||
#include "esp_color_view.h"
|
#include "esp_color_view.h"
|
||||||
#include "esp_range_view.h"
|
#include "esp_range_view.h"
|
||||||
|
#include "esphome/core/color.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
#include "light_output.h"
|
#include "light_output.h"
|
||||||
#include "light_state.h"
|
#include "light_state.h"
|
||||||
#include "transformers.h"
|
#include "transformers.h"
|
||||||
@@ -17,8 +17,6 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace light {
|
namespace light {
|
||||||
|
|
||||||
using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color;
|
|
||||||
|
|
||||||
/// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness).
|
/// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness).
|
||||||
Color color_from_light_color_values(LightColorValues val);
|
Color color_from_light_color_values(LightColorValues val);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/helpers.h"
|
|
||||||
#include "color_mode.h"
|
#include "color_mode.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
@@ -31,26 +31,6 @@ class LightTraits {
|
|||||||
return this->supported_color_modes_.has_capability(color_capability);
|
return this->supported_color_modes_.has_capability(color_capability);
|
||||||
}
|
}
|
||||||
|
|
||||||
ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21")
|
|
||||||
bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); }
|
|
||||||
ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.", "v1.21")
|
|
||||||
bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); }
|
|
||||||
ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.", "v1.21")
|
|
||||||
bool get_supports_rgb_white_value() const {
|
|
||||||
return this->supports_color_mode(ColorMode::RGB_WHITE) ||
|
|
||||||
this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE);
|
|
||||||
}
|
|
||||||
ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.", "v1.21")
|
|
||||||
bool get_supports_color_temperature() const {
|
|
||||||
return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE);
|
|
||||||
}
|
|
||||||
ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.", "v1.21")
|
|
||||||
bool get_supports_color_interlock() const {
|
|
||||||
return this->supports_color_mode(ColorMode::RGB) &&
|
|
||||||
(this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) ||
|
|
||||||
this->supports_color_mode(ColorMode::COLOR_TEMPERATURE));
|
|
||||||
}
|
|
||||||
|
|
||||||
float get_min_mireds() const { return this->min_mireds_; }
|
float get_min_mireds() const { return this->min_mireds_; }
|
||||||
void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; }
|
void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; }
|
||||||
float get_max_mireds() const { return this->max_mireds_; }
|
float get_max_mireds() const { return this->max_mireds_; }
|
||||||
|
|||||||
@@ -1291,9 +1291,6 @@ void Nextion::check_pending_waveform_() {
|
|||||||
|
|
||||||
void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; }
|
void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; }
|
||||||
|
|
||||||
ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20")
|
|
||||||
void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); }
|
|
||||||
|
|
||||||
bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
|
bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
|
||||||
|
|
||||||
} // namespace nextion
|
} // namespace nextion
|
||||||
|
|||||||
@@ -142,13 +142,8 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
|||||||
|
|
||||||
void stop() override {
|
void stop() override {
|
||||||
// Clear all queued items to free memory immediately
|
// Clear all queued items to free memory immediately
|
||||||
if (this->var_queue_) {
|
// Resetting the array automatically destroys all unique_ptrs and their contents
|
||||||
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
|
this->var_queue_.reset();
|
||||||
for (size_t i = 0; i < queue_capacity; i++) {
|
|
||||||
this->var_queue_[i].reset();
|
|
||||||
}
|
|
||||||
this->var_queue_.reset();
|
|
||||||
}
|
|
||||||
this->num_queued_ = 0;
|
this->num_queued_ = 0;
|
||||||
this->queue_front_ = 0;
|
this->queue_front_ = 0;
|
||||||
Script<Ts...>::stop();
|
Script<Ts...>::stop();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from esphome import core, loader, pins, yaml_util
|
from esphome import core, loader, pins, yaml_util
|
||||||
from esphome.config_helpers import Extend, Remove, merge_dicts_ordered
|
from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
@@ -324,13 +324,7 @@ def iter_ids(config, path=None):
|
|||||||
yield from iter_ids(value, path + [key])
|
yield from iter_ids(value, path + [key])
|
||||||
|
|
||||||
|
|
||||||
def recursive_check_replaceme(value):
|
def check_replaceme(value):
|
||||||
if isinstance(value, list):
|
|
||||||
return cv.Schema([recursive_check_replaceme])(value)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return cv.Schema({cv.valid: recursive_check_replaceme})(value)
|
|
||||||
if isinstance(value, ESPLiteralValue):
|
|
||||||
pass
|
|
||||||
if isinstance(value, str) and value == "REPLACEME":
|
if isinstance(value, str) and value == "REPLACEME":
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
"Found 'REPLACEME' in configuration, this is most likely an error. "
|
"Found 'REPLACEME' in configuration, this is most likely an error. "
|
||||||
@@ -339,7 +333,86 @@ def recursive_check_replaceme(value):
|
|||||||
"If you want to use the literal REPLACEME string, "
|
"If you want to use the literal REPLACEME string, "
|
||||||
'please use "!literal REPLACEME"'
|
'please use "!literal REPLACEME"'
|
||||||
)
|
)
|
||||||
return value
|
|
||||||
|
|
||||||
|
def _build_list_index(lst):
|
||||||
|
index = OrderedDict()
|
||||||
|
extensions, removals = [], set()
|
||||||
|
for item in lst:
|
||||||
|
if item is None:
|
||||||
|
removals.add(None)
|
||||||
|
continue
|
||||||
|
item_id = None
|
||||||
|
if isinstance(item, dict) and (item_id := item.get(CONF_ID)):
|
||||||
|
if isinstance(item_id, Extend):
|
||||||
|
extensions.append(item)
|
||||||
|
continue
|
||||||
|
if isinstance(item_id, Remove):
|
||||||
|
removals.add(item_id.value)
|
||||||
|
continue
|
||||||
|
if not item_id or item_id in index:
|
||||||
|
# no id or duplicate -> pass through with identity-based key
|
||||||
|
item_id = id(item)
|
||||||
|
index[item_id] = item
|
||||||
|
return index, extensions, removals
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extend_remove(value, is_key=None):
|
||||||
|
if isinstance(value, ESPLiteralValue):
|
||||||
|
return # do not check inside literal blocks
|
||||||
|
if isinstance(value, list):
|
||||||
|
index, extensions, removals = _build_list_index(value)
|
||||||
|
if extensions or removals:
|
||||||
|
# Rebuild the original list after
|
||||||
|
# processing all extensions and removals
|
||||||
|
for item in extensions:
|
||||||
|
item_id = item[CONF_ID].value
|
||||||
|
if item_id in removals:
|
||||||
|
continue
|
||||||
|
old = index.get(item_id)
|
||||||
|
if old is None:
|
||||||
|
# Failed to find source for extension
|
||||||
|
# Find index of item to show error at correct position
|
||||||
|
i = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i, d in enumerate(value)
|
||||||
|
if d.get(CONF_ID) == item[CONF_ID]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with cv.prepend_path(i):
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Source for extension of ID '{item_id}' was not found."
|
||||||
|
)
|
||||||
|
item[CONF_ID] = item_id
|
||||||
|
index[item_id] = merge_config(old, item)
|
||||||
|
for item_id in removals:
|
||||||
|
index.pop(item_id, None)
|
||||||
|
|
||||||
|
value[:] = index.values()
|
||||||
|
|
||||||
|
for i, item in enumerate(value):
|
||||||
|
with cv.prepend_path(i):
|
||||||
|
resolve_extend_remove(item, False)
|
||||||
|
return
|
||||||
|
if isinstance(value, dict):
|
||||||
|
removals = []
|
||||||
|
for k, v in value.items():
|
||||||
|
with cv.prepend_path(k):
|
||||||
|
if isinstance(v, Remove):
|
||||||
|
removals.append(k)
|
||||||
|
continue
|
||||||
|
resolve_extend_remove(k, True)
|
||||||
|
resolve_extend_remove(v, False)
|
||||||
|
for k in removals:
|
||||||
|
value.pop(k, None)
|
||||||
|
return
|
||||||
|
if is_key:
|
||||||
|
return # do not check keys (yet)
|
||||||
|
|
||||||
|
check_replaceme(value)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class ConfigValidationStep(abc.ABC):
|
class ConfigValidationStep(abc.ABC):
|
||||||
@@ -437,19 +510,6 @@ class LoadValidationStep(ConfigValidationStep):
|
|||||||
continue
|
continue
|
||||||
p_name = p_config.get("platform")
|
p_name = p_config.get("platform")
|
||||||
if p_name is None:
|
if p_name is None:
|
||||||
p_id = p_config.get(CONF_ID)
|
|
||||||
if isinstance(p_id, Extend):
|
|
||||||
result.add_str_error(
|
|
||||||
f"Source for extension of ID '{p_id.value}' was not found.",
|
|
||||||
path + [CONF_ID],
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if isinstance(p_id, Remove):
|
|
||||||
result.add_str_error(
|
|
||||||
f"Source for removal of ID '{p_id.value}' was not found.",
|
|
||||||
path + [CONF_ID],
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
result.add_str_error(
|
result.add_str_error(
|
||||||
f"'{self.domain}' requires a 'platform' key but it was not specified.",
|
f"'{self.domain}' requires a 'platform' key but it was not specified.",
|
||||||
path,
|
path,
|
||||||
@@ -934,9 +994,10 @@ def validate_config(
|
|||||||
|
|
||||||
CORE.raw_config = config
|
CORE.raw_config = config
|
||||||
|
|
||||||
# 1.1. Check for REPLACEME special value
|
# 1.1. Resolve !extend and !remove and check for REPLACEME
|
||||||
|
# After this step, there will not be any Extend or Remove values in the config anymore
|
||||||
try:
|
try:
|
||||||
recursive_check_replaceme(config)
|
resolve_extend_remove(config)
|
||||||
except vol.Invalid as err:
|
except vol.Invalid as err:
|
||||||
result.add_error(err)
|
result.add_error(err)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ID,
|
|
||||||
CONF_LEVEL,
|
CONF_LEVEL,
|
||||||
CONF_LOGGER,
|
CONF_LOGGER,
|
||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
@@ -75,73 +74,28 @@ class Remove:
|
|||||||
return isinstance(b, Remove) and self.value == b.value
|
return isinstance(b, Remove) and self.value == b.value
|
||||||
|
|
||||||
|
|
||||||
def merge_config(full_old, full_new):
|
def merge_config(old, new):
|
||||||
def merge(old, new):
|
if isinstance(new, Remove):
|
||||||
if isinstance(new, dict):
|
|
||||||
if not isinstance(old, dict):
|
|
||||||
return new
|
|
||||||
# Preserve OrderedDict type by copying to OrderedDict if either input is OrderedDict
|
|
||||||
if isinstance(old, OrderedDict) or isinstance(new, OrderedDict):
|
|
||||||
res = OrderedDict(old)
|
|
||||||
else:
|
|
||||||
res = old.copy()
|
|
||||||
for k, v in new.items():
|
|
||||||
if isinstance(v, Remove) and k in old:
|
|
||||||
del res[k]
|
|
||||||
else:
|
|
||||||
res[k] = merge(old[k], v) if k in old else v
|
|
||||||
return res
|
|
||||||
if isinstance(new, list):
|
|
||||||
if not isinstance(old, list):
|
|
||||||
return new
|
|
||||||
res = old.copy()
|
|
||||||
ids = {
|
|
||||||
v_id: i
|
|
||||||
for i, v in enumerate(res)
|
|
||||||
if isinstance(v, dict)
|
|
||||||
and (v_id := v.get(CONF_ID))
|
|
||||||
and isinstance(v_id, str)
|
|
||||||
}
|
|
||||||
extend_ids = {
|
|
||||||
v_id.value: i
|
|
||||||
for i, v in enumerate(res)
|
|
||||||
if isinstance(v, dict)
|
|
||||||
and (v_id := v.get(CONF_ID))
|
|
||||||
and isinstance(v_id, Extend)
|
|
||||||
}
|
|
||||||
|
|
||||||
ids_to_delete = []
|
|
||||||
for v in new:
|
|
||||||
if isinstance(v, dict) and (new_id := v.get(CONF_ID)):
|
|
||||||
if isinstance(new_id, Extend):
|
|
||||||
new_id = new_id.value
|
|
||||||
if new_id in ids:
|
|
||||||
v[CONF_ID] = new_id
|
|
||||||
res[ids[new_id]] = merge(res[ids[new_id]], v)
|
|
||||||
continue
|
|
||||||
elif isinstance(new_id, Remove):
|
|
||||||
new_id = new_id.value
|
|
||||||
if new_id in ids:
|
|
||||||
ids_to_delete.append(ids[new_id])
|
|
||||||
continue
|
|
||||||
elif (
|
|
||||||
new_id in extend_ids
|
|
||||||
): # When a package is extending a non-packaged item
|
|
||||||
extend_res = res[extend_ids[new_id]]
|
|
||||||
extend_res[CONF_ID] = new_id
|
|
||||||
new_v = merge(v, extend_res)
|
|
||||||
res[extend_ids[new_id]] = new_v
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
ids[new_id] = len(res)
|
|
||||||
res.append(v)
|
|
||||||
return [v for i, v in enumerate(res) if i not in ids_to_delete]
|
|
||||||
if new is None:
|
|
||||||
return old
|
|
||||||
|
|
||||||
return new
|
return new
|
||||||
|
if isinstance(new, dict):
|
||||||
|
if not isinstance(old, dict):
|
||||||
|
return new
|
||||||
|
# Preserve OrderedDict type by copying to OrderedDict if either input is OrderedDict
|
||||||
|
if isinstance(old, OrderedDict) or isinstance(new, OrderedDict):
|
||||||
|
res = OrderedDict(old)
|
||||||
|
else:
|
||||||
|
res = old.copy()
|
||||||
|
for k, v in new.items():
|
||||||
|
res[k] = merge_config(old.get(k), v)
|
||||||
|
return res
|
||||||
|
if isinstance(new, list):
|
||||||
|
if not isinstance(old, list):
|
||||||
|
return new
|
||||||
|
return old + new
|
||||||
|
if new is None:
|
||||||
|
return old
|
||||||
|
|
||||||
return merge(full_old, full_new)
|
return new
|
||||||
|
|
||||||
|
|
||||||
def filter_source_files_from_platform(
|
def filter_source_files_from_platform(
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from esphome import core
|
from esphome import core
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.config_helpers import Extend, Remove
|
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
ALLOWED_NAME_CHARS,
|
ALLOWED_NAME_CHARS,
|
||||||
CONF_AVAILABILITY,
|
CONF_AVAILABILITY,
|
||||||
@@ -624,12 +623,6 @@ def declare_id(type):
|
|||||||
if value is None:
|
if value is None:
|
||||||
return core.ID(None, is_declaration=True, type=type)
|
return core.ID(None, is_declaration=True, type=type)
|
||||||
|
|
||||||
if isinstance(value, Extend):
|
|
||||||
raise Invalid(f"Source for extension of ID '{value.value}' was not found.")
|
|
||||||
|
|
||||||
if isinstance(value, Remove):
|
|
||||||
raise Invalid(f"Source for Removal of ID '{value.value}' was not found.")
|
|
||||||
|
|
||||||
return core.ID(validate_id_name(value), is_declaration=True, type=type)
|
return core.ID(validate_id_name(value), is_declaration=True, type=type)
|
||||||
|
|
||||||
return validator
|
return validator
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from esphome.components.packages import do_packages_pass
|
from esphome.components.packages import do_packages_pass
|
||||||
|
from esphome.config import resolve_extend_remove
|
||||||
from esphome.config_helpers import Extend, Remove
|
from esphome.config_helpers import Extend, Remove
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -64,13 +65,20 @@ def fixture_basic_esphome():
|
|||||||
return {CONF_NAME: TEST_DEVICE_NAME, CONF_PLATFORM: TEST_PLATFORM}
|
return {CONF_NAME: TEST_DEVICE_NAME, CONF_PLATFORM: TEST_PLATFORM}
|
||||||
|
|
||||||
|
|
||||||
|
def packages_pass(config):
|
||||||
|
"""Wrapper around packages_pass that also resolves Extend and Remove."""
|
||||||
|
config = do_packages_pass(config)
|
||||||
|
resolve_extend_remove(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def test_package_unused(basic_esphome, basic_wifi):
|
def test_package_unused(basic_esphome, basic_wifi):
|
||||||
"""
|
"""
|
||||||
Ensures do_package_pass does not change a config if packages aren't used.
|
Ensures do_package_pass does not change a config if packages aren't used.
|
||||||
"""
|
"""
|
||||||
config = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
|
config = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == config
|
assert actual == config
|
||||||
|
|
||||||
|
|
||||||
@@ -83,7 +91,7 @@ def test_package_invalid_dict(basic_esphome, basic_wifi):
|
|||||||
config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi | {CONF_URL: ""}}
|
config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi | {CONF_URL: ""}}
|
||||||
|
|
||||||
with pytest.raises(cv.Invalid):
|
with pytest.raises(cv.Invalid):
|
||||||
do_packages_pass(config)
|
packages_pass(config)
|
||||||
|
|
||||||
|
|
||||||
def test_package_include(basic_wifi, basic_esphome):
|
def test_package_include(basic_wifi, basic_esphome):
|
||||||
@@ -99,7 +107,7 @@ def test_package_include(basic_wifi, basic_esphome):
|
|||||||
|
|
||||||
expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
|
expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -124,7 +132,7 @@ def test_package_append(basic_wifi, basic_esphome):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -148,7 +156,7 @@ def test_package_override(basic_wifi, basic_esphome):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -177,7 +185,7 @@ def test_multiple_package_order():
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -233,7 +241,7 @@ def test_package_list_merge():
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -311,7 +319,7 @@ def test_package_list_merge_by_id():
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -350,13 +358,13 @@ def test_package_merge_by_id_with_list():
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
def test_package_merge_by_missing_id():
|
def test_package_merge_by_missing_id():
|
||||||
"""
|
"""
|
||||||
Ensures that components with missing IDs are not merged.
|
Ensures that a validation error is thrown when trying to extend a missing ID.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@@ -379,25 +387,15 @@ def test_package_merge_by_missing_id():
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
expected = {
|
error_raised = False
|
||||||
CONF_SENSOR: [
|
try:
|
||||||
{
|
packages_pass(config)
|
||||||
CONF_ID: TEST_SENSOR_ID_1,
|
assert False, "Expected validation error for missing ID"
|
||||||
CONF_FILTERS: [{CONF_MULTIPLY: 42.0}],
|
except cv.Invalid as err:
|
||||||
},
|
error_raised = True
|
||||||
{
|
assert err.path == [CONF_SENSOR, 2]
|
||||||
CONF_ID: TEST_SENSOR_ID_1,
|
|
||||||
CONF_FILTERS: [{CONF_MULTIPLY: 10.0}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
CONF_ID: Extend(TEST_SENSOR_ID_2),
|
|
||||||
CONF_FILTERS: [{CONF_OFFSET: 146.0}],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
assert error_raised
|
||||||
assert actual == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_package_list_remove_by_id():
|
def test_package_list_remove_by_id():
|
||||||
@@ -447,7 +445,7 @@ def test_package_list_remove_by_id():
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -493,7 +491,7 @@ def test_multiple_package_list_remove_by_id():
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -514,7 +512,7 @@ def test_package_dict_remove_by_id(basic_wifi, basic_esphome):
|
|||||||
CONF_ESPHOME: basic_esphome,
|
CONF_ESPHOME: basic_esphome,
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -545,7 +543,6 @@ def test_package_remove_by_missing_id():
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
"missing_key": Remove(),
|
|
||||||
CONF_SENSOR: [
|
CONF_SENSOR: [
|
||||||
{
|
{
|
||||||
CONF_ID: TEST_SENSOR_ID_1,
|
CONF_ID: TEST_SENSOR_ID_1,
|
||||||
@@ -555,14 +552,10 @@ def test_package_remove_by_missing_id():
|
|||||||
CONF_ID: TEST_SENSOR_ID_1,
|
CONF_ID: TEST_SENSOR_ID_1,
|
||||||
CONF_FILTERS: [{CONF_MULTIPLY: 10.0}],
|
CONF_FILTERS: [{CONF_MULTIPLY: 10.0}],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
CONF_ID: Remove(TEST_SENSOR_ID_2),
|
|
||||||
CONF_FILTERS: [{CONF_OFFSET: 146.0}],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -634,7 +627,7 @@ def test_remote_packages_with_files_list(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@@ -730,5 +723,5 @@ def test_remote_packages_with_files_and_vars(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = do_packages_pass(config)
|
actual = packages_pass(config)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
substitutions:
|
||||||
|
A: component1
|
||||||
|
B: component2
|
||||||
|
C: component3
|
||||||
|
some_component:
|
||||||
|
- id: component1
|
||||||
|
value: 2
|
||||||
|
- id: component2
|
||||||
|
value: 5
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
substitutions:
|
||||||
|
A: component1
|
||||||
|
B: component2
|
||||||
|
C: component3
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- some_component:
|
||||||
|
- id: component1
|
||||||
|
value: 1
|
||||||
|
- id: !extend ${B}
|
||||||
|
value: 4
|
||||||
|
- id: !extend ${B}
|
||||||
|
value: 5
|
||||||
|
- id: component3
|
||||||
|
value: 6
|
||||||
|
|
||||||
|
some_component:
|
||||||
|
- id: !extend ${A}
|
||||||
|
value: 2
|
||||||
|
- id: component2
|
||||||
|
value: 3
|
||||||
|
- id: !remove ${C}
|
||||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from esphome import config as config_module, yaml_util
|
from esphome import config as config_module, yaml_util
|
||||||
from esphome.components import substitutions
|
from esphome.components import substitutions
|
||||||
|
from esphome.config import resolve_extend_remove
|
||||||
from esphome.config_helpers import merge_config
|
from esphome.config_helpers import merge_config
|
||||||
from esphome.const import CONF_PACKAGES, CONF_SUBSTITUTIONS
|
from esphome.const import CONF_PACKAGES, CONF_SUBSTITUTIONS
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
@@ -81,6 +82,8 @@ def test_substitutions_fixtures(fixture_path):
|
|||||||
|
|
||||||
substitutions.do_substitution_pass(config, None)
|
substitutions.do_substitution_pass(config, None)
|
||||||
|
|
||||||
|
resolve_extend_remove(config)
|
||||||
|
|
||||||
# Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE
|
# Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE
|
||||||
if expected_path.is_file():
|
if expected_path.is_file():
|
||||||
expected = yaml_util.load_yaml(expected_path)
|
expected = yaml_util.load_yaml(expected_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user