diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82d7ae5ee8..096b00f0f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.11.0 + uses: pypa/gh-action-pypi-publish@v1.12.2 deploy-docker: name: Build ESPHome ${{ matrix.platform }} diff --git a/docker/Dockerfile b/docker/Dockerfile index 44ee879a12..ed6ce083a8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,9 +32,9 @@ RUN \ python3-setuptools=66.1.1-1 \ python3-venv=3.11.2-1+b1 \ python3-wheel=0.38.4-2 \ - iputils-ping=3:20221126-1 \ + iputils-ping=3:20221126-1+deb12u1 \ git=1:2.39.5-0+deb12u1 \ - curl=7.88.1-10+deb12u7 \ + curl=7.88.1-10+deb12u8 \ openssh-client=1:9.2p1-2+deb12u3 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ @@ -97,7 +97,7 @@ BUILD_DEPS=" zlib1g-dev=1:1.2.13.dfsg-1 libjpeg-dev=1:2.1.5-2 libfreetype-dev=2.12.1+dfsg-5+deb12u3 - libssl-dev=3.0.14-1~deb12u2 + libssl-dev=3.0.15-1~deb12u1 libffi-dev=3.4.4-1 libopenjp2-7=2.5.0-2 libtiff6=4.5.0-6+deb12u1 diff --git a/esphome/__main__.py b/esphome/__main__.py index cf2741dbdb..86d529e1bf 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -20,6 +20,8 @@ from esphome.const import ( CONF_DEASSERT_RTS_DTR, CONF_DISABLED, CONF_ESPHOME, + CONF_LEVEL, + CONF_LOG_TOPIC, CONF_LOGGER, CONF_MDNS, CONF_MQTT, @@ -30,6 +32,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_PORT, CONF_SUBSTITUTIONS, + CONF_TOPIC, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -38,7 +41,7 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine -from esphome.helpers import indent, is_ip_address, get_bool_env +from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import Fore, color, setup_log from esphome.util import ( get_serial_ports, @@ -95,8 +98,12 @@ def choose_upload_log_host( options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == "OTA": return CORE.address - if show_mqtt and CONF_MQTT in CORE.config: - options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) + if ( + show_mqtt + and (mqtt_config := CORE.config.get(CONF_MQTT)) + and mqtt_logging_enabled(mqtt_config) + ): + options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) if default == "OTA": return "MQTT" if default is not None: @@ -106,6 +113,17 @@ def choose_upload_log_host( return choose_prompt(options, purpose=purpose) +def mqtt_logging_enabled(mqtt_config): + log_topic = mqtt_config[CONF_LOG_TOPIC] + if log_topic is None: + return False + if CONF_TOPIC not in log_topic: + return False + if log_topic.get(CONF_LEVEL, None) == "NONE": + return False + return True + + def get_port_type(port): if port.startswith("/") or port.startswith("COM"): return "SERIAL" @@ -378,7 +396,7 @@ def show_logs(config, args, port): port = mqtt.get_esphome_device_ip( config, args.username, args.password, args.client_id - ) + )[0] from esphome.components.api.client import run_logs diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index 9ef5cbecd5..afa5583e59 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -8,8 +8,13 @@ extern "C" { uint8_t temprature_sens_read(); } #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32C2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #include "driver/temp_sensor.h" +#else +#include "driver/temperature_sensor.h" +#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -25,6 +30,13 @@ namespace esphome { namespace internal_temperature { static const char *const TAG = "internal_temperature"; +#ifdef USE_ESP32 +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ + (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2)) +static temperature_sensor_handle_t tsensNew = NULL; +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32 void InternalTemperatureSensor::update() { float temperature = NAN; @@ -36,7 +48,9 @@ void InternalTemperatureSensor::update() { temperature = (raw - 32) / 1.8f; success = (raw != 128); #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32C2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); temp_sensor_set_config(tsens); temp_sensor_start(); @@ -47,6 +61,13 @@ void InternalTemperatureSensor::update() { esp_err_t result = temp_sensor_read_celsius(&temperature); temp_sensor_stop(); success = (result == ESP_OK); +#else + esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature); + success = (result == ESP_OK); + if (!success) { + ESP_LOGE(TAG, "Failed to get temperature: %d", result); + } +#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -75,6 +96,32 @@ void InternalTemperatureSensor::update() { } } +void InternalTemperatureSensor::setup() { +#ifdef USE_ESP32 +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ + (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2)) + ESP_LOGCONFIG(TAG, "Setting up temperature sensor..."); + + temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); + + esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew); + if (result != ESP_OK) { + ESP_LOGE(TAG, "Failed to install temperature sensor: %d", result); + this->mark_failed(); + return; + } + + result = temperature_sensor_enable(tsensNew); + if (result != ESP_OK) { + ESP_LOGE(TAG, "Failed to enable temperature sensor: %d", result); + this->mark_failed(); + return; + } +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32 +} + void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); } } // namespace internal_temperature diff --git a/esphome/components/internal_temperature/internal_temperature.h b/esphome/components/internal_temperature/internal_temperature.h index 0e46a69769..78e3bcef7d 100644 --- a/esphome/components/internal_temperature/internal_temperature.h +++ b/esphome/components/internal_temperature/internal_temperature.h @@ -8,6 +8,7 @@ namespace internal_temperature { class InternalTemperatureSensor : public sensor::Sensor, public PollingComponent { public: + void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 4a1a26cc0b..7476c0a09c 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -27,7 +27,7 @@ from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code from .gradient import GRADIENT_SCHEMA, gradients_to_code from .hello_world import get_hello_world from .lv_validation import lv_bool, lv_images_used -from .lvcode import LvContext, LvglComponent +from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( DISP_BG_SCHEMA, FLEX_OBJ_SCHEMA, @@ -152,41 +152,70 @@ def generate_lv_conf_h(): return LV_CONF_H_FORMAT.format("\n".join(definitions)) -def final_validation(config): - if pages := config.get(CONF_PAGES): - if all(p[df.CONF_SKIP] for p in pages): - raise cv.Invalid("At least one page must not be skipped") +def multi_conf_validate(configs: list[dict]): + displays = [config[df.CONF_DISPLAYS] for config in configs] + # flatten the display list + display_list = [disp for disps in displays for disp in disps] + if len(display_list) != len(set(display_list)): + raise cv.Invalid("A display ID may be used in only one LVGL instance") + base_config = configs[0] + for config in configs[1:]: + for item in ( + df.CONF_LOG_LEVEL, + df.CONF_COLOR_DEPTH, + df.CONF_BYTE_ORDER, + df.CONF_TRANSPARENCY_KEY, + ): + if base_config[item] != config[item]: + raise cv.Invalid( + f"Config item '{item}' must be the same for all LVGL instances" + ) + + +def final_validation(configs): + multi_conf_validate(configs) global_config = full_config.get() - for display_id in config[df.CONF_DISPLAYS]: - path = global_config.get_path_for_id(display_id)[:-1] - display = global_config.get_config_for_path(path) - if CONF_LAMBDA in display: - raise cv.Invalid("Using lambda: in display config not compatible with LVGL") - if display[CONF_AUTO_CLEAR_ENABLED]: - raise cv.Invalid( - "Using auto_clear_enabled: true in display config not compatible with LVGL" - ) - buffer_frac = config[CONF_BUFFER_SIZE] - if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: - LOGGER.warning("buffer_size: may need to be reduced without PSRAM") - for image_id in lv_images_used: - path = global_config.get_path_for_id(image_id)[:-1] - image_conf = global_config.get_config_for_path(path) - if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): - raise cv.Invalid( - "Using RGBA or RGB24 in image config not compatible with LVGL", path - ) - for w in focused_widgets: - path = global_config.get_path_for_id(w) - widget_conf = global_config.get_config_for_path(path[:-1]) - if df.CONF_ADJUSTABLE in widget_conf and not widget_conf[df.CONF_ADJUSTABLE]: - raise cv.Invalid( - "A non adjustable arc may not be focused", - path, - ) + for config in configs: + if pages := config.get(CONF_PAGES): + if all(p[df.CONF_SKIP] for p in pages): + raise cv.Invalid("At least one page must not be skipped") + for display_id in config[df.CONF_DISPLAYS]: + path = global_config.get_path_for_id(display_id)[:-1] + display = global_config.get_config_for_path(path) + if CONF_LAMBDA in display: + raise cv.Invalid( + "Using lambda: in display config not compatible with LVGL" + ) + if display[CONF_AUTO_CLEAR_ENABLED]: + raise cv.Invalid( + "Using auto_clear_enabled: true in display config not compatible with LVGL" + ) + buffer_frac = config[CONF_BUFFER_SIZE] + if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: + LOGGER.warning("buffer_size: may need to be reduced without PSRAM") + for image_id in lv_images_used: + path = global_config.get_path_for_id(image_id)[:-1] + image_conf = global_config.get_config_for_path(path) + if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): + raise cv.Invalid( + "Using RGBA or RGB24 in image config not compatible with LVGL", path + ) + for w in focused_widgets: + path = global_config.get_path_for_id(w) + widget_conf = global_config.get_config_for_path(path[:-1]) + if ( + df.CONF_ADJUSTABLE in widget_conf + and not widget_conf[df.CONF_ADJUSTABLE] + ): + raise cv.Invalid( + "A non adjustable arc may not be focused", + path, + ) -async def to_code(config): +async def to_code(configs): + config_0 = configs[0] + # Global configuration cg.add_library("lvgl/lvgl", "8.4.0") cg.add_define("USE_LVGL") # suppress default enabling of extra widgets @@ -203,53 +232,33 @@ async def to_code(config): add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') add_define( - "LV_LOG_LEVEL", f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config[df.CONF_LOG_LEVEL]]}" + "LV_LOG_LEVEL", + f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[df.CONF_LOG_LEVEL]]}", ) cg.add_define( "LVGL_LOG_LEVEL", - cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}"), + cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"), ) - add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH]) + add_define("LV_COLOR_DEPTH", config_0[df.CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: add_define(f"LV_FONT_{font.upper()}") - if config[df.CONF_COLOR_DEPTH] == 16: + if config_0[df.CONF_COLOR_DEPTH] == 16: add_define( "LV_COLOR_16_SWAP", - "1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0", + "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0", ) add_define( "LV_COLOR_CHROMA_KEY", - await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]), + await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]), ) cg.add_build_flag("-Isrc") cg.add_global(lvgl_ns.using) - frac = config[CONF_BUFFER_SIZE] - if frac >= 0.75: - frac = 1 - elif frac >= 0.375: - frac = 2 - elif frac > 0.19: - frac = 4 - else: - frac = 8 - displays = [await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]] - lv_component = cg.new_Pvariable( - config[CONF_ID], - displays, - frac, - config[df.CONF_FULL_REFRESH], - config[df.CONF_DRAW_ROUNDING], - config[df.CONF_RESUME_ON_INPUT], - ) - await cg.register_component(lv_component, config) - Widget.create(config[CONF_ID], lv_component, obj_spec, config) - for font in helpers.esphome_fonts_used: await cg.get_variable(font) cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) - default_font = config[df.CONF_DEFAULT_FONT] + default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): add_define( "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" @@ -265,39 +274,71 @@ async def to_code(config): add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) + cg.add(lvgl_static.esphome_lvgl_init()) - lv_scr_act = get_scr_act(lv_component) - async with LvContext(lv_component): - await touchscreens_to_code(lv_component, config) - await encoders_to_code(lv_component, config) - await theme_to_code(config) - await styles_to_code(config) - await gradients_to_code(config) - await set_obj_properties(lv_scr_act, config) - await add_widgets(lv_scr_act, config) - await add_pages(lv_component, config) - await add_top_layer(lv_component, config) - await msgboxes_to_code(lv_component, config) - await disp_update(lv_component.get_disp(), config) + for config in configs: + frac = config[CONF_BUFFER_SIZE] + if frac >= 0.75: + frac = 1 + elif frac >= 0.375: + frac = 2 + elif frac > 0.19: + frac = 4 + else: + frac = 8 + displays = [ + await cg.get_variable(display) for display in config[df.CONF_DISPLAYS] + ] + lv_component = cg.new_Pvariable( + config[CONF_ID], + displays, + frac, + config[df.CONF_FULL_REFRESH], + config[df.CONF_DRAW_ROUNDING], + config[df.CONF_RESUME_ON_INPUT], + ) + await cg.register_component(lv_component, config) + Widget.create(config[CONF_ID], lv_component, obj_spec, config) + + lv_scr_act = get_scr_act(lv_component) + async with LvContext(): + await touchscreens_to_code(lv_component, config) + await encoders_to_code(lv_component, config) + await theme_to_code(config) + await styles_to_code(config) + await gradients_to_code(config) + await set_obj_properties(lv_scr_act, config) + await add_widgets(lv_scr_act, config) + await add_pages(lv_component, config) + await add_top_layer(lv_component, config) + await msgboxes_to_code(lv_component, config) + await disp_update(lv_component.get_disp(), config) # Set this directly since we are limited in how many methods can be added to the Widget class. Widget.widgets_completed = True - async with LvContext(lv_component): - await generate_triggers(lv_component) - await generate_page_triggers(lv_component, config) - await initial_focus_to_code(config) - for conf in config.get(CONF_ON_IDLE, ()): - templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) - idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) - await build_automation(idle_trigger, [], conf) - for conf in config.get(df.CONF_ON_PAUSE, ()): - pause_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, True) - await build_automation(pause_trigger, [], conf) - for conf in config.get(df.CONF_ON_RESUME, ()): - resume_trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], lv_component, False - ) - await build_automation(resume_trigger, [], conf) + async with LvContext(): + await generate_triggers() + for config in configs: + lv_component = await cg.get_variable(config[CONF_ID]) + await generate_page_triggers(config) + await initial_focus_to_code(config) + for conf in config.get(CONF_ON_IDLE, ()): + templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) + idle_trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], lv_component, templ + ) + await build_automation(idle_trigger, [], conf) + for conf in config.get(df.CONF_ON_PAUSE, ()): + pause_trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], lv_component, True + ) + await build_automation(pause_trigger, [], conf) + for conf in config.get(df.CONF_ON_RESUME, ()): + resume_trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], lv_component, False + ) + await build_automation(resume_trigger, [], conf) + # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") if "transform_angle" in styles_used: @@ -312,7 +353,10 @@ async def to_code(config): def display_schema(config): value = cv.ensure_list(cv.use_id(Display))(config) - return value or [cv.use_id(Display)(config)] + value = value or [cv.use_id(Display)(config)] + if len(set(value)) != len(value): + raise cv.Invalid("Display IDs must be unique") + return value def add_hello_world(config): @@ -324,7 +368,7 @@ def add_hello_world(config): FINAL_VALIDATE_SCHEMA = final_validation -CONFIG_SCHEMA = ( +LVGL_SCHEMA = ( cv.polling_component_schema("1s") .extend(obj_schema(obj_spec)) .extend( @@ -393,3 +437,16 @@ CONFIG_SCHEMA = ( .extend(DISP_BG_SCHEMA) .add_extra(add_hello_world) ) + + +def lvgl_config_schema(config): + """ + Can't use cv.ensure_list here because it converts an empty config to an empty list, + rather than a default config. + """ + if not config or isinstance(config, dict): + return [LVGL_SCHEMA(config)] + return cv.Schema([LVGL_SCHEMA])(config) + + +CONFIG_SCHEMA = lvgl_config_schema diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 48472354f8..c26ae54892 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -1,5 +1,4 @@ -from collections.abc import Awaitable -from typing import Callable +from typing import Any, Callable from esphome import automation import esphome.codegen as cg @@ -23,7 +22,6 @@ from .lvcode import ( UPDATE_EVENT, LambdaContext, LocalVariable, - LvglComponent, ReturnStatement, add_line_marks, lv, @@ -58,7 +56,7 @@ focused_widgets = set() async def action_to_code( widgets: list[Widget], - action: Callable[[Widget], Awaitable[None]], + action: Callable[[Widget], Any], action_id, template_arg, args, @@ -137,20 +135,18 @@ async def disp_update(disp, config: dict): cv.maybe_simple_value( { cv.Required(CONF_ID): cv.use_id(lv_obj_t), - cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), }, key=CONF_ID, ), - cv.Schema( - { - cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), - } - ), + LVGL_SCHEMA, ), ) async def obj_invalidate_to_code(config, action_id, template_arg, args): - lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) - widgets = await get_widgets(config) or [get_scr_act(lv_comp)] + if CONF_LVGL_ID in config: + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) + widgets = [get_scr_act(lv_comp)] + else: + widgets = await get_widgets(config) async def do_invalidate(widget: Widget): lv_obj.invalidate(widget.obj) @@ -161,14 +157,12 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): @automation.register_action( "lvgl.update", LvglAction, - DISP_BG_SCHEMA.extend( - { - cv.GenerateID(): cv.use_id(LvglComponent), - } - ).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)), + DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra( + cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE) + ), ) async def lvgl_update_to_code(config, action_id, template_arg, args): - widgets = await get_widgets(config) + widgets = await get_widgets(config, CONF_LVGL_ID) w = widgets[0] disp = literal(f"{w.obj}->get_disp()") async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: @@ -181,32 +175,33 @@ async def lvgl_update_to_code(config, action_id, template_arg, args): @automation.register_action( "lvgl.pause", LvglAction, - { - cv.GenerateID(): cv.use_id(LvglComponent), - cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, - }, + LVGL_SCHEMA.extend( + { + cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, + } + ), ) async def pause_action_to_code(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) async with LambdaContext(LVGL_COMP_ARG) as context: add_line_marks(where=action_id) lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - await cg.register_parented(var, config[CONF_ID]) + await cg.register_parented(var, lv_comp) return var @automation.register_action( "lvgl.resume", LvglAction, - { - cv.GenerateID(): cv.use_id(LvglComponent), - }, + LVGL_SCHEMA, ) async def resume_action_to_code(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: lv_add(lvgl_comp.set_paused(False, False)) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - await cg.register_parented(var, config[CONF_ID]) + await cg.register_parented(var, lv_comp) return var @@ -265,14 +260,15 @@ def focused_id(value): ObjUpdateAction, cv.Any( cv.maybe_simple_value( - { - cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), - cv.Required(CONF_ACTION): cv.one_of( - "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True - ), - cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), - cv.Optional(CONF_FREEZE, default=False): cv.boolean, - }, + LVGL_SCHEMA.extend( + { + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + cv.Required(CONF_ACTION): cv.one_of( + "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True + ), + cv.Optional(CONF_FREEZE, default=False): cv.boolean, + } + ), key=CONF_ACTION, ), cv.maybe_simple_value( diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py index 56984405aa..ffbdc977b2 100644 --- a/esphome/components/lvgl/binary_sensor/__init__.py +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -1,4 +1,3 @@ -import esphome.codegen as cg from esphome.components.binary_sensor import ( BinarySensor, binary_sensor_schema, @@ -6,36 +5,30 @@ from esphome.components.binary_sensor import ( ) import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import EVENT_ARG, LambdaContext, LvContext -from ..schemas import LVGL_SCHEMA +from ..defines import CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static from ..types import LV_EVENT, lv_pseudo_button_t from ..widgets import Widget, get_widgets, wait_for_widgets -CONFIG_SCHEMA = ( - binary_sensor_schema(BinarySensor) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), - } - ) +CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } ) async def to_code(config): sensor = await new_binary_sensor(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] assert isinstance(widget, Widget) await wait_for_widgets() async with LambdaContext(EVENT_ARG) as pressed_ctx: pressed_ctx.add(sensor.publish_state(widget.is_pressed())) - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add(sensor.publish_initial_state(widget.is_pressed())) ctx.add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await pressed_ctx.get_lambda(), LV_EVENT.PRESSING, diff --git a/esphome/components/lvgl/light/__init__.py b/esphome/components/lvgl/light/__init__.py index 8031ae8221..dcdf67a520 100644 --- a/esphome/components/lvgl/light/__init__.py +++ b/esphome/components/lvgl/light/__init__.py @@ -4,9 +4,8 @@ from esphome.components.light import LightOutput import esphome.config_validation as cv from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID -from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..defines import CONF_WIDGET from ..lvcode import LvContext -from ..schemas import LVGL_SCHEMA from ..types import LvType, lvgl_ns from ..widgets import get_widgets, wait_for_widgets @@ -18,16 +17,15 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( cv.Required(CONF_WIDGET): cv.use_id(lv_led_t), cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight), } -).extend(LVGL_SCHEMA) +) async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add(var.set_obj(widget.obj)) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 37d6670b84..6b98cc4251 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -178,10 +178,9 @@ class LvContext(LambdaContext): added_lambda_count = 0 - def __init__(self, lv_component, args=None): + def __init__(self, args=None): self.args = args or LVGL_COMP_ARG super().__init__(parameters=self.args) - self.lv_component = lv_component async def __aexit__(self, exc_type, exc_val, exc_tb): await super().__aexit__(exc_type, exc_val, exc_tb) @@ -298,6 +297,7 @@ lv_expr = LvExpr("lv_") lv_obj = MockLv("lv_obj_") # Operations on the LVGL component lvgl_comp = MockObj(LVGL_COMP, "->") +lvgl_static = MockObj("LvglComponent", "::") # equivalent to cg.add() for the current code context diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 70cfb859de..41346bc732 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -98,19 +98,24 @@ void LvglComponent::set_paused(bool paused, bool show_snow) { this->pause_callbacks_.call(paused); } +void LvglComponent::esphome_lvgl_init() { + lv_init(); + lv_update_event = static_cast(lv_event_register_id()); + lv_api_event = static_cast(lv_event_register_id()); +} void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { - lv_obj_add_event_cb(obj, callback, event, this); + lv_obj_add_event_cb(obj, callback, event, nullptr); } void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2) { - this->add_event_cb(obj, callback, event1); - this->add_event_cb(obj, callback, event2); + add_event_cb(obj, callback, event1); + add_event_cb(obj, callback, event2); } void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3) { - this->add_event_cb(obj, callback, event1); - this->add_event_cb(obj, callback, event2); - this->add_event_cb(obj, callback, event3); + add_event_cb(obj, callback, event1); + add_event_cb(obj, callback, event2); + add_event_cb(obj, callback, event3); } void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); @@ -218,8 +223,10 @@ PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue paused) } #ifdef USE_LVGL_TOUCHSCREEN -LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { +LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { + this->set_parent(parent); lv_indev_drv_init(&this->drv_); + this->drv_.disp = parent->get_disp(); this->drv_.long_press_repeat_time = long_press_repeat_time; this->drv_.long_press_time = long_press_time; this->drv_.type = LV_INDEV_TYPE_POINTER; @@ -235,6 +242,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r } }; } + void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); if (this->touch_pressed_) @@ -405,9 +413,6 @@ LvglComponent::LvglComponent(std::vector displays, float buf buffer_frac_(buffer_frac), full_refresh_(full_refresh), resume_on_input_(resume_on_input) { - lv_init(); - lv_update_event = static_cast(lv_event_register_id()); - lv_api_event = static_cast(lv_event_register_id()); auto *display = this->displays_[0]; size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index f357c4950c..dae07d5153 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -146,10 +146,14 @@ class LvglComponent : public PollingComponent { } } - void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); - void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); - void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, - lv_event_code_t event3); + /** + * Initialize the LVGL library and register custom events. + */ + static void esphome_lvgl_init(); + static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); + static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); + static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, + lv_event_code_t event3); void add_page(LvPageType *page); void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); void show_next_page(lv_scr_load_anim_t anim, uint32_t time); @@ -231,7 +235,7 @@ template class LvglCondition : public Condition, public P #ifdef USE_LVGL_TOUCHSCREEN class LVTouchListener : public touchscreen::TouchListener, public Parented { public: - LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time); + LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent); void update(const touchscreen::TouchPoints_t &tpoints) override; void release() override { touch_pressed_ = false; diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 07f92635b5..b41a36bc0f 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -3,7 +3,7 @@ from esphome.components import number import esphome.config_validation as cv from esphome.cpp_generator import MockObj -from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_UPDATE_ON_RELEASE, CONF_WIDGET +from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET from ..lv_validation import animated from ..lvcode import ( API_EVENT, @@ -13,28 +13,23 @@ from ..lvcode import ( LvContext, lv, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvNumber, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) -CONFIG_SCHEMA = ( - number.number_schema(LVGLNumber) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvNumber), - cv.Optional(CONF_ANIMATED, default=True): animated, - cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, - } - ) +CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + cv.Optional(CONF_ANIMATED, default=True): animated, + cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, + } ) async def to_code(config): - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] var = await number.new_number( @@ -58,10 +53,10 @@ async def to_code(config): if not config[CONF_UPDATE_ON_RELEASE] else LV_EVENT.RELEASED ) - async with LvContext(paren): + async with LvContext(): lv_add(var.set_control_lambda(await control.get_lambda())) lv_add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code ) ) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index bb14c11ddd..516627708e 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -391,7 +391,9 @@ def container_validator(schema, widget_type: WidgetType): add_lv_use(ltype) if value == SCHEMA_EXTRACT: return result - result = result.extend(LAYOUT_SCHEMAS[ltype.lower()]) + result = result.extend( + LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) + ) return result(value) return validator diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py index 5e50b6b385..bd5ef8f237 100644 --- a/esphome/components/lvgl/select/__init__.py +++ b/esphome/components/lvgl/select/__init__.py @@ -1,25 +1,19 @@ -import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv from esphome.const import CONF_OPTIONS -from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET, literal +from ..defines import CONF_ANIMATED, CONF_WIDGET, literal from ..lvcode import LvContext -from ..schemas import LVGL_SCHEMA from ..types import LvSelect, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) -CONFIG_SCHEMA = ( - select.select_schema(LVGLSelect) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvSelect), - cv.Optional(CONF_ANIMATED, default=False): cv.boolean, - } - ) +CONFIG_SCHEMA = select.select_schema(LVGLSelect).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvSelect), + cv.Optional(CONF_ANIMATED, default=False): cv.boolean, + } ) @@ -28,9 +22,8 @@ async def to_code(config): widget = widget[0] options = widget.config.get(CONF_OPTIONS, []) selector = await select.new_select(config, options=options) - paren = await cg.get_variable(config[CONF_LVGL_ID]) await wait_for_widgets() - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add( selector.set_widget( widget.var, diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index a2a2298c27..03b2638ed0 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -1,8 +1,7 @@ -import esphome.codegen as cg from esphome.components.sensor import Sensor, new_sensor, sensor_schema import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..defines import CONF_WIDGET from ..lvcode import ( API_EVENT, EVENT_ARG, @@ -11,34 +10,29 @@ from ..lvcode import ( LambdaContext, LvContext, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvNumber from ..widgets import Widget, get_widgets, wait_for_widgets -CONFIG_SCHEMA = ( - sensor_schema(Sensor) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvNumber), - } - ) +CONFIG_SCHEMA = sensor_schema(Sensor).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + } ) async def to_code(config): sensor = await new_sensor(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] assert isinstance(widget, Widget) await wait_for_widgets() async with LambdaContext(EVENT_ARG) as lamb: lv_add(sensor.publish_state(widget.get_value())) - async with LvContext(paren, LVGL_COMP_ARG): + async with LvContext(LVGL_COMP_ARG): lv_add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py index 8c090543f9..4e1e7f72e0 100644 --- a/esphome/components/lvgl/switch/__init__.py +++ b/esphome/components/lvgl/switch/__init__.py @@ -3,7 +3,7 @@ from esphome.components.switch import Switch, new_switch, switch_schema import esphome.config_validation as cv from esphome.cpp_generator import MockObj -from ..defines import CONF_LVGL_ID, CONF_WIDGET, literal +from ..defines import CONF_WIDGET, literal from ..lvcode import ( API_EVENT, EVENT_ARG, @@ -13,26 +13,21 @@ from ..lvcode import ( LvContext, lv, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) -CONFIG_SCHEMA = ( - switch_schema(LVGLSwitch) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), - } - ) +CONFIG_SCHEMA = switch_schema(LVGLSwitch).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } ) async def to_code(config): switch = await new_switch(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() @@ -45,10 +40,10 @@ async def to_code(config): widget.clear_state(LV_STATE.CHECKED) lv.event_send(widget.obj, API_EVENT, cg.nullptr) control.add(switch.publish_state(literal("v"))) - async with LvContext(paren) as ctx: + async with LvContext() as ctx: lv_add(switch.set_control_lambda(await control.get_lambda())) ctx.add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await checked_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py index a59e703591..89db139a6a 100644 --- a/esphome/components/lvgl/text/__init__.py +++ b/esphome/components/lvgl/text/__init__.py @@ -3,7 +3,7 @@ from esphome.components import text from esphome.components.text import new_text import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..defines import CONF_WIDGET from ..lvcode import ( API_EVENT, EVENT_ARG, @@ -12,14 +12,14 @@ from ..lvcode import ( LvContext, lv, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvText, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLText = lvgl_ns.class_("LVGLText", text.Text) -CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( +CONFIG_SCHEMA = text.TEXT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(LVGLText), cv.Required(CONF_WIDGET): cv.use_id(LvText), @@ -29,7 +29,6 @@ CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( async def to_code(config): textvar = await new_text(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() @@ -39,10 +38,10 @@ async def to_code(config): control.add(textvar.publish_state(widget.get_value())) async with LambdaContext(EVENT_ARG) as lamb: lv_add(textvar.publish_state(widget.get_value())) - async with LvContext(paren): + async with LvContext(): lv_add(textvar.set_control_lambda(await control.get_lambda())) lv_add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py index ae39eec291..4728fd137a 100644 --- a/esphome/components/lvgl/text_sensor/__init__.py +++ b/esphome/components/lvgl/text_sensor/__init__.py @@ -1,4 +1,3 @@ -import esphome.codegen as cg from esphome.components.text_sensor import ( TextSensor, new_text_sensor, @@ -6,34 +5,35 @@ from esphome.components.text_sensor import ( ) import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext -from ..schemas import LVGL_SCHEMA +from ..defines import CONF_WIDGET +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lvgl_static, +) from ..types import LV_EVENT, LvText from ..widgets import get_widgets, wait_for_widgets -CONFIG_SCHEMA = ( - text_sensor_schema(TextSensor) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvText), - } - ) +CONFIG_SCHEMA = text_sensor_schema(TextSensor).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } ) async def to_code(config): sensor = await new_text_sensor(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() async with LambdaContext(EVENT_ARG) as pressed_ctx: pressed_ctx.add(sensor.publish_state(widget.get_value())) - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await pressed_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index 4d430a428e..f2dd013f6d 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -33,13 +33,12 @@ def touchscreen_schema(config): return [TOUCHSCREENS_CONFIG(config)] -async def touchscreens_to_code(var, config): +async def touchscreens_to_code(lv_component, config): for tconf in config[CONF_TOUCHSCREENS]: lvgl_components_required.add(CONF_TOUCHSCREEN) touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds - listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt) - await cg.register_parented(listener, var) + listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component) lv.indev_drv_register(listener.get_drv()) cg.add(touchscreen.register_listener(listener)) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index eb6e483203..fb856df04e 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -20,17 +20,16 @@ from .lvcode import ( lv, lv_add, lv_event_t_ptr, + lvgl_static, ) from .types import LV_EVENT from .widgets import widget_map -async def generate_triggers(lv_component): +async def generate_triggers(): """ Generate LVGL triggers for all defined widgets Must be done after all widgets completed - :param lv_component: The parent component - :return: """ for w in widget_map.values(): @@ -43,11 +42,10 @@ async def generate_triggers(lv_component): conf = conf[0] w.add_flag("LV_OBJ_FLAG_CLICKABLE") event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) - await add_trigger(conf, lv_component, w, event) + await add_trigger(conf, w, event) for conf in w.config.get(CONF_ON_VALUE, ()): await add_trigger( conf, - lv_component, w, LV_EVENT.VALUE_CHANGED, API_EVENT, @@ -63,7 +61,7 @@ async def generate_triggers(lv_component): lv.obj_align_to(w.obj, target, align, x, y) -async def add_trigger(conf, lv_component, w, *events): +async def add_trigger(conf, w, *events): tid = conf[CONF_TRIGGER_ID] trigger = cg.new_Pvariable(tid) args = w.get_args() + [(lv_event_t_ptr, "event")] @@ -72,4 +70,4 @@ async def add_trigger(conf, lv_component, w, *events): async with LambdaContext(EVENT_ARG, where=tid) as context: with LvConditional(w.is_selected()): lv_add(trigger.trigger(*value, literal("event"))) - lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events)) + lv_add(lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *events)) diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py index 3b20008c3d..8adea72ad3 100644 --- a/esphome/components/lvgl/widgets/animimg.py +++ b/esphome/components/lvgl/widgets/animimg.py @@ -60,9 +60,10 @@ class AnimimgType(WidgetType): lvgl_components_required.add(CONF_IMAGE) lvgl_components_required.add(CONF_ANIMIMG) if CONF_SRC in config: - for x in config[CONF_SRC]: - await cg.get_variable(x) - srcs = [await lv_image.process(x) for x in config[CONF_SRC]] + srcs = [ + await lv_image.process(await cg.get_variable(x)) + for x in config[CONF_SRC] + ] src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs) count = len(config[CONF_SRC]) lv.animimg_set_src(w.obj, src_id, count) diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index 59b2c97c63..931d0c0b5b 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,3 +1,4 @@ +import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ANGLE, CONF_MODE @@ -64,6 +65,7 @@ class ImgType(WidgetType): async def to_code(self, w: Widget, config): if src := config.get(CONF_SRC): + src = await cg.get_variable(src) lv.img_set_src(w.obj, await lv_image.process(src)) if (cf_angle := config.get(CONF_ANGLE)) is not None: pivot_x = config[CONF_PIVOT_X] diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py index 0e84ab6791..a754a9cb9a 100644 --- a/esphome/components/lvgl/widgets/page.py +++ b/esphome/components/lvgl/widgets/page.py @@ -20,6 +20,7 @@ from ..lvcode import ( add_line_marks, lv_add, lvgl_comp, + lvgl_static, ) from ..schemas import LVGL_SCHEMA from ..types import LvglAction, lv_page_t @@ -139,7 +140,7 @@ async def add_pages(lv_component, config): await add_widgets(page, pconf) -async def generate_page_triggers(lv_component, config): +async def generate_page_triggers(config): for pconf in config.get(CONF_PAGES, ()): page = (await get_widgets(pconf))[0] for ev in (CONF_ON_LOAD, CONF_ON_UNLOAD): @@ -149,7 +150,7 @@ async def generate_page_triggers(lv_component, config): async with LambdaContext(EVENT_ARG, where=id) as context: lv_add(trigger.trigger()) lv_add( - lv_component.add_event_cb( + lvgl_static.add_event_cb( page.obj, await context.get_lambda(), literal(f"LV_EVENT_SCREEN_{ev[3:].upper()}_START"), diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 8851581ea0..86d163e61d 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_DISCOVERY_UNIQUE_ID_GENERATOR, + CONF_ENABLE_ON_BOOT, CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, @@ -99,6 +100,8 @@ MQTTMessage = mqtt_ns.struct("MQTTMessage") MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component) MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action) MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action) +MQTTEnableAction = mqtt_ns.class_("MQTTEnableAction", automation.Action) +MQTTDisableAction = mqtt_ns.class_("MQTTDisableAction", automation.Action) MQTTMessageTrigger = mqtt_ns.class_( "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component ) @@ -208,6 +211,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(MQTTClientComponent), cv.Required(CONF_BROKER): cv.string_strict, + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_PORT, default=1883): cv.port, cv.Optional(CONF_USERNAME, default=""): cv.string, cv.Optional(CONF_PASSWORD, default=""): cv.string, @@ -325,6 +329,7 @@ async def to_code(config): cg.add_global(mqtt_ns.using) cg.add(var.set_broker_address(config[CONF_BROKER])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_broker_port(config[CONF_PORT])) cg.add(var.set_username(config[CONF_USERNAME])) cg.add(var.set_password(config[CONF_PASSWORD])) @@ -555,3 +560,31 @@ async def register_mqtt_component(var, config): async def mqtt_connected_to_code(config, condition_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren) + + +@automation.register_action( + "mqtt.enable", + MQTTEnableAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), +) +async def mqtt_enable_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "mqtt.disable", + MQTTDisableAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), +) +async def mqtt_disable_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index b5ac285026..106192c0e3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -50,6 +50,8 @@ void MQTTClientComponent::setup() { } }); this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) { + if (this->state_ == MQTT_CLIENT_DISABLED) + return; this->state_ = MQTT_CLIENT_DISCONNECTED; this->disconnect_reason_ = reason; }); @@ -77,8 +79,9 @@ void MQTTClientComponent::setup() { topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); } - this->last_connected_ = millis(); - this->start_dnslookup_(); + if (this->enable_on_boot_) { + this->enable(); + } } void MQTTClientComponent::send_device_info_() { @@ -163,7 +166,9 @@ void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str()); } } -bool MQTTClientComponent::can_proceed() { return network::is_disabled() || this->is_connected(); } +bool MQTTClientComponent::can_proceed() { + return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected(); +} void MQTTClientComponent::start_dnslookup_() { for (auto &subscription : this->subscriptions_) { @@ -339,6 +344,8 @@ void MQTTClientComponent::loop() { const uint32_t now = millis(); switch (this->state_) { + case MQTT_CLIENT_DISABLED: + return; // Return to avoid a reboot when disabled case MQTT_CLIENT_DISCONNECTED: if (now - this->connect_begin_ > 5000) { this->start_dnslookup_(); @@ -501,6 +508,23 @@ bool MQTTClientComponent::publish_json(const std::string &topic, const json::jso return this->publish(topic, message, qos, retain); } +void MQTTClientComponent::enable() { + if (this->state_ != MQTT_CLIENT_DISABLED) + return; + ESP_LOGD(TAG, "Enabling MQTT..."); + this->state_ = MQTT_CLIENT_DISCONNECTED; + this->last_connected_ = millis(); + this->start_dnslookup_(); +} + +void MQTTClientComponent::disable() { + if (this->state_ == MQTT_CLIENT_DISABLED) + return; + ESP_LOGD(TAG, "Disabling MQTT..."); + this->state_ = MQTT_CLIENT_DISABLED; + this->on_shutdown(); +} + /** Check if the message topic matches the given subscription topic * * INFO: MQTT spec mandates that topics must not be empty and must be valid NULL-terminated UTF-8 strings. diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 887800f201..7ae3a6c5e8 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -87,7 +87,8 @@ struct MQTTDiscoveryInfo { }; enum MQTTClientState { - MQTT_CLIENT_DISCONNECTED = 0, + MQTT_CLIENT_DISABLED = 0, + MQTT_CLIENT_DISCONNECTED, MQTT_CLIENT_RESOLVING_ADDRESS, MQTT_CLIENT_CONNECTING, MQTT_CLIENT_CONNECTED, @@ -247,6 +248,9 @@ class MQTTClientComponent : public Component { void register_mqtt_component(MQTTComponent *component); bool is_connected(); + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + void enable(); + void disable(); void on_shutdown() override; @@ -314,10 +318,11 @@ class MQTTClientComponent : public Component { MQTTBackendLibreTiny mqtt_backend_; #endif - MQTTClientState state_{MQTT_CLIENT_DISCONNECTED}; + MQTTClientState state_{MQTT_CLIENT_DISABLED}; network::IPAddress ip_; bool dns_resolved_{false}; bool dns_resolve_error_{false}; + bool enable_on_boot_{true}; std::vector children_; uint32_t reboot_timeout_{300000}; uint32_t connect_begin_; @@ -414,6 +419,26 @@ template class MQTTConnectedCondition : public Condition MQTTClientComponent *parent_; }; +template class MQTTEnableAction : public Action { + public: + MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->enable(); } + + protected: + MQTTClientComponent *parent_; +}; + +template class MQTTDisableAction : public Action { + public: + MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->disable(); } + + protected: + MQTTClientComponent *parent_; +}; + } // namespace mqtt } // namespace esphome diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 27338b8608..9dbad27102 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -335,19 +335,28 @@ def sensor_schema( return SENSOR_SCHEMA.extend(schema) -@FILTER_REGISTRY.register("offset", OffsetFilter, cv.float_) +@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) async def offset_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config) + template_ = await cg.templatable(config, [], float) + return cg.new_Pvariable(filter_id, template_) -@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.float_) +@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.templatable(cv.float_)) async def multiply_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config) + template_ = await cg.templatable(config, [], float) + return cg.new_Pvariable(filter_id, template_) -@FILTER_REGISTRY.register("filter_out", FilterOutValueFilter, cv.float_) +@FILTER_REGISTRY.register( + "filter_out", + FilterOutValueFilter, + cv.Any(cv.templatable(cv.float_), [cv.templatable(cv.float_)]), +) async def filter_out_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config) + if not isinstance(config, list): + config = [config] + template_ = [await cg.templatable(x, [], float) for x in config] + return cg.new_Pvariable(filter_id, template_) QUANTILE_SCHEMA = cv.All( @@ -573,7 +582,7 @@ async def heartbeat_filter_to_code(config, filter_id): TIMEOUT_SCHEMA = cv.maybe_simple_value( { cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, - cv.Optional(CONF_VALUE, default="nan"): cv.float_, + cv.Optional(CONF_VALUE, default="nan"): cv.templatable(cv.float_), }, key=CONF_TIMEOUT, ) @@ -581,7 +590,8 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], config[CONF_VALUE]) + template_ = await cg.templatable(config[CONF_VALUE], [], float) + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) return var diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index bcf1fc8269..0a8740dd5b 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -288,36 +288,36 @@ optional LambdaFilter::new_value(float value) { } // OffsetFilter -OffsetFilter::OffsetFilter(float offset) : offset_(offset) {} +OffsetFilter::OffsetFilter(TemplatableValue offset) : offset_(std::move(offset)) {} -optional OffsetFilter::new_value(float value) { return value + this->offset_; } +optional OffsetFilter::new_value(float value) { return value + this->offset_.value(); } // MultiplyFilter -MultiplyFilter::MultiplyFilter(float multiplier) : multiplier_(multiplier) {} +MultiplyFilter::MultiplyFilter(TemplatableValue multiplier) : multiplier_(std::move(multiplier)) {} -optional MultiplyFilter::new_value(float value) { return value * this->multiplier_; } +optional MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } // FilterOutValueFilter -FilterOutValueFilter::FilterOutValueFilter(float value_to_filter_out) : value_to_filter_out_(value_to_filter_out) {} +FilterOutValueFilter::FilterOutValueFilter(std::vector> values_to_filter_out) + : values_to_filter_out_(std::move(values_to_filter_out)) {} optional FilterOutValueFilter::new_value(float value) { - if (std::isnan(this->value_to_filter_out_)) { - if (std::isnan(value)) { - return {}; - } else { - return value; + int8_t accuracy = this->parent_->get_accuracy_decimals(); + float accuracy_mult = powf(10.0f, accuracy); + for (auto filter_value : this->values_to_filter_out_) { + if (std::isnan(filter_value.value())) { + if (std::isnan(value)) { + return {}; + } + continue; } - } else { - int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = powf(10.0f, accuracy); - float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_); + float rounded_filter_out = roundf(accuracy_mult * filter_value.value()); float rounded_value = roundf(accuracy_mult * value); if (rounded_filter_out == rounded_value) { return {}; - } else { - return value; } } + return value; } // ThrottleFilter @@ -383,11 +383,12 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { // TimeoutFilter optional TimeoutFilter::new_value(float value) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_); }); + this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value()); }); return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period, float new_value) : time_period_(time_period), value_(new_value) {} +TimeoutFilter::TimeoutFilter(uint32_t time_period, TemplatableValue new_value) + : time_period_(time_period), value_(std::move(new_value)) {} float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } // DebounceFilter diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 92b1d8d240..86586b458d 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -5,6 +5,7 @@ #include #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/automation.h" namespace esphome { namespace sensor { @@ -273,34 +274,33 @@ class LambdaFilter : public Filter { /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: - explicit OffsetFilter(float offset); + explicit OffsetFilter(TemplatableValue offset); optional new_value(float value) override; protected: - float offset_; + TemplatableValue offset_; }; /// A simple filter that multiplies to each value it receives by `multiplier`. class MultiplyFilter : public Filter { public: - explicit MultiplyFilter(float multiplier); - + explicit MultiplyFilter(TemplatableValue multiplier); optional new_value(float value) override; protected: - float multiplier_; + TemplatableValue multiplier_; }; /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. class FilterOutValueFilter : public Filter { public: - explicit FilterOutValueFilter(float value_to_filter_out); + explicit FilterOutValueFilter(std::vector> values_to_filter_out); optional new_value(float value) override; protected: - float value_to_filter_out_; + std::vector> values_to_filter_out_; }; class ThrottleFilter : public Filter { @@ -316,8 +316,7 @@ class ThrottleFilter : public Filter { class TimeoutFilter : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period, float new_value); - void set_value(float new_value) { this->value_ = new_value; } + explicit TimeoutFilter(uint32_t time_period, TemplatableValue new_value); optional new_value(float value) override; @@ -325,7 +324,7 @@ class TimeoutFilter : public Filter, public Component { protected: uint32_t time_period_; - float value_; + TemplatableValue value_; }; class DebounceFilter : public Filter, public Component { diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index fdf19bb56e..52afbf365e 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,40 +1,37 @@ import re +from esphome import pins import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv from esphome.components.esp32.const import ( KEY_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32S3, VARIANT_ESP32C2, VARIANT_ESP32C3, VARIANT_ESP32C6, VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, ) -from esphome import pins +import esphome.config_validation as cv from esphome.const import ( CONF_CLK_PIN, + CONF_CS_PIN, + CONF_DATA_PINS, + CONF_DATA_RATE, CONF_ID, + CONF_INVERTED, CONF_MISO_PIN, CONF_MOSI_PIN, - CONF_SPI_ID, - CONF_CS_PIN, CONF_NUMBER, - CONF_INVERTED, + CONF_SPI_ID, KEY_CORE, KEY_TARGET_PLATFORM, KEY_VARIANT, - CONF_DATA_RATE, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, - CONF_DATA_PINS, -) -from esphome.core import ( - coroutine_with_priority, - CORE, ) +from esphome.core import CORE, coroutine_with_priority +import esphome.final_validate as fv CODEOWNERS = ["@esphome/core", "@clydebarrow"] spi_ns = cg.esphome_ns.namespace("spi") @@ -69,6 +66,10 @@ SPI_MODE_OPTIONS = { 1: SPIMode.MODE1, 2: SPIMode.MODE2, 3: SPIMode.MODE3, + "0": SPIMode.MODE0, + "1": SPIMode.MODE1, + "2": SPIMode.MODE2, + "3": SPIMode.MODE3, } CONF_SPI_MODE = "spi_mode" diff --git a/esphome/components/spi_device/__init__.py b/esphome/components/spi_device/__init__.py index 65e7ee6fc6..2f23d8a011 100644 --- a/esphome/components/spi_device/__init__.py +++ b/esphome/components/spi_device/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import spi +import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE DEPENDENCIES = ["spi"] @@ -11,18 +11,6 @@ spi_device_ns = cg.esphome_ns.namespace("spi_device") spi_device = spi_device_ns.class_("SPIDeviceComponent", cg.Component, spi.SPIDevice) -Mode = spi.spi_ns.enum("SPIMode") -MODES = { - "0": Mode.MODE0, - "1": Mode.MODE1, - "2": Mode.MODE2, - "3": Mode.MODE3, - "MODE0": Mode.MODE0, - "MODE1": Mode.MODE1, - "MODE2": Mode.MODE2, - "MODE3": Mode.MODE3, -} - BitOrder = spi.spi_ns.enum("SPIBitOrder") ORDERS = { "msb_first": BitOrder.BIT_ORDER_MSB_FIRST, @@ -34,7 +22,9 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(spi_device), cv.Optional(CONF_BIT_ORDER, default="msb_first"): cv.enum(ORDERS, lower=True), - cv.Optional(CONF_MODE, default="0"): cv.enum(MODES, upper=True), + cv.Optional(CONF_MODE): cv.invalid( + "The 'mode' option has been renamed to 'spi_mode'." + ), } ).extend(spi.spi_device_schema(False, "1MHz")) @@ -42,6 +32,5 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - cg.add(var.set_mode(config[CONF_MODE])) cg.add(var.set_bit_order(config[CONF_BIT_ORDER])) await spi.register_spi_device(var, config) diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index bd212bc563..9f6399ca8b 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -26,7 +26,7 @@ class MDNSStatus: self.host_mdns_state: dict[str, bool | None] = {} self._loop = asyncio.get_running_loop() - async def async_resolve_host(self, host_name: str) -> str | None: + async def async_resolve_host(self, host_name: str) -> list[str] | None: """Resolve a host name to an address in a thread-safe manner.""" if aiozc := self.aiozc: return await aiozc.async_resolve_host(host_name) @@ -50,13 +50,12 @@ class MDNSStatus: poll_names.setdefault(entry.name, set()).add(entry) elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: entries.async_set_state(entry, bool_to_entry_state(online)) - if poll_names and self.aiozc: results = await asyncio.gather( *(self.aiozc.async_resolve_host(name) for name in poll_names) ) - for name, address in zip(poll_names, results): - result = bool(address) + for name, address_list in zip(poll_names, results): + result = bool(address_list) host_mdns_state[name] = result for entry in poll_names[name]: entries.async_set_state(entry, bool_to_entry_state(result)) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9aeece9aab..07f7f019f8 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -320,12 +320,12 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): and "api" in entry.loaded_integrations ): if (mdns := dashboard.mdns_status) and ( - address := await mdns.async_resolve_host(entry.name) + address_list := await mdns.async_resolve_host(entry.name) ): # Use the IP address if available but only # if the API is loaded and the device is online # since MQTT logging will not work otherwise - port = address + port = address_list[0] elif ( entry.address and ( diff --git a/esphome/espota2.py b/esphome/espota2.py index 580536153a..94b845b246 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -10,7 +10,7 @@ import sys import time from esphome.core import EsphomeError -from esphome.helpers import is_ip_address, resolve_ip_address +from esphome.helpers import resolve_ip_address RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 @@ -311,44 +311,45 @@ def perform_ota( def run_ota_impl_(remote_host, remote_port, password, filename): - if is_ip_address(remote_host): - _LOGGER.info("Connecting to %s", remote_host) - ip = remote_host - else: - _LOGGER.info("Resolving IP address of %s", remote_host) - try: - ip = resolve_ip_address(remote_host) - except EsphomeError as err: - _LOGGER.error( - "Error resolving IP address of %s. Is it connected to WiFi?", - remote_host, - ) - _LOGGER.error( - "(If this error persists, please set a static IP address: " - "https://esphome.io/components/wifi.html#manual-ips)" - ) - raise OTAError(err) from err - _LOGGER.info(" -> %s", ip) - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(10.0) try: - sock.connect((ip, remote_port)) - except OSError as err: - sock.close() - _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err) - return 1 + res = resolve_ip_address(remote_host, remote_port) + except EsphomeError as err: + _LOGGER.error( + "Error resolving IP address of %s. Is it connected to WiFi?", + remote_host, + ) + _LOGGER.error( + "(If this error persists, please set a static IP address: " + "https://esphome.io/components/wifi.html#manual-ips)" + ) + raise OTAError(err) from err - with open(filename, "rb") as file_handle: + for r in res: + af, socktype, _, _, sa = r + _LOGGER.info("Connecting to %s port %s...", sa[0], sa[1]) + sock = socket.socket(af, socktype) + sock.settimeout(10.0) try: - perform_ota(sock, password, file_handle, filename) - except OTAError as err: - _LOGGER.error(str(err)) - return 1 - finally: + sock.connect(sa) + except OSError as err: sock.close() + _LOGGER.error("Connecting to %s port %s failed: %s", sa[0], sa[1], err) + continue - return 0 + _LOGGER.info("Connected to %s", sa[0]) + with open(filename, "rb") as file_handle: + try: + perform_ota(sock, password, file_handle, filename) + except OTAError as err: + _LOGGER.error(str(err)) + return 1 + finally: + sock.close() + + return 0 + + _LOGGER.error("Connection failed.") + return 1 def run_ota(remote_host, remote_port, password, filename): diff --git a/esphome/helpers.py b/esphome/helpers.py index 2a7e5cd9b6..8aae43c2bb 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,5 +1,6 @@ import codecs from contextlib import suppress +import ipaddress import logging import os from pathlib import Path @@ -91,12 +92,8 @@ def mkdir_p(path): def is_ip_address(host): - parts = host.split(".") - if len(parts) != 4: - return False try: - for p in parts: - int(p) + ipaddress.ip_address(host) return True except ValueError: return False @@ -127,25 +124,80 @@ def _resolve_with_zeroconf(host): return info -def resolve_ip_address(host): +def addr_preference_(res): + # Trivial alternative to RFC6724 sorting. Put sane IPv6 first, then + # Legacy IP, then IPv6 link-local addresses without an actual link. + sa = res[4] + ip = ipaddress.ip_address(sa[0]) + if ip.version == 4: + return 2 + if ip.is_link_local and sa[3] == 0: + return 3 + return 1 + + +def resolve_ip_address(host, port): import socket from esphome.core import EsphomeError + # There are five cases here. The host argument could be one of: + # • a *list* of IP addresses discovered by MQTT, + # • a single IP address specified by the user, + # • a .local hostname to be resolved by mDNS, + # • a normal hostname to be resolved in DNS, or + # • A URL from which we should extract the hostname. + # + # In each of the first three cases, we end up with IP addresses in + # string form which need to be converted to a 5-tuple to be used + # for the socket connection attempt. The easiest way to construct + # those is to pass the IP address string to getaddrinfo(). Which, + # coincidentally, is how we do hostname lookups in the other cases + # too. So first build a list which contains either IP addresses or + # a single hostname, then call getaddrinfo() on each element of + # that list. + errs = [] + if isinstance(host, list): + addr_list = host + elif is_ip_address(host): + addr_list = [host] + else: + url = urlparse(host) + if url.scheme != "": + host = url.hostname - if host.endswith(".local"): + addr_list = [] + if host.endswith(".local"): + try: + _LOGGER.info("Resolving IP address of %s in mDNS", host) + addr_list = _resolve_with_zeroconf(host) + except EsphomeError as err: + errs.append(str(err)) + + # If not mDNS, or if mDNS failed, use normal DNS + if not addr_list: + addr_list = [host] + + # Now we have a list containing either IP addresses or a hostname + res = [] + for addr in addr_list: + if not is_ip_address(addr): + _LOGGER.info("Resolving IP address of %s", host) try: - return _resolve_with_zeroconf(host) - except EsphomeError as err: + r = socket.getaddrinfo(addr, port, proto=socket.IPPROTO_TCP) + except OSError as err: errs.append(str(err)) + raise EsphomeError( + f"Error resolving IP address: {', '.join(errs)}" + ) from err - try: - host_url = host if (urlparse(host).scheme != "") else "http://" + host - return socket.gethostbyname(urlparse(host_url).hostname) - except OSError as err: - errs.append(str(err)) - raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err + res = res + r + + # Zeroconf tends to give us link-local IPv6 addresses without specifying + # the link. Put those last in the list to be attempted. + res.sort(key=addr_preference_) + return res def get_bool_env(var, default=False): diff --git a/esphome/mqtt.py b/esphome/mqtt.py index d55fb0202d..2f90c49025 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -175,8 +175,15 @@ def get_esphome_device_ip( _LOGGER.Warn("Wrong device answer") return - if "ip" in data: - dev_ip = data["ip"] + dev_ip = [] + key = "ip" + n = 0 + while key in data: + dev_ip.append(data[key]) + n = n + 1 + key = "ip" + str(n) + + if dev_ip: client.disconnect() def on_connect(client, userdata, flags, return_code): diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index b3ee64e259..5a92a4ed7c 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -176,24 +176,26 @@ def _make_host_resolver(host: str) -> HostResolver: class EsphomeZeroconf(Zeroconf): - def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: + def resolve_host(self, host: str, timeout: float = 3.0) -> list[str] | None: """Resolve a host name to an IP address.""" info = _make_host_resolver(host) if ( info.load_from_cache(self) or (timeout and info.request(self, timeout * 1000)) - ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): - return str(addresses[0]) + ) and (addresses := info.parsed_scoped_addresses(IPVersion.All)): + return addresses return None class AsyncEsphomeZeroconf(AsyncZeroconf): - async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None: + async def async_resolve_host( + self, host: str, timeout: float = 3.0 + ) -> list[str] | None: """Resolve a host name to an IP address.""" info = _make_host_resolver(host) if ( info.load_from_cache(self.zeroconf) or (timeout and await info.async_request(self.zeroconf, timeout * 1000)) - ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): - return str(addresses[0]) + ) and (addresses := info.parsed_scoped_addresses(IPVersion.All)): + return addresses return None diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 3a490bbe15..34918cb113 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -1,5 +1,12 @@ display: - platform: sdl + id: sdl0 + auto_clear_enabled: false + dimensions: + width: 480 + height: 320 + - platform: sdl + id: sdl1 auto_clear_enabled: false dimensions: width: 480 @@ -7,5 +14,30 @@ display: touchscreen: - platform: sdl + display: sdl0 + sdl_id: sdl0 lvgl: + - id: lvgl_0 + displays: sdl0 + - id: lvgl_1 + displays: sdl1 + on_idle: + timeout: 8s + then: + if: + condition: + lvgl.is_idle: + lvgl_id: lvgl_1 + timeout: 5s + then: + logger.log: Lvgl2 is idle + widgets: + - button: + align: center + widgets: + - label: + text: Click ME + on_click: + logger.log: Clicked + diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 75c34bec56..d22fe9579f 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -10,6 +10,7 @@ mqtt: port: 1883 username: debug password: debug + enable_on_boot: false clean_session: True client_id: someclient use_abbreviations: false @@ -87,6 +88,8 @@ button: state_topic: some/topic/button qos: 2 on_press: + - mqtt.disable + - mqtt.enable - mqtt.publish: topic: some/topic/button payload: Hello diff --git a/tests/components/spi_device/test.esp32-ard.yaml b/tests/components/spi_device/test.esp32-ard.yaml index cad8ca49f8..b539cb3ec4 100644 --- a/tests/components/spi_device/test.esp32-ard.yaml +++ b/tests/components/spi_device/test.esp32-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-c3-ard.yaml b/tests/components/spi_device/test.esp32-c3-ard.yaml index 49e2733676..99c0ac1ebb 100644 --- a/tests/components/spi_device/test.esp32-c3-ard.yaml +++ b/tests/components/spi_device/test.esp32-c3-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-c3-idf.yaml b/tests/components/spi_device/test.esp32-c3-idf.yaml index 49e2733676..99c0ac1ebb 100644 --- a/tests/components/spi_device/test.esp32-c3-idf.yaml +++ b/tests/components/spi_device/test.esp32-c3-idf.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml index cad8ca49f8..b539cb3ec4 100644 --- a/tests/components/spi_device/test.esp32-idf.yaml +++ b/tests/components/spi_device/test.esp32-idf.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp8266-ard.yaml b/tests/components/spi_device/test.esp8266-ard.yaml index 1b191bdb6a..988825ce2d 100644 --- a/tests/components/spi_device/test.esp8266-ard.yaml +++ b/tests/components/spi_device/test.esp8266-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.rp2040-ard.yaml b/tests/components/spi_device/test.rp2040-ard.yaml index c70493c70d..6020643f21 100644 --- a/tests/components/spi_device/test.rp2040-ard.yaml +++ b/tests/components/spi_device/test.rp2040-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml index 3565926933..79201fbe07 100644 --- a/tests/components/template/common.yaml +++ b/tests/components/template/common.yaml @@ -9,6 +9,25 @@ sensor: return 0.0; } update_interval: 60s + filters: + - offset: 10 + - multiply: 1 + - offset: !lambda return 10; + - multiply: !lambda return 2; + - filter_out: + - 10 + - 20 + - !lambda return 10; + - filter_out: 10 + - filter_out: !lambda return NAN; + - timeout: + timeout: 10s + value: !lambda return 10; + - timeout: + timeout: 1h + value: 20.0 + - timeout: + timeout: 1d esphome: on_boot: