mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'dev' into gsm
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -138,3 +138,5 @@ sdkconfig.* | ||||
| .tests/ | ||||
|  | ||||
| /components | ||||
| /managed_components | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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; } | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
							
								
								
									
										188
									
								
								esphome/components/lvgl/automation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								esphome/components/lvgl/automation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
| ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
| @@ -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<lv_coord_t>(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 | ||||
|  | ||||
|   | ||||
| @@ -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 <lvgl.h> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
| #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<void(float)>; | ||||
| using event_callback_t = void(_lv_event_t *); | ||||
| using text_lambda_t = std::function<const char *()>; | ||||
|  | ||||
| template<typename... Ts> class ObjUpdateAction : public Action<Ts...> { | ||||
|  public: | ||||
|   explicit ObjUpdateAction(std::function<void(Ts...)> &&lamb) : lamb_(std::move(lamb)) {} | ||||
|  | ||||
|   void play(Ts... x) override { this->lamb_(x...); } | ||||
|  | ||||
|  protected: | ||||
|   std::function<void(Ts...)> 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<void(uint32_t)> &&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<void(lv_disp_t *)> &lamb) { this->init_lambdas_.push_back(lamb); } | ||||
|   void add_init_lambda(const std::function<void(LvglComponent *)> &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<display::Display *> 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<std::function<void(lv_disp_t *)>> init_lambdas_; | ||||
|   std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_; | ||||
|   CallbackManager<void(uint32_t)> idle_callbacks_{}; | ||||
|   size_t buffer_frac_{1}; | ||||
|   bool full_refresh_{}; | ||||
| }; | ||||
|  | ||||
| class IdleTrigger : public Trigger<> { | ||||
|  public: | ||||
|   explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> 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<uint32_t> timeout_; | ||||
|   bool is_idle_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> { | ||||
|  public: | ||||
|   explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {} | ||||
|   void play(Ts... x) override { this->action_(this->parent_); } | ||||
|  | ||||
|  protected: | ||||
|   std::function<void(LvglComponent *)> action_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<LvglComponent> { | ||||
|  public: | ||||
|   LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda) | ||||
|       : condition_lambda_(std::move(condition_lambda)) {} | ||||
|   bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } | ||||
|  | ||||
|  protected: | ||||
|   std::function<bool(LvglComponent *)> condition_lambda_{}; | ||||
| }; | ||||
|  | ||||
| #ifdef USE_LVGL_TOUCHSCREEN | ||||
| class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> { | ||||
|  public: | ||||
| @@ -160,7 +248,62 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC | ||||
|   bool touch_pressed_{}; | ||||
| }; | ||||
| #endif  // USE_LVGL_TOUCHSCREEN | ||||
|  | ||||
| #ifdef USE_LVGL_KEY_LISTENER | ||||
| class LVEncoderListener : public Parented<LvglComponent> { | ||||
|  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<LVEncoderListener *>(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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										62
									
								
								esphome/components/lvgl/rotary_encoders.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/lvgl/rotary_encoders.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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()) | ||||
| @@ -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, | ||||
| } | ||||
|   | ||||
| @@ -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( | ||||
|     { | ||||
|   | ||||
							
								
								
									
										61
									
								
								esphome/components/lvgl/trigger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								esphome/components/lvgl/trigger.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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))) | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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 | ||||
| ) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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<float>(sum) / static_cast<float>(255 * this->sliding_window_size_); | ||||
|  | ||||
|   return sliding_window_average > this->probability_cutoff_; | ||||
| } | ||||
|  | ||||
| }  // namespace micro_wake_word | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -472,5 +472,13 @@ optional<float> RoundFilter::new_value(float value) { | ||||
|   return value; | ||||
| } | ||||
|  | ||||
| RoundMultipleFilter::RoundMultipleFilter(float multiple) : multiple_(multiple) {} | ||||
| optional<float> RoundMultipleFilter::new_value(float value) { | ||||
|   if (std::isfinite(value)) { | ||||
|     return value - remainderf(value, this->multiple_); | ||||
|   } | ||||
|   return value; | ||||
| } | ||||
|  | ||||
| }  // namespace sensor | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -431,5 +431,14 @@ class RoundFilter : public Filter { | ||||
|   uint8_t precision_; | ||||
| }; | ||||
|  | ||||
| class RoundMultipleFilter : public Filter { | ||||
|  public: | ||||
|   explicit RoundMultipleFilter(float multiple); | ||||
|   optional<float> new_value(float value) override; | ||||
|  | ||||
|  protected: | ||||
|   float multiple_; | ||||
| }; | ||||
|  | ||||
| }  // namespace sensor | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										23
									
								
								esphome/components/update/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/update/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "update_entity.h" | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace update { | ||||
|  | ||||
| template<typename... Ts> class PerformAction : public Action<Ts...>, public Parented<UpdateEntity> { | ||||
|   TEMPLATABLE_VALUE(bool, force) | ||||
|  | ||||
|  public: | ||||
|   void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class IsAvailableCondition : public Condition<Ts...>, public Parented<UpdateEntity> { | ||||
|  public: | ||||
|   bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } | ||||
| }; | ||||
|  | ||||
| }  // namespace update | ||||
| }  // namespace esphome | ||||
| @@ -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_; | ||||
|   | ||||
| @@ -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): | ||||
| @@ -2181,3 +2182,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 | ||||
|   | ||||
| @@ -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" | ||||
| @@ -501,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" | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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[] | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1 +1,4 @@ | ||||
| substitutions: | ||||
|   verify_ssl: "false" | ||||
|  | ||||
| <<: !include common.yaml | ||||
|   | ||||
| @@ -1 +1,4 @@ | ||||
| substitutions: | ||||
|   verify_ssl: "false" | ||||
|  | ||||
| <<: !include common.yaml | ||||
|   | ||||
| @@ -1 +1,4 @@ | ||||
| substitutions: | ||||
|   verify_ssl: "false" | ||||
|  | ||||
| <<: !include common.yaml | ||||
|   | ||||
		Reference in New Issue
	
	Block a user