diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 24a854ec39..25fbfb7599 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -489,7 +489,8 @@ def final_validate_device_schema( return value def validate_stop_bits(value): - if value != stop_bits: + # usb_uart channels store stop bits as strings ("1", "1.5", "2"). + if float(value) != stop_bits: raise cv.Invalid( f"Component {name} requires {stop_bits} stop bits for the uart referenced by {uart_bus}" ) diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 22563209da..c99992d878 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -155,7 +155,22 @@ class USBUartChannelBase : public uart::UARTComponent, public Parentedparity_ = parity; } + void set_parity(UARTParityOptions parity) { + this->parity_ = parity; + // Keep the base-class parity in sync so uart::UARTComponent::get_parity() reports the configured value. + // MARK/SPACE have no uart:: equivalent and report as NONE. + switch (parity) { + case UART_CONFIG_PARITY_EVEN: + uart::UARTComponent::set_parity(uart::UART_CONFIG_PARITY_EVEN); + break; + case UART_CONFIG_PARITY_ODD: + uart::UARTComponent::set_parity(uart::UART_CONFIG_PARITY_ODD); + break; + default: + uart::UARTComponent::set_parity(uart::UART_CONFIG_PARITY_NONE); + break; + } + } void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } diff --git a/tests/component_tests/uart/test_final_validate.py b/tests/component_tests/uart/test_final_validate.py new file mode 100644 index 0000000000..b2ad5f4390 --- /dev/null +++ b/tests/component_tests/uart/test_final_validate.py @@ -0,0 +1,46 @@ +"""Tests for final validation of UART device requirements against usb_uart channels.""" + +from pathlib import Path + +import pytest + +from esphome import config, yaml_util +from esphome.core import CORE + +USB_UART_CONFIG = """ +esphome: + name: usb-uart-final-validate +esp32: + variant: esp32s3 + framework: + type: esp-idf +usb_uart: + - type: ch340 + channels: + - id: meter_uart + baud_rate: 1200 + stop_bits: {stop_bits} +sensor: + - platform: kamstrup_kmp + uart_id: meter_uart + heat_energy: + name: Heat energy +""" + + +@pytest.mark.parametrize( + ("stop_bits", "valid"), [("2", True), ("1", False), ("1.5", False)] +) +def test_stop_bits_requirement_on_usb_uart_channel( + stop_bits: str, valid: bool, tmp_path: Path +) -> None: + """usb_uart stores stop bits as strings; they must still satisfy a numeric requirement.""" + path = tmp_path / "usb_uart.yaml" + path.write_text(USB_UART_CONFIG.format(stop_bits=stop_bits), encoding="utf-8") + CORE.config_path = path + result = config.validate_config(yaml_util.load_yaml(path), {}) + errors = [str(err) for err in result.errors] + if valid: + assert errors == [] + else: + assert any("requires 2 stop bits" in err for err in errors), errors