mirror of
https://github.com/esphome/esphome.git
synced 2026-10-25 23:42:15 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a5cb7d957 | ||
|
|
058d9db4dc | ||
|
|
0077231478 | ||
|
|
2faaaa4c3c | ||
|
|
371884d97e | ||
|
|
55182cda22 | ||
|
|
46fde171b6 | ||
|
|
8132569898 | ||
|
|
b41e8ff924 |
No files matched your search
@@ -108,7 +108,8 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
hub = fv.get_declaring_config(config[CONF_AS734X_ID])
|
||||
full = fv.full_config.get()
|
||||
hub = full.get_config_for_path(full.get_path_for_id(config[CONF_AS734X_ID])[:-1])
|
||||
if hub[CONF_TYPE] != "AS7341":
|
||||
return config
|
||||
for key in _BAND_KEYS:
|
||||
|
||||
@@ -66,7 +66,9 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if config[CONF_TYPE] not in _MAGNETIC_FIELDS:
|
||||
return
|
||||
bmi270_config = fv.get_declaring_config(config[CONF_BMI270_ID])
|
||||
full_config = fv.full_config.get()
|
||||
bmi270_path = full_config.get_path_for_id(config[CONF_BMI270_ID])[:-1]
|
||||
bmi270_config = full_config.get_config_for_path(bmi270_path)
|
||||
if bmi270_config.get(CONF_AUX_DEVICE) != "BMM150":
|
||||
raise cv.Invalid(
|
||||
"This sensor requires the parent bmi270's 'aux_device' to be set to 'BMM150'",
|
||||
|
||||
@@ -7,7 +7,6 @@ from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, CONF_WATCHDOG
|
||||
from esphome.core import CORE, ID, EsphomeError, Lambda, TimePeriod
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
CODEOWNERS = ["@Rapsssito"]
|
||||
@@ -360,7 +359,8 @@ async def bq25186_configure_to_code(
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# Options the action leaves out fall back to the component's configuration
|
||||
parent_config = fv.get_declaring_config(config[CONF_ID], CORE.config)
|
||||
path = CORE.config.get_path_for_id(config[CONF_ID])[:-1]
|
||||
parent_config = CORE.config.get_config_for_path(path)
|
||||
merged = {**parent_config, **config}
|
||||
try:
|
||||
_check_watchdog(merged)
|
||||
|
||||
@@ -3,7 +3,8 @@ from collections.abc import Callable
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MODE, CONF_MODEL
|
||||
from esphome.const import CONF_ID, CONF_MODE, CONF_MODEL
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -58,11 +59,16 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
def get_hub_config(root: ConfigType, hub_id: ID) -> ConfigType:
|
||||
"""Return the configuration of the hub with the given id."""
|
||||
return next(conf for conf in root[DOMAIN] if conf[CONF_ID] == hub_id)
|
||||
|
||||
|
||||
def final_validate_mode(keys: dict[str, str]) -> Callable[[ConfigType], None]:
|
||||
"""Reject entity keys that need a different hub mode; `keys` maps each key to the mode it needs."""
|
||||
|
||||
def validator(config: ConfigType) -> None:
|
||||
hub = fv.get_declaring_config(config[CONF_DFROBOT_SEN0609_ID])
|
||||
hub = get_hub_config(fv.full_config.get(), config[CONF_DFROBOT_SEN0609_ID])
|
||||
for key, mode in keys.items():
|
||||
if key in config and hub[CONF_MODE] != mode:
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -18,7 +18,6 @@ from esphome.const import (
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
@@ -29,6 +28,7 @@ from .. import (
|
||||
DFRobotSEN0609Component,
|
||||
dfrobot_sen0609_ns,
|
||||
final_validate_mode,
|
||||
get_hub_config,
|
||||
)
|
||||
|
||||
DFRobotSEN0609Number = dfrobot_sen0609_ns.class_("DFRobotSEN0609Number", number.Number)
|
||||
@@ -181,7 +181,7 @@ FINAL_VALIDATE_SCHEMA = final_validate_mode(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub_id = config[CONF_DFROBOT_SEN0609_ID]
|
||||
hub = await cg.get_variable(hub_id)
|
||||
max_range = MAX_RANGES[fv.get_declaring_config(hub_id, CORE.config)[CONF_MODEL]]
|
||||
max_range = MAX_RANGES[get_hub_config(CORE.config, hub_id)[CONF_MODEL]]
|
||||
numbers = number.sub_numbers(config, parent=hub)
|
||||
for s in SETTINGS:
|
||||
await numbers(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
from esphome import automation, core, pins
|
||||
from esphome import automation, core
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import (
|
||||
@@ -14,7 +13,6 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_FROM,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
@@ -28,7 +26,7 @@ from esphome.const import (
|
||||
SCHEDULER_DONT_RUN,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.final_validate import full_config, get_declaring_config
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DOMAIN = "display"
|
||||
@@ -56,33 +54,6 @@ DISPLAY_ROTATIONS = {
|
||||
}
|
||||
|
||||
|
||||
class DisplayModel(Protocol):
|
||||
"""The parts of a display model used by enable_pin_schema()."""
|
||||
|
||||
def get_default(self, key: str, fallback: Any = ...) -> Any: ...
|
||||
|
||||
|
||||
def enable_pin_schema(
|
||||
model: DisplayModel, config: ConfigType
|
||||
) -> dict[cv.Optional, Any]:
|
||||
"""
|
||||
Get the schema entry for a display's enable pins.
|
||||
Pins given in the user config are used as they are. Otherwise the model's default enable
|
||||
pins are used, except any that is also used elsewhere in the config, so a user can take
|
||||
control of that pin, e.g. with a switch.
|
||||
:param config: The display config as given by the user
|
||||
"""
|
||||
schema = cv.ensure_list(pins.gpio_output_pin_schema)
|
||||
default = model.get_default(CONF_ENABLE_PIN, None)
|
||||
if default is None or CONF_ENABLE_PIN in config:
|
||||
return {cv.Optional(CONF_ENABLE_PIN): schema}
|
||||
return {
|
||||
cv.Optional(
|
||||
CONF_ENABLE_PIN, default=default
|
||||
): pins.PIN_SCHEMA_REGISTRY.optional(schema)
|
||||
}
|
||||
|
||||
|
||||
def validate_rotation(value):
|
||||
value = cv.string(value)
|
||||
value = value.removesuffix("°")
|
||||
@@ -225,7 +196,9 @@ def get_display_metadata(display_id: ID) -> DisplayMetaData:
|
||||
return meta
|
||||
# No metadata found, display driver may not yet support it.
|
||||
# Read the raw config to populate the returned data
|
||||
disp_config = get_declaring_config(display_id)
|
||||
global_config = full_config.get()
|
||||
path = global_config.get_path_for_id(display_id)[:-1]
|
||||
disp_config = global_config.get_config_for_path(path)
|
||||
dimensions = disp_config.get(CONF_DIMENSIONS, (0, 0))
|
||||
if isinstance(dimensions, dict):
|
||||
dimensions = (dimensions.get(CONF_WIDTH, 0), dimensions.get(CONF_HEIGHT, 0))
|
||||
|
||||
@@ -32,7 +32,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
"""Validate that the channel is within the parent's channel count."""
|
||||
parent_config = fv.get_declaring_config(config[CONF_DS248X_ID])
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_DS248X_ID])[:-1]
|
||||
parent_config = fconf.get_config_for_path(path)
|
||||
channel_count = get_channel_count(parent_config)
|
||||
channel = config[CONF_CHANNEL]
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ def model_schema(config):
|
||||
cv.GenerateID(): cv.declare_id(class_name),
|
||||
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(model),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
|
||||
map_sequence
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import Any, Self
|
||||
|
||||
from esphome.components.mipi import check_model_requirements
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_WIDTH
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -128,5 +128,24 @@ class EpaperModel:
|
||||
return model
|
||||
|
||||
def check_requirements(self) -> None:
|
||||
"""Check that the components this model needs are configured, see check_model_requirements()."""
|
||||
check_model_requirements(self.name, self.defaults)
|
||||
"""
|
||||
Raise a friendly error if any component this model requires is not configured.
|
||||
|
||||
This runs during schema validation (before ID references are resolved) so that a
|
||||
model whose default pins live on a pin expander reports the missing expander clearly
|
||||
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
|
||||
"""
|
||||
if requirements := self.get_default("requires", set()):
|
||||
# ``raw_config`` is populated before any component schema runs during a real
|
||||
# validation, so presence of a required component is simply a top-level key.
|
||||
# When it is absent (e.g. a unit test that invokes the schema directly) there
|
||||
# is no config to check against, so skip.
|
||||
global_config = CORE.raw_config
|
||||
if global_config is None:
|
||||
return
|
||||
missing = {x for x in requirements if x not in global_config}
|
||||
if missing:
|
||||
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
|
||||
raise cv.Invalid(
|
||||
f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
|
||||
)
|
||||
@@ -63,7 +63,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
def _validate_model(config: ConfigType) -> None:
|
||||
climate_conf = fv.get_declaring_config(config[CONF_GREE_ID])
|
||||
full_config = fv.full_config.get()
|
||||
climate_path = full_config.get_path_for_id(config[CONF_GREE_ID])[:-1]
|
||||
climate_conf = full_config.get_config_for_path(climate_path)
|
||||
if climate_conf[CONF_MODEL] not in SUPPORTED_MODELS:
|
||||
raise cv.Invalid(
|
||||
"Gree switches are only supported for the "
|
||||
|
||||
@@ -61,10 +61,12 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
def _final_validate(config) -> None:
|
||||
full_config = fv.full_config.get()
|
||||
for switch_type in [CONF_BEEPER, CONF_QUIET_MODE]:
|
||||
# Check switches that are only supported for HonClimate
|
||||
if config.get(switch_type):
|
||||
climate_conf = fv.get_declaring_config(config[CONF_HAIER_ID])
|
||||
climate_path = full_config.get_path_for_id(config[CONF_HAIER_ID])[:-1]
|
||||
climate_conf = full_config.get_config_for_path(climate_path)
|
||||
protocol_type = climate_conf.get(CONF_PROTOCOL)
|
||||
if protocol_type.casefold() != PROTOCOL_HON.casefold():
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -42,7 +42,9 @@ def _final_validate(config: dict[str, Any]) -> None:
|
||||
|
||||
# Get the transmitter configuration
|
||||
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
|
||||
transmitter_config = fv.get_declaring_config(transmitter_id)
|
||||
full_config = fv.full_config.get()
|
||||
transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1]
|
||||
transmitter_config = full_config.get_config_for_path(transmitter_path)
|
||||
|
||||
# Check if carrier_duty_percent set to 0 or 100
|
||||
# Note: remote_transmitter schema requires this field and validates 1-100%,
|
||||
|
||||
@@ -35,7 +35,11 @@ def _final_validate(config: ConfigType) -> None:
|
||||
if CONF_REMOTE_TRANSMITTER_ID not in config:
|
||||
return
|
||||
|
||||
transmitter_config = fv.get_declaring_config(config[CONF_REMOTE_TRANSMITTER_ID])
|
||||
full_config = fv.full_config.get()
|
||||
transmitter_path = full_config.get_path_for_id(config[CONF_REMOTE_TRANSMITTER_ID])[
|
||||
:-1
|
||||
]
|
||||
transmitter_config = full_config.get_config_for_path(transmitter_path)
|
||||
|
||||
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
|
||||
if duty_percent is not None and duty_percent != 100:
|
||||
|
||||
@@ -274,7 +274,9 @@ def _model_schema(config: ConfigType) -> cv.Schema:
|
||||
cv.Optional(CONF_UPDATE_MODE): update_mode,
|
||||
# One or more GPIOs driven high during setup to power on the panel
|
||||
# (e.g. board power-enable rails), before reset and init.
|
||||
**display.enable_pin_schema(model, config),
|
||||
cv.Optional(
|
||||
CONF_ENABLE_PIN, default=model.get_default(CONF_ENABLE_PIN, [])
|
||||
): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||
|
||||
@@ -101,7 +101,8 @@ def final_validate(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
if config.get(CONF_WAKE):
|
||||
hub_config = fv.get_declaring_config(hub_id, full_config)
|
||||
hub_path = full_config.get_path_for_id(hub_id)
|
||||
hub_config = full_config.get_config_for_path(hub_path[:-1])
|
||||
if hub_config.get(CONF_WAKEUP_PIN) is None:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_WAKE} requires {CONF_WAKEUP_PIN} on the parent ld6002b component",
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// clang-tidy cannot parse the Realtek SDK headers behind wiring_data.h
|
||||
#if defined(USE_BK72XX) || (defined(USE_RTL87XX) && !defined(CLANG_TIDY))
|
||||
#include <wiring_data.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::libretiny {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "libretiny.gpio");
|
||||
@@ -26,16 +31,10 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags) {
|
||||
}
|
||||
}
|
||||
|
||||
struct ISRPinArg {
|
||||
uint8_t pin;
|
||||
bool inverted;
|
||||
};
|
||||
|
||||
// The ISR copy is the pin itself, so both share the stored mode and level. Pins are never freed.
|
||||
ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const {
|
||||
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
arg->pin = pin_;
|
||||
arg->inverted = inverted_;
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
return ISRInternalGPIOPin(
|
||||
const_cast<ArduinoInternalGPIOPin *>(this)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
|
||||
}
|
||||
|
||||
void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
@@ -61,8 +60,59 @@ void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg,
|
||||
attachInterruptParam(pin_, func, arduino_mode, arg);
|
||||
}
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
// BDK GPIO config register (REG_GPIO_0_CONFIG, GCFG_OUTPUT_BIT): 0 enables the output, bit 1 is the output level
|
||||
static constexpr uintptr_t BK_GPIO_CFG_BASE = 0x00802800;
|
||||
static constexpr uint32_t BK_GPIO_OUTPUT_BIT = 1 << 1;
|
||||
|
||||
// LibreTiny's switch to output always starts the pin LOW. Enable the output at its level in one store, then update
|
||||
// LibreTiny's pin state the way its own switch does (cores/beken-72xx/arduino/src/wiring_digital.c).
|
||||
static bool bk_enter_output(uint8_t pin, bool level) {
|
||||
PinInfo *info = pinInfo(pin);
|
||||
if (info == nullptr || info->gpio > 31)
|
||||
return false;
|
||||
PinData *data = pinData(info);
|
||||
if (pinEnabled(info, PIN_GPIO) && data->gpioMode == OUTPUT)
|
||||
return false;
|
||||
pinRemoveMode(info, PIN_PWM);
|
||||
auto *cfg =
|
||||
reinterpret_cast<volatile uint32_t *>(BK_GPIO_CFG_BASE + info->gpio * 4); // NOLINT(performance-no-int-to-ptr)
|
||||
*cfg = level ? BK_GPIO_OUTPUT_BIT : 0;
|
||||
pinEnable(info, PIN_GPIO);
|
||||
data->gpioMode = OUTPUT;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)
|
||||
// The switch to output keeps the HAL data register, so write the level to it first
|
||||
static void rtl_preset_level(uint8_t pin, bool level) {
|
||||
PinInfo *info = pinInfo(pin);
|
||||
if (info == nullptr)
|
||||
return;
|
||||
if (info->data == nullptr || info->data->gpio == nullptr)
|
||||
pinMode(pin, INPUT); // NOLINT - LibreTiny creates the gpio_t on first use
|
||||
if (info->data != nullptr && info->data->gpio != nullptr)
|
||||
gpio_write(info->data->gpio, level);
|
||||
}
|
||||
#endif
|
||||
|
||||
void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
pinMode(pin_, flags_to_mode(flags)); // NOLINT
|
||||
const int mode = flags_to_mode(flags);
|
||||
this->output_ = flags & gpio::FLAG_OUTPUT;
|
||||
#ifdef USE_BK72XX
|
||||
if (mode == OUTPUT && bk_enter_output(this->pin_, this->level_))
|
||||
return;
|
||||
#endif
|
||||
#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)
|
||||
// The preset avoids a LOW blip when going HIGH, but a LOW only takes after the switch (measured on a BW15),
|
||||
// so the write below is still needed
|
||||
if (mode == OUTPUT)
|
||||
rtl_preset_level(this->pin_, this->level_);
|
||||
#endif
|
||||
pinMode(this->pin_, mode); // NOLINT
|
||||
if (this->output_)
|
||||
digitalWrite(this->pin_, this->level_); // NOLINT
|
||||
}
|
||||
|
||||
size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
@@ -70,10 +120,12 @@ size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
}
|
||||
|
||||
bool ArduinoInternalGPIOPin::digital_read() {
|
||||
return bool(digitalRead(pin_)) ^ inverted_; // NOLINT
|
||||
return bool(digitalRead(this->pin_)) ^ this->inverted_; // NOLINT
|
||||
}
|
||||
void ArduinoInternalGPIOPin::digital_write(bool value) {
|
||||
digitalWrite(pin_, value ^ inverted_); // NOLINT
|
||||
this->level_ = value ^ this->inverted_;
|
||||
if (this->output_)
|
||||
digitalWrite(this->pin_, this->level_); // NOLINT
|
||||
}
|
||||
void ArduinoInternalGPIOPin::detach_interrupt() const {
|
||||
detachInterrupt(pin_); // NOLINT
|
||||
@@ -86,20 +138,16 @@ namespace esphome {
|
||||
using namespace libretiny;
|
||||
|
||||
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
return bool(digitalRead(arg->pin)) ^ arg->inverted; // NOLINT
|
||||
return reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->digital_read();
|
||||
}
|
||||
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
digitalWrite(arg->pin, value ^ arg->inverted); // NOLINT
|
||||
reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->digital_write(value);
|
||||
}
|
||||
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
detachInterrupt(arg->pin);
|
||||
detachInterrupt(reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->get_pin());
|
||||
}
|
||||
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
pinMode(arg->pin, flags_to_mode(flags)); // NOLINT
|
||||
reinterpret_cast<ArduinoInternalGPIOPin *>(this->arg_)->pin_mode(flags);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -30,7 +30,12 @@ class ArduinoInternalGPIOPin final : public GPIOPin {
|
||||
void attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_{};
|
||||
bool inverted_ : 1 {};
|
||||
// LibreTiny drops writes to a pin that is not an output; keep the level until it becomes one.
|
||||
// Remove with bk_enter_output() and rtl_preset_level() once LibreTiny handles this.
|
||||
// These bits share a byte with the ISR copy, so a pin must not be written from an ISR and the loop at once.
|
||||
bool output_ : 1 {};
|
||||
bool level_ : 1 {};
|
||||
gpio::Flags flags_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -380,7 +380,8 @@ def _final_validate(config: ConfigType) -> None:
|
||||
|
||||
for ref in refs:
|
||||
try:
|
||||
light_config = fv.get_declaring_config(ref.light_id, fconf)
|
||||
light_path = fconf.get_path_for_id(ref.light_id)[:-1]
|
||||
light_config = fconf.get_config_for_path(light_path)
|
||||
except KeyError:
|
||||
# Light ID not found — ID validation will have already reported this
|
||||
continue
|
||||
@@ -397,7 +398,8 @@ def _final_validate(config: ConfigType) -> None:
|
||||
|
||||
for ref in cycle_refs:
|
||||
try:
|
||||
light_config = fv.get_declaring_config(ref.light_id, fconf)
|
||||
light_path = fconf.get_path_for_id(ref.light_id)[:-1]
|
||||
light_config = fconf.get_config_for_path(light_path)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, EsphomeError, Lambda
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
import esphome.final_validate as fv
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -254,7 +253,8 @@ def _resolve_effect_index(config: ConfigType, original_name: str) -> int:
|
||||
from . import effect_not_found_message, find_effect_index
|
||||
|
||||
light_id = config[CONF_ID]
|
||||
light_config = fv.get_declaring_config(light_id, CORE.config)
|
||||
light_path = CORE.config.get_path_for_id(light_id)[:-1]
|
||||
light_config = CORE.config.get_config_for_path(light_path)
|
||||
effects = light_config.get(CONF_EFFECTS, [])
|
||||
index = find_effect_index(effects, original_name)
|
||||
if index is not None:
|
||||
|
||||
@@ -28,7 +28,6 @@ from esphome.const import (
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_types import FixedVector
|
||||
|
||||
from ..automation import action_to_code
|
||||
@@ -42,7 +41,6 @@ from ..defines import (
|
||||
CONF_RADIUS,
|
||||
CONF_SRC,
|
||||
CONF_START_ANGLE,
|
||||
LValidator,
|
||||
addr,
|
||||
literal,
|
||||
)
|
||||
@@ -59,7 +57,7 @@ from ..lv_validation import (
|
||||
)
|
||||
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
||||
from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property
|
||||
from ..types import LvType, ObjUpdateAction, lv_gradient_t, lv_point_precise_t
|
||||
from ..types import LvType, ObjUpdateAction, lv_point_precise_t
|
||||
from . import Widget, WidgetType, get_widgets
|
||||
from .img import CONF_IMAGE
|
||||
|
||||
@@ -223,6 +221,7 @@ RECT_PROPS = {
|
||||
"radius",
|
||||
"bg_opa",
|
||||
"bg_color",
|
||||
"bg_grad",
|
||||
"border_color",
|
||||
"border_width",
|
||||
"border_opa",
|
||||
@@ -240,30 +239,6 @@ RECT_PROPS = {
|
||||
"shadow_opa",
|
||||
)
|
||||
}
|
||||
RECT_PROPS.update(
|
||||
bg_grad=LValidator(
|
||||
cv.use_id(lv_gradient_t), lv_gradient_t, retmapper=lambda v: MockObj(f"*{v}")
|
||||
)
|
||||
)
|
||||
RECT_SCHEMA = {
|
||||
**{cv.Optional(prop): validator for prop, validator in RECT_PROPS.items()},
|
||||
cv.Optional(CONF_OPA): cv.invalid(
|
||||
"Rectangles have no single opacity; use bg_opa, border_opa, outline_opa or shadow_opa"
|
||||
),
|
||||
}
|
||||
POLYGON_PROPS = {
|
||||
p: RECT_PROPS[p] for p in ("border_color", "border_width", "border_opa")
|
||||
}
|
||||
POLYGON_SCHEMA = {
|
||||
**{cv.Optional(prop): validator for prop, validator in POLYGON_PROPS.items()},
|
||||
**{
|
||||
cv.Optional(prop): cv.invalid(
|
||||
"Polygons are drawn as an outline; use border_color, border_width and border_opa"
|
||||
)
|
||||
for prop in (*RECT_PROPS, CONF_OPA)
|
||||
if prop not in POLYGON_PROPS
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _draw_line(layer, dsc, points):
|
||||
@@ -289,10 +264,10 @@ def _draw_line(layer, dsc, points):
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
**DRAW_SCHEMA,
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_WIDTH): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_HEIGHT): cv.templatable(cv.int_),
|
||||
**RECT_SCHEMA,
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
@@ -448,8 +423,8 @@ async def canvas_draw_image(config, action_id, template_arg, args):
|
||||
LINE_PROPS = {
|
||||
"width": STYLE_PROPS["line_width"],
|
||||
"color": STYLE_PROPS["line_color"],
|
||||
"dash_width": STYLE_PROPS["line_dash_width"],
|
||||
"dash_gap": STYLE_PROPS["line_dash_gap"],
|
||||
"dash-width": STYLE_PROPS["line_dash_width"],
|
||||
"dash-gap": STYLE_PROPS["line_dash_gap"],
|
||||
"round_start": lv_bool,
|
||||
"round_end": lv_bool,
|
||||
}
|
||||
@@ -499,7 +474,7 @@ async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
**POLYGON_SCHEMA,
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
},
|
||||
).add_extra(_validate_points),
|
||||
synchronous=True,
|
||||
@@ -528,7 +503,7 @@ async def canvas_draw_polygon(config, action_id, template_arg, args):
|
||||
_draw_line(layer, line_dsc, points)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "rect", POLYGON_PROPS, do_draw_polygon, action_id, template_arg, args
|
||||
config, "rect", RECT_PROPS, do_draw_polygon, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
CONF_X,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_types import nullptr
|
||||
|
||||
@@ -65,7 +64,7 @@ from ..lv_validation import (
|
||||
)
|
||||
from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj
|
||||
from ..schemas import STATE_SCHEMA
|
||||
from ..styles import LVStyle, create_style
|
||||
from ..styles import LVStyle
|
||||
from ..types import (
|
||||
LV_EVENT,
|
||||
LvCompound,
|
||||
@@ -101,12 +100,6 @@ CONF_SCALES = "scales"
|
||||
CONF_STRIDE = "stride"
|
||||
CONF_TICK_STYLE = "tick_style"
|
||||
|
||||
|
||||
def _arc_style_name(indicator_id: str) -> str:
|
||||
"""Name of the style variable generated for an arc indicator."""
|
||||
return f"meter_arc_{indicator_id}_style"
|
||||
|
||||
|
||||
# LVGL 9.4 Migration: Use scale widget instead of removed meter widget
|
||||
#
|
||||
# The lv_meter widget was removed in LVGL 9.4 and replaced with the more
|
||||
@@ -430,9 +423,11 @@ class MeterType(WidgetType):
|
||||
get_warnings().add(
|
||||
"The 'r_mod' indicator property is not supported in LVGL 9.x and will be ignored."
|
||||
)
|
||||
style_var = await create_style(_arc_style_name(iid.id), props)
|
||||
arc_style = LVStyle(f"meter_arc_{iid.id}", props)
|
||||
tvar = cg.Pvariable(iid, lv_expr.scale_add_section(scale_var))
|
||||
lv.scale_section_set_style(tvar, LV_PART.MAIN, style_var)
|
||||
lv.scale_section_set_style(
|
||||
tvar, LV_PART.MAIN, await arc_style.get_var()
|
||||
)
|
||||
lw = Widget.create(iid, tvar, arc_indicator_type)
|
||||
lw.parent = scale_var
|
||||
await set_indicator_values(scale_var, lw, v)
|
||||
@@ -615,15 +610,6 @@ async def indicator_update_to_code(config, action_id, template_arg, args):
|
||||
|
||||
async def set_value(w: Widget):
|
||||
await set_indicator_values(w.parent, w, config)
|
||||
if (opa := config.get(CONF_OPA)) is None:
|
||||
return
|
||||
opa = await opacity.process(opa)
|
||||
if w.type is arc_indicator_type:
|
||||
style = await cg.get_variable(ID(_arc_style_name(str(w.var))))
|
||||
lv.style_set_arc_opa(style, opa)
|
||||
lv_obj.invalidate(w.parent)
|
||||
else:
|
||||
lv_obj.set_style_opa(w.obj, opa, LV_PART.MAIN)
|
||||
|
||||
return await action_to_code(widget, set_value, action_id, template_arg, args)
|
||||
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
import functools
|
||||
import importlib.util
|
||||
from typing import Any, Self
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome import pins
|
||||
from esphome.components.const import CONF_COLOR_DEPTH
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns
|
||||
import esphome.config_validation as cv
|
||||
@@ -25,7 +23,6 @@ from esphome.const import (
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_NUMBER,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_OFFSET_WIDTH,
|
||||
CONF_PAGES,
|
||||
@@ -309,57 +306,6 @@ def model_schema_extractor(
|
||||
return decorate
|
||||
|
||||
|
||||
def _is_pin_expander(key: str) -> bool:
|
||||
"""Check if a pin config key names a component that provides pins, e.g. an I/O expander."""
|
||||
from esphome import loader
|
||||
|
||||
if key not in pins.PIN_SCHEMA_REGISTRY:
|
||||
if importlib.util.find_spec(f"esphome.components.{key}") is None:
|
||||
return False
|
||||
# Loading the component registers its pin schema. If it fails to load (the
|
||||
# loader logs why), still require it rather than quietly dropping it.
|
||||
if loader.get_component(key) is None:
|
||||
return True
|
||||
return key in pins.PIN_SCHEMA_REGISTRY
|
||||
|
||||
|
||||
def _pin_expanders(value: Any) -> set[str]:
|
||||
"""Find the pin expander components used by any pin configs in a model's defaults."""
|
||||
if isinstance(value, (list, tuple)):
|
||||
return set().union(*(_pin_expanders(v) for v in value))
|
||||
if not isinstance(value, dict):
|
||||
return set()
|
||||
found = set().union(*(_pin_expanders(v) for v in value.values()))
|
||||
if CONF_NUMBER in value:
|
||||
found.update(k for k in value if k != CONF_NUMBER and _is_pin_expander(k))
|
||||
return found
|
||||
|
||||
|
||||
def check_model_requirements(name: str, defaults: dict[str, Any]) -> None:
|
||||
"""
|
||||
Raise a friendly error if any component a display model requires is not configured.
|
||||
These are the components in its ``requires`` default plus any pin expander used by its
|
||||
default pins, which is needed even if the user config overrides those pins.
|
||||
|
||||
This runs during schema validation (before ID references are resolved) so that a
|
||||
model whose default pins live on a pin expander reports the missing expander clearly
|
||||
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
|
||||
"""
|
||||
# ``raw_config`` is populated before any component schema runs during a real
|
||||
# validation, so presence of a required component is simply a top-level key.
|
||||
# When it is absent (e.g. a unit test that invokes the schema directly) there
|
||||
# is no config to check against, so skip.
|
||||
global_config = CORE.raw_config
|
||||
if global_config is None:
|
||||
return
|
||||
requirements = set(defaults.get("requires", ())) | _pin_expanders(defaults)
|
||||
if missing := {x for x in requirements if x not in global_config}:
|
||||
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
|
||||
raise cv.Invalid(
|
||||
f"{name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
|
||||
)
|
||||
|
||||
|
||||
class DriverChip:
|
||||
"""
|
||||
A class representing a MIPI DBI driver chip model.
|
||||
@@ -725,14 +671,32 @@ class DriverChip:
|
||||
|
||||
def check_requirements(self) -> None:
|
||||
"""
|
||||
Check that the components this model needs are configured, see check_model_requirements().
|
||||
Raise a friendly error if any component this model requires is not configured.
|
||||
|
||||
This runs during schema validation (before ID references are resolved) so that a
|
||||
model whose default pins live on a pin expander reports the missing expander clearly
|
||||
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
|
||||
|
||||
Also logs a warning if the model is deprecated.
|
||||
"""
|
||||
if deprecation_reason := self.get_default("deprecation_reason"):
|
||||
LOGGER.warning(
|
||||
"Display model %s is deprecated: %s", self.name, deprecation_reason
|
||||
)
|
||||
check_model_requirements(self.name, self.defaults)
|
||||
if requirements := self.get_default("requires", set()):
|
||||
# ``raw_config`` is populated before any component schema runs during a real
|
||||
# validation, so presence of a required component is simply a top-level key.
|
||||
# When it is absent (e.g. a unit test that invokes the schema directly) there
|
||||
# is no config to check against, so skip.
|
||||
global_config = CORE.raw_config
|
||||
if global_config is None:
|
||||
return
|
||||
missing = {x for x in requirements if x not in global_config}
|
||||
if missing:
|
||||
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
|
||||
raise cv.Invalid(
|
||||
f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
|
||||
)
|
||||
|
||||
|
||||
def requires_buffer(config) -> bool:
|
||||
|
||||
@@ -85,7 +85,7 @@ COLOR_DEPTHS = {
|
||||
}
|
||||
|
||||
|
||||
def model_schema(config: ConfigType) -> cv.Schema:
|
||||
def model_schema(config: ConfigType) -> cv.All:
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
transform = model.transform_schema()
|
||||
# CUSTOM model will need to provide a custom init sequence
|
||||
@@ -97,14 +97,16 @@ def model_schema(config: ConfigType) -> cv.Schema:
|
||||
# Dimensions are optional if the model has a default width
|
||||
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
||||
pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24")
|
||||
return display.FULL_DISPLAY_SCHEMA.extend(
|
||||
schema = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
cv.GenerateID(): cv.declare_id(MipiDsi),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
|
||||
model.get_default(CONF_DRAW_ROUNDING, 1)
|
||||
),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
|
||||
@@ -138,6 +140,11 @@ def model_schema(config: ConfigType) -> cv.Schema:
|
||||
model.option(CONF_VSYNC_FRONT_PORCH): cv.int_,
|
||||
}
|
||||
)
|
||||
return cv.All(
|
||||
schema,
|
||||
cv.only_on_esp32,
|
||||
only_on_variant(supported=[VARIANT_ESP32P4]),
|
||||
)
|
||||
|
||||
|
||||
@model_schema_extractor(MODELS, model_schema)
|
||||
@@ -148,13 +155,9 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
config = cv.All(cv.only_on_esp32, only_on_variant(supported=[VARIANT_ESP32P4]))(
|
||||
config
|
||||
)
|
||||
# Check before the schema, which would fail on default pins of a missing expander
|
||||
config = model_schema(config)(config)
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.check_requirements()
|
||||
config = model_schema(config)(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ DsiDriverChip(
|
||||
pclk_frequency="60MHz",
|
||||
lane_bit_rate="730Mbps",
|
||||
color_order="RGB",
|
||||
requires={"psram"},
|
||||
requires={"psram", "pi4ioe5v6408"},
|
||||
reset_pin={"pi4ioe5v6408": {"address": 0x43}, "number": 4},
|
||||
initsequence=[
|
||||
(0xFF, 0x98, 0x81, 0x01), # Select Page 1
|
||||
@@ -69,7 +69,7 @@ TAB5_ST7123 = DsiDriverChip(
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="960Mbps",
|
||||
color_order="RGB",
|
||||
requires={"psram"},
|
||||
requires={"psram", "pi4ioe5v6408"},
|
||||
reset_pin={"pi4ioe5v6408": {"address": 0x43}, "number": 4},
|
||||
initsequence=[
|
||||
(0x01,),
|
||||
@@ -122,7 +122,7 @@ DsiDriverChip(
|
||||
pclk_frequency="70MHz",
|
||||
lane_bit_rate="965Mbps",
|
||||
color_order="RGB",
|
||||
requires={"psram"},
|
||||
requires={"psram", "pi4ioe5v6408"},
|
||||
reset_pin={"pi4ioe5v6408": {"address": 0x43}, "number": 4},
|
||||
initsequence=[
|
||||
(0x01,),
|
||||
|
||||
@@ -17,7 +17,7 @@ DsiDriverChip(
|
||||
color_order="RGB",
|
||||
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
|
||||
reset_pin={"xl9535": None, "number": 2},
|
||||
requires={"psram"},
|
||||
requires={"psram", "xl9535"},
|
||||
initsequence=(
|
||||
(0xE0, 0x00),
|
||||
(0xE1, 0x93),
|
||||
|
||||
@@ -154,7 +154,9 @@ def model_schema(config: ConfigType) -> cv.Schema:
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
|
||||
model.get_default(CONF_DRAW_ROUNDING, 1)
|
||||
),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
model.option(CONF_COLOR_ORDER, MODE_RGB): cv.enum(COLOR_ORDERS, upper=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
|
||||
@@ -224,14 +226,14 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
schema = model_schema(config)
|
||||
config = cv.All(
|
||||
schema,
|
||||
cv.only_on_esp32,
|
||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4, VARIANT_ESP32S31]),
|
||||
)(config)
|
||||
# Check before the schema, which would fail on default pins of a missing expander
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.check_requirements()
|
||||
config = model_schema(config)(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ ST7701S(
|
||||
spi_mode="MODE3",
|
||||
cs_pin={"xl9535": None, "number": 17},
|
||||
reset_pin={"xl9535": None, "number": 5},
|
||||
requires={"psram"},
|
||||
requires={"psram", "xl9535"},
|
||||
hsync_pin=39,
|
||||
vsync_pin=40,
|
||||
pclk_pin=41,
|
||||
@@ -57,7 +57,7 @@ t_rgb = ST7701S(
|
||||
invert_colors=False,
|
||||
spi_mode="MODE3",
|
||||
cs_pin={"xl9535": None, "number": 3},
|
||||
requires={"psram"},
|
||||
requires={"psram", "xl9535"},
|
||||
de_pin=45,
|
||||
hsync_pin=47,
|
||||
vsync_pin=41,
|
||||
|
||||
@@ -115,7 +115,7 @@ st7701s.extend(
|
||||
vsync_pulse_width=8,
|
||||
vsync_back_porch=20,
|
||||
cs_pin={"pca9554": None, "number": 4},
|
||||
requires={"psram"},
|
||||
requires={"psram", "pca9554"},
|
||||
de_pin=18,
|
||||
hsync_pin=16,
|
||||
vsync_pin=17,
|
||||
|
||||
@@ -12,7 +12,7 @@ wave_4_3 = RgbDriverChip(
|
||||
pclk_frequency="16MHz",
|
||||
reset_pin={"ch422g": None, "number": 3},
|
||||
enable_pin={"ch422g": None, "number": 2},
|
||||
requires={"psram"},
|
||||
requires={"psram", "ch422g"},
|
||||
de_pin=5,
|
||||
hsync_pin={"number": 46, "ignore_strapping_warning": True},
|
||||
vsync_pin={"number": 3, "ignore_strapping_warning": True},
|
||||
|
||||
@@ -197,7 +197,9 @@ def model_schema(config: ConfigType) -> cv.All | cv.Schema:
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MipiSpi),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(1),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
|
||||
COLOR_ORDERS, upper=True
|
||||
),
|
||||
|
||||
@@ -41,7 +41,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
display_config = fv.get_declaring_config(config[CONF_DISPLAY_ID])
|
||||
full_config = fv.full_config.get()
|
||||
display_path = full_config.get_path_for_id(config[CONF_DISPLAY_ID])[:-1]
|
||||
display_config = full_config.get_config_for_path(display_path)
|
||||
if CONF_BRIGHTNESS not in display_config:
|
||||
model = display_config[CONF_MODEL]
|
||||
if model == "CUSTOM":
|
||||
|
||||
@@ -22,6 +22,7 @@ DriverChip(
|
||||
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
|
||||
use_axis_flips=True,
|
||||
reset_delay=20,
|
||||
requires={"pca9554"},
|
||||
initsequence=(
|
||||
delay(100),
|
||||
(0xFF, 0x20, 0x10, 0x10),
|
||||
|
||||
@@ -310,7 +310,9 @@ def migrate_custom_command(config: ConfigType) -> None:
|
||||
frame = config.get(CONF_CUSTOM_COMMAND)
|
||||
if frame is None:
|
||||
return
|
||||
controller = fv.get_declaring_config(config[CONF_MODBUS_CONTROLLER_ID])
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
# the controller's DEVICE address (from modbus_device_schema)
|
||||
address = controller[CONF_ADDRESS]
|
||||
if frame[0] != address:
|
||||
@@ -340,7 +342,9 @@ def _reject_continuous_write_custom_pdu(config: ConfigType) -> None:
|
||||
pdu = config.get(CONF_CUSTOM_PDU)
|
||||
if pdu is None or not modbus.is_function_code_write(pdu[0]):
|
||||
return
|
||||
controller = fv.get_declaring_config(config[CONF_MODBUS_CONTROLLER_ID])
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
if controller.get(CONF_CONTINUOUS) is True:
|
||||
raise cv.Invalid(
|
||||
f"a '{CONF_CUSTOM_PDU}' with a write function code (0x{pdu[0] & 0x7F:02X}) can't be polled "
|
||||
@@ -355,7 +359,9 @@ def _reject_broadcastable_custom_pdu(config: ConfigType) -> None:
|
||||
pdu = config.get(CONF_CUSTOM_PDU)
|
||||
if pdu is None or not modbus.is_function_code_broadcastable(pdu[0]):
|
||||
return
|
||||
controller = fv.get_declaring_config(config[CONF_MODBUS_CONTROLLER_ID])
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
if (
|
||||
controller.get(CONF_ADDRESS) == modbus.BROADCAST_ADDRESS
|
||||
and controller.get(modbus.CONF_ALLOW_BROADCAST_READ) is True
|
||||
@@ -378,7 +384,9 @@ def validate_custom_pdu_item(config: ConfigType) -> None:
|
||||
def _reject_write_option_off_broadcast(config: ConfigType) -> None:
|
||||
if not any(config.get(key) is True for key in modbus.broadcast_only_option_keys()):
|
||||
return
|
||||
controller = fv.get_declaring_config(config[CONF_MODBUS_CONTROLLER_ID])
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
if controller.get(CONF_ADDRESS) != modbus.BROADCAST_ADDRESS:
|
||||
raise cv.Invalid(
|
||||
f"'{modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE}' only applies when the "
|
||||
|
||||
@@ -50,7 +50,9 @@ MAX_RECOMMENDED_UPDATE_INTERVAL_MS = 100
|
||||
|
||||
def get_motion_config(motion_id: ID) -> dict:
|
||||
"""Look up the (already-validated) config of the motion hub referenced by motion_id."""
|
||||
return fv.get_declaring_config(motion_id)
|
||||
full_config = fv.full_config.get()
|
||||
motion_path = full_config.get_path_for_id(motion_id)[:-1]
|
||||
return full_config.get_config_for_path(motion_path)
|
||||
|
||||
|
||||
def check_update_interval(motion_id: ID, feature_name: str) -> None:
|
||||
|
||||
@@ -67,7 +67,8 @@ def _final_validate(config: ConfigType) -> None:
|
||||
"Max touch channel must equal the highest binary sensor channel or be removed for auto calculation",
|
||||
path=[CONF_MAX_TOUCH_CHANNEL],
|
||||
)
|
||||
this_config = fv.get_declaring_config(config[CONF_ID], fconf)
|
||||
path = fconf.get_path_for_id(config[CONF_ID])[:-1]
|
||||
this_config = fconf.get_config_for_path(path)
|
||||
this_config[CONF_MAX_TOUCH_CHANNEL] = max_touch_channel
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ CONF_PRECISION = "precision"
|
||||
CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake"
|
||||
CONF_START_UP_PAGE = "start_up_page"
|
||||
CONF_STARTUP_OVERRIDE_MS = "startup_override_ms"
|
||||
CONF_TFT_UPLOAD_HTTP_BUFFER_SIZE_TX = "tft_upload_http_buffer_size_tx"
|
||||
CONF_TFT_UPLOAD_HTTP_RETRIES = "tft_upload_http_retries"
|
||||
CONF_TFT_UPLOAD_HTTP_TIMEOUT = "tft_upload_http_timeout"
|
||||
CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT = "tft_upload_watchdog_timeout"
|
||||
|
||||
@@ -33,7 +33,6 @@ from .base_component import (
|
||||
CONF_SKIP_CONNECTION_HANDSHAKE,
|
||||
CONF_START_UP_PAGE,
|
||||
CONF_STARTUP_OVERRIDE_MS,
|
||||
CONF_TFT_UPLOAD_HTTP_BUFFER_SIZE_TX,
|
||||
CONF_TFT_UPLOAD_HTTP_RETRIES,
|
||||
CONF_TFT_UPLOAD_HTTP_TIMEOUT,
|
||||
CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT,
|
||||
@@ -54,9 +53,10 @@ def AUTO_LOAD() -> list[str]:
|
||||
|
||||
|
||||
def _validate_tft_upload(config):
|
||||
for key in (CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT, CONF_TFT_UPLOAD_HTTP_BUFFER_SIZE_TX):
|
||||
if key in config and not CORE.is_esp32:
|
||||
raise cv.Invalid(f"{key} is only available on ESP32")
|
||||
if CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT in config and not CORE.is_esp32:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT} is only available on ESP32"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -105,9 +105,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
|
||||
cv.Optional(CONF_TFT_UPLOAD_HTTP_BUFFER_SIZE_TX): cv.int_range(
|
||||
min=512, max=8192
|
||||
),
|
||||
cv.Optional(CONF_TFT_UPLOAD_HTTP_RETRIES): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_TFT_UPLOAD_HTTP_TIMEOUT): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
@@ -139,7 +136,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
(
|
||||
CONF_TFT_UPLOAD_HTTP_TIMEOUT,
|
||||
CONF_TFT_UPLOAD_HTTP_RETRIES,
|
||||
CONF_TFT_UPLOAD_HTTP_BUFFER_SIZE_TX,
|
||||
CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT,
|
||||
),
|
||||
CONF_TFT_URL,
|
||||
@@ -248,14 +244,6 @@ async def to_code(config):
|
||||
)
|
||||
)
|
||||
|
||||
# TFT upload HTTP TX buffer size (ESP32 only; unset = ESP-IDF default)
|
||||
if CONF_TFT_UPLOAD_HTTP_BUFFER_SIZE_TX in config:
|
||||
cg.add(
|
||||
var.set_tft_upload_http_buffer_size_tx(
|
||||
config[CONF_TFT_UPLOAD_HTTP_BUFFER_SIZE_TX]
|
||||
)
|
||||
)
|
||||
|
||||
# TFT upload HTTP retries (default: 5)
|
||||
if CONF_TFT_UPLOAD_HTTP_RETRIES in config:
|
||||
cg.add(
|
||||
|
||||
@@ -1089,19 +1089,6 @@ class Nextion final : public NextionBase, public PollingComponent, public uart::
|
||||
void set_tft_upload_http_timeout(uint16_t timeout_ms) { this->tft_upload_http_timeout_ = timeout_ms; }
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/**
|
||||
* @brief Set the HTTP client TX buffer size for TFT upload requests.
|
||||
*
|
||||
* The TX buffer must hold the request line, which carries the full URL. After a redirect
|
||||
* to a long signed URL (e.g. a GitHub release asset, ~600+ characters) the ESP-IDF default
|
||||
* is too small and the request fails with "HTTP_CLIENT: Out of buffer".
|
||||
*
|
||||
* A value of 0 keeps the ESP-IDF default (default).
|
||||
*
|
||||
* @param size Buffer size in bytes.
|
||||
*/
|
||||
void set_tft_upload_http_buffer_size_tx(uint16_t size) { this->tft_upload_http_buffer_size_tx_ = size; }
|
||||
|
||||
/**
|
||||
* @brief Set the watchdog timeout during TFT upload.
|
||||
*
|
||||
@@ -1586,8 +1573,7 @@ class Nextion final : public NextionBase, public PollingComponent, public uart::
|
||||
uint8_t tft_upload_http_retries_{5}; ///< HTTP retry count (default: 5)
|
||||
|
||||
#ifdef USE_ESP32
|
||||
uint32_t tft_upload_watchdog_timeout_{0}; ///< WDT timeout in ms (0 = no adjustment)
|
||||
uint16_t tft_upload_http_buffer_size_tx_{0}; ///< HTTP TX buffer in bytes (0 = ESP-IDF default)
|
||||
uint32_t tft_upload_watchdog_timeout_{0}; ///< WDT timeout in ms (0 = no adjustment)
|
||||
|
||||
/**
|
||||
* will request 4096 bytes chunks from the web server
|
||||
|
||||
@@ -283,7 +283,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
|
||||
.timeout_ms = static_cast<int>(this->tft_upload_http_timeout_),
|
||||
.disable_auto_redirect = false,
|
||||
.max_redirection_count = 10,
|
||||
.buffer_size_tx = this->tft_upload_http_buffer_size_tx_, // 0 = ESP-IDF default
|
||||
};
|
||||
// Initialize the HTTP client with the configuration
|
||||
esp_http_client_handle_t http_client = esp_http_client_init(&config);
|
||||
|
||||
@@ -48,7 +48,9 @@ def _final_validate(config: ConfigType) -> None:
|
||||
if config[CONF_TYPE] != CONF_STATUS:
|
||||
# Only run this validation if a status sensor is being configured
|
||||
return
|
||||
transport_config = fv.get_declaring_config(config[CONF_TRANSPORT_ID])
|
||||
full_config = fv.full_config.get()
|
||||
transport_path = full_config.get_path_for_id(config[CONF_TRANSPORT_ID])[:-1]
|
||||
transport_config = full_config.get_config_for_path(transport_path)
|
||||
if transport_config[CONF_PING_PONG_ENABLE] and any(
|
||||
CONF_ENCRYPTION in p
|
||||
for p in transport_config[CONF_PROVIDERS]
|
||||
|
||||
@@ -35,8 +35,11 @@ def validate_from_to(value: ConfigType) -> ConfigType:
|
||||
|
||||
|
||||
def validate_segment(config: ConfigType) -> None:
|
||||
fconf = fv.full_config.get()
|
||||
|
||||
if CONF_ID in config: # only validate addressable segments
|
||||
segment_light_config = fv.get_declaring_config(config[CONF_ID])
|
||||
path = fconf.get_path_for_id(config[CONF_ID])[:-1]
|
||||
segment_light_config = fconf.get_config_for_path(path)
|
||||
|
||||
if CONF_NUM_LEDS in segment_light_config:
|
||||
segment_len = segment_light_config[CONF_NUM_LEDS]
|
||||
|
||||
@@ -56,7 +56,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
hub_conf = fv.get_declaring_config(config[CONF_TAS58XX_ID])
|
||||
fconf = fv.full_config.get()
|
||||
hub_path = fconf.get_path_for_id(config[CONF_TAS58XX_ID])
|
||||
hub_conf = fconf.get_config_for_path(hub_path[:-1])
|
||||
|
||||
if hub_conf[CONF_MODEL] == DAC_TAS5825M:
|
||||
return config
|
||||
|
||||
@@ -65,7 +65,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
"""Resolve the key to the number the device reports, checked against the keypad's matrix."""
|
||||
keypad = fv.get_declaring_config(config[CONF_KEYPAD_ID])
|
||||
full_config = fv.full_config.get()
|
||||
keypad_path = full_config.get_path_for_id(config[CONF_KEYPAD_ID])[:-1]
|
||||
keypad = full_config.get_config_for_path(keypad_path)
|
||||
rows = keypad.get(CONF_ROWS, 0)
|
||||
columns = keypad.get(CONF_COLUMNS, 0)
|
||||
|
||||
|
||||
@@ -59,7 +59,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
if CONF_ACTUAL_CURRENT not in config:
|
||||
return config
|
||||
if fv.get_declaring_config(config[CONF_TMC2209_ID]).get(CONF_ANALOG_CURRENT_SCALE):
|
||||
full_config = fv.full_config.get()
|
||||
path = full_config.get_path_for_id(config[CONF_TMC2209_ID])[:-1]
|
||||
if full_config.get_config_for_path(path).get(CONF_ANALOG_CURRENT_SCALE):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_ACTUAL_CURRENT}' can't be used with '{CONF_ANALOG_CURRENT_SCALE}', "
|
||||
"the current then depends on VREF",
|
||||
|
||||
@@ -56,7 +56,8 @@ _validate_unique = unique_child("number")
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
_validate_unique(config)
|
||||
parent = fv.get_declaring_config(config[CONF_TMP102_ID])
|
||||
full = fv.full_config.get()
|
||||
parent = full.get_config_for_path(full.get_path_for_id(config[CONF_TMP102_ID])[:-1])
|
||||
add_number_parent(config[CONF_TMP102_ID], parent.get(CONF_EXTENDED_MODE, False))
|
||||
return config
|
||||
|
||||
|
||||
+3
-14
@@ -1136,7 +1136,9 @@ class IDPassValidationStep(ConfigValidationStep):
|
||||
m
|
||||
for m in matches
|
||||
if isinstance(
|
||||
candidate_conf := fv.get_declaring_config(m, result),
|
||||
candidate_conf := result.get_config_for_path(
|
||||
result.get_path_for_id(m)[:-1]
|
||||
),
|
||||
dict,
|
||||
)
|
||||
and all(
|
||||
@@ -1262,18 +1264,6 @@ class CoreFinalValidateStep(ConfigValidationStep):
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
class OptionalPinValidationStep(ConfigValidationStep):
|
||||
"""Remove optional pins that are also used elsewhere"""
|
||||
|
||||
# After the ID pass, and before component final validations which may inspect pin use
|
||||
priority = -15.0
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if result.errors:
|
||||
return
|
||||
pins.PIN_SCHEMA_REGISTRY.remove_optional_conflicts(result)
|
||||
|
||||
|
||||
class PinUseValidationCheck(ConfigValidationStep):
|
||||
"""Check for pin reuse"""
|
||||
|
||||
@@ -1447,7 +1437,6 @@ def validate_config(
|
||||
result.add_validation_step(PrefetchRemoteFilesValidationStep())
|
||||
result.add_validation_step(IDPassValidationStep())
|
||||
result.add_validation_step(CoreFinalValidateStep())
|
||||
result.add_validation_step(OptionalPinValidationStep())
|
||||
result.add_validation_step(PinUseValidationCheck())
|
||||
|
||||
result.add_validation_step(RemoveReferenceValidationStep())
|
||||
|
||||
@@ -646,12 +646,14 @@ def inherit_property_from(property_to_inherit, parent_id_property, transform=Non
|
||||
|
||||
# Get config for the parent entity
|
||||
parent_id = _walk_config(config, parent_id_property)
|
||||
parent_config = fv.get_declaring_config(parent_id, fconf)
|
||||
parent_path = fconf.get_path_for_id(parent_id)[:-1]
|
||||
parent_config = fconf.get_config_for_path(parent_path)
|
||||
|
||||
# If parent sensor has the property set, inherit it
|
||||
if property in parent_config:
|
||||
path = fconf.get_path_for_id(config[CONF_ID])[:-1]
|
||||
this_config = _walk_config(
|
||||
fv.get_declaring_config(config[CONF_ID], fconf), property_path
|
||||
fconf.get_config_for_path(path), property_path
|
||||
)
|
||||
value = parent_config[property]
|
||||
if transform:
|
||||
|
||||
+17
-115
@@ -1,10 +1,8 @@
|
||||
from collections.abc import Callable, Hashable
|
||||
from contextvars import ContextVar
|
||||
from functools import reduce
|
||||
import logging
|
||||
from logging import Logger
|
||||
import operator
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -23,65 +21,17 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
import esphome.final_validate as fv
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.schema_extractors import schema_extractor
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.config import Config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# A use of a pin: the path of the component using it, the pin provider's id and the pin config
|
||||
PinUse = tuple[list, Any, ConfigType]
|
||||
|
||||
# Set while validating pins that should give way to any other use of the same pin
|
||||
_validating_optional = ContextVar("validating_optional_pins", default=False)
|
||||
|
||||
|
||||
def _remove_value(conf: Any, target: Any) -> bool:
|
||||
"""Remove the object ``target`` from wherever it is nested in ``conf``."""
|
||||
items = conf.items() if isinstance(conf, dict) else enumerate(conf)
|
||||
for key, value in items:
|
||||
if value is target:
|
||||
del conf[key]
|
||||
return True
|
||||
if isinstance(value, (dict, list)) and _remove_value(value, target):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PinRegistry(dict):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pins_used = {}
|
||||
# Entries of pins_used that give way to other uses, keyed by id()
|
||||
self.optional_pins = {}
|
||||
|
||||
def reset(self):
|
||||
self.pins_used = {}
|
||||
self.optional_pins = {}
|
||||
|
||||
def optional(self, validator: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
||||
"""
|
||||
Wrap a pin validator so the pins it validates give way to any other use of the
|
||||
same pin. Instead of the pin being reported as used in multiple places,
|
||||
remove_optional_conflicts() removes it from the config, unless every other use
|
||||
sets allow_other_uses, in which case the pin is shared. Use this for pins a
|
||||
component supplies as defaults that a user may want to take over.
|
||||
"""
|
||||
|
||||
@schema_extractor("schema")
|
||||
def wrapper(value: Any) -> Any:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return validator
|
||||
token = _validating_optional.set(True)
|
||||
try:
|
||||
return validator(value)
|
||||
finally:
|
||||
_validating_optional.reset(token)
|
||||
|
||||
return wrapper
|
||||
|
||||
def get_count(self, key, id, number):
|
||||
"""
|
||||
@@ -136,10 +86,7 @@ class PinRegistry(dict):
|
||||
self.pins_used[pin_key] = []
|
||||
# client_id identifies the instance of the providing component
|
||||
client_id = result.get(key)
|
||||
entry = (path_context.get(), client_id, result)
|
||||
self.pins_used[pin_key].append(entry)
|
||||
if _validating_optional.get():
|
||||
self.optional_pins[id(entry)] = entry
|
||||
self.pins_used[pin_key].append((path_context.get(), client_id, result))
|
||||
# return the validated pin config
|
||||
return result
|
||||
|
||||
@@ -163,76 +110,31 @@ class PinRegistry(dict):
|
||||
key = self.get_key(key) if isinstance(key, dict) else key
|
||||
return self[key][0]
|
||||
|
||||
def _grouped(self) -> dict[tuple, list[PinUse]]:
|
||||
"""Group pin uses by (schema key, resolved provider id, pin number)."""
|
||||
# Regroup by (schema key, resolved provider id, pin number) rather than
|
||||
# the validate()-time bucket, so the same physical pin reached via an
|
||||
# explicit id and via {address: ...} (or an omitted id) is recognized as
|
||||
# one reuse group regardless of which syntax was used to reference the hub.
|
||||
grouped: dict[tuple, list[PinUse]] = {}
|
||||
for (key, _, number), pin_list in self.pins_used.items():
|
||||
for entry in pin_list:
|
||||
client_id = entry[1]
|
||||
provider_id = client_id.id if isinstance(client_id, ID) else client_id
|
||||
grouped.setdefault((key, provider_id, number), []).append(entry)
|
||||
return grouped
|
||||
|
||||
def remove_optional_conflicts(self, fconf: "Config") -> None:
|
||||
"""
|
||||
Remove optional pins that are also used elsewhere from the config, or mark them
|
||||
as shared if every other use sets allow_other_uses. This runs once ids are resolved and before the components' final validation,
|
||||
so those only see the pins that will be used.
|
||||
:param fconf: The full config
|
||||
"""
|
||||
if not self.optional_pins:
|
||||
return
|
||||
for (key, provider_id, _), pin_list in self._grouped().items():
|
||||
optional = [e for e in pin_list if id(e) in self.optional_pins]
|
||||
others = [e for e in pin_list if id(e) not in self.optional_pins]
|
||||
if not optional or not others:
|
||||
continue
|
||||
if all(e[2].get(CONF_ALLOW_OTHER_USES, False) for e in others):
|
||||
# The other uses ask to share the pin, so share it with the optional ones too
|
||||
for _, _, pin_config in optional:
|
||||
pin_config[CONF_ALLOW_OTHER_USES] = True
|
||||
continue
|
||||
for entry in optional:
|
||||
self._remove_optional(fconf, key, provider_id, entry)
|
||||
|
||||
def _remove_optional(
|
||||
self, fconf: "Config", key: str, provider_id: str | None, entry: PinUse
|
||||
) -> None:
|
||||
"""
|
||||
Remove an optional pin from the config and from the pins used. If it cannot be
|
||||
found in the config it is kept, so final_validate() reports the conflict.
|
||||
"""
|
||||
pin_path, _, pin_config = entry
|
||||
if not _remove_value(fconf.get_config_for_path(pin_path), pin_config):
|
||||
return
|
||||
for pin_list in self.pins_used.values():
|
||||
pin_list[:] = [e for e in pin_list if e is not entry]
|
||||
del self.optional_pins[id(entry)]
|
||||
pin = f"{key} {provider_id} pin" if provider_id is not None else "Pin"
|
||||
_LOGGER.info(
|
||||
"%s %s is used elsewhere, so is not applied as a default for %s",
|
||||
pin,
|
||||
pin_config[CONF_NUMBER],
|
||||
"->".join(str(p) for p in pin_path),
|
||||
)
|
||||
|
||||
def final_validate(self, fconf):
|
||||
"""
|
||||
Run the final validation for all pins, and check for reuse
|
||||
:param fconf: The full config
|
||||
"""
|
||||
for (key, _, _), pin_list in self._grouped().items():
|
||||
# Regroup by (schema key, resolved provider id, pin number) rather than
|
||||
# the validate()-time bucket, so the same physical pin reached via an
|
||||
# explicit id and via {address: ...} (or an omitted id) is recognized as
|
||||
# one reuse group regardless of which syntax was used to reference the hub.
|
||||
grouped: dict[tuple, list] = {}
|
||||
for (key, _, number), pin_list in self.pins_used.items():
|
||||
for entry in pin_list:
|
||||
client_id = entry[1]
|
||||
provider_id = client_id.id if isinstance(client_id, ID) else client_id
|
||||
grouped.setdefault((key, provider_id, number), []).append(entry)
|
||||
|
||||
for (key, _, _), pin_list in grouped.items():
|
||||
count = len(pin_list) # number of places same pin used.
|
||||
final_val_fun = self[key][2] # final validation function
|
||||
for pin_path, client_id, pin_config in pin_list:
|
||||
with fconf.catch_error([cv.ROOT_CONFIG_PATH] + pin_path):
|
||||
if final_val_fun is not None:
|
||||
# Get the containing path of the config providing this pin.
|
||||
parent_config = fv.get_declaring_config(client_id, fconf)
|
||||
parent_path = fconf.get_path_for_id(client_id)[:-1]
|
||||
parent_config = fconf.get_config_for_path(parent_path)
|
||||
final_val_fun(pin_config, parent_config)
|
||||
allow_others = pin_config.get(CONF_ALLOW_OTHER_USES, False)
|
||||
if count != 1 and not allow_others and not CORE.testing_mode:
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-sticky
|
||||
@@ -1,23 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
# Takes over the model's default enable pin
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: panel_power
|
||||
pin: GPIO47
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-sticky
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Tests for applying the default enable pins of epaper_spi models."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def _enable_pin_numbers(main_cpp: str) -> list[int]:
|
||||
"""Get the GPIO numbers passed to set_enable_pins() in the generated code."""
|
||||
match = re.search(r"epaper_display->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
if match is None:
|
||||
return []
|
||||
numbers = []
|
||||
for pin_id in match.group(1).split(", "):
|
||||
pin = re.search(rf"{pin_id}->set_pin\(::GPIO_NUM_(\d+)\);", main_cpp)
|
||||
assert pin is not None
|
||||
numbers.append(int(pin.group(1)))
|
||||
return numbers
|
||||
|
||||
|
||||
def test_default_enable_pin(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The model's enable pin is applied when it is not used elsewhere."""
|
||||
main_cpp = generate_main(component_config_path("enable_pin_default.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [47]
|
||||
|
||||
|
||||
def test_default_enable_pin_used_elsewhere(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A default enable pin used by a switch is left out instead of failing validation."""
|
||||
main_cpp = generate_main(component_config_path("enable_pin_used_elsewhere.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == []
|
||||
assert "panel_power->set_pin(" in main_cpp
|
||||
|
||||
|
||||
def test_user_enable_pins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Enable pins given in the config are applied as given."""
|
||||
main_cpp = generate_main(component_config_path("enable_pin_test.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [25, 26]
|
||||
@@ -1,26 +0,0 @@
|
||||
esphome:
|
||||
name: it8951-enable-pins
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
spi:
|
||||
clk_pin: 7
|
||||
mosi_pin: 9
|
||||
miso_pin: 8
|
||||
|
||||
# Takes over the model's default 1.8V logic supply enable pin
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: logic_supply
|
||||
pin: GPIO21
|
||||
|
||||
display:
|
||||
- platform: it8951
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-e1003
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Tests for it8951 configuration validation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
@@ -28,18 +26,3 @@ def test_reterminal_e1003_enable_pins_hold_state(
|
||||
enable_pins = config[CONF_ENABLE_PIN]
|
||||
assert [pin[CONF_NUMBER] for pin in enable_pins] == [21, 11]
|
||||
assert all(pin[CONF_HOLD_STATE] for pin in enable_pins)
|
||||
|
||||
|
||||
def test_reterminal_e1003_enable_pin_used_elsewhere(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A default enable pin used by a switch is left out, and the other is still applied."""
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins.yaml"))
|
||||
|
||||
match = re.search(r"epaper_display->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
assert match is not None
|
||||
pin_ids = match.group(1).split(", ")
|
||||
assert len(pin_ids) == 1
|
||||
assert f"{pin_ids[0]}->set_pin(::GPIO_NUM_11);" in main_cpp
|
||||
assert "logic_supply->set_pin(" in main_cpp
|
||||
@@ -1,50 +0,0 @@
|
||||
esphome:
|
||||
name: test-canvas-draw-options
|
||||
on_boot:
|
||||
- lvgl.canvas.draw_rectangle:
|
||||
id: test_canvas
|
||||
x: 0
|
||||
y: 0
|
||||
width: 20
|
||||
height: 10
|
||||
bg_grad: test_grad
|
||||
- lvgl.canvas.draw_line:
|
||||
id: test_canvas
|
||||
dash_width: 4
|
||||
dash_gap: 2
|
||||
points:
|
||||
- 0,0
|
||||
- 10,10
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
gradients:
|
||||
- id: test_grad
|
||||
direction: hor
|
||||
stops:
|
||||
- color: 0xFF0000
|
||||
position: 0
|
||||
- color: 0x0000FF
|
||||
position: 255
|
||||
widgets:
|
||||
- canvas:
|
||||
id: test_canvas
|
||||
width: 32
|
||||
height: 32
|
||||
@@ -1,58 +0,0 @@
|
||||
esphome:
|
||||
name: test-indicator-update-opa
|
||||
on_boot:
|
||||
- lvgl.indicator.update:
|
||||
id: test_arc
|
||||
opa: 50%
|
||||
- lvgl.indicator.update:
|
||||
id: test_needle
|
||||
value: 5
|
||||
opa: 25%
|
||||
- lvgl.indicator.update:
|
||||
id: test_image
|
||||
opa: 75%
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: image.png
|
||||
id: meter_image
|
||||
resize: 8x8
|
||||
type: binary
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- meter:
|
||||
height: 100px
|
||||
width: 100px
|
||||
scales:
|
||||
- id: test_scale
|
||||
range_from: 0
|
||||
range_to: 10
|
||||
indicators:
|
||||
- arc:
|
||||
id: test_arc
|
||||
start_value: 0
|
||||
end_value: 5
|
||||
- line:
|
||||
id: test_needle
|
||||
- image:
|
||||
id: test_image
|
||||
src: meter_image
|
||||
@@ -1,69 +0,0 @@
|
||||
"""Canvas draw options must map onto the LVGL 9 draw descriptor fields."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
import esphome.components.lvgl.widgets.canvas # noqa: F401 - registers the actions
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "canvas_draw_options_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_rect_gradient_is_copied_by_value(main_cpp: str) -> None:
|
||||
assert ".bg_grad = *test_grad;" in main_cpp
|
||||
|
||||
|
||||
def test_line_dash_fields(main_cpp: str) -> None:
|
||||
assert ".dash_width = 4;" in main_cpp
|
||||
assert ".dash_gap = 2;" in main_cpp
|
||||
|
||||
|
||||
def test_rect_opa_names_the_per_part_keys() -> None:
|
||||
with pytest.raises(cv.Invalid, match="bg_opa"):
|
||||
ACTION_REGISTRY["lvgl.canvas.draw_rectangle"].schema(
|
||||
{"id": "cv", "x": 0, "y": 0, "width": 10, "height": 10, "opa": "50%"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key", ["opa", "bg_color", "bg_grad", "radius", "shadow_width"]
|
||||
)
|
||||
def test_polygon_rejects_non_border_keys(key: str) -> None:
|
||||
with pytest.raises(cv.Invalid, match="drawn as an outline"):
|
||||
ACTION_REGISTRY["lvgl.canvas.draw_polygon"].schema(
|
||||
{"id": "cv", "points": [{"x": 0, "y": 0}], key: 1}
|
||||
)
|
||||
|
||||
|
||||
def test_polygon_accepts_border_keys() -> None:
|
||||
config = ACTION_REGISTRY["lvgl.canvas.draw_polygon"].schema(
|
||||
{
|
||||
"id": "cv",
|
||||
"points": [{"x": 0, "y": 0}],
|
||||
"border_color": 0xFF0000,
|
||||
"border_width": 2,
|
||||
"border_opa": "50%",
|
||||
}
|
||||
)
|
||||
assert config["border_width"] == 2
|
||||
@@ -1,49 +0,0 @@
|
||||
"""lvgl.indicator.update must apply opa to arc, needle and image indicators."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "indicator_update_opa_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_arc_section_opa_is_applied(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_style_set_arc_opa(meter_arc_test_arc_style, static_cast<uint8_t>(127.5f));"
|
||||
in main_cpp
|
||||
)
|
||||
assert "lv_obj_invalidate(test_scale);" in main_cpp
|
||||
|
||||
|
||||
def test_needle_opa_is_applied(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_obj_set_style_opa(test_needle->obj, static_cast<uint8_t>(63.75f), LV_PART_MAIN);"
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_image_opa_is_applied(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_obj_set_style_opa(test_image, static_cast<uint8_t>(191.25f), LV_PART_MAIN);"
|
||||
in main_cpp
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
esphome:
|
||||
name: enable-pins-address
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
speed: 200MHz
|
||||
|
||||
esp_ldo:
|
||||
- channel: 3
|
||||
voltage: 2.5V
|
||||
|
||||
i2c:
|
||||
sda: GPIO7
|
||||
scl: GPIO8
|
||||
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe1
|
||||
address: 0x43
|
||||
- id: pi4ioe2
|
||||
address: 0x44
|
||||
|
||||
# Takes over the default enable pin on the hub at 0x44, given by address
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: takeover
|
||||
pin:
|
||||
pi4ioe5v6408:
|
||||
address: 0x44
|
||||
number: 1
|
||||
|
||||
display:
|
||||
- platform: mipi_dsi
|
||||
id: default_pins
|
||||
model: TEST-ENABLE-BY-ADDRESS
|
||||
@@ -1,40 +0,0 @@
|
||||
"""Tests for applying the default enable pins of mipi_dsi models."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.mipi import DriverChip
|
||||
|
||||
|
||||
def test_default_enable_pins_by_address(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Default enable pins naming a hub by address resolve to that hub, and only
|
||||
the pin used elsewhere is left out, not the same pin number on the other hub."""
|
||||
from esphome.components.mipi_dsi.display import MODELS
|
||||
|
||||
models = set(DriverChip.models)
|
||||
model = MODELS["M5STACK-TAB5"].extend(
|
||||
"TEST-ENABLE-BY-ADDRESS",
|
||||
enable_pin=[
|
||||
{"pi4ioe5v6408": {"address": 0x43}, "number": 1},
|
||||
{"pi4ioe5v6408": {"address": 0x44}, "number": 1},
|
||||
],
|
||||
)
|
||||
for name in set(DriverChip.models) - models:
|
||||
monkeypatch.delitem(DriverChip.models, name)
|
||||
monkeypatch.setitem(MODELS, model.name, model)
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins_address.yaml"))
|
||||
|
||||
match = re.search(r"default_pins->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
assert match is not None
|
||||
pin_ids = match.group(1).split(", ")
|
||||
assert len(pin_ids) == 1
|
||||
assert f"{pin_ids[0]}->set_parent(pi4ioe1);" in main_cpp
|
||||
assert f"{pin_ids[0]}->set_pin(1);" in main_cpp
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4, VARIANT_ESP32S3
|
||||
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4
|
||||
|
||||
# Importing the I/O expanders registers their pin schemas with
|
||||
# pins.PIN_SCHEMA_REGISTRY so that models whose defaults reference expander-backed
|
||||
@@ -215,20 +215,3 @@ def test_code_generation(
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" not in main_cpp
|
||||
assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp
|
||||
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
|
||||
|
||||
|
||||
def test_unsupported_variant_reported_before_requirements(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""On an unsupported chip the variant error is shown, not the missing components."""
|
||||
from esphome.components.mipi_dsi.display import CONFIG_SCHEMA
|
||||
from esphome.core import CORE
|
||||
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(cv.Invalid, match=r"This feature is only available on ESP32P4"):
|
||||
CONFIG_SCHEMA({"model": "SEEED-RETERMINAL-D1001"})
|
||||
@@ -1,32 +0,0 @@
|
||||
esphome:
|
||||
name: enable-pins-test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
i2c:
|
||||
sda: 8
|
||||
scl: 9
|
||||
|
||||
ch422g:
|
||||
- id: ch422g_hub
|
||||
|
||||
# Takes over the model's default backlight enable pin
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: backlight
|
||||
pin:
|
||||
ch422g: ch422g_hub
|
||||
number: 2
|
||||
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
id: default_pins
|
||||
model: ESP32-S3-TOUCH-LCD-7-800X480
|
||||
update_interval: never
|
||||
@@ -1,24 +0,0 @@
|
||||
esphome:
|
||||
name: enable-pins-test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
i2c:
|
||||
sda: 8
|
||||
scl: 9
|
||||
|
||||
ch422g:
|
||||
- id: ch422g_hub
|
||||
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
id: default_pins
|
||||
model: ESP32-S3-TOUCH-LCD-7-800X480
|
||||
update_interval: never
|
||||
@@ -1,36 +0,0 @@
|
||||
"""Tests for applying the default enable pins of mipi_rgb models."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def _enable_pin_numbers(main_cpp: str) -> list[int]:
|
||||
"""Get the expander pin numbers passed to set_enable_pins() in the generated code."""
|
||||
match = re.search(r"default_pins->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
assert match is not None
|
||||
numbers = []
|
||||
for pin_id in match.group(1).split(", "):
|
||||
pin = re.search(rf"{pin_id}->set_pin\((\d+)\);", main_cpp)
|
||||
assert pin is not None
|
||||
numbers.append(int(pin.group(1)))
|
||||
return numbers
|
||||
|
||||
|
||||
def test_default_enable_pins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""All of the model's enable pins are applied when none are used elsewhere."""
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins_default.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [2, 6]
|
||||
|
||||
|
||||
def test_default_enable_pin_used_elsewhere(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A default enable pin used by a switch is left out instead of failing validation."""
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [6]
|
||||
assert "backlight->set_pin(" in main_cpp
|
||||
@@ -8,12 +8,7 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32S3
|
||||
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
|
||||
# Importing pca9554 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that
|
||||
@@ -92,78 +87,3 @@ def test_model_requires_psram_and_expander(
|
||||
"spi_id": "spi_bus",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_model_requires_expander_when_pins_overridden(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Any,
|
||||
) -> None:
|
||||
"""A model with default pins on an expander requires it even when the user config
|
||||
overrides all of those pins, and without the expander being listed in ``requires``."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
set_component_config("psram", True)
|
||||
CORE.raw_config = {"psram": True}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"ESP32-S3-TOUCH-LCD-7-800X480 requires component 'ch422g' to be configured",
|
||||
):
|
||||
_validated(
|
||||
{
|
||||
"model": "ESP32-S3-TOUCH-LCD-7-800X480",
|
||||
"reset_pin": 10,
|
||||
"enable_pin": [11, 12],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_model_requires_expander_with_default_pins(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Any,
|
||||
) -> None:
|
||||
"""The missing expander is reported before the model's default pins on it are validated."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
set_component_config("psram", True)
|
||||
CORE.raw_config = {"psram": True}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"ESP32-S3-TOUCH-LCD-7-800X480 requires component 'ch422g' to be configured",
|
||||
):
|
||||
_validated({"model": "ESP32-S3-TOUCH-LCD-7-800X480"})
|
||||
|
||||
|
||||
def test_unsupported_variant_reported_before_requirements(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""On an unsupported chip the variant error is shown, not the missing components."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-c3-devkitm-1", KEY_VARIANT: VARIANT_ESP32C3},
|
||||
)
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"This feature is only available on ESP32S3, ESP32P4, ESP32S31",
|
||||
):
|
||||
_validated({"model": "ESP32-S3-TOUCH-LCD-7-800X480"})
|
||||
|
||||
|
||||
def test_expander_that_fails_to_load_is_still_required(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""An expander component that exists but fails to load is still counted as required."""
|
||||
from esphome import loader, pins
|
||||
from esphome.components import mipi
|
||||
|
||||
monkeypatch.setattr(pins, "PIN_SCHEMA_REGISTRY", pins.PinRegistry())
|
||||
monkeypatch.setattr(loader, "get_component", lambda key: None)
|
||||
defaults = {"enable_pin": {"xl9535": "hub", "number": 1, "mode": "output"}}
|
||||
assert mipi._pin_expanders(defaults) == {"xl9535"}
|
||||
@@ -107,13 +107,6 @@ lvgl:
|
||||
id: meter_arc_indicator
|
||||
start_value: 0
|
||||
end_value: 180
|
||||
- lvgl.indicator.update:
|
||||
id: meter_arc_indicator
|
||||
opa: 50%
|
||||
- lvgl.indicator.update:
|
||||
id: minute_hand
|
||||
value: 30
|
||||
opa: !lambda return 128;
|
||||
- lvgl.animation.start:
|
||||
id:
|
||||
- anim_slide
|
||||
@@ -1115,21 +1108,6 @@ lvgl:
|
||||
skew_y: 5
|
||||
pivot_x: 25
|
||||
pivot_y: 25
|
||||
- lvgl.canvas.draw_rectangle:
|
||||
x: 10
|
||||
y: 10
|
||||
width: 120
|
||||
height: 80
|
||||
bg_grad: color_bar
|
||||
- lvgl.canvas.draw_line:
|
||||
color: green
|
||||
width: 2
|
||||
opa: 60%
|
||||
dash_width: 6
|
||||
dash_gap: 3
|
||||
points:
|
||||
- 10,10
|
||||
- 90,90
|
||||
- lvgl.canvas.draw_line:
|
||||
color: blue
|
||||
width: 4
|
||||
@@ -1142,6 +1120,7 @@ lvgl:
|
||||
- 200, 50
|
||||
- 50,50
|
||||
- lvgl.canvas.draw_polygon:
|
||||
bg_color: teal
|
||||
border_color: white
|
||||
border_width: 2
|
||||
border_opa: cover
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
display:
|
||||
- id: !extend main_lcd
|
||||
tft_upload_watchdog_timeout: 30s
|
||||
tft_upload_http_buffer_size_tx: 1024
|
||||
@@ -1,84 +0,0 @@
|
||||
esphome:
|
||||
name: test-copy-entity-state
|
||||
on_boot:
|
||||
# Before the copies' setup
|
||||
- priority: 800
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: early_sensor
|
||||
state: 1.5
|
||||
- binary_sensor.template.publish:
|
||||
id: early_binary
|
||||
state: true
|
||||
- text_sensor.template.publish:
|
||||
id: early_text
|
||||
state: early
|
||||
# After the copies' setup
|
||||
- priority: -100
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: late_sensor
|
||||
state: 2.5
|
||||
- text_sensor.template.publish:
|
||||
id: late_text
|
||||
state: late
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
logger:
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: early_sensor
|
||||
update_interval: never
|
||||
- platform: template
|
||||
id: late_sensor
|
||||
update_interval: never
|
||||
- platform: copy
|
||||
source_id: early_sensor
|
||||
name: Copy Early Sensor
|
||||
- platform: copy
|
||||
source_id: late_sensor
|
||||
name: Copy Late Sensor
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: early_binary
|
||||
- platform: copy
|
||||
source_id: early_binary
|
||||
name: Copy Early Binary
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
id: early_text
|
||||
update_interval: never
|
||||
- platform: template
|
||||
id: late_text
|
||||
update_interval: never
|
||||
- platform: copy
|
||||
source_id: early_text
|
||||
name: Copy Early Text
|
||||
- platform: copy
|
||||
source_id: late_text
|
||||
name: Copy Late Text
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Update Sources
|
||||
on_press:
|
||||
- sensor.template.publish:
|
||||
id: early_sensor
|
||||
state: 3.5
|
||||
- sensor.template.publish:
|
||||
id: late_sensor
|
||||
state: 7.25
|
||||
- binary_sensor.template.publish:
|
||||
id: early_binary
|
||||
state: false
|
||||
- text_sensor.template.publish:
|
||||
id: early_text
|
||||
state: changed
|
||||
- text_sensor.template.publish:
|
||||
id: late_text
|
||||
state: updated
|
||||
@@ -1,45 +0,0 @@
|
||||
esphome:
|
||||
name: test-copy-initially-off
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
logger:
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 9600
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 9600
|
||||
|
||||
globals:
|
||||
- id: changes
|
||||
type: int
|
||||
|
||||
binary_sensor:
|
||||
# Holds off from its constructor, so it never publishes a first state
|
||||
- platform: uart
|
||||
id: source_binary
|
||||
uart_id: mock_uart
|
||||
data: [0x01]
|
||||
- platform: copy
|
||||
source_id: source_binary
|
||||
name: Copy Binary
|
||||
on_state_change:
|
||||
- lambda: id(changes)++;
|
||||
- component.update: change_count
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: change_count
|
||||
name: Change Count
|
||||
update_interval: never
|
||||
lambda: return id(changes);
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Integration test for the copy sensor, binary sensor and text sensor platforms."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import (
|
||||
BinarySensorState,
|
||||
ButtonInfo,
|
||||
EntityState,
|
||||
SensorState,
|
||||
TextSensorState,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .state_utils import require_entity, subscribe_after_initial_states
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
INITIAL: dict[str, tuple[type[EntityState], float | bool | str]] = {
|
||||
"copy_early_sensor": (SensorState, 1.5),
|
||||
"copy_late_sensor": (SensorState, 2.5),
|
||||
"copy_early_binary": (BinarySensorState, True),
|
||||
"copy_early_text": (TextSensorState, "early"),
|
||||
"copy_late_text": (TextSensorState, "late"),
|
||||
}
|
||||
UPDATED: dict[str, tuple[type[EntityState], float | bool | str]] = {
|
||||
"copy_early_sensor": (SensorState, 3.5),
|
||||
"copy_late_sensor": (SensorState, 7.25),
|
||||
"copy_early_binary": (BinarySensorState, False),
|
||||
"copy_early_text": (TextSensorState, "changed"),
|
||||
"copy_late_text": (TextSensorState, "updated"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_copy_entity_state(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Copies hold the state their source published before and after their setup, then follow it."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
keys = {name: require_entity(entities, name).key for name in INITIAL}
|
||||
button = require_entity(entities, "update_sources", ButtonInfo)
|
||||
|
||||
sub = await subscribe_after_initial_states(client, entities)
|
||||
for name, (cls, state) in INITIAL.items():
|
||||
key = keys[name]
|
||||
assert sub.initial_states[key] == cls(key=key, state=state), name
|
||||
|
||||
names = list(UPDATED)
|
||||
waits = [sub.waiter.next_state(keys[name]) for name in names]
|
||||
client.button_command(button.key)
|
||||
for name, got in zip(names, await asyncio.gather(*waits), strict=True):
|
||||
cls, state = UPDATED[name]
|
||||
assert got == cls(key=keys[name], state=state), name
|
||||
@@ -1,28 +0,0 @@
|
||||
"""Integration test for a copy binary sensor whose source starts off."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aioesphomeapi import BinarySensorInfo, BinarySensorState, SensorInfo, SensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import require_entity, subscribe_after_initial_states
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_copy_initially_off(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""The copy takes the source's constructor state and its on_state_change sees it."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
copy = require_entity(entities, "copy_binary", BinarySensorInfo)
|
||||
count = require_entity(entities, "change_count", SensorInfo)
|
||||
|
||||
sub = await subscribe_after_initial_states(client, entities)
|
||||
assert sub.initial_states[copy.key] == BinarySensorState(
|
||||
key=copy.key, state=False
|
||||
)
|
||||
assert sub.initial_states[count.key] == SensorState(key=count.key, state=1.0)
|
||||
@@ -141,21 +141,6 @@ def test_convert_walks_callable_schema_extractor() -> None:
|
||||
assert "foo" in config_var["schema"]["config_vars"]
|
||||
|
||||
|
||||
def test_convert_sees_pin_through_optional_pin_wrapper() -> None:
|
||||
"""A pin schema wrapped by PIN_SCHEMA_REGISTRY.optional() is still dumped as a pin."""
|
||||
from esphome import pins
|
||||
|
||||
_bls.add_pin_validators()
|
||||
schema = pins.gpio_output_pin_schema
|
||||
expected: dict = {}
|
||||
_bls.convert(schema, expected, "/test")
|
||||
config_var: dict = {}
|
||||
_bls.convert(pins.PIN_SCHEMA_REGISTRY.optional(schema), config_var, "/test")
|
||||
|
||||
assert expected["type"] == "pin"
|
||||
assert config_var == expected
|
||||
|
||||
|
||||
def test_convert_emits_key_constraints() -> None:
|
||||
base = cv.Schema({cv.Optional("transport", default="tcp"): cv.string})
|
||||
schema = cv.All(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Unit tests for esphome/pins.py."""
|
||||
|
||||
from collections.abc import Iterator
|
||||
import functools
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -286,136 +285,6 @@ def test_registry_final_validate_runs_hook_with_parent_config(
|
||||
assert not config.errors
|
||||
|
||||
|
||||
def _validate_at(
|
||||
registry: pins.PinRegistry, path: list, conf: dict, optional: bool = False
|
||||
) -> dict:
|
||||
"""Validate a hub pin as if it is in the component at ``path``."""
|
||||
token = path_context.set(path)
|
||||
try:
|
||||
validator = functools.partial(registry.validate, key="hub")
|
||||
if optional:
|
||||
validator = registry.optional(validator)
|
||||
return validator(conf)
|
||||
finally:
|
||||
path_context.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("pin_path")
|
||||
def test_registry_final_validate_removes_optional_pin_used_elsewhere(
|
||||
registry: pins.PinRegistry, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""An optional pin also used elsewhere is removed from its config without error."""
|
||||
client_id = ID("h1", is_declaration=True)
|
||||
switch_pin = _validate_at(
|
||||
registry, ["switch", 0], {"hub": client_id, CONF_NUMBER: 2}
|
||||
)
|
||||
kept = _validate_at(
|
||||
registry, ["display", 0], {"hub": client_id, CONF_NUMBER: 6}, optional=True
|
||||
)
|
||||
dropped = _validate_at(
|
||||
registry, ["display", 0], {"hub": client_id, CONF_NUMBER: 2}, optional=True
|
||||
)
|
||||
config = _final_config(client_id)
|
||||
config["switch"] = [{"pin": switch_pin}]
|
||||
config["display"] = [{"enable_pin": [dropped, kept]}]
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
registry.remove_optional_conflicts(config)
|
||||
|
||||
# Removed before final validation, so later checks of pin use don't see it
|
||||
assert config["display"][0]["enable_pin"] == [kept]
|
||||
assert config["switch"][0]["pin"] is switch_pin
|
||||
assert registry.get_count("hub", client_id, 2) == 1
|
||||
assert "hub h1 pin 2 is used elsewhere" in caplog.text
|
||||
registry.final_validate(config)
|
||||
assert not config.errors
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("pin_path")
|
||||
def test_registry_shares_optional_pin_when_others_allow_it(
|
||||
registry: pins.PinRegistry,
|
||||
) -> None:
|
||||
"""An optional pin is shared, not removed, when every other use sets allow_other_uses."""
|
||||
client_id = ID("h1", is_declaration=True)
|
||||
switch_pin = _validate_at(
|
||||
registry,
|
||||
["switch", 0],
|
||||
{"hub": client_id, CONF_NUMBER: 2, CONF_ALLOW_OTHER_USES: True},
|
||||
)
|
||||
optional = _validate_at(
|
||||
registry, ["display", 0], {"hub": client_id, CONF_NUMBER: 2}, optional=True
|
||||
)
|
||||
config = _final_config(client_id)
|
||||
config["switch"] = [{"pin": switch_pin}]
|
||||
config["display"] = [{"enable_pin": [optional]}]
|
||||
|
||||
registry.remove_optional_conflicts(config)
|
||||
registry.final_validate(config)
|
||||
|
||||
assert not config.errors
|
||||
assert config["display"][0]["enable_pin"] == [optional]
|
||||
assert optional[CONF_ALLOW_OTHER_USES] is True
|
||||
assert registry.get_count("hub", client_id, 2) == 2
|
||||
|
||||
|
||||
def test_optional_wrapper_is_transparent_to_schema_extraction() -> None:
|
||||
"""The schema dumper sees the wrapped validator through the optional() wrapper."""
|
||||
validator = cv.ensure_list(pins.gpio_output_pin_schema)
|
||||
|
||||
assert pins.PIN_SCHEMA_REGISTRY.optional(validator)(SCHEMA_EXTRACT) is validator
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("pin_path")
|
||||
def test_registry_keeps_optional_pin_missing_from_config(
|
||||
registry: pins.PinRegistry,
|
||||
) -> None:
|
||||
"""An optional pin that cannot be found in the config is kept, so the conflict is reported."""
|
||||
client_id = ID("h1", is_declaration=True)
|
||||
switch_pin = _validate_at(
|
||||
registry, ["switch", 0], {"hub": client_id, CONF_NUMBER: 2}
|
||||
)
|
||||
optional = _validate_at(
|
||||
registry, ["display", 0], {"hub": client_id, CONF_NUMBER: 2}, optional=True
|
||||
)
|
||||
config = _final_config(client_id)
|
||||
config["switch"] = [{"pin": switch_pin}]
|
||||
config["display"] = [{"enable_pin": [dict(optional)]}]
|
||||
|
||||
registry.remove_optional_conflicts(config)
|
||||
registry.final_validate(config)
|
||||
|
||||
assert registry.get_count("hub", client_id, 2) == 2
|
||||
assert config.errors
|
||||
assert all("used in multiple places" in str(err) for err in config.errors)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("pin_path")
|
||||
def test_registry_final_validate_optional_pins_alone(
|
||||
registry: pins.PinRegistry,
|
||||
) -> None:
|
||||
"""Optional pins only give way to other uses: one is kept, and two still conflict."""
|
||||
client_id = ID("h1", is_declaration=True)
|
||||
single = _validate_at(
|
||||
registry, ["display", 0], {"hub": client_id, CONF_NUMBER: 6}, optional=True
|
||||
)
|
||||
first = _validate_at(
|
||||
registry, ["display", 0], {"hub": client_id, CONF_NUMBER: 2}, optional=True
|
||||
)
|
||||
second = _validate_at(
|
||||
registry, ["display", 1], {"hub": client_id, CONF_NUMBER: 2}, optional=True
|
||||
)
|
||||
config = _final_config(client_id)
|
||||
config["display"] = [{"enable_pin": [first, single]}, {"enable_pin": [second]}]
|
||||
|
||||
registry.remove_optional_conflicts(config)
|
||||
registry.final_validate(config)
|
||||
|
||||
assert config["display"][0]["enable_pin"] == [first, single]
|
||||
assert config["display"][1]["enable_pin"] == [second]
|
||||
assert config.errors
|
||||
assert all("used in multiple places" in str(err) for err in config.errors)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "expected"),
|
||||
[
|
||||
@@ -488,37 +357,3 @@ def test_check_strapping_pin(caplog: pytest.LogCaptureFixture) -> None:
|
||||
pins.check_strapping_pin(
|
||||
{CONF_NUMBER: 3, CONF_IGNORE_STRAPPING_WARNING: True}, {2}, logger
|
||||
)
|
||||
|
||||
|
||||
def test_optional_pins_removed_between_id_pass_and_final_validation() -> None:
|
||||
"""Optional pins are removed once ids are resolved, and before components inspect pin use."""
|
||||
from esphome.config import (
|
||||
FinalValidateValidationStep,
|
||||
IDPassValidationStep,
|
||||
OptionalPinValidationStep,
|
||||
)
|
||||
|
||||
assert (
|
||||
IDPassValidationStep.priority
|
||||
> OptionalPinValidationStep.priority
|
||||
> FinalValidateValidationStep.priority
|
||||
)
|
||||
|
||||
|
||||
def test_optional_pin_step_skips_config_with_errors(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Optional pins are only removed when the config has no errors so far."""
|
||||
from esphome.config import OptionalPinValidationStep
|
||||
|
||||
remove = MagicMock()
|
||||
monkeypatch.setattr(pins.PIN_SCHEMA_REGISTRY, "remove_optional_conflicts", remove)
|
||||
config = Config()
|
||||
|
||||
OptionalPinValidationStep().run(config)
|
||||
remove.assert_called_once_with(config)
|
||||
|
||||
remove.reset_mock()
|
||||
config.add_str_error("Invalid", ["display", 0])
|
||||
OptionalPinValidationStep().run(config)
|
||||
remove.assert_not_called()
|
||||
Reference in new issue
Block a user