diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 8f7edc69a8..13f66691b2 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -31,7 +31,6 @@ from esphome.const import ( CONF_TRANSFORM, CONF_UPDATE_INTERVAL, CONF_WIDTH, - SCHEDULER_DONT_RUN, ) from esphome.cpp_generator import RawExpression from esphome.final_validate import full_config @@ -73,49 +72,46 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} def model_schema(config): model = MODELS[config[CONF_MODEL]] class_name = epaper_spi_ns.class_(model.class_name, EPaperBase) + minimum_update_interval = update_interval( + model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s") + ) cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required - return ( - display.full_display_schema("60s") - .extend( - spi.spi_device_schema( - cs_pin_required=False, - default_mode="MODE0", - default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), - ) - ) - .extend( - { - model.option(pin): pins.gpio_output_pin_schema - for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) - } - ) - .extend( - { - cv.Optional(CONF_ROTATION, default=0): validate_rotation, - cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - } - ), - cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), - model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, - 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.option(CONF_ENABLE_PIN): cv.ensure_list( - pins.gpio_output_pin_schema - ), - model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( - map_sequence - ), - model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(max=core.TimePeriod(milliseconds=500)), - ), - } + return display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), ) + ).extend( + { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( + update_interval, cv.Range(min=minimum_update_interval) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), + model.option(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + model.option(CONF_CS_PIN): pins.gpio_output_pin_schema, + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + model.option(CONF_RESET_PIN): pins.gpio_output_pin_schema, + 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.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } ) @@ -146,22 +142,15 @@ def _final_validate(config): global_config = full_config.get() from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN - # If no drawing methods are configured, and LVGL is not enabled, show a test card - if ( - CONF_LAMBDA not in config - and CONF_PAGES not in config - and LVGL_DOMAIN not in global_config - ): - config[CONF_SHOW_TEST_CARD] = True - - interval = config[CONF_UPDATE_INTERVAL] - if interval != SCHEDULER_DONT_RUN: - model = MODELS[config[CONF_MODEL]] - minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")) - if interval < minimum: - raise cv.Invalid( - f"update_interval must be at least {minimum} for {model.name}, got {interval}" - ) + if CONF_LAMBDA not in config and CONF_PAGES not in config: + if LVGL_DOMAIN in global_config: + if CONF_UPDATE_INTERVAL not in config: + config[CONF_UPDATE_INTERVAL] = update_interval("never") + else: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + elif CONF_UPDATE_INTERVAL not in config: + config[CONF_UPDATE_INTERVAL] = update_interval("1min") return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 33ae7b7dbd..e1e3164b68 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -706,7 +706,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = { "recommended": cv.Version(55, 3, 36), "latest": cv.Version(55, 3, 36), - "dev": cv.Version(55, 3, 36), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index ba05abe8e0..090e4dcf32 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -132,18 +132,15 @@ void RD03DComponent::process_frame_() { // Header is 4 bytes, each target is 8 bytes uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); - // Extract raw bytes for this target - // Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution, - // actual radar output has Resolution before Speed (verified empirically - - // stationary targets were showing non-zero speed with original field order) + // Extract raw bytes for this target (per datasheet Table 5-2: X, Y, Speed, Resolution) uint8_t x_low = this->buffer_[offset + 0]; uint8_t x_high = this->buffer_[offset + 1]; uint8_t y_low = this->buffer_[offset + 2]; uint8_t y_high = this->buffer_[offset + 3]; - uint8_t res_low = this->buffer_[offset + 4]; - uint8_t res_high = this->buffer_[offset + 5]; - uint8_t speed_low = this->buffer_[offset + 6]; - uint8_t speed_high = this->buffer_[offset + 7]; + uint8_t speed_low = this->buffer_[offset + 4]; + uint8_t speed_high = this->buffer_[offset + 5]; + uint8_t res_low = this->buffer_[offset + 6]; + uint8_t res_high = this->buffer_[offset + 7]; // Decode values per RD-03D format int16_t x = decode_value(x_low, x_high); diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index c044148b32..7e917a9d70 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -84,8 +84,8 @@ def validate_number_of_ep(config: ConfigType) -> None: raise cv.Invalid("At least one zigbee device need to be included") count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if count == 1: - raise cv.Invalid( - "Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888" + _LOGGER.warning( + "Single endpoint requires ZHA or at leatst Zigbee2MQTT 2.8.0. For older versions of Zigbee2MQTT use multiple endpoints" ) if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode: raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}") @@ -151,7 +151,7 @@ def consume_endpoint(config: ConfigType) -> ConfigType: return config if CONF_NAME in config and " " in config[CONF_NAME]: _LOGGER.warning( - "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", + "Spaces in '%s' requires ZHA or at least Zigbee2MQTT 2.8.0. For older version of Zigbee2MQTT use '%s'", config[CONF_NAME], config[CONF_NAME].replace(" ", "_"), ) diff --git a/requirements.txt b/requirements.txt index 821324262b..bb118c5f9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tornado==6.5.4 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile +platformio==6.1.19 esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 diff --git a/tests/component_tests/epaper_spi/test_init.py b/tests/component_tests/epaper_spi/test_init.py index 71e66cd043..a9f5735fca 100644 --- a/tests/component_tests/epaper_spi/test_init.py +++ b/tests/component_tests/epaper_spi/test_init.py @@ -289,3 +289,56 @@ def test_model_with_full_update_every( "full_update_every": 10, } ) + + +def test_busy_pin_input_mode_ssd1677( + set_core_config: SetCoreConfigCallable, + set_component_config: Callable[[str, Any], None], +) -> None: + """Test that busy_pin has input mode and cs/dc/reset pins have output mode for ssd1677.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + # Configure SPI component which is required by epaper_spi + set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) + + result = run_schema_validation( + { + "id": "test_display", + "model": "ssd1677", + "dc_pin": 21, + "busy_pin": 22, + "reset_pin": 23, + "cs_pin": 5, + "dimensions": { + "width": 200, + "height": 200, + }, + } + ) + + # Verify that busy_pin has input mode set + assert CONF_BUSY_PIN in result + busy_pin_config = result[CONF_BUSY_PIN] + assert "mode" in busy_pin_config + assert busy_pin_config["mode"]["input"] is True + + # Verify that cs_pin has output mode set + assert CONF_CS_PIN in result + cs_pin_config = result[CONF_CS_PIN] + assert "mode" in cs_pin_config + assert cs_pin_config["mode"]["output"] is True + + # Verify that dc_pin has output mode set + assert CONF_DC_PIN in result + dc_pin_config = result[CONF_DC_PIN] + assert "mode" in dc_pin_config + assert dc_pin_config["mode"]["output"] is True + + # Verify that reset_pin has output mode set + assert CONF_RESET_PIN in result + reset_pin_config = result[CONF_RESET_PIN] + assert "mode" in reset_pin_config + assert reset_pin_config["mode"]["output"] is True