From dff6884bedd1585b49d7864e52039f0f14e74fa8 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 30 Jul 2024 16:57:51 -0400 Subject: [PATCH 01/13] [micro_wake_word] Fix VAD detection and modify detection computation (#7164) --- esphome/components/micro_wake_word/streaming_model.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 013fa2ce6e..d0d2e2df05 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -148,7 +148,7 @@ WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutof }; bool WakeWordModel::determine_detected() { - int32_t sum = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { sum += prob; } @@ -175,12 +175,14 @@ VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t }; bool VADModel::determine_detected() { - uint8_t max = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { - max = std::max(prob, max); + sum += prob; } - return max > this->probability_cutoff_; + float sliding_window_average = static_cast(sum) / static_cast(255 * this->sliding_window_size_); + + return sliding_window_average > this->probability_cutoff_; } } // namespace micro_wake_word From dd3dd7a136b9f81a5d3ff7c7296edd031af38f4f Mon Sep 17 00:00:00 2001 From: Adam Allport Date: Tue, 30 Jul 2024 22:30:15 +0100 Subject: [PATCH 02/13] fix: Add `pin->setup();` to matrix_keypad.cpp (#7163) --- esphome/components/matrix_keypad/matrix_keypad.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 902e574846..f62c75c869 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -8,6 +8,7 @@ static const char *const TAG = "matrix_keypad"; void MatrixKeypad::setup() { for (auto *pin : this->rows_) { + pin->setup(); if (!has_diodes_) { pin->pin_mode(gpio::FLAG_INPUT); } else { @@ -15,6 +16,7 @@ void MatrixKeypad::setup() { } } for (auto *pin : this->columns_) { + pin->setup(); if (has_pulldowns_) { pin->pin_mode(gpio::FLAG_INPUT); } else { From 8849443bf6e9f56e5c0f682a25a78a6782007a1e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:08:11 +1200 Subject: [PATCH 03/13] [update] Implement ``update.perform`` action and ``update.is_available`` condition (#7165) * [update] Fix unimplemented yaml action/condition * Add/update tests --------- Co-authored-by: Keith Burzinski --- .../update/http_request_update.cpp | 4 +- .../http_request/update/http_request_update.h | 2 +- esphome/components/update/__init__.py | 48 +++++++++++++------ esphome/components/update/automation.h | 23 +++++++++ esphome/components/update/update_entity.h | 4 +- tests/components/update/common.yaml | 27 +++++++++++ tests/components/update/test.esp32-ard.yaml | 3 ++ tests/components/update/test.esp8266-ard.yaml | 3 ++ tests/components/update/test.rp2040-ard.yaml | 3 ++ 9 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 esphome/components/update/automation.h diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 0a14dfd933..059148e7e5 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -138,8 +138,8 @@ void HttpRequestUpdate::update() { this->publish_state(); } -void HttpRequestUpdate::perform() { - if (this->state_ != update::UPDATE_STATE_AVAILABLE) { +void HttpRequestUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { return; } diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index a6bc97392b..943231a906 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -15,7 +15,7 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { void setup() override; void update() override; - void perform() override; + void perform(bool force) override; void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 45bf082fa4..ba3b2f20df 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -1,10 +1,11 @@ from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server import esphome.config_validation as cv -import esphome.codegen as cg from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, + CONF_FORCE_UPDATE, CONF_ID, CONF_MQTT_ID, CONF_WEB_SERVER_ID, @@ -23,8 +24,12 @@ UpdateEntity = update_ns.class_("UpdateEntity", cg.EntityBase) UpdateInfo = update_ns.struct("UpdateInfo") -PerformAction = update_ns.class_("PerformAction", automation.Action) -IsAvailableCondition = update_ns.class_("IsAvailableCondition", automation.Condition) +PerformAction = update_ns.class_( + "PerformAction", automation.Action, cg.Parented.template(UpdateEntity) +) +IsAvailableCondition = update_ns.class_( + "IsAvailableCondition", automation.Condition, cg.Parented.template(UpdateEntity) +) DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, @@ -92,24 +97,37 @@ async def to_code(config): cg.add_global(update_ns.using) -UPDATE_AUTOMATION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(UpdateEntity), - } +@automation.register_action( + "update.perform", + PerformAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean), + } + ), ) - - -@automation.register_action("update.perform", PerformAction, UPDATE_AUTOMATION_SCHEMA) async def update_perform_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, paren, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + force = await cg.templatable(config[CONF_FORCE_UPDATE], args, cg.bool_) + cg.add(var.set_force(force)) + return var @automation.register_condition( - "update.is_available", IsAvailableCondition, UPDATE_AUTOMATION_SCHEMA + "update.is_available", + IsAvailableCondition, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + } + ), ) async def update_is_available_condition_to_code( config, condition_id, template_arg, args ): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(condition_id, paren, paren) + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h new file mode 100644 index 0000000000..df50f86a0c --- /dev/null +++ b/esphome/components/update/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "update_entity.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace update { + +template class PerformAction : public Action, public Parented { + TEMPLATABLE_VALUE(bool, force) + + public: + void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } +}; + +template class IsAvailableCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } +}; + +} // namespace update +} // namespace esphome diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 5984c8e35b..568fbe3bb0 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -32,7 +32,9 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { void publish_state(); - virtual void perform() = 0; + void perform() { this->perform(false); } + + virtual void perform(bool force) = 0; const UpdateInfo &update_info = update_info_; const UpdateState &state = state_; diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml index 91b8669505..dcb4f42527 100644 --- a/tests/components/update/common.yaml +++ b/tests/components/update/common.yaml @@ -1 +1,28 @@ +substitutions: + verify_ssl: "true" + +esphome: + on_boot: + then: + - if: + condition: + update.is_available: + then: + - logger.log: "Update available" + - update.perform: + force_update: true + +wifi: + ssid: MySSID + password: password1 + +http_request: + verify_ssl: ${verify_ssl} + +ota: + - platform: http_request + update: + - platform: http_request + name: Firmware Update + source: http://example.com/manifest.json diff --git a/tests/components/update/test.esp32-ard.yaml b/tests/components/update/test.esp32-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.esp32-ard.yaml +++ b/tests/components/update/test.esp32-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml diff --git a/tests/components/update/test.esp8266-ard.yaml b/tests/components/update/test.esp8266-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.esp8266-ard.yaml +++ b/tests/components/update/test.esp8266-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml diff --git a/tests/components/update/test.rp2040-ard.yaml b/tests/components/update/test.rp2040-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.rp2040-ard.yaml +++ b/tests/components/update/test.rp2040-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml From 3920029aff9dd85fd5f0775945b8426c03da471c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:31:15 +1000 Subject: [PATCH 04/13] [lvgl] PR stage 3 (#7160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 58 +++++-- esphome/components/lvgl/automation.py | 188 +++++++++++++++++++++ esphome/components/lvgl/btn.py | 6 +- esphome/components/lvgl/defines.py | 30 +++- esphome/components/lvgl/lv_validation.py | 46 ++--- esphome/components/lvgl/lvcode.py | 64 ++++--- esphome/components/lvgl/lvgl_esphome.cpp | 75 +++++++- esphome/components/lvgl/lvgl_esphome.h | 165 ++++++++++++++++-- esphome/components/lvgl/obj.py | 13 +- esphome/components/lvgl/rotary_encoders.py | 62 +++++++ esphome/components/lvgl/schemas.py | 80 ++++++++- esphome/components/lvgl/touchscreens.py | 5 +- esphome/components/lvgl/trigger.py | 61 +++++++ esphome/components/lvgl/types.py | 23 ++- esphome/components/lvgl/widget.py | 58 +++++-- esphome/core/defines.h | 3 + tests/components/lvgl/lvgl-package.yaml | 35 ++++ tests/components/lvgl/test.esp32-idf.yaml | 21 +++ 18 files changed, 895 insertions(+), 98 deletions(-) create mode 100644 esphome/components/lvgl/automation.py create mode 100644 esphome/components/lvgl/rotary_encoders.py create mode 100644 esphome/components/lvgl/trigger.py diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c454a61957..182d04e038 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,5 +1,6 @@ import logging +from esphome.automation import build_automation, register_action, validate_automation import esphome.codegen as cg from esphome.components.display import Display import esphome.config_validation as cv @@ -8,7 +9,11 @@ from esphome.const import ( CONF_BUFFER_SIZE, CONF_ID, CONF_LAMBDA, + CONF_ON_IDLE, CONF_PAGES, + CONF_TIMEOUT, + CONF_TRIGGER_ID, + CONF_TYPE, ) from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj @@ -16,21 +21,26 @@ from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid +from .automation import update_to_code from .btn import btn_spec from .label import label_spec -from .lvcode import ConstantLiteral, LvContext +from .lv_validation import lv_images_used +from .lvcode import LvContext from .obj import obj_spec -from .schemas import any_widget_schema, obj_schema +from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code +from .schemas import any_widget_schema, create_modify_schema, obj_schema from .touchscreens import touchscreen_schema, touchscreens_to_code +from .trigger import generate_triggers from .types import ( WIDGET_TYPES, FontEngine, + IdleTrigger, LvglComponent, - lv_disp_t_ptr, + ObjUpdateAction, lv_font_t, lvgl_ns, ) -from .widget import LvScrActType, Widget, add_widgets, set_obj_properties +from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties DOMAIN = "lvgl" DEPENDENCIES = ("display",) @@ -41,17 +51,21 @@ LOGGER = logging.getLogger(__name__) for w_type in (label_spec, obj_spec, btn_spec): WIDGET_TYPES[w_type.name] = w_type -lv_scr_act_spec = LvScrActType() -lv_scr_act = Widget.create( - None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None -) - WIDGET_SCHEMA = any_widget_schema() +for w_type in WIDGET_TYPES.values(): + register_action( + f"lvgl.{w_type.name}.update", + ObjUpdateAction, + create_modify_schema(w_type), + )(update_to_code) + async def add_init_lambda(lv_component, init): if init: - lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")]) + lamb = await cg.process_lambda( + Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")] + ) cg.add(lv_component.add_init_lambda(lamb)) @@ -99,6 +113,13 @@ def final_validation(config): 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 + ) async def to_code(config): @@ -174,9 +195,15 @@ async def to_code(config): with LvContext(): await touchscreens_to_code(lv_component, config) + await rotary_encoders_to_code(lv_component, config) await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) - Widget.set_completed() + Widget.set_completed() + await generate_triggers(lv_component) + 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) await add_init_lambda(lv_component, LvContext.get_code()) for comp in helpers.lvgl_components_required: CORE.add_define(f"USE_LVGL_{comp.upper()}") @@ -212,9 +239,18 @@ CONFIG_SCHEMA = ( cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( "big_endian", "little_endian" ), + cv.Optional(CONF_ON_IDLE): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, + cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG, } ) ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py new file mode 100644 index 0000000000..4fd0be185e --- /dev/null +++ b/esphome/components/lvgl/automation.py @@ -0,0 +1,188 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TIMEOUT +from esphome.core import Lambda +from esphome.cpp_generator import RawStatement +from esphome.cpp_types import nullptr + +from .defines import CONF_LVGL_ID, CONF_SHOW_SNOW, literal +from .lv_validation import lv_bool +from .lvcode import ( + LambdaContext, + ReturnStatement, + add_line_marks, + lv, + lv_add, + lv_obj, + lvgl_comp, +) +from .schemas import ACTION_SCHEMA, LVGL_SCHEMA +from .types import ( + LvglAction, + LvglComponent, + LvglComponentPtr, + LvglCondition, + ObjUpdateAction, + lv_obj_t, +) +from .widget import Widget, get_widget, lv_scr_act, set_obj_properties + + +async def action_to_code(action: list, action_id, widget: Widget, template_arg, args): + with LambdaContext() as context: + lv.cond_if(widget.obj == nullptr) + lv_add(RawStatement(" return;")) + lv.cond_endif() + code = context.get_code() + code.extend(action) + action = "\n".join(code) + "\n\n" + lamb = await cg.process_lambda(Lambda(action), args) + var = cg.new_Pvariable(action_id, template_arg, lamb) + return var + + +async def update_to_code(config, action_id, template_arg, args): + if config is not None: + widget = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + await set_obj_properties(widget, config) + await widget.type.to_code(widget, config) + if ( + widget.type.w_type.value_property is not None + and widget.type.w_type.value_property in config + ): + lv.event_send(widget.obj, literal("LV_EVENT_VALUE_CHANGED"), nullptr) + return await action_to_code( + context.get_code(), action_id, widget, template_arg, args + ) + + +@automation.register_condition( + "lvgl.is_paused", + LvglCondition, + LVGL_SCHEMA, +) +async def lvgl_is_paused(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + with LambdaContext( + [(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(lvgl_comp.is_paused())) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_condition( + "lvgl.is_idle", + LvglCondition, + LVGL_SCHEMA.extend( + { + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ) + } + ), +) +async def lvgl_is_idle(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) + with LambdaContext( + [(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_action( + "lvgl.widget.redraw", + ObjUpdateAction, + cv.Schema( + { + cv.Optional(CONF_ID): cv.use_id(lv_obj_t), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + } + ), +) +async def obj_invalidate_to_code(config, action_id, template_arg, args): + if CONF_ID in config: + w = await get_widget(config) + else: + w = lv_scr_act + with LambdaContext() as context: + add_line_marks(action_id) + lv_obj.invalidate(w.obj) + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action( + "lvgl.pause", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, + }, +) +async def pause_action_to_code(config, action_id, template_arg, args): + with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: + add_line_marks(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]) + return var + + +@automation.register_action( + "lvgl.resume", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + }, +) +async def resume_action_to_code(config, action_id, template_arg, args): + with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: + add_line_marks(action_id) + 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]) + return var + + +@automation.register_action("lvgl.widget.disable", ObjUpdateAction, ACTION_SCHEMA) +async def obj_disable_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.add_state("LV_STATE_DISABLED") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA) +async def obj_enable_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.clear_state("LV_STATE_DISABLED") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA) +async def obj_hide_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.add_flag("LV_OBJ_FLAG_HIDDEN") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA) +async def obj_show_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.clear_flag("LV_OBJ_FLAG_HIDDEN") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) diff --git a/esphome/components/lvgl/btn.py b/esphome/components/lvgl/btn.py index 4f5f88d9e6..064d886d47 100644 --- a/esphome/components/lvgl/btn.py +++ b/esphome/components/lvgl/btn.py @@ -9,9 +9,6 @@ class BtnType(WidgetType): def __init__(self): super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) - async def to_code(self, w, config): - return [] - def obj_creator(self, parent: MockObjClass, config: dict): """ LVGL 8 calls buttons `btn` @@ -21,5 +18,8 @@ class BtnType(WidgetType): def get_uses(self): return ("btn",) + async def to_code(self, w, config): + return [] + btn_spec = BtnType() diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index a2b4ac13fb..9f349e3943 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -4,12 +4,32 @@ Constants already defined in esphome.const are not duplicated here and must be i """ +from typing import Union + from esphome import codegen as cg, config_validation as cv from esphome.core import ID, Lambda +from esphome.cpp_generator import Literal from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor -from .lvcode import ConstantLiteral +from .helpers import requires_component + + +class ConstantLiteral(Literal): + __slots__ = ("constant",) + + def __init__(self, constant: str): + super().__init__() + self.constant = constant + + def __str__(self): + return self.constant + + +def literal(arg: Union[str, ConstantLiteral]): + if isinstance(arg, str): + return ConstantLiteral(arg) + return arg class LValidator: @@ -18,14 +38,19 @@ class LValidator: has `process()` to convert a value during code generation """ - def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None): + def __init__( + self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None + ): self.validator = validator self.rtype = rtype self.idtype = idtype self.idexpr = idexpr self.retmapper = retmapper + self.requires = requires def __call__(self, value): + if self.requires: + value = requires_component(self.requires)(value) if isinstance(value, cv.Lambda): return cv.returning_lambda(value) if self.idtype is not None and isinstance(value, ID): @@ -422,6 +447,7 @@ CONF_RECOLOR = "recolor" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" +CONF_ROTARY_ENCODERS = "rotary_encoders" CONF_ROWS = "rows" CONF_SCALES = "scales" CONF_SCALE_LINES = "scale_lines" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 533dc582f0..818bde6aed 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components.binary_sensor import BinarySensor from esphome.components.color import ColorStruct from esphome.components.font import Font +from esphome.components.image import Image_ from esphome.components.sensor import Sensor from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv @@ -13,22 +14,15 @@ from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from . import types as ty -from .defines import LV_FONTS, LValidator, LvConstant +from .defines import LV_FONTS, ConstantLiteral, LValidator, LvConstant, literal from .helpers import ( esphome_fonts_used, lv_fonts_used, lvgl_components_required, requires_component, ) -from .lvcode import ConstantLiteral, lv_expr -from .types import lv_font_t - - -def literal_mapper(value, args=()): - if isinstance(value, str): - return ConstantLiteral(value) - return value - +from .lvcode import lv_expr +from .types import lv_font_t, lv_img_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -43,7 +37,7 @@ def opacity_validator(value): return value -opacity = LValidator(opacity_validator, uint32, retmapper=literal_mapper) +opacity = LValidator(opacity_validator, uint32, retmapper=literal) @schema_extractor("one_of") @@ -79,9 +73,7 @@ def pixels_or_percent_validator(value): return f"lv_pct({int(cv.percentage(value) * 100)})" -pixels_or_percent = LValidator( - pixels_or_percent_validator, uint32, retmapper=literal_mapper -) +pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal) def zoom(value): @@ -115,7 +107,7 @@ def size_validator(value): return f"lv_pct({int(cv.percentage(value) * 100)})" -size = LValidator(size_validator, uint32, retmapper=literal_mapper) +size = LValidator(size_validator, uint32, retmapper=literal) radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") @@ -130,21 +122,37 @@ def radius_validator(value): return value +radius = LValidator(radius_validator, uint32, retmapper=literal) + + def id_name(value): if value == SCHEMA_EXTRACT: return "id" return cv.validate_id_name(value) -radius = LValidator(radius_validator, uint32, retmapper=literal_mapper) - - def stop_value(value): return cv.int_range(0, 255)(value) +lv_images_used = set() + + +def image_validator(value): + value = requires_component("image")(value) + value = cv.use_id(Image_)(value) + lv_images_used.add(value) + return value + + +lv_image = LValidator( + image_validator, + lv_img_t, + retmapper=lambda x: lv_expr.img_from(MockObj(x)), + requires="image", +) lv_bool = LValidator( - cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper + cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal ) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 13b4862b4d..3a8a958f2e 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -8,8 +8,8 @@ from esphome.cpp_generator import ( AssignmentExpression, CallExpression, Expression, + ExpressionStatement, LambdaExpression, - Literal, MockObj, RawExpression, RawStatement, @@ -19,7 +19,9 @@ from esphome.cpp_generator import ( statement, ) +from .defines import ConstantLiteral from .helpers import get_line_marks +from .types import lv_group_t _LOGGER = logging.getLogger(__name__) @@ -105,29 +107,40 @@ class LambdaContext(CodeContext): def __init__( self, - parameters: list[tuple[SafeExpType, str]], - return_type: SafeExpType = None, + parameters: list[tuple[SafeExpType, str]] = None, + return_type: SafeExpType = cg.void, + capture: str = "", ): super().__init__() self.code_list: list[Statement] = [] self.parameters = parameters self.return_type = return_type + self.capture = capture def add(self, expression: Union[Expression, Statement]): self.code_list.append(expression) return expression - async def code(self) -> LambdaExpression: + async def get_lambda(self) -> LambdaExpression: + code_text = self.get_code() + return await cg.process_lambda( + Lambda("\n".join(code_text) + "\n\n"), + self.parameters, + capture=self.capture, + return_type=self.return_type, + ) + + def get_code(self): code_text = [] for exp in self.code_list: text = str(statement(exp)) text = text.rstrip() code_text.append(text) - return await cg.process_lambda( - Lambda("\n".join(code_text) + "\n\n"), - self.parameters, - return_type=self.return_type, - ) + return code_text + + def __enter__(self): + super().__enter__() + return self class LocalVariable(MockObj): @@ -187,13 +200,18 @@ class MockLv: return result def cond_if(self, expression: Expression): - CodeContext.append(RawExpression(f"if({expression}) {{")) + CodeContext.append(RawStatement(f"if {expression} {{")) def cond_else(self): - CodeContext.append(RawExpression("} else {")) + CodeContext.append(RawStatement("} else {")) def cond_endif(self): - CodeContext.append(RawExpression("}")) + CodeContext.append(RawStatement("}")) + + +class ReturnStatement(ExpressionStatement): + def __str__(self): + return f"return {self.expression};" class LvExpr(MockLv): @@ -210,6 +228,7 @@ lv = MockLv("lv_") lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls lv_obj = MockLv("lv_obj_") +lvgl_comp = MockObj("lvgl_comp", "->") # equivalent to cg.add() for the lvgl init context @@ -226,12 +245,19 @@ def lv_assign(target, expression): lv_add(RawExpression(f"{target} = {expression}")) -class ConstantLiteral(Literal): - __slots__ = ("constant",) +lv_groups = {} # Widget group names - def __init__(self, constant: str): - super().__init__() - self.constant = constant - def __str__(self): - return self.constant +def add_group(name): + if name is None: + return None + fullname = f"lv_esp_group_{name}" + if name not in lv_groups: + gid = ID(fullname, True, type=lv_group_t.operator("ptr")) + lv_add( + AssignmentExpression( + type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create() + ) + ) + lv_groups[name] = ConstantLiteral(fullname) + return lv_groups[name] diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 74a1b0e7af..34f8eaf21f 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -19,13 +19,35 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { } void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - auto now = millis(); - this->draw_buffer_(area, (const uint8_t *) color_p); - ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + if (!this->paused_) { + auto now = millis(); + this->draw_buffer_(area, (const uint8_t *) color_p); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), + lv_area_get_height(area), (int) (millis() - now)); + } lv_disp_flush_ready(disp_drv); } +void LvglComponent::write_random_() { + // length of 2 lines in 32 bit units + // we write 2 lines for the benefit of displays that won't write one line at a time. + size_t line_len = this->disp_drv_.hor_res * LV_COLOR_DEPTH / 8 / 4 * 2; + for (size_t i = 0; i != line_len; i++) { + ((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32(); + } + lv_area_t area; + area.x1 = 0; + area.x2 = this->disp_drv_.hor_res - 1; + if (this->snow_line_ == this->disp_drv_.ver_res / 2) { + area.y1 = static_cast(random_uint32() % (this->disp_drv_.ver_res / 2) * 2); + } else { + area.y1 = this->snow_line_++ * 2; + } + // write 2 lines + area.y2 = area.y1 + 1; + this->draw_buffer_(&area, (const uint8_t *) this->draw_buf_.buf1); +} + void LvglComponent::setup() { ESP_LOGCONFIG(TAG, "LVGL Setup starts"); #if LV_USE_LOG @@ -74,10 +96,53 @@ void LvglComponent::setup() { ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated); this->disp_ = lv_disp_drv_register(&this->disp_drv_); for (const auto &v : this->init_lambdas_) - v(this->disp_); + v(this); lv_disp_trig_activity(this->disp_); ESP_LOGCONFIG(TAG, "LVGL Setup complete"); } + +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { + if (img_dsc == nullptr) + img_dsc = new lv_img_dsc_t(); // NOLINT + img_dsc->header.always_zero = 0; + img_dsc->header.reserved = 0; + img_dsc->header.w = src->get_width(); + img_dsc->header.h = src->get_height(); + img_dsc->data = src->get_data_start(); + img_dsc->data_size = image_type_to_width_stride(img_dsc->header.w * img_dsc->header.h, src->get_type()); + switch (src->get_type()) { + case image::IMAGE_TYPE_BINARY: + img_dsc->header.cf = LV_IMG_CF_ALPHA_1BIT; + break; + + case image::IMAGE_TYPE_GRAYSCALE: + img_dsc->header.cf = LV_IMG_CF_ALPHA_8BIT; + break; + + case image::IMAGE_TYPE_RGB24: + img_dsc->header.cf = LV_IMG_CF_RGB888; + break; + + case image::IMAGE_TYPE_RGB565: +#if LV_COLOR_DEPTH == 16 + img_dsc->header.cf = src->has_transparency() ? LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED : LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGB565; +#endif + break; + + case image::IMAGE_TYPE_RGBA: +#if LV_COLOR_DEPTH == 32 + img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGBA8888; +#endif + break; + } + return img_dsc; +} +#endif } // namespace lvgl } // namespace esphome diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index a884a27042..a0d3d226ce 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -1,23 +1,32 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_LVGL + +#ifdef USE_LVGL_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif // USE_LVGL_BINARY_SENSOR +#ifdef USE_LVGL_ROTARY_ENCODER +#include "esphome/components/rotary_encoder/rotary_encoder.h" +#endif // USE_LVGL_ROTARY_ENCODER // required for clang-tidy #ifndef LV_CONF_H #define LV_CONF_SKIP 1 // NOLINT -#endif +#endif // LV_CONF_H #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #include "esphome/core/component.h" -#include "esphome/core/hal.h" #include "esphome/core/log.h" #include +#include #include +#ifdef USE_LVGL_IMAGE +#include "esphome/components/image/image.h" +#endif // USE_LVGL_IMAGE #ifdef USE_LVGL_FONT #include "esphome/components/font/font.h" -#endif +#endif // USE_LVGL_FONT #ifdef USE_LVGL_TOUCHSCREEN #include "esphome/components/touchscreen/touchscreen.h" #endif // USE_LVGL_TOUCHSCREEN @@ -40,7 +49,7 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT // Parent class for things that wrap an LVGL object class LvCompound final { public: - virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } + void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } lv_obj_t *obj{}; }; @@ -49,6 +58,15 @@ using set_value_lambda_t = std::function; using event_callback_t = void(_lv_event_t *); using text_lambda_t = std::function; +template class ObjUpdateAction : public Action { + public: + explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} + + void play(Ts... x) override { this->lamb_(x...); } + + protected: + std::function lamb_; +}; #ifdef USE_LVGL_FONT class FontEngine { public: @@ -67,6 +85,9 @@ class FontEngine { lv_font_t lv_font_{}; }; #endif // USE_LVGL_FONT +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr); +#endif // USE_LVGL_IMAGE class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; @@ -92,27 +113,54 @@ class LvglComponent : public PollingComponent { area->y2++; } - void loop() override { lv_timer_handler_run_in_period(5); } void setup() override; - void update() override {} + void update() override { + // update indicators + if (this->paused_) { + return; + } + this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); + } + void loop() override { + if (this->paused_) { + if (this->show_snow_) + this->write_random_(); + } + lv_timer_handler_run_in_period(5); + } + + void add_on_idle_callback(std::function &&callback) { + this->idle_callbacks_.add(std::move(callback)); + } void add_display(display::Display *display) { this->displays_.push_back(display); } - void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } + void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } void dump_config() override; void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; } + bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } lv_disp_t *get_disp() { return this->disp_; } void set_paused(bool paused, bool show_snow) { this->paused_ = paused; + this->show_snow_ = show_snow; + this->snow_line_ = 0; if (!paused && lv_scr_act() != nullptr) { lv_disp_trig_activity(this->disp_); // resets the inactivity time lv_obj_invalidate(lv_scr_act()); } } + + void 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); + if (event == LV_EVENT_VALUE_CHANGED) { + lv_obj_add_event_cb(obj, callback, lv_custom_event, this); + } + } bool is_paused() const { return this->paused_; } protected: + void write_random_(); void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); std::vector displays_{}; @@ -120,12 +168,52 @@ class LvglComponent : public PollingComponent { lv_disp_drv_t disp_drv_{}; lv_disp_t *disp_{}; bool paused_{}; + bool show_snow_{}; + lv_coord_t snow_line_{}; - std::vector> init_lambdas_; + std::vector> init_lambdas_; + CallbackManager idle_callbacks_{}; size_t buffer_frac_{1}; bool full_refresh_{}; }; +class IdleTrigger : public Trigger<> { + public: + explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { + parent->add_on_idle_callback([this](uint32_t idle_time) { + if (!this->is_idle_ && idle_time > this->timeout_.value()) { + this->is_idle_ = true; + this->trigger(); + } else if (this->is_idle_ && idle_time < this->timeout_.value()) { + this->is_idle_ = false; + } + }); + } + + protected: + TemplatableValue timeout_; + bool is_idle_{}; +}; + +template class LvglAction : public Action, public Parented { + public: + explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} + void play(Ts... x) override { this->action_(this->parent_); } + + protected: + std::function action_{}; +}; + +template class LvglCondition : public Condition, public Parented { + public: + LvglCondition(std::function &&condition_lambda) + : condition_lambda_(std::move(condition_lambda)) {} + bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + + protected: + std::function condition_lambda_{}; +}; + #ifdef USE_LVGL_TOUCHSCREEN class LVTouchListener : public touchscreen::TouchListener, public Parented { public: @@ -160,7 +248,62 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented { + public: + LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { + lv_indev_drv_init(&this->drv_); + this->drv_.type = type; + this->drv_.user_data = this; + this->drv_.long_press_time = lpt; + this->drv_.long_press_repeat_time = lprt; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + data->key = l->key_; + data->enc_diff = (int16_t) (l->count_ - l->last_count_); + l->last_count_ = l->count_; + data->continue_reading = false; + }; + } + + void set_left_button(binary_sensor::BinarySensor *left_button) { + left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); + } + void set_right_button(binary_sensor::BinarySensor *right_button) { + right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); }); + } + + void set_enter_button(binary_sensor::BinarySensor *enter_button) { + enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); + } + + void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) { + sensor->register_listener([this](int32_t count) { this->set_count(count); }); + } + + void event(int key, bool pressed) { + if (!this->parent_->is_paused()) { + this->pressed_ = pressed; + this->key_ = key; + } + } + + void set_count(int32_t count) { + if (!this->parent_->is_paused()) + this->count_ = count; + } + + lv_indev_drv_t *get_drv() { return &this->drv_; } + + protected: + lv_indev_drv_t drv_{}; + bool pressed_{}; + int32_t count_{}; + int32_t last_count_{}; + int key_{}; +}; +#endif // USE_LVGL_KEY_LISTENER } // namespace lvgl } // namespace esphome - -#endif // USE_LVGL diff --git a/esphome/components/lvgl/obj.py b/esphome/components/lvgl/obj.py index 92c4f63d2d..40d7e55381 100644 --- a/esphome/components/lvgl/obj.py +++ b/esphome/components/lvgl/obj.py @@ -1,5 +1,9 @@ +from esphome import automation + +from .automation import update_to_code from .defines import CONF_MAIN, CONF_OBJ -from .types import WidgetType, lv_obj_t +from .schemas import create_modify_schema +from .types import ObjUpdateAction, WidgetType, lv_obj_t class ObjType(WidgetType): @@ -15,3 +19,10 @@ class ObjType(WidgetType): obj_spec = ObjType() + + +@automation.register_action( + "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec) +) +async def obj_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/rotary_encoders.py b/esphome/components/lvgl/rotary_encoders.py new file mode 100644 index 0000000000..77dc397c3e --- /dev/null +++ b/esphome/components/lvgl/rotary_encoders.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import BinarySensor +from esphome.components.rotary_encoder.sensor import RotaryEncoderSensor +import esphome.config_validation as cv +from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR + +from .defines import ( + CONF_ENTER_BUTTON, + CONF_LEFT_BUTTON, + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + CONF_RIGHT_BUTTON, + CONF_ROTARY_ENCODERS, +) +from .helpers import lvgl_components_required +from .lvcode import add_group, lv, lv_add, lv_expr +from .schemas import ENCODER_SCHEMA +from .types import lv_indev_type_t + +ROTARY_ENCODER_CONFIG = cv.ensure_list( + ENCODER_SCHEMA.extend( + { + cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_SENSOR): cv.Any( + cv.use_id(RotaryEncoderSensor), + cv.Schema( + { + cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_RIGHT_BUTTON): cv.use_id(BinarySensor), + } + ), + ), + } + ) +) + + +async def rotary_encoders_to_code(var, config): + for enc_conf in config.get(CONF_ROTARY_ENCODERS, ()): + lvgl_components_required.add("KEY_LISTENER") + lvgl_components_required.add("ROTARY_ENCODER") + lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable( + enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_ENCODER, lpt, lprt + ) + await cg.register_parented(listener, var) + if sensor_config := enc_conf.get(CONF_SENSOR): + if isinstance(sensor_config, dict): + b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON]) + cg.add(listener.set_left_button(b_sensor)) + b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON]) + cg.add(listener.set_right_button(b_sensor)) + else: + sensor_config = await cg.get_variable(sensor_config) + lv_add(listener.set_sensor(sensor_config)) + b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON]) + cg.add(listener.set_enter_button(b_sensor)) + if group := add_group(enc_conf.get(CONF_GROUP)): + lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + else: + lv.indev_drv_register(listener.get_drv()) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 9f6d984545..ebef56a882 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,10 +1,21 @@ from esphome import config_validation as cv -from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE +from esphome.automation import Trigger, validate_automation +from esphome.const import ( + CONF_ARGS, + CONF_FORMAT, + CONF_GROUP, + CONF_ID, + CONF_ON_VALUE, + CONF_STATE, + CONF_TRIGGER_ID, + CONF_TYPE, +) +from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid, types as ty from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import lv_font +from .lv_validation import id_name, lv_font from .types import WIDGET_TYPES, WidgetType # A schema for text properties @@ -27,6 +38,28 @@ TEXT_SCHEMA = cv.Schema( } ) +ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), + }, + key=CONF_ID, +) + +PRESS_TIME = cv.All( + lvalid.lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535)) +) + +ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.All( + cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor") + ), + cv.Optional(CONF_GROUP): lvalid.id_name, + cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, + cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, + } +) + # All LVGL styles and their validators STYLE_PROPS = { "align": df.CHILD_ALIGNMENTS.one_of, @@ -43,6 +76,7 @@ STYLE_PROPS = { "bg_image_opa": lvalid.opacity, "bg_image_recolor": lvalid.lv_color, "bg_image_recolor_opa": lvalid.opacity, + "bg_image_src": lvalid.lv_image, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, "border_color": lvalid.lv_color, @@ -151,6 +185,39 @@ def part_schema(widget_type: WidgetType): ) +def automation_schema(typ: ty.LvType): + if typ.has_on_value: + events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) + else: + events = df.LV_EVENT_TRIGGERS + if isinstance(typ, ty.LvType): + template = Trigger.template(typ.get_arg_type()) + else: + template = Trigger.template() + return { + cv.Optional(event): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(template), + } + ) + for event in events + } + + +def create_modify_schema(widget_type): + return ( + part_schema(widget_type) + .extend( + { + cv.Required(CONF_ID): cv.use_id(widget_type), + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + } + ) + .extend(FLAG_SCHEMA) + .extend(widget_type.modify_schema) + ) + + def obj_schema(widget_type: WidgetType): """ Create a schema for a widget type itself i.e. no allowance for children @@ -161,10 +228,12 @@ def obj_schema(widget_type: WidgetType): part_schema(widget_type) .extend(FLAG_SCHEMA) .extend(ALIGN_TO_SCHEMA) + .extend(automation_schema(widget_type.w_type)) .extend( cv.Schema( { cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): id_name, } ) ) @@ -188,6 +257,13 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT ) +# For use by platform components +LVGL_SCHEMA = cv.Schema( + { + cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent), + } +) + ALL_STYLES = { **STYLE_PROPS, } diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index a0d4a3e4ad..499b33aa02 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import CORE, TimePeriod +from esphome.core import CORE from .defines import ( CONF_LONG_PRESS_REPEAT_TIME, @@ -10,11 +10,10 @@ from .defines import ( CONF_TOUCHSCREENS, ) from .helpers import lvgl_components_required -from .lv_validation import lv_milliseconds from .lvcode import lv +from .schemas import PRESS_TIME from .types import LVTouchListener -PRESS_TIME = cv.All(lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535))) CONF_TOUCHSCREEN = "touchscreen" TOUCHSCREENS_CONFIG = cv.maybe_simple_value( { diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py new file mode 100644 index 0000000000..bf92bda5b0 --- /dev/null +++ b/esphome/components/lvgl/trigger.py @@ -0,0 +1,61 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_TRIGGER_ID + +from .defines import ( + CONF_ALIGN, + CONF_ALIGN_TO, + CONF_X, + CONF_Y, + LV_EVENT, + LV_EVENT_TRIGGERS, + literal, +) +from .lvcode import LambdaContext, add_line_marks, lv, lv_add +from .widget import widget_map + +lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") + + +async def generate_triggers(lv_component): + """ + 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(): + if w.config: + for event, conf in { + event: conf + for event, conf in w.config.items() + if event in LV_EVENT_TRIGGERS + }.items(): + conf = conf[0] + w.add_flag("LV_OBJ_FLAG_CLICKABLE") + event = "LV_EVENT_" + LV_EVENT[event[3:].upper()] + await add_trigger(conf, event, lv_component, w) + for conf in w.config.get(CONF_ON_VALUE, ()): + await add_trigger(conf, "LV_EVENT_VALUE_CHANGED", lv_component, w) + + # Generate align to directives while we're here + if align_to := w.config.get(CONF_ALIGN_TO): + target = widget_map[align_to[CONF_ID]].obj + align = align_to[CONF_ALIGN] + x = align_to[CONF_X] + y = align_to[CONF_Y] + lv.obj_align_to(w.obj, target, align, x, y) + + +async def add_trigger(conf, event, lv_component, w): + tid = conf[CONF_TRIGGER_ID] + add_line_marks(tid) + trigger = cg.new_Pvariable(tid) + args = w.get_args() + value = w.get_value() + await automation.build_automation(trigger, args, conf) + with LambdaContext([(lv_event_t_ptr, "event_data")]) as context: + add_line_marks(tid) + lv_add(trigger.trigger(value)) + lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), literal(event))) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 60291ea54a..6997207dac 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,4 +1,4 @@ -from esphome import codegen as cg +from esphome import automation, codegen as cg from esphome.core import ID from esphome.cpp_generator import MockObjClass @@ -23,8 +23,14 @@ lvgl_ns = cg.esphome_ns.namespace("lvgl") char_ptr = cg.global_ns.namespace("char").operator("ptr") void_ptr = cg.void.operator("ptr") LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) +LvglComponentPtr = LvglComponent.operator("ptr") lv_event_code_t = cg.global_ns.namespace("lv_event_code_t") +lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") FontEngine = lvgl_ns.class_("FontEngine") +IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) +ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) +LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) +LvglAction = lvgl_ns.class_("LvglAction", automation.Action) LvCompound = lvgl_ns.class_("LvCompound") lv_font_t = cg.global_ns.class_("lv_font_t") lv_style_t = cg.global_ns.struct("lv_style_t") @@ -33,9 +39,11 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") lv_color_t = cg.global_ns.struct("lv_color_t") +lv_group_t = cg.global_ns.struct("lv_group_t") LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") +lv_img_t = LvType("lv_img_t") # this will be populated later, in __init__.py to avoid circular imports. @@ -58,7 +66,7 @@ class LvBoolean(LvType): super().__init__( *args, largs=[(cg.bool_, "x")], - lvalue=lambda w: w.is_checked(), + lvalue=lambda w: w.has_state("LV_STATE_CHECKED"), has_on_value=True, **kwargs, ) @@ -83,11 +91,14 @@ class WidgetType: self.name = name self.w_type = w_type self.parts = parts - self.schema = schema or {} - if modify_schema is None: - self.modify_schema = schema + if schema is None: + self.schema = {} else: - self.modify_schema = modify_schema + self.schema = schema + if modify_schema is None: + self.modify_schema = self.schema + else: + self.modify_schema = self.schema @property def animated(self): diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widget.py index 4755d8b21d..83aed341e7 100644 --- a/esphome/components/lvgl/widget.py +++ b/esphome/components/lvgl/widget.py @@ -4,9 +4,9 @@ from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.config_validation import Invalid from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE -from esphome.core import ID, TimePeriod +from esphome.core import CORE, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression from .defines import ( CONF_DEFAULT, @@ -16,13 +16,15 @@ from .defines import ( OBJ_FLAGS, PARTS, STATES, + ConstantLiteral, LValidator, join_enums, + literal, ) from .helpers import add_lv_use -from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj +from .lvcode import add_group, add_line_marks, lv, lv_add, lv_assign, lv_expr, lv_obj from .schemas import ALL_STYLES, STYLE_REMAP -from .types import WIDGET_TYPES, WidgetType, lv_obj_t +from .types import WIDGET_TYPES, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr EVENT_LAMB = "event_lamb__" @@ -76,17 +78,20 @@ class Widget: return f"{self.var}->obj" return self.var - def add_state(self, *args): - return lv_obj.add_state(self.obj, *args) + def add_state(self, state): + return lv_obj.add_state(self.obj, literal(state)) - def clear_state(self, *args): - return lv_obj.clear_state(self.obj, *args) + def clear_state(self, state): + return lv_obj.clear_state(self.obj, literal(state)) - def add_flag(self, *args): - return lv_obj.add_flag(self.obj, *args) + def has_state(self, state): + return lv_expr.obj_get_state(self.obj) & literal(state) != 0 - def clear_flag(self, *args): - return lv_obj.clear_flag(self.obj, *args) + def add_flag(self, flag): + return lv_obj.add_flag(self.obj, literal(flag)) + + def clear_flag(self, flag): + return lv_obj.clear_flag(self.obj, literal(flag)) def set_property(self, prop, value, animated: bool = None, ltype=None): if isinstance(value, dict): @@ -125,6 +130,16 @@ class Widget: def __str__(self): return f"({self.var}, {self.type})" + def get_args(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.args + return [(lv_obj_t_ptr, "obj")] + + def get_value(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.value(self) + return self.obj + # Map of widgets to their config, used for trigger generation widget_map: dict[Any, Widget] = {} @@ -146,7 +161,8 @@ def get_widget_generator(wid): yield -async def get_widget(wid: ID) -> Widget: +async def get_widget(config: dict, id: str = CONF_ID) -> Widget: + wid = config[id] if obj := widget_map.get(wid): return obj return await FakeAwaitable(get_widget_generator(wid)) @@ -204,9 +220,10 @@ async def set_obj_properties(w: Widget, config): }.items(): if isinstance(ALL_STYLES[prop], LValidator): value = await ALL_STYLES[prop].process(value) - # Remapping for backwards compatibility of style names prop_r = STYLE_REMAP.get(prop, prop) w.set_style(prop_r, value, lv_state) + if group := add_group(config.get(CONF_GROUP)): + lv.group_add_obj(group, w.obj) flag_clr = set() flag_set = set() props = parts[CONF_MAIN][CONF_DEFAULT] @@ -241,7 +258,7 @@ async def set_obj_properties(w: Widget, config): w.clear_state(clears) for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) - state = ConstantLiteral(f"LV_STATE_{key.upper}") + state = f"LV_STATE_{key.upper}" lv.cond_if(lamb) w.add_state(state) lv.cond_else() @@ -281,10 +298,19 @@ async def widget_to_code(w_cnfig, w_type, parent): var = cg.new_Pvariable(wid) lv_add(var.set_obj(creator)) else: - var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t) + var = MockObj(wid, "->") + decl = VariableDeclarationExpression(lv_obj_t, "*", wid) + CORE.add_global(decl) + CORE.register_variable(wid, var) lv_assign(var, creator) widget = Widget.create(wid, var, spec, w_cnfig, parent) await set_obj_properties(widget, w_cnfig) await add_widgets(widget, w_cnfig) await spec.to_code(widget, w_cnfig) + + +lv_scr_act_spec = LvScrActType() +lv_scr_act = Widget.create( + None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None +) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 6ba5b64761..726db24592 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -39,9 +39,12 @@ #define USE_LOCK #define USE_LOGGER #define USE_LVGL +#define USE_LVGL_BINARY_SENSOR #define USE_LVGL_FONT #define USE_LVGL_IMAGE +#define USE_LVGL_KEY_LISTENER #define USE_LVGL_TOUCHSCREEN +#define USE_LVGL_ROTARY_ENCODER #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 696c749876..fde700e0bd 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -7,6 +7,7 @@ lvgl: long_press_time: 500ms widgets: - label: + id: hello_label text: Hello world text_color: 0xFF8000 align: center @@ -95,9 +96,43 @@ lvgl: height: 10% pressed: bg_color: light_blue + checkable: true + checked: + bg_color: 0x000000 widgets: - label: text: Button + on_click: + lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + on_value: + logger.log: + format: "state now %d" + args: [x] + on_short_click: + lvgl.widget.hide: hello_label + on_long_press: + lvgl.widget.show: hello_label + on_cancel: + lvgl.widget.enable: hello_label + on_ready: + lvgl.widget.disable: hello_label + on_defocus: + lvgl.widget.hide: hello_label + on_focus: + logger.log: Button clicked + on_scroll: + logger.log: Button clicked + on_scroll_end: + logger.log: Button clicked + on_scroll_begin: + logger.log: Button clicked + on_release: + logger.log: Button clicked + on_long_press_repeat: + logger.log: Button clicked font: - file: "gfonts://Roboto" diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index eab75b05f3..0f740db980 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -6,6 +6,23 @@ i2c: sda: GPIO18 scl: GPIO19 +sensor: + - platform: rotary_encoder + name: "Rotary Encoder" + id: encoder + pin_a: 2 + pin_b: 1 + internal: true + +binary_sensor: + - platform: gpio + id: pushbutton + name: Pushbutton + pin: + number: 0 + inverted: true + ignore_strapping_warning: true + display: - platform: ili9xxx model: st7789v @@ -50,5 +67,9 @@ lvgl: displays: - tft_display - second_display + rotary_encoders: + sensor: encoder + enter_button: pushbutton + group: general <<: !include common.yaml From dfacf1bbfe3aef5ca6796f92e8a46cb62212e924 Mon Sep 17 00:00:00 2001 From: thevogoncoder <6619878+thevogoncoder@users.noreply.github.com> Date: Thu, 25 Jul 2024 04:06:23 +0200 Subject: [PATCH 05/13] Add delay after sending REG_READ_START (#7130) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/pmwcs3/pmwcs3.cpp | 61 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/esphome/components/pmwcs3/pmwcs3.cpp b/esphome/components/pmwcs3/pmwcs3.cpp index 812018b52e..97ce4c9ae0 100644 --- a/esphome/components/pmwcs3/pmwcs3.cpp +++ b/esphome/components/pmwcs3/pmwcs3.cpp @@ -72,43 +72,44 @@ void PMWCS3Component::dump_config() { LOG_SENSOR(" ", "vwc", this->vwc_sensor_); } void PMWCS3Component::read_data_() { - uint8_t data[8]; - float e25, ec, temperature, vwc; - /////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) //// - if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) { this->status_set_warning(); ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!"); return; } - // NOLINT delay(100); - if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { - ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); - this->mark_failed(); - return; - } - if (this->e25_sensor_ != nullptr) { - e25 = ((data[1] << 8) | data[0]) / 100.0; - this->e25_sensor_->publish_state(e25); - ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); - } - if (this->ec_sensor_ != nullptr) { - ec = ((data[3] << 8) | data[2]) / 10.0; - this->ec_sensor_->publish_state(ec); - ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); - } - if (this->temperature_sensor_ != nullptr) { - temperature = ((data[5] << 8) | data[4]) / 100.0; - this->temperature_sensor_->publish_state(temperature); - ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); - } - if (this->vwc_sensor_ != nullptr) { - vwc = ((data[7] << 8) | data[6]) / 10.0; - this->vwc_sensor_->publish_state(vwc); - ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); - } + // Wait for the sensor to be ready. + // 80ms empirically determined (conservative). + this->set_timeout(80, [this] { + uint8_t data[8]; + float e25, ec, temperature, vwc; + if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { + ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); + this->mark_failed(); + return; + } + if (this->e25_sensor_ != nullptr) { + e25 = ((data[1] << 8) | data[0]) / 100.0; + this->e25_sensor_->publish_state(e25); + ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); + } + if (this->ec_sensor_ != nullptr) { + ec = ((data[3] << 8) | data[2]) / 10.0; + this->ec_sensor_->publish_state(ec); + ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); + } + if (this->temperature_sensor_ != nullptr) { + temperature = ((data[5] << 8) | data[4]) / 100.0; + this->temperature_sensor_->publish_state(temperature); + ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); + } + if (this->vwc_sensor_ != nullptr) { + vwc = ((data[7] << 8) | data[6]) / 10.0; + this->vwc_sensor_->publish_state(vwc); + ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); + } + }); } } // namespace pmwcs3 From a70f926971264501287ac93b8e39d533461990dc Mon Sep 17 00:00:00 2001 From: RubyBailey <60991881+RubyBailey@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:20:29 -0700 Subject: [PATCH 06/13] Fix for Mitsubishi units that only support cooling (#7143) --- esphome/components/mitsubishi/mitsubishi.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index a02aabf14d..449c8fc712 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -110,7 +110,7 @@ void MitsubishiClimate::transmit_state() { // Byte 15: HVAC specfic, i.e. POWERFUL, SMART SET, PLASMA, always 0x00 // Byte 16: Constant 0x00 // Byte 17: Checksum: SUM[Byte0...Byte16] - uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x00, + uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; switch (this->mode) { @@ -136,6 +136,12 @@ void MitsubishiClimate::transmit_state() { break; case climate::CLIMATE_MODE_OFF: default: + remote_state[6] = MITSUBISHI_MODE_COOL; + remote_state[8] = MITSUBISHI_MODE_A_COOL; + if (this->supports_heat_) { + remote_state[6] = MITSUBISHI_MODE_HEAT; + remote_state[8] = MITSUBISHI_MODE_A_HEAT; + } remote_state[5] = MITSUBISHI_OFF; break; } From 5ac9d301eaca1d46cc0db942e828a7995289640e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 30 Jul 2024 16:57:51 -0400 Subject: [PATCH 07/13] [micro_wake_word] Fix VAD detection and modify detection computation (#7164) --- esphome/components/micro_wake_word/streaming_model.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 013fa2ce6e..d0d2e2df05 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -148,7 +148,7 @@ WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutof }; bool WakeWordModel::determine_detected() { - int32_t sum = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { sum += prob; } @@ -175,12 +175,14 @@ VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t }; bool VADModel::determine_detected() { - uint8_t max = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { - max = std::max(prob, max); + sum += prob; } - return max > this->probability_cutoff_; + float sliding_window_average = static_cast(sum) / static_cast(255 * this->sliding_window_size_); + + return sliding_window_average > this->probability_cutoff_; } } // namespace micro_wake_word From 0af10c58f53466da19d9e065f764124e3bd31810 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:51:23 +1200 Subject: [PATCH 08/13] Bump version to 2024.7.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9abfafc4a4..1f63497982 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.7.2" +__version__ = "2024.7.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From cb9906b9215f17903ac004af160cc1537998b5a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:38:36 +1200 Subject: [PATCH 09/13] [api] ``homeassistant.action`` replaces ``homeassistant.service`` (#7171) --- esphome/components/api/__init__.py | 130 +++++++++++++-------- esphome/config_validation.py | 10 ++ esphome/const.py | 2 + tests/components/api/common.yaml | 16 +-- tests/components/homeassistant/common.yaml | 8 +- 5 files changed, 103 insertions(+), 63 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index d6b4416af8..38b50d4b9d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,25 +1,27 @@ import base64 -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_ACTION, + CONF_ACTIONS, CONF_DATA, CONF_DATA_TEMPLATE, + CONF_EVENT, CONF_ID, CONF_KEY, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SERVICE, - CONF_VARIABLES, CONF_SERVICES, - CONF_TRIGGER_ID, - CONF_EVENT, CONF_TAG, - CONF_ON_CLIENT_CONNECTED, - CONF_ON_CLIENT_DISCONNECTED, + CONF_TRIGGER_ID, + CONF_VARIABLES, ) from esphome.core import coroutine_with_priority @@ -63,40 +65,51 @@ def validate_encryption_key(value): return value -CONFIG_SCHEMA = cv.Schema( +ACTIONS_SCHEMA = automation.validate_automation( { - cv.GenerateID(): cv.declare_id(APIServer), - cv.Optional(CONF_PORT, default=6053): cv.port, - cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, - cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SERVICES): automation.validate_automation( + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), - cv.Required(CONF_SERVICE): cv.valid_name, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - { - cv.validate_id_name: cv.one_of( - *SERVICE_ARG_NATIVE_TYPES, lower=True - ), - } - ), + cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), } ), - cv.Optional(CONF_ENCRYPTION): cv.Schema( - { - cv.Required(CONF_KEY): validate_encryption_key, - } - ), - cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( - single=True - ), - cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( - single=True - ), - } -).extend(cv.COMPONENT_SCHEMA) + }, + cv.All( + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), + ), +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(APIServer), + cv.Optional(CONF_PORT, default=6053): cv.port, + cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Exclusive( + CONF_SERVICES, group_of_exclusion=CONF_ACTIONS + ): ACTIONS_SCHEMA, + cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), + cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.rename_key(CONF_SERVICES, CONF_ACTIONS), +) @coroutine_with_priority(40.0) @@ -108,7 +121,7 @@ async def to_code(config): cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - for conf in config.get(CONF_SERVICES, []): + for conf in config.get(CONF_ACTIONS, []): template_args = [] func_args = [] service_arg_names = [] @@ -119,7 +132,7 @@ async def to_code(config): service_arg_names.append(name) templ = cg.TemplateArguments(*template_args) trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names ) cg.add(var.register_user_service(trigger)) await automation.build_automation(trigger, func_args, conf) @@ -152,28 +165,43 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) -HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(APIServer), - cv.Required(CONF_SERVICE): cv.templatable(cv.string), - cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - {cv.string: cv.returning_lambda} - ), - } + +HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( + {cv.string: cv.returning_lambda} + ), + } + ), + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), ) +@automation.register_action( + "homeassistant.action", + HomeAssistantServiceCallAction, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, +) @automation.register_action( "homeassistant.service", HomeAssistantServiceCallAction, - HOMEASSISTANT_SERVICE_ACTION_SCHEMA, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) async def homeassistant_service_to_code(config, action_id, template_arg, args): serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = await cg.templatable(config[CONF_SERVICE], args, None) + templ = await cg.templatable(config[CONF_ACTION], args, None) cg.add(var.set_service(templ)) for key, value in config[CONF_DATA].items(): templ = await cg.templatable(value, args, None) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1cd1d6aa31..d93f8aed9a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2181,3 +2181,13 @@ SOURCE_SCHEMA = Any( } ), ) + + +def rename_key(old_key, new_key): + def validator(config: dict) -> dict: + config = config.copy() + if old_key in config: + config[new_key] = config.pop(old_key) + return config + + return validator diff --git a/esphome/const.py b/esphome/const.py index 37844e1047..39dd48d3f8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -37,8 +37,10 @@ CONF_ACCELERATION_Y = "acceleration_y" CONF_ACCELERATION_Z = "acceleration_z" CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" +CONF_ACTION = "action" CONF_ACTION_ID = "action_id" CONF_ACTION_STATE_TOPIC = "action_state_topic" +CONF_ACTIONS = "actions" CONF_ACTIVE = "active" CONF_ACTIVE_POWER = "active_power" CONF_ACTUAL_GAIN = "actual_gain" diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index e0b900f92d..6c2a333598 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -5,8 +5,8 @@ esphome: event: esphome.button_pressed data: message: Button was pressed - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: message: Button was pressed - homeassistant.tag_scanned: pulse @@ -21,8 +21,8 @@ api: reboot_timeout: 0min encryption: key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= - services: - - service: hello_world + actions: + - action: hello_world variables: name: string then: @@ -30,10 +30,10 @@ api: format: Hello World %s! args: - name.c_str() - - service: empty_service + - action: empty_action then: - - logger.log: Service Called - - service: all_types + - logger.log: Action Called + - action: all_types variables: bool_: bool int_: int @@ -41,7 +41,7 @@ api: string_: string then: - logger.log: Something happened - - service: array_types + - action: array_types variables: bool_arr: bool[] int_arr: int[] diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index ae016a3bea..07a6e8090c 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -13,12 +13,12 @@ esphome: message: The humidity is {{ my_variable }}%. variables: my_variable: "return id(ha_hello_world_temperature).state;" - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: message: Button was pressed - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: title: New Humidity data_template: From a5f18dfe7fda26d0311cfeb2fde42d0bf528f576 Mon Sep 17 00:00:00 2001 From: SimoPk Date: Thu, 1 Aug 2024 12:39:54 +0200 Subject: [PATCH 10/13] ade7953_spi wrong size specified in read_array call (#7172) --- esphome/components/ade7953_spi/ade7953_spi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ade7953_spi/ade7953_spi.cpp b/esphome/components/ade7953_spi/ade7953_spi.cpp index cfd5d71d0a..77a2a8adc7 100644 --- a/esphome/components/ade7953_spi/ade7953_spi.cpp +++ b/esphome/components/ade7953_spi/ade7953_spi.cpp @@ -60,7 +60,7 @@ bool AdE7953Spi::ade_read_16(uint16_t reg, uint16_t *value) { this->write_byte16(reg); this->transfer_byte(0x80); uint8_t recv[2]; - this->read_array(recv, 4); + this->read_array(recv, 2); *value = encode_uint16(recv[0], recv[1]); this->disable(); return false; From aedfb32482cf11feaa6373b73cc8ce8a2da50658 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:01:21 +1200 Subject: [PATCH 11/13] Bump improv library to 1.2.4 (#7174) --- esphome/components/improv_base/__init__.py | 5 ++--- platformio.ini | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index 5c2853a5c6..aa75f4d89c 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -1,8 +1,7 @@ import re -import esphome.config_validation as cv import esphome.codegen as cg - +import esphome.config_validation as cv from esphome.const import __version__ CODEOWNERS = ["@esphome/core"] @@ -39,4 +38,4 @@ def _process_next_url(url: str): async def setup_improv_core(var, config): if CONF_NEXT_URL in config: cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) - cg.add_library("esphome/Improv", "1.2.3") + cg.add_library("improv/Improv", "1.2.4") diff --git a/platformio.ini b/platformio.ini index baf0a85d73..e4f363d650 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,7 +35,7 @@ build_flags = lib_deps = esphome/noise-c@0.1.4 ; api makuna/NeoPixelBus@2.7.3 ; neopixelbus - esphome/Improv@1.2.3 ; improv_serial / esp32_improv + improv/Improv@1.2.4 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 From 4a7570770b0b36cfc2495ad3795d4ae3c7df66bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ebbinghaus?= Date: Fri, 2 Aug 2024 01:58:59 +0200 Subject: [PATCH 12/13] Implement 'round to nearest multiple' filter (#7142) --- esphome/components/sensor/__init__.py | 19 +++++++++++++++++++ esphome/components/sensor/filter.cpp | 8 ++++++++ esphome/components/sensor/filter.h | 9 +++++++++ esphome/config_validation.py | 1 + esphome/const.py | 1 + 5 files changed, 38 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 262e69d75b..3b76466dec 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_IGNORE_OUT_OF_RANGE, + CONF_MULTIPLE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -249,6 +250,7 @@ CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) ClampFilter = sensor_ns.class_("ClampFilter", Filter) RoundFilter = sensor_ns.class_("RoundFilter", Filter) +RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) validate_unit_of_measurement = cv.string_strict validate_accuracy_decimals = cv.int_ @@ -734,6 +736,23 @@ async def round_filter_to_code(config, filter_id): ) +@FILTER_REGISTRY.register( + "round_to_multiple_of", + RoundMultipleFilter, + cv.maybe_simple_value( + { + cv.Required(CONF_MULTIPLE): cv.positive_not_null_float, + }, + key=CONF_MULTIPLE, + ), +) +async def round_multiple_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_MULTIPLE], + ) + + async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index eaa909429b..bcf1fc8269 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -472,5 +472,13 @@ optional RoundFilter::new_value(float value) { return value; } +RoundMultipleFilter::RoundMultipleFilter(float multiple) : multiple_(multiple) {} +optional RoundMultipleFilter::new_value(float value) { + if (std::isfinite(value)) { + return value - remainderf(value, this->multiple_); + } + return value; +} + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index c13cb3420a..92b1d8d240 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -431,5 +431,14 @@ class RoundFilter : public Filter { uint8_t precision_; }; +class RoundMultipleFilter : public Filter { + public: + explicit RoundMultipleFilter(float multiple); + optional new_value(float value) override; + + protected: + float multiple_; +}; + } // namespace sensor } // namespace esphome diff --git a/esphome/config_validation.py b/esphome/config_validation.py index d93f8aed9a..6e1d3ba2f9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -464,6 +464,7 @@ zero_to_one_float = float_range(min=0, max=1) negative_one_to_one_float = float_range(min=-1, max=1) positive_int = int_range(min=0) positive_not_null_int = int_range(min=0, min_included=False) +positive_not_null_float = float_range(min=0, min_included=False) def validate_id_name(value): diff --git a/esphome/const.py b/esphome/const.py index 39dd48d3f8..fcb630badd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -503,6 +503,7 @@ CONF_MOTION = "motion" CONF_MOVEMENT_COUNTER = "movement_counter" CONF_MQTT = "mqtt" CONF_MQTT_ID = "mqtt_id" +CONF_MULTIPLE = "multiple" CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLY = "multiply" CONF_NAME = "name" From 61c65811233eb1b678fcfa077d6370e7f07ba091 Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Sat, 3 Aug 2024 01:00:18 +0200 Subject: [PATCH 13/13] git ignore managed_components (#7180) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0c9a878400..79820249ac 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ sdkconfig.* .tests/ /components +/managed_components +