1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-12 12:55:46 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
Jonathan Swoboda
47a7f729dd Merge pull request #11857 from esphome/bump-2025.10.5
2025.10.5
2025-11-11 20:09:41 -05:00
Jonathan Swoboda
a59888224c Bump version to 2025.10.5 2025-11-11 19:44:37 -05:00
Clyde Stubbs
58ad4759f0 [lvgl] Fix rotation with unusual width (#11680) 2025-11-11 19:44:37 -05:00
Clyde Stubbs
87f79290ba [usb_uart] Fixes for transfer queue allocation (#11548) 2025-11-11 19:44:37 -05:00
Jonathan Swoboda
9326d78439 [core] Don't allow python 3.14 (#11527) 2025-11-11 19:44:37 -05:00
Stuart Parmenter
a93887a790 [const] Add CONF_ROWS (#11249) 2025-11-11 19:44:37 -05:00
Jesse Hills
00155989af Merge pull request #11703 from esphome/bump-2025.10.4
2025.10.4
2025-11-04 16:04:04 +13:00
Jesse Hills
a3583da17d Bump version to 2025.10.4 2025-11-04 11:25:33 +13:00
Clyde Stubbs
0f6fd91304 [sdl] Fix keymappings (#11635) 2025-11-04 11:25:33 +13:00
Clyde Stubbs
2f5f1da16f [lvgl] Fix event for binary sensor (#11636) 2025-11-04 11:25:33 +13:00
Clyde Stubbs
51745d1d5e [image] Catch and report svg load errors (#11619) 2025-11-04 11:25:33 +13:00
J. Nick Koston
fecc8399a5 [lvgl] Fix nested lambdas in automations unable to access parameters (#11583)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-11-04 11:25:33 +13:00
Clyde Stubbs
db395a662d [mipi_rgb] Fix rotation with custom model (#11585) 2025-11-04 11:25:33 +13:00
Anton Sergunov
641dd24b21 Fix the LiberTiny bug with UART pin setup (#11518) 2025-11-04 11:25:32 +13:00
Keith Burzinski
57f2e32b00 [uart] Fix order of initialization calls (#11510) 2025-11-04 11:25:32 +13:00
Jesse Hills
6a478b9070 Merge pull request #11506 from esphome/bump-2025.10.3
2025.10.3
2025-10-24 14:08:12 +13:00
Jesse Hills
a32a1d11fb Bump version to 2025.10.3 2025-10-24 07:51:38 +13:00
Markus
daeb8ef88c [core] handle mixed IP and DNS addresses correctly in resolve_ip_address (#11503)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-24 07:51:38 +13:00
Anton Sergunov
febee437d6 [uart] Make rx pin respect pullup and pulldown settings (#9248) 2025-10-24 07:51:38 +13:00
Peter Zich
de2f475dbd [hdc1080] Make HDC1080_CMD_CONFIGURATION failure a warning (and log it) (#11355)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-24 07:51:38 +13:00
31 changed files with 611 additions and 395 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.10.2 PROJECT_NUMBER = 2025.10.5
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@@ -11,4 +11,5 @@ CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_RECEIVE = "on_receive" CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_USE_PSRAM = "use_psram" CONF_USE_PSRAM = "use_psram"

View File

@@ -16,7 +16,8 @@ void HDC1080Component::setup() {
// if configuration fails - there is a problem // if configuration fails - there is a problem
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
this->mark_failed(); ESP_LOGW(TAG, "Failed to configure HDC1080");
this->status_set_warning();
return; return;
} }
} }

View File

@@ -671,18 +671,33 @@ async def write_image(config, all_frames=False):
resize = config.get(CONF_RESIZE) resize = config.get(CONF_RESIZE)
if is_svg_file(path): if is_svg_file(path):
# Local import so use of non-SVG files needn't require cairosvg installed # Local import so use of non-SVG files needn't require cairosvg installed
from pyexpat import ExpatError
from xml.etree.ElementTree import ParseError
from cairosvg import svg2png from cairosvg import svg2png
from cairosvg.helpers import PointError
if not resize: if not resize:
resize = (None, None) resize = (None, None)
with open(path, "rb") as file: try:
image = svg2png( with open(path, "rb") as file:
file_obj=file, image = svg2png(
output_width=resize[0], file_obj=file,
output_height=resize[1], output_width=resize[0],
) output_height=resize[1],
image = Image.open(io.BytesIO(image)) )
width, height = image.size image = Image.open(io.BytesIO(image))
width, height = image.size
except (
ValueError,
ParseError,
IndexError,
ExpatError,
AttributeError,
TypeError,
PointError,
) as e:
raise core.EsphomeError(f"Could not load SVG image {path}: {e}") from e
else: else:
image = Image.open(path) image = Image.open(path)
width, height = image.size width, height = image.size

View File

@@ -31,7 +31,7 @@ async def to_code(config):
lvgl_static.add_event_cb( lvgl_static.add_event_cb(
widget.obj, widget.obj,
await pressed_ctx.get_lambda(), await pressed_ctx.get_lambda(),
LV_EVENT.PRESSING, LV_EVENT.PRESSED,
LV_EVENT.RELEASED, LV_EVENT.RELEASED,
) )
) )

View File

@@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
""" """
import logging import logging
from typing import TYPE_CHECKING, Any
from esphome import codegen as cg, config_validation as cv from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS from esphome.const import CONF_ITEMS
@@ -12,6 +13,7 @@ from esphome.core import ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32 from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from .helpers import requires_component from .helpers import requires_component
@@ -42,7 +44,13 @@ def static_cast(type, value):
def call_lambda(lamb: LambdaExpression): def call_lambda(lamb: LambdaExpression):
expr = lamb.content.strip() expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"): if expr.startswith("return") and expr.endswith(";"):
return expr[6:][:-1].strip() return expr[6:-1].strip()
# If lambda has parameters, call it with those parameter names
# Parameter names come from hardcoded component code (like "x", "it", "event")
# not from user input, so they're safe to use directly
if lamb.parameters and lamb.parameters.parameters:
param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters)
return f"{lamb}({param_names})"
return f"{lamb}()" return f"{lamb}()"
@@ -65,10 +73,20 @@ class LValidator:
return cv.returning_lambda(value) return cv.returning_lambda(value)
return self.validator(value) return self.validator(value)
async def process(self, value, args=()): async def process(
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
if value is None: if value is None:
return None return None
if isinstance(value, Lambda): if isinstance(value, Lambda):
# Local import to avoid circular import
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
return cg.RawExpression( return cg.RawExpression(
call_lambda( call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype) await cg.process_lambda(value, args, return_type=self.rtype)
@@ -486,7 +504,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input"
CONF_RIGHT_BUTTON = "right_button" CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover" CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn" CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROWS = "rows"
CONF_SCALE_LINES = "scale_lines" CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SELECTED_INDEX = "selected_index" CONF_SELECTED_INDEX = "selected_index"

View File

@@ -1,3 +1,5 @@
from typing import TYPE_CHECKING, Any
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import image from esphome.components import image
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
@@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj
from esphome.cpp_types import ESPTime, int32, uint32 from esphome.cpp_types import ESPTime, int32, uint32
from esphome.helpers import cpp_string_escape from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from . import types as ty from . import types as ty
from .defines import ( from .defines import (
@@ -388,11 +391,23 @@ class TextValidator(LValidator):
return value return value
return super().__call__(value) return super().__call__(value)
async def process(self, value, args=()): async def process(
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
# Local import to avoid circular import at module level
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
if isinstance(value, dict): if isinstance(value, dict):
if format_str := value.get(CONF_FORMAT): if format_str := value.get(CONF_FORMAT):
args = [str(x) for x in value[CONF_ARGS]] str_args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args)) arg_expr = cg.RawExpression(",".join(str_args))
format_str = cpp_string_escape(format_str) format_str = cpp_string_escape(format_str)
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
if time_format := value.get(CONF_TIME_FORMAT): if time_format := value.get(CONF_TIME_FORMAT):

View File

@@ -164,6 +164,9 @@ class LambdaContext(CodeContext):
code_text.append(text) code_text.append(text)
return code_text return code_text
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
return self.parameters
async def __aenter__(self): async def __aenter__(self):
await super().__aenter__() await super().__aenter__()
add_line_marks(self.where) add_line_marks(self.where)
@@ -178,9 +181,8 @@ class LvContext(LambdaContext):
added_lambda_count = 0 added_lambda_count = 0
def __init__(self, args=None): def __init__(self):
self.args = args or LVGL_COMP_ARG super().__init__(parameters=LVGL_COMP_ARG)
super().__init__(parameters=self.args)
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb) await super().__aexit__(exc_type, exc_val, exc_tb)
@@ -189,6 +191,11 @@ class LvContext(LambdaContext):
cg.add(expression) cg.add(expression)
return expression return expression
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
# When generating automations, we don't want the `lv_component` parameter to be passed
# to the lambda.
return []
def __call__(self, *args): def __call__(self, *args):
return self.add(*args) return self.add(*args)

View File

@@ -156,6 +156,7 @@ bool LvPageType::is_showing() const { return this->parent_->get_current_page() =
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area); auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area); auto height = lv_area_get_height(area);
auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
auto x1 = area->x1; auto x1 = area->x1;
auto y1 = area->y1; auto y1 = area->y1;
lv_color_t *dst = this->rotate_buf_; lv_color_t *dst = this->rotate_buf_;
@@ -163,13 +164,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
case display::DISPLAY_ROTATION_90_DEGREES: case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height; x-- != 0;) { for (lv_coord_t x = height; x-- != 0;) {
for (lv_coord_t y = 0; y != width; y++) { for (lv_coord_t y = 0; y != width; y++) {
dst[y * height + x] = *ptr++; dst[y * height_rounded + x] = *ptr++;
} }
} }
y1 = x1; y1 = x1;
x1 = this->disp_drv_.ver_res - area->y1 - height; x1 = this->disp_drv_.ver_res - area->y1 - height;
width = height; height = width;
height = lv_area_get_width(area); width = height_rounded;
break; break;
case display::DISPLAY_ROTATION_180_DEGREES: case display::DISPLAY_ROTATION_180_DEGREES:
@@ -185,13 +186,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
case display::DISPLAY_ROTATION_270_DEGREES: case display::DISPLAY_ROTATION_270_DEGREES:
for (lv_coord_t x = 0; x != height; x++) { for (lv_coord_t x = 0; x != height; x++) {
for (lv_coord_t y = width; y-- != 0;) { for (lv_coord_t y = width; y-- != 0;) {
dst[y * height + x] = *ptr++; dst[y * height_rounded + x] = *ptr++;
} }
} }
x1 = y1; x1 = y1;
y1 = this->disp_drv_.hor_res - area->x1 - width; y1 = this->disp_drv_.hor_res - area->x1 - width;
width = height; height = width;
height = lv_area_get_width(area); width = height_rounded;
break; break;
default: default:
@@ -435,8 +436,10 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
void LvglComponent::setup() { void LvglComponent::setup() {
auto *display = this->displays_[0]; auto *display = this->displays_[0];
auto width = display->get_width(); auto rounding = this->draw_rounding;
auto height = display->get_height(); // cater for displays with dimensions that don't divide by the required rounding
auto width = (display->get_width() + rounding - 1) / rounding * rounding;
auto height = (display->get_height() + rounding - 1) / rounding * rounding;
auto frac = this->buffer_frac_; auto frac = this->buffer_frac_;
if (frac == 0) if (frac == 0)
frac = 1; frac = 1;
@@ -461,9 +464,8 @@ void LvglComponent::setup() {
} }
this->buffer_frac_ = frac; this->buffer_frac_ = frac;
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
this->disp_drv_.hor_res = width; this->disp_drv_.hor_res = display->get_width();
this->disp_drv_.ver_res = height; this->disp_drv_.ver_res = display->get_height();
// this->setup_driver_(display->get_width(), display->get_height());
lv_disp_drv_update(this->disp_, &this->disp_drv_); lv_disp_drv_update(this->disp_, &this->disp_drv_);
this->rotation = display->get_rotation(); this->rotation = display->get_rotation();
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {

View File

@@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET
from ..lvcode import ( from ..lvcode import (
API_EVENT, API_EVENT,
EVENT_ARG, EVENT_ARG,
LVGL_COMP_ARG,
UPDATE_EVENT, UPDATE_EVENT,
LambdaContext, LambdaContext,
LvContext, LvContext,
@@ -30,7 +29,7 @@ async def to_code(config):
await wait_for_widgets() await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as lamb: async with LambdaContext(EVENT_ARG) as lamb:
lv_add(sensor.publish_state(widget.get_value())) lv_add(sensor.publish_state(widget.get_value()))
async with LvContext(LVGL_COMP_ARG): async with LvContext():
lv_add( lv_add(
lvgl_static.add_event_cb( lvgl_static.add_event_cb(
widget.obj, widget.obj,

View File

@@ -1,5 +1,6 @@
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.const import CONF_ROWS
from esphome.components.key_provider import KeyProvider from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
@@ -15,7 +16,6 @@ from ..defines import (
CONF_ONE_CHECKED, CONF_ONE_CHECKED,
CONF_PAD_COLUMN, CONF_PAD_COLUMN,
CONF_PAD_ROW, CONF_PAD_ROW,
CONF_ROWS,
CONF_SELECTED, CONF_SELECTED,
) )
from ..helpers import lvgl_components_required from ..helpers import lvgl_components_required

View File

@@ -1,6 +1,7 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import key_provider from esphome.components import key_provider
from esphome.components.const import CONF_ROWS
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
@@ -19,7 +20,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_(
) )
CONF_KEYPAD_ID = "keypad_id" CONF_KEYPAD_ID = "keypad_id"
CONF_ROWS = "rows"
CONF_COLUMNS = "columns" CONF_COLUMNS = "columns"
CONF_KEYS = "keys" CONF_KEYS = "keys"
CONF_DEBOUNCE_TIME = "debounce_time" CONF_DEBOUNCE_TIME = "debounce_time"

View File

@@ -384,6 +384,18 @@ class DriverChip:
transform[CONF_TRANSFORM] = True transform[CONF_TRANSFORM] = True
return transform return transform
def swap_xy_schema(self):
uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def add_madctl(self, sequence: list, config: dict): def add_madctl(self, sequence: list, config: dict):
# Add the MADCTL command to the sequence based on the configuration. # Add the MADCTL command to the sequence based on the configuration.
use_flip = config.get(CONF_USE_AXIS_FLIPS) use_flip = config.get(CONF_USE_AXIS_FLIPS)

View File

@@ -46,6 +46,7 @@ from esphome.const import (
CONF_DATA_RATE, CONF_DATA_RATE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_DIMENSIONS, CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN, CONF_ENABLE_PIN,
CONF_GREEN, CONF_GREEN,
CONF_HSYNC_PIN, CONF_HSYNC_PIN,
@@ -117,16 +118,16 @@ def data_pin_set(length):
def model_schema(config): def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()] model = MODELS[config[CONF_MODEL].upper()]
if transforms := model.transforms: transform = cv.Any(
transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) cv.Schema(
for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): {
if x not in transforms: cv.Required(CONF_MIRROR_X): cv.boolean,
transform = transform.extend( cv.Required(CONF_MIRROR_Y): cv.boolean,
{cv.Optional(x): cv.invalid(f"{x} not supported by this model")} **model.swap_xy_schema(),
) }
else: ),
transform = cv.invalid("This model does not support transforms") cv.one_of(CONF_DISABLED, lower=True),
)
# RPI model does not use an init sequence, indicates with empty list # RPI model does not use an init sequence, indicates with empty list
if model.initsequence is None: if model.initsequence is None:
# Custom model requires an init sequence # Custom model requires an init sequence
@@ -135,12 +136,16 @@ def model_schema(config):
else: else:
iseqconf = cv.Optional(CONF_INIT_SEQUENCE) iseqconf = cv.Optional(CONF_INIT_SEQUENCE)
uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0
swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) # Dimensions are optional if the model has a default width and the x-y transform is not overridden
transform_config = config.get(CONF_TRANSFORM, {})
# Dimensions are optional if the model has a default width and the swap_xy transform is not overridden is_swapped = (
cv_dimensions = ( isinstance(transform_config, dict)
cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required and transform_config.get(CONF_SWAP_XY, False) is True
) )
cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
)
pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18")
schema = display.FULL_DISPLAY_SCHEMA.extend( schema = display.FULL_DISPLAY_SCHEMA.extend(
{ {
@@ -157,7 +162,7 @@ def model_schema(config):
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
*pixel_modes, lower=True *pixel_modes, lower=True
), ),
model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, cv.Optional(CONF_TRANSFORM): transform,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
model.option(CONF_INVERT_COLORS, False): cv.boolean, model.option(CONF_INVERT_COLORS, False): cv.boolean,
model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean,
@@ -270,7 +275,6 @@ async def to_code(config):
cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED]))
cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY]))
index = 0
dpins = [] dpins = []
if CONF_RED in config[CONF_DATA_PINS]: if CONF_RED in config[CONF_DATA_PINS]:
red_pins = config[CONF_DATA_PINS][CONF_RED] red_pins = config[CONF_DATA_PINS][CONF_RED]

View File

@@ -131,19 +131,6 @@ def denominator(config):
) from StopIteration ) from StopIteration
def swap_xy_schema(model):
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def model_schema(config): def model_schema(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
bus_mode = config[CONF_BUS_MODE] bus_mode = config[CONF_BUS_MODE]
@@ -152,7 +139,7 @@ def model_schema(config):
{ {
cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model), **model.swap_xy_schema(),
} }
), ),
cv.one_of(CONF_DISABLED, lower=True), cv.one_of(CONF_DISABLED, lower=True),

View File

@@ -12,241 +12,256 @@ CODEOWNERS = ["@bdm310"]
STATE_ARG = "state" STATE_ARG = "state"
SDL_KEYMAP = { SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
"SDLK_UNKNOWN": 0,
"SDLK_FIRST": 0, SDL_KEYS = (
"SDLK_BACKSPACE": 8, "SDLK_UNKNOWN",
"SDLK_TAB": 9, "SDLK_RETURN",
"SDLK_CLEAR": 12, "SDLK_ESCAPE",
"SDLK_RETURN": 13, "SDLK_BACKSPACE",
"SDLK_PAUSE": 19, "SDLK_TAB",
"SDLK_ESCAPE": 27, "SDLK_SPACE",
"SDLK_SPACE": 32, "SDLK_EXCLAIM",
"SDLK_EXCLAIM": 33, "SDLK_QUOTEDBL",
"SDLK_QUOTEDBL": 34, "SDLK_HASH",
"SDLK_HASH": 35, "SDLK_PERCENT",
"SDLK_DOLLAR": 36, "SDLK_DOLLAR",
"SDLK_AMPERSAND": 38, "SDLK_AMPERSAND",
"SDLK_QUOTE": 39, "SDLK_QUOTE",
"SDLK_LEFTPAREN": 40, "SDLK_LEFTPAREN",
"SDLK_RIGHTPAREN": 41, "SDLK_RIGHTPAREN",
"SDLK_ASTERISK": 42, "SDLK_ASTERISK",
"SDLK_PLUS": 43, "SDLK_PLUS",
"SDLK_COMMA": 44, "SDLK_COMMA",
"SDLK_MINUS": 45, "SDLK_MINUS",
"SDLK_PERIOD": 46, "SDLK_PERIOD",
"SDLK_SLASH": 47, "SDLK_SLASH",
"SDLK_0": 48, "SDLK_0",
"SDLK_1": 49, "SDLK_1",
"SDLK_2": 50, "SDLK_2",
"SDLK_3": 51, "SDLK_3",
"SDLK_4": 52, "SDLK_4",
"SDLK_5": 53, "SDLK_5",
"SDLK_6": 54, "SDLK_6",
"SDLK_7": 55, "SDLK_7",
"SDLK_8": 56, "SDLK_8",
"SDLK_9": 57, "SDLK_9",
"SDLK_COLON": 58, "SDLK_COLON",
"SDLK_SEMICOLON": 59, "SDLK_SEMICOLON",
"SDLK_LESS": 60, "SDLK_LESS",
"SDLK_EQUALS": 61, "SDLK_EQUALS",
"SDLK_GREATER": 62, "SDLK_GREATER",
"SDLK_QUESTION": 63, "SDLK_QUESTION",
"SDLK_AT": 64, "SDLK_AT",
"SDLK_LEFTBRACKET": 91, "SDLK_LEFTBRACKET",
"SDLK_BACKSLASH": 92, "SDLK_BACKSLASH",
"SDLK_RIGHTBRACKET": 93, "SDLK_RIGHTBRACKET",
"SDLK_CARET": 94, "SDLK_CARET",
"SDLK_UNDERSCORE": 95, "SDLK_UNDERSCORE",
"SDLK_BACKQUOTE": 96, "SDLK_BACKQUOTE",
"SDLK_a": 97, "SDLK_a",
"SDLK_b": 98, "SDLK_b",
"SDLK_c": 99, "SDLK_c",
"SDLK_d": 100, "SDLK_d",
"SDLK_e": 101, "SDLK_e",
"SDLK_f": 102, "SDLK_f",
"SDLK_g": 103, "SDLK_g",
"SDLK_h": 104, "SDLK_h",
"SDLK_i": 105, "SDLK_i",
"SDLK_j": 106, "SDLK_j",
"SDLK_k": 107, "SDLK_k",
"SDLK_l": 108, "SDLK_l",
"SDLK_m": 109, "SDLK_m",
"SDLK_n": 110, "SDLK_n",
"SDLK_o": 111, "SDLK_o",
"SDLK_p": 112, "SDLK_p",
"SDLK_q": 113, "SDLK_q",
"SDLK_r": 114, "SDLK_r",
"SDLK_s": 115, "SDLK_s",
"SDLK_t": 116, "SDLK_t",
"SDLK_u": 117, "SDLK_u",
"SDLK_v": 118, "SDLK_v",
"SDLK_w": 119, "SDLK_w",
"SDLK_x": 120, "SDLK_x",
"SDLK_y": 121, "SDLK_y",
"SDLK_z": 122, "SDLK_z",
"SDLK_DELETE": 127, "SDLK_CAPSLOCK",
"SDLK_WORLD_0": 160, "SDLK_F1",
"SDLK_WORLD_1": 161, "SDLK_F2",
"SDLK_WORLD_2": 162, "SDLK_F3",
"SDLK_WORLD_3": 163, "SDLK_F4",
"SDLK_WORLD_4": 164, "SDLK_F5",
"SDLK_WORLD_5": 165, "SDLK_F6",
"SDLK_WORLD_6": 166, "SDLK_F7",
"SDLK_WORLD_7": 167, "SDLK_F8",
"SDLK_WORLD_8": 168, "SDLK_F9",
"SDLK_WORLD_9": 169, "SDLK_F10",
"SDLK_WORLD_10": 170, "SDLK_F11",
"SDLK_WORLD_11": 171, "SDLK_F12",
"SDLK_WORLD_12": 172, "SDLK_PRINTSCREEN",
"SDLK_WORLD_13": 173, "SDLK_SCROLLLOCK",
"SDLK_WORLD_14": 174, "SDLK_PAUSE",
"SDLK_WORLD_15": 175, "SDLK_INSERT",
"SDLK_WORLD_16": 176, "SDLK_HOME",
"SDLK_WORLD_17": 177, "SDLK_PAGEUP",
"SDLK_WORLD_18": 178, "SDLK_DELETE",
"SDLK_WORLD_19": 179, "SDLK_END",
"SDLK_WORLD_20": 180, "SDLK_PAGEDOWN",
"SDLK_WORLD_21": 181, "SDLK_RIGHT",
"SDLK_WORLD_22": 182, "SDLK_LEFT",
"SDLK_WORLD_23": 183, "SDLK_DOWN",
"SDLK_WORLD_24": 184, "SDLK_UP",
"SDLK_WORLD_25": 185, "SDLK_NUMLOCKCLEAR",
"SDLK_WORLD_26": 186, "SDLK_KP_DIVIDE",
"SDLK_WORLD_27": 187, "SDLK_KP_MULTIPLY",
"SDLK_WORLD_28": 188, "SDLK_KP_MINUS",
"SDLK_WORLD_29": 189, "SDLK_KP_PLUS",
"SDLK_WORLD_30": 190, "SDLK_KP_ENTER",
"SDLK_WORLD_31": 191, "SDLK_KP_1",
"SDLK_WORLD_32": 192, "SDLK_KP_2",
"SDLK_WORLD_33": 193, "SDLK_KP_3",
"SDLK_WORLD_34": 194, "SDLK_KP_4",
"SDLK_WORLD_35": 195, "SDLK_KP_5",
"SDLK_WORLD_36": 196, "SDLK_KP_6",
"SDLK_WORLD_37": 197, "SDLK_KP_7",
"SDLK_WORLD_38": 198, "SDLK_KP_8",
"SDLK_WORLD_39": 199, "SDLK_KP_9",
"SDLK_WORLD_40": 200, "SDLK_KP_0",
"SDLK_WORLD_41": 201, "SDLK_KP_PERIOD",
"SDLK_WORLD_42": 202, "SDLK_APPLICATION",
"SDLK_WORLD_43": 203, "SDLK_POWER",
"SDLK_WORLD_44": 204, "SDLK_KP_EQUALS",
"SDLK_WORLD_45": 205, "SDLK_F13",
"SDLK_WORLD_46": 206, "SDLK_F14",
"SDLK_WORLD_47": 207, "SDLK_F15",
"SDLK_WORLD_48": 208, "SDLK_F16",
"SDLK_WORLD_49": 209, "SDLK_F17",
"SDLK_WORLD_50": 210, "SDLK_F18",
"SDLK_WORLD_51": 211, "SDLK_F19",
"SDLK_WORLD_52": 212, "SDLK_F20",
"SDLK_WORLD_53": 213, "SDLK_F21",
"SDLK_WORLD_54": 214, "SDLK_F22",
"SDLK_WORLD_55": 215, "SDLK_F23",
"SDLK_WORLD_56": 216, "SDLK_F24",
"SDLK_WORLD_57": 217, "SDLK_EXECUTE",
"SDLK_WORLD_58": 218, "SDLK_HELP",
"SDLK_WORLD_59": 219, "SDLK_MENU",
"SDLK_WORLD_60": 220, "SDLK_SELECT",
"SDLK_WORLD_61": 221, "SDLK_STOP",
"SDLK_WORLD_62": 222, "SDLK_AGAIN",
"SDLK_WORLD_63": 223, "SDLK_UNDO",
"SDLK_WORLD_64": 224, "SDLK_CUT",
"SDLK_WORLD_65": 225, "SDLK_COPY",
"SDLK_WORLD_66": 226, "SDLK_PASTE",
"SDLK_WORLD_67": 227, "SDLK_FIND",
"SDLK_WORLD_68": 228, "SDLK_MUTE",
"SDLK_WORLD_69": 229, "SDLK_VOLUMEUP",
"SDLK_WORLD_70": 230, "SDLK_VOLUMEDOWN",
"SDLK_WORLD_71": 231, "SDLK_KP_COMMA",
"SDLK_WORLD_72": 232, "SDLK_KP_EQUALSAS400",
"SDLK_WORLD_73": 233, "SDLK_ALTERASE",
"SDLK_WORLD_74": 234, "SDLK_SYSREQ",
"SDLK_WORLD_75": 235, "SDLK_CANCEL",
"SDLK_WORLD_76": 236, "SDLK_CLEAR",
"SDLK_WORLD_77": 237, "SDLK_PRIOR",
"SDLK_WORLD_78": 238, "SDLK_RETURN2",
"SDLK_WORLD_79": 239, "SDLK_SEPARATOR",
"SDLK_WORLD_80": 240, "SDLK_OUT",
"SDLK_WORLD_81": 241, "SDLK_OPER",
"SDLK_WORLD_82": 242, "SDLK_CLEARAGAIN",
"SDLK_WORLD_83": 243, "SDLK_CRSEL",
"SDLK_WORLD_84": 244, "SDLK_EXSEL",
"SDLK_WORLD_85": 245, "SDLK_KP_00",
"SDLK_WORLD_86": 246, "SDLK_KP_000",
"SDLK_WORLD_87": 247, "SDLK_THOUSANDSSEPARATOR",
"SDLK_WORLD_88": 248, "SDLK_DECIMALSEPARATOR",
"SDLK_WORLD_89": 249, "SDLK_CURRENCYUNIT",
"SDLK_WORLD_90": 250, "SDLK_CURRENCYSUBUNIT",
"SDLK_WORLD_91": 251, "SDLK_KP_LEFTPAREN",
"SDLK_WORLD_92": 252, "SDLK_KP_RIGHTPAREN",
"SDLK_WORLD_93": 253, "SDLK_KP_LEFTBRACE",
"SDLK_WORLD_94": 254, "SDLK_KP_RIGHTBRACE",
"SDLK_WORLD_95": 255, "SDLK_KP_TAB",
"SDLK_KP0": 256, "SDLK_KP_BACKSPACE",
"SDLK_KP1": 257, "SDLK_KP_A",
"SDLK_KP2": 258, "SDLK_KP_B",
"SDLK_KP3": 259, "SDLK_KP_C",
"SDLK_KP4": 260, "SDLK_KP_D",
"SDLK_KP5": 261, "SDLK_KP_E",
"SDLK_KP6": 262, "SDLK_KP_F",
"SDLK_KP7": 263, "SDLK_KP_XOR",
"SDLK_KP8": 264, "SDLK_KP_POWER",
"SDLK_KP9": 265, "SDLK_KP_PERCENT",
"SDLK_KP_PERIOD": 266, "SDLK_KP_LESS",
"SDLK_KP_DIVIDE": 267, "SDLK_KP_GREATER",
"SDLK_KP_MULTIPLY": 268, "SDLK_KP_AMPERSAND",
"SDLK_KP_MINUS": 269, "SDLK_KP_DBLAMPERSAND",
"SDLK_KP_PLUS": 270, "SDLK_KP_VERTICALBAR",
"SDLK_KP_ENTER": 271, "SDLK_KP_DBLVERTICALBAR",
"SDLK_KP_EQUALS": 272, "SDLK_KP_COLON",
"SDLK_UP": 273, "SDLK_KP_HASH",
"SDLK_DOWN": 274, "SDLK_KP_SPACE",
"SDLK_RIGHT": 275, "SDLK_KP_AT",
"SDLK_LEFT": 276, "SDLK_KP_EXCLAM",
"SDLK_INSERT": 277, "SDLK_KP_MEMSTORE",
"SDLK_HOME": 278, "SDLK_KP_MEMRECALL",
"SDLK_END": 279, "SDLK_KP_MEMCLEAR",
"SDLK_PAGEUP": 280, "SDLK_KP_MEMADD",
"SDLK_PAGEDOWN": 281, "SDLK_KP_MEMSUBTRACT",
"SDLK_F1": 282, "SDLK_KP_MEMMULTIPLY",
"SDLK_F2": 283, "SDLK_KP_MEMDIVIDE",
"SDLK_F3": 284, "SDLK_KP_PLUSMINUS",
"SDLK_F4": 285, "SDLK_KP_CLEAR",
"SDLK_F5": 286, "SDLK_KP_CLEARENTRY",
"SDLK_F6": 287, "SDLK_KP_BINARY",
"SDLK_F7": 288, "SDLK_KP_OCTAL",
"SDLK_F8": 289, "SDLK_KP_DECIMAL",
"SDLK_F9": 290, "SDLK_KP_HEXADECIMAL",
"SDLK_F10": 291, "SDLK_LCTRL",
"SDLK_F11": 292, "SDLK_LSHIFT",
"SDLK_F12": 293, "SDLK_LALT",
"SDLK_F13": 294, "SDLK_LGUI",
"SDLK_F14": 295, "SDLK_RCTRL",
"SDLK_F15": 296, "SDLK_RSHIFT",
"SDLK_NUMLOCK": 300, "SDLK_RALT",
"SDLK_CAPSLOCK": 301, "SDLK_RGUI",
"SDLK_SCROLLOCK": 302, "SDLK_MODE",
"SDLK_RSHIFT": 303, "SDLK_AUDIONEXT",
"SDLK_LSHIFT": 304, "SDLK_AUDIOPREV",
"SDLK_RCTRL": 305, "SDLK_AUDIOSTOP",
"SDLK_LCTRL": 306, "SDLK_AUDIOPLAY",
"SDLK_RALT": 307, "SDLK_AUDIOMUTE",
"SDLK_LALT": 308, "SDLK_MEDIASELECT",
"SDLK_RMETA": 309, "SDLK_WWW",
"SDLK_LMETA": 310, "SDLK_MAIL",
"SDLK_LSUPER": 311, "SDLK_CALCULATOR",
"SDLK_RSUPER": 312, "SDLK_COMPUTER",
"SDLK_MODE": 313, "SDLK_AC_SEARCH",
"SDLK_COMPOSE": 314, "SDLK_AC_HOME",
"SDLK_HELP": 315, "SDLK_AC_BACK",
"SDLK_PRINT": 316, "SDLK_AC_FORWARD",
"SDLK_SYSREQ": 317, "SDLK_AC_STOP",
"SDLK_BREAK": 318, "SDLK_AC_REFRESH",
"SDLK_MENU": 319, "SDLK_AC_BOOKMARKS",
"SDLK_POWER": 320, "SDLK_BRIGHTNESSDOWN",
"SDLK_EURO": 321, "SDLK_BRIGHTNESSUP",
"SDLK_UNDO": 322, "SDLK_DISPLAYSWITCH",
} "SDLK_KBDILLUMTOGGLE",
"SDLK_KBDILLUMDOWN",
"SDLK_KBDILLUMUP",
"SDLK_EJECT",
"SDLK_SLEEP",
"SDLK_APP1",
"SDLK_APP2",
"SDLK_AUDIOREWIND",
"SDLK_AUDIOFASTFORWARD",
"SDLK_SOFTLEFT",
"SDLK_SOFTRIGHT",
"SDLK_CALL",
"SDLK_ENDCALL",
)
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(BinarySensor) binary_sensor.binary_sensor_schema(BinarySensor)

View File

@@ -56,6 +56,13 @@ uint32_t ESP8266UartComponent::get_config() {
} }
void ESP8266UartComponent::setup() { void ESP8266UartComponent::setup() {
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
// Use Arduino HardwareSerial UARTs if all used pins match the ones // Use Arduino HardwareSerial UARTs if all used pins match the ones
// preconfigured by the platform. For example if RX disabled but TX pin // preconfigured by the platform. For example if RX disabled but TX pin
// is 1 we still want to use Serial. // is 1 we still want to use Serial.

View File

@@ -6,6 +6,9 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/gpio.h"
#include "driver/gpio.h"
#include "soc/gpio_num.h"
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
@@ -96,23 +99,48 @@ void IDFUARTComponent::setup() {
} }
void IDFUARTComponent::load_settings(bool dump_config) { void IDFUARTComponent::load_settings(bool dump_config) {
uart_config_t uart_config = this->get_config_(); esp_err_t err;
esp_err_t err = uart_param_config(this->uart_num_, &uart_config);
if (uart_is_driver_installed(this->uart_num_)) {
err = uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
}
err = uart_driver_install(this->uart_num_, // UART number
this->rx_buffer_size_, // RX ring buffer size
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
// block task until all data has been sent out
20, // event queue size/depth
&this->uart_event_queue_, // event queue
0 // Flags used to allocate the interrupt
);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
this->mark_failed(); this->mark_failed();
return; return;
} }
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
uint32_t invert = 0; uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV; invert |= UART_SIGNAL_TXD_INV;
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) }
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) {
invert |= UART_SIGNAL_RXD_INV; invert |= UART_SIGNAL_RXD_INV;
}
err = uart_set_line_inverse(this->uart_num_, invert); err = uart_set_line_inverse(this->uart_num_, invert);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -128,26 +156,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return; return;
} }
if (uart_is_driver_installed(this->uart_num_)) {
uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
}
err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_,
/* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will
block task until all data have been sent out.*/
0,
/* UART event queue size/depth. */ 20, &(this->uart_event_queue_),
/* Flags used to allocate the interrupt. */ 0);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
@@ -163,24 +171,32 @@ void IDFUARTComponent::load_settings(bool dump_config) {
} }
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
err = uart_set_mode(this->uart_num_, mode); err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install()
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
this->mark_failed(); this->mark_failed();
return; return;
} }
uart_config_t uart_config = this->get_config_();
err = uart_param_config(this->uart_num_, &uart_config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
if (dump_config) { if (dump_config) {
ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_);
this->dump_config(); this->dump_config();
} }
} }
void IDFUARTComponent::dump_config() { void IDFUARTComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_);
LOG_PIN(" TX Pin: ", tx_pin_); LOG_PIN(" TX Pin: ", this->tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_); LOG_PIN(" RX Pin: ", this->rx_pin_);
LOG_PIN(" Flow Control Pin: ", flow_control_pin_); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
if (this->rx_pin_ != nullptr) { if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" RX Buffer Size: %u\n" " RX Buffer Size: %u\n"

View File

@@ -51,28 +51,53 @@ void LibreTinyUARTComponent::setup() {
bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted();
bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted(); bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted();
auto shouldFallbackToSoftwareSerial = [&]() -> bool {
auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool {
return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE;
};
if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) ||
hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) {
#if LT_ARD_HAS_SOFTSERIAL
ESP_LOGI(TAG, "Pins has flags set. Using Software Serial");
return true;
#else
ESP_LOGW(TAG, "Pin flags are set but not supported for hardware serial. Ignoring");
#endif
}
return false;
};
if (false) if (false)
return; return;
#if LT_HW_UART0 #if LT_HW_UART0
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) { else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX) &&
!shouldFallbackToSoftwareSerial()) {
this->serial_ = &Serial0; this->serial_ = &Serial0;
this->hardware_idx_ = 0; this->hardware_idx_ = 0;
} }
#endif #endif
#if LT_HW_UART1 #if LT_HW_UART1
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX)) { else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX) &&
!shouldFallbackToSoftwareSerial()) {
this->serial_ = &Serial1; this->serial_ = &Serial1;
this->hardware_idx_ = 1; this->hardware_idx_ = 1;
} }
#endif #endif
#if LT_HW_UART2 #if LT_HW_UART2
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX)) { else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX) &&
!shouldFallbackToSoftwareSerial()) {
this->serial_ = &Serial2; this->serial_ = &Serial2;
this->hardware_idx_ = 2; this->hardware_idx_ = 2;
} }
#endif #endif
else { else {
#if LT_ARD_HAS_SOFTSERIAL #if LT_ARD_HAS_SOFTSERIAL
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted); this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted);
#else #else
this->serial_ = &Serial; this->serial_ = &Serial;

View File

@@ -52,6 +52,13 @@ uint16_t RP2040UartComponent::get_config() {
} }
void RP2040UartComponent::setup() { void RP2040UartComponent::setup() {
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
uint16_t config = get_config(); uint16_t config = get_config();
constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28});

View File

@@ -55,7 +55,7 @@ static const uint8_t USB_DIR_IN = 1 << 7;
static const uint8_t USB_DIR_OUT = 0; static const uint8_t USB_DIR_OUT = 0;
static const size_t SETUP_PACKET_SIZE = 8; static const size_t SETUP_PACKET_SIZE = 8;
static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible.
static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32");
// Select appropriate bitmask type for tracking allocation of TransferRequest slots. // Select appropriate bitmask type for tracking allocation of TransferRequest slots.
@@ -65,6 +65,7 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet
// This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32. // This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32.
// If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated. // If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated.
using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type;
static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1;
static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop
static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples)
@@ -133,11 +134,11 @@ class USBClient : public Component {
float get_setup_priority() const override { return setup_priority::IO; } float get_setup_priority() const override { return setup_priority::IO; }
void on_opened(uint8_t addr); void on_opened(uint8_t addr);
void on_removed(usb_device_handle_t handle); void on_removed(usb_device_handle_t handle);
void control_transfer_callback(const usb_transfer_t *xfer) const; bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
void dump_config() override; void dump_config() override;
void release_trq(TransferRequest *trq); void release_trq(TransferRequest *trq);
trq_bitmask_t get_trq_in_use() const { return trq_in_use_; }
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
const std::vector<uint8_t> &data = {}); const std::vector<uint8_t> &data = {});
@@ -147,7 +148,6 @@ class USBClient : public Component {
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool; EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
protected: protected:
bool register_();
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
virtual void disconnect(); virtual void disconnect();
virtual void on_connected() {} virtual void on_connected() {}
@@ -158,7 +158,7 @@ class USBClient : public Component {
// USB task management // USB task management
static void usb_task_fn(void *arg); static void usb_task_fn(void *arg);
void usb_task_loop(); [[noreturn]] void usb_task_loop() const;
TaskHandle_t usb_task_handle_{nullptr}; TaskHandle_t usb_task_handle_{nullptr};

View File

@@ -188,9 +188,9 @@ void USBClient::setup() {
} }
// Pre-allocate USB transfer buffers for all slots at startup // Pre-allocate USB transfer buffers for all slots at startup
// This avoids any dynamic allocation during runtime // This avoids any dynamic allocation during runtime
for (size_t i = 0; i < MAX_REQUESTS; i++) { for (auto &request : this->requests_) {
usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); usb_host_transfer_alloc(64, 0, &request.transfer);
this->requests_[i].client = this; // Set once, never changes request.client = this; // Set once, never changes
} }
// Create and start USB task // Create and start USB task
@@ -210,8 +210,7 @@ void USBClient::usb_task_fn(void *arg) {
auto *client = static_cast<USBClient *>(arg); auto *client = static_cast<USBClient *>(arg);
client->usb_task_loop(); client->usb_task_loop();
} }
void USBClient::usb_task_loop() const {
void USBClient::usb_task_loop() {
while (true) { while (true) {
usb_host_client_handle_events(this->handle_, portMAX_DELAY); usb_host_client_handle_events(this->handle_, portMAX_DELAY);
} }
@@ -334,22 +333,23 @@ static void control_callback(const usb_transfer_t *xfer) {
// This multi-threaded access is intentional for performance - USB task can // This multi-threaded access is intentional for performance - USB task can
// immediately restart transfers without waiting for main loop scheduling. // immediately restart transfers without waiting for main loop scheduling.
TransferRequest *USBClient::get_trq_() { TransferRequest *USBClient::get_trq_() {
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed); trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire);
// Find first available slot (bit = 0) and try to claim it atomically // Find first available slot (bit = 0) and try to claim it atomically
// We use a while loop to allow retrying the same slot after CAS failure // We use a while loop to allow retrying the same slot after CAS failure
size_t i = 0; for (;;) {
while (i != MAX_REQUESTS) { if (mask == ALL_REQUESTS_IN_USE) {
if (mask & (static_cast<trq_bitmask_t>(1) << i)) { ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
// Slot is in use, move to next slot return nullptr;
i++;
continue;
} }
// find the least significant zero bit
trq_bitmask_t lsb = ~mask & (mask + 1);
// Slot i appears available, try to claim it atomically // Slot i appears available, try to claim it atomically
trq_bitmask_t desired = mask | (static_cast<trq_bitmask_t>(1) << i); // Set bit i to mark as in-use trq_bitmask_t desired = mask | lsb;
if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) { if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) {
auto i = __builtin_ctz(lsb); // count trailing zeroes
// Successfully claimed slot i - prepare the TransferRequest // Successfully claimed slot i - prepare the TransferRequest
auto *trq = &this->requests_[i]; auto *trq = &this->requests_[i];
trq->transfer->context = trq; trq->transfer->context = trq;
@@ -358,13 +358,9 @@ TransferRequest *USBClient::get_trq_() {
} }
// CAS failed - another thread modified the bitmask // CAS failed - another thread modified the bitmask
// mask was already updated by compare_exchange_weak with the current value // mask was already updated by compare_exchange_weak with the current value
// No need to reload - the CAS already did that for us
i = 0;
} }
ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
return nullptr;
} }
void USBClient::disconnect() { void USBClient::disconnect() {
this->on_disconnected(); this->on_disconnected();
auto err = usb_host_device_close(this->handle_, this->device_handle_); auto err = usb_host_device_close(this->handle_, this->device_handle_);
@@ -446,11 +442,11 @@ static void transfer_callback(usb_transfer_t *xfer) {
* *
* @throws None. * @throws None.
*/ */
void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
auto *trq = this->get_trq_(); auto *trq = this->get_trq_();
if (trq == nullptr) { if (trq == nullptr) {
ESP_LOGE(TAG, "Too many requests queued"); ESP_LOGE(TAG, "Too many requests queued");
return; return false;
} }
trq->callback = callback; trq->callback = callback;
trq->transfer->callback = transfer_callback; trq->transfer->callback = transfer_callback;
@@ -460,7 +456,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
this->release_trq(trq); this->release_trq(trq);
return false;
} }
return true;
} }
/** /**
@@ -476,11 +474,11 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
* *
* @throws None. * @throws None.
*/ */
void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) {
auto *trq = this->get_trq_(); auto *trq = this->get_trq_();
if (trq == nullptr) { if (trq == nullptr) {
ESP_LOGE(TAG, "Too many requests queued"); ESP_LOGE(TAG, "Too many requests queued");
return; return false;
} }
trq->callback = callback; trq->callback = callback;
trq->transfer->callback = transfer_callback; trq->transfer->callback = transfer_callback;
@@ -491,7 +489,9 @@ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback,
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
this->release_trq(trq); this->release_trq(trq);
return false;
} }
return true;
} }
void USBClient::dump_config() { void USBClient::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
@@ -505,7 +505,7 @@ void USBClient::dump_config() {
// - Main loop: When transfer submission fails // - Main loop: When transfer submission fails
// //
// THREAD SAFETY: Lock-free using atomic AND to clear bit // THREAD SAFETY: Lock-free using atomic AND to clear bit
// Thread-safe atomic operation allows multi-threaded deallocation // Thread-safe atomic operation allows multithreaded deallocation
void USBClient::release_trq(TransferRequest *trq) { void USBClient::release_trq(TransferRequest *trq) {
if (trq == nullptr) if (trq == nullptr)
return; return;
@@ -517,10 +517,10 @@ void USBClient::release_trq(TransferRequest *trq) {
return; return;
} }
// Atomically clear bit i to mark slot as available // Atomically clear the bit to mark slot as available
// fetch_and with inverted bitmask clears the bit atomically // fetch_and with inverted bitmask clears the bit atomically
trq_bitmask_t bit = static_cast<trq_bitmask_t>(1) << index; trq_bitmask_t mask = ~(static_cast<trq_bitmask_t>(1) << index);
this->trq_in_use_.fetch_and(static_cast<trq_bitmask_t>(~bit), std::memory_order_release); this->trq_in_use_.fetch_and(mask, std::memory_order_release);
} }
} // namespace usb_host } // namespace usb_host

View File

@@ -214,7 +214,7 @@ void USBUartComponent::dump_config() {
} }
} }
void USBUartComponent::start_input(USBUartChannel *channel) { void USBUartComponent::start_input(USBUartChannel *channel) {
if (!channel->initialised_.load() || channel->input_started_.load()) if (!channel->initialised_.load())
return; return;
// THREAD CONTEXT: Called from both USB task and main loop threads // THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediate restart after successful transfer for continuous data flow // - USB task: Immediate restart after successful transfer for continuous data flow
@@ -226,12 +226,18 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
// //
// The underlying transfer_in() uses lock-free atomic allocation from the // The underlying transfer_in() uses lock-free atomic allocation from the
// TransferRequest pool, making this multi-threaded access safe // TransferRequest pool, making this multi-threaded access safe
// if already started, don't restart. A spurious failure in compare_exchange_weak
// is not a problem, as it will be retried on the next read_array()
auto started = false;
if (!channel->input_started_.compare_exchange_weak(started, true))
return;
const auto *ep = channel->cdc_dev_.in_ep; const auto *ep = channel->cdc_dev_.in_ep;
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
auto callback = [this, channel](const usb_host::TransferStatus &status) { auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) { if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code));
// On failure, don't restart - let next read_array() trigger it // On failure, don't restart - let next read_array() trigger it
channel->input_started_.store(false); channel->input_started_.store(false);
return; return;
@@ -263,8 +269,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
channel->input_started_.store(false); channel->input_started_.store(false);
this->start_input(channel); this->start_input(channel);
}; };
channel->input_started_.store(true); if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); channel->input_started_.store(false);
}
} }
void USBUartComponent::start_output(USBUartChannel *channel) { void USBUartComponent::start_output(USBUartChannel *channel) {
@@ -357,11 +364,12 @@ void USBUartTypeCdcAcm::on_disconnected() {
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
} }
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
channel->initialised_.store(false); // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts
channel->input_started_.store(false); channel->input_started_.store(true);
channel->output_started_.store(false); channel->output_started_.store(true);
channel->input_buffer_.clear(); channel->input_buffer_.clear();
channel->output_buffer_.clear(); channel->output_buffer_.clear();
channel->initialised_.store(false);
} }
USBClient::on_disconnected(); USBClient::on_disconnected();
} }

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2025.10.2" __version__ = "2025.10.5"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -224,36 +224,37 @@ def resolve_ip_address(
return res return res
# Process hosts # Process hosts
cached_addresses: list[str] = []
uncached_hosts: list[str] = [] uncached_hosts: list[str] = []
has_cache = address_cache is not None
for h in hosts: for h in hosts:
if is_ip_address(h): if is_ip_address(h):
if has_cache: _add_ip_addresses_to_addrinfo([h], port, res)
# If we have a cache, treat IPs as cached
cached_addresses.append(h)
else:
# If no cache, pass IPs through to resolver with hostnames
uncached_hosts.append(h)
elif address_cache and (cached := address_cache.get_addresses(h)): elif address_cache and (cached := address_cache.get_addresses(h)):
# Found in cache _add_ip_addresses_to_addrinfo(cached, port, res)
cached_addresses.extend(cached)
else: else:
# Not cached, need to resolve # Not cached, need to resolve
if address_cache and address_cache.has_cache(): if address_cache and address_cache.has_cache():
_LOGGER.info("Host %s not in cache, will need to resolve", h) _LOGGER.info("Host %s not in cache, will need to resolve", h)
uncached_hosts.append(h) uncached_hosts.append(h)
# Process cached addresses (includes direct IPs and cached lookups)
_add_ip_addresses_to_addrinfo(cached_addresses, port, res)
# If we have uncached hosts (only non-IP hostnames), resolve them # If we have uncached hosts (only non-IP hostnames), resolve them
if uncached_hosts: if uncached_hosts:
from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo
from esphome.core import EsphomeError
from esphome.resolver import AsyncResolver from esphome.resolver import AsyncResolver
resolver = AsyncResolver(uncached_hosts, port) resolver = AsyncResolver(uncached_hosts, port)
addr_infos = resolver.resolve() addr_infos: list[AioAddrInfo] = []
try:
addr_infos = resolver.resolve()
except EsphomeError as err:
if not res:
# No pre-resolved addresses available, DNS resolution is fatal
raise
_LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res))
# Convert aioesphomeapi AddrInfo to our format # Convert aioesphomeapi AddrInfo to our format
for addr_info in addr_infos: for addr_info in addr_infos:
sockaddr = addr_info.sockaddr sockaddr = addr_info.sockaddr

View File

@@ -19,7 +19,9 @@ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Topic :: Home Automation", "Topic :: Home Automation",
] ]
requires-python = ">=3.11.0"
# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502
requires-python = ">=3.11.0,<3.14"
dynamic = ["dependencies", "optional-dependencies", "version"] dynamic = ["dependencies", "optional-dependencies", "version"]

View File

@@ -52,6 +52,19 @@ number:
widget: spinbox_id widget: spinbox_id
id: lvgl_spinbox_number id: lvgl_spinbox_number
name: LVGL Spinbox Number name: LVGL Spinbox Number
- platform: template
id: test_brightness
name: "Test Brightness"
min_value: 0
max_value: 255
step: 1
optimistic: true
# Test lambda in automation accessing x parameter directly
# This is a real-world pattern from user configs
on_value:
- lambda: !lambda |-
// Direct use of x parameter in automation
ESP_LOGD("test", "Brightness: %.0f", x);
light: light:
- platform: lvgl - platform: lvgl
@@ -110,3 +123,21 @@ text:
platform: lvgl platform: lvgl
widget: hello_label widget: hello_label
mode: text mode: text
text_sensor:
- platform: template
id: test_text_sensor
name: "Test Text Sensor"
# Test nested lambdas in LVGL actions can access automation parameters
on_value:
- lvgl.label.update:
id: hello_label
text: !lambda return x.c_str();
- lvgl.label.update:
id: hello_label
text: !lambda |-
// Test complex lambda with conditionals accessing x parameter
if (x == "*") {
return "WILDCARD";
}
return x.c_str();

View File

@@ -257,7 +257,30 @@ lvgl:
text: "Hello shiny day" text: "Hello shiny day"
text_color: 0xFFFFFF text_color: 0xFFFFFF
align: bottom_mid align: bottom_mid
- label:
id: setup_lambda_label
# Test lambda in widget property during setup (LvContext)
# Should NOT receive lv_component parameter
text: !lambda |-
char buf[32];
snprintf(buf, sizeof(buf), "Setup: %d", 42);
return std::string(buf);
align: top_mid
text_font: space16 text_font: space16
- label:
id: chip_info_label
# Test complex setup lambda (real-world pattern)
# Should NOT receive lv_component parameter
text: !lambda |-
// Test conditional compilation and string formatting
char buf[64];
#ifdef USE_ESP_IDF
snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR);
#else
snprintf(buf, sizeof(buf), "Arduino");
#endif
return std::string(buf);
align: top_left
- obj: - obj:
align: center align: center
arc_opa: COVER arc_opa: COVER

View File

@@ -14,10 +14,10 @@ display:
binary_sensor: binary_sensor:
- platform: sdl - platform: sdl
id: key_up id: key_up
key: SDLK_a key: SDLK_UP
- platform: sdl - platform: sdl
id: key_down id: key_down
key: SDLK_d key: SDLK_DOWN
- platform: sdl - platform: sdl
id: key_enter id: key_enter
key: SDLK_s key: SDLK_RETURN

View File

@@ -1,3 +1,6 @@
usb_host:
max_transfer_requests: 32
usb_uart: usb_uart:
- id: uart_0 - id: uart_0
type: cdc_acm type: cdc_acm

View File

@@ -454,9 +454,27 @@ def test_resolve_ip_address_mixed_list() -> None:
# Mix of IP and hostname - should use async resolver # Mix of IP and hostname - should use async resolver
result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053)
assert len(result) == 2
assert result[0][4][0] == "192.168.1.100"
assert result[1][4][0] == "192.168.1.200"
MockResolver.assert_called_once_with(["test.local"], 6053)
mock_resolver.resolve.assert_called_once()
def test_resolve_ip_address_mixed_list_fail() -> None:
"""Test resolving a mix of IPs and hostnames with resolve failed."""
with patch("esphome.resolver.AsyncResolver") as MockResolver:
mock_resolver = MockResolver.return_value
mock_resolver.resolve.side_effect = EsphomeError(
"Error resolving IP address: [test.local]"
)
# Mix of IP and hostname - should use async resolver
result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053)
assert len(result) == 1 assert len(result) == 1
assert result[0][4][0] == "192.168.1.200" assert result[0][4][0] == "192.168.1.100"
MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053) MockResolver.assert_called_once_with(["test.local"], 6053)
mock_resolver.resolve.assert_called_once() mock_resolver.resolve.assert_called_once()