mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'dev' into gsm
This commit is contained in:
		| @@ -279,6 +279,7 @@ esphome/components/nfc/* @jesserockz @kbx81 | ||||
| esphome/components/noblex/* @AGalfra | ||||
| esphome/components/number/* @esphome/core | ||||
| esphome/components/one_wire/* @ssieb | ||||
| esphome/components/online_image/* @guillempages | ||||
| esphome/components/ota/* @esphome/core | ||||
| esphome/components/output/* @esphome/core | ||||
| esphome/components/pca6416a/* @Mat931 | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #!/bin/bash | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| # If /cache is mounted, use that as PIO's coredir | ||||
| # otherwise use path in /config (so that PIO packages aren't downloaded on each compile) | ||||
|   | ||||
| @@ -972,13 +972,6 @@ def run_esphome(argv): | ||||
|         args.command == "dashboard", | ||||
|     ) | ||||
|  | ||||
|     if sys.version_info < (3, 8, 0): | ||||
|         _LOGGER.error( | ||||
|             "You're running ESPHome with Python <3.8. ESPHome is no longer compatible " | ||||
|             "with this Python version. Please reinstall ESPHome with Python 3.8+" | ||||
|         ) | ||||
|         return 1 | ||||
|  | ||||
|     if args.command in PRE_CONFIG_ACTIONS: | ||||
|         try: | ||||
|             return PRE_CONFIG_ACTIONS[args.command](args) | ||||
|   | ||||
| @@ -1,16 +1,17 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import web_server | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_CODE, | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_CODE, | ||||
|     CONF_WEB_SERVER_ID, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@grahambrown11", "@hwstar"] | ||||
| @@ -77,67 +78,72 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_( | ||||
|     "AlarmControlPanelCondition", automation.Condition | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|     web_server.WEBSERVER_SORTING_SCHEMA | ||||
| ).extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AlarmControlPanel), | ||||
|         cv.Optional(CONF_ON_STATE): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMING): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_PENDING): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_DISARMED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_CLEARED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_CHIME): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_READY): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ALARM_CONTROL_PANEL_SCHEMA = ( | ||||
|     cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) | ||||
|     .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AlarmControlPanel), | ||||
|             cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( | ||||
|                 mqtt.MQTTAlarmControlPanelComponent | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_STATE): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMING): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_PENDING): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_DISARMED): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_CLEARED): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_CHIME): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_READY): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), | ||||
|                 } | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( | ||||
| @@ -192,6 +198,9 @@ async def setup_alarm_control_panel_core_(var, config): | ||||
|     if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None: | ||||
|         web_server_ = await cg.get_variable(webserver_id) | ||||
|         web_server.add_entity_to_sorting_list(web_server_, var, config) | ||||
|     if mqtt_id := config.get(CONF_MQTT_ID): | ||||
|         mqtt_ = cg.new_Pvariable(mqtt_id, var) | ||||
|         await mqtt.register_mqtt_component(mqtt_, config) | ||||
|  | ||||
|  | ||||
| async def register_alarm_control_panel(var, config): | ||||
|   | ||||
| @@ -1872,6 +1872,11 @@ message UpdateStateResponse { | ||||
|   string release_summary = 9; | ||||
|   string release_url = 10; | ||||
| } | ||||
| enum UpdateCommand { | ||||
|   UPDATE_COMMAND_NONE = 0; | ||||
|   UPDATE_COMMAND_UPDATE = 1; | ||||
|   UPDATE_COMMAND_CHECK = 2; | ||||
| } | ||||
| message UpdateCommandRequest { | ||||
|   option (id) = 118; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
| @@ -1879,5 +1884,5 @@ message UpdateCommandRequest { | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   bool install = 2; | ||||
|   UpdateCommand command = 2; | ||||
| } | ||||
|   | ||||
| @@ -1328,7 +1328,17 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { | ||||
|   if (update == nullptr) | ||||
|     return; | ||||
|  | ||||
|   update->perform(); | ||||
|   switch (msg.command) { | ||||
|     case enums::UPDATE_COMMAND_UPDATE: | ||||
|       update->perform(); | ||||
|       break; | ||||
|     case enums::UPDATE_COMMAND_CHECK: | ||||
|       update->check(); | ||||
|       break; | ||||
|     default: | ||||
|       ESP_LOGW(TAG, "Unknown update command: %d", msg.command); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
|   | ||||
| @@ -567,6 +567,20 @@ template<> const char *proto_enum_to_string<enums::ValveOperation>(enums::ValveO | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateCommand value) { | ||||
|   switch (value) { | ||||
|     case enums::UPDATE_COMMAND_NONE: | ||||
|       return "UPDATE_COMMAND_NONE"; | ||||
|     case enums::UPDATE_COMMAND_UPDATE: | ||||
|       return "UPDATE_COMMAND_UPDATE"; | ||||
|     case enums::UPDATE_COMMAND_CHECK: | ||||
|       return "UPDATE_COMMAND_CHECK"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
| @@ -8596,7 +8610,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { | ||||
| bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->install = value.as_bool(); | ||||
|       this->command = value.as_enum<enums::UpdateCommand>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
| @@ -8615,7 +8629,7 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
| } | ||||
| void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_bool(2, this->install); | ||||
|   buffer.encode_enum<enums::UpdateCommand>(2, this->command); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void UpdateCommandRequest::dump_to(std::string &out) const { | ||||
| @@ -8626,8 +8640,8 @@ void UpdateCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  install: "); | ||||
|   out.append(YESNO(this->install)); | ||||
|   out.append("  command: "); | ||||
|   out.append(proto_enum_to_string<enums::UpdateCommand>(this->command)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
|   | ||||
| @@ -227,6 +227,11 @@ enum ValveOperation : uint32_t { | ||||
|   VALVE_OPERATION_IS_OPENING = 1, | ||||
|   VALVE_OPERATION_IS_CLOSING = 2, | ||||
| }; | ||||
| enum UpdateCommand : uint32_t { | ||||
|   UPDATE_COMMAND_NONE = 0, | ||||
|   UPDATE_COMMAND_UPDATE = 1, | ||||
|   UPDATE_COMMAND_CHECK = 2, | ||||
| }; | ||||
|  | ||||
| }  // namespace enums | ||||
|  | ||||
| @@ -2175,7 +2180,7 @@ class UpdateStateResponse : public ProtoMessage { | ||||
| class UpdateCommandRequest : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   bool install{false}; | ||||
|   enums::UpdateCommand command{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome import automation, core | ||||
| from esphome.automation import Condition, maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DELAY, | ||||
|     CONF_DEVICE_CLASS, | ||||
| @@ -16,6 +14,7 @@ from esphome.const import ( | ||||
|     CONF_INVERTED, | ||||
|     CONF_MAX_LENGTH, | ||||
|     CONF_MIN_LENGTH, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_CLICK, | ||||
|     CONF_ON_DOUBLE_CLICK, | ||||
|     CONF_ON_MULTI_CLICK, | ||||
| @@ -26,7 +25,6 @@ from esphome.const import ( | ||||
|     CONF_STATE, | ||||
|     CONF_TIMING, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_BATTERY_CHARGING, | ||||
| @@ -59,6 +57,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_WINDOW, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.util import Registry | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_ICON, | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_PRESS, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     DEVICE_CLASS_EMPTY, | ||||
|     DEVICE_CLASS_IDENTIFY, | ||||
| @@ -18,8 +18,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_UPDATE, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ACTION_STATE_TOPIC, | ||||
|     CONF_AWAY, | ||||
| @@ -21,6 +20,7 @@ from esphome.const import ( | ||||
|     CONF_MODE, | ||||
|     CONF_MODE_COMMAND_TOPIC, | ||||
|     CONF_MODE_STATE_TOPIC, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_CONTROL, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_PRESET, | ||||
| @@ -33,20 +33,20 @@ from esphome.const import ( | ||||
|     CONF_TARGET_HUMIDITY_STATE_TOPIC, | ||||
|     CONF_TARGET_TEMPERATURE, | ||||
|     CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, | ||||
|     CONF_TARGET_TEMPERATURE_STATE_TOPIC, | ||||
|     CONF_TARGET_TEMPERATURE_HIGH, | ||||
|     CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC, | ||||
|     CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC, | ||||
|     CONF_TARGET_TEMPERATURE_LOW, | ||||
|     CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC, | ||||
|     CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC, | ||||
|     CONF_TARGET_TEMPERATURE_STATE_TOPIC, | ||||
|     CONF_TEMPERATURE_STEP, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VISUAL, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|  | ||||
|   | ||||
| @@ -1,23 +1,23 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id, Condition | ||||
| from esphome.automation import Condition, maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_STATE, | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_OPEN, | ||||
|     CONF_POSITION, | ||||
|     CONF_POSITION_COMMAND_TOPIC, | ||||
|     CONF_POSITION_STATE_TOPIC, | ||||
|     CONF_STATE, | ||||
|     CONF_STOP, | ||||
|     CONF_TILT, | ||||
|     CONF_TILT_COMMAND_TOPIC, | ||||
|     CONF_TILT_STATE_TOPIC, | ||||
|     CONF_STOP, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     DEVICE_CLASS_AWNING, | ||||
|     DEVICE_CLASS_BLIND, | ||||
|     DEVICE_CLASS_CURTAIN, | ||||
|   | ||||
| @@ -5,13 +5,17 @@ namespace cst226 { | ||||
|  | ||||
| void CST226Touchscreen::setup() { | ||||
|   esph_log_config(TAG, "Setting up CST226 Touchscreen..."); | ||||
|   this->reset_pin_->setup(); | ||||
|   this->reset_pin_->digital_write(true); | ||||
|   delay(5); | ||||
|   this->reset_pin_->digital_write(false); | ||||
|   delay(5); | ||||
|   this->reset_pin_->digital_write(true); | ||||
|   this->set_timeout(30, [this] { this->continue_setup_(); }); | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|     this->reset_pin_->digital_write(true); | ||||
|     delay(5); | ||||
|     this->reset_pin_->digital_write(false); | ||||
|     delay(5); | ||||
|     this->reset_pin_->digital_write(true); | ||||
|     this->set_timeout(30, [this] { this->continue_setup_(); }); | ||||
|   } else { | ||||
|     this->continue_setup_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void CST226Touchscreen::update_touches() { | ||||
|   | ||||
| @@ -35,7 +35,7 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice | ||||
|   void continue_setup_(); | ||||
|  | ||||
|   InternalGPIOPin *interrupt_pin_{}; | ||||
|   GPIOPin *reset_pin_{NULL_PIN}; | ||||
|   GPIOPin *reset_pin_{}; | ||||
|   uint8_t chip_id_{}; | ||||
|   bool setup_complete_{}; | ||||
| }; | ||||
|   | ||||
| @@ -1,32 +1,30 @@ | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.components import mqtt, web_server, time | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, time, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DATE, | ||||
|     CONF_DATETIME, | ||||
|     CONF_DAY, | ||||
|     CONF_HOUR, | ||||
|     CONF_ID, | ||||
|     CONF_MINUTE, | ||||
|     CONF_MONTH, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_TIME, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_SECOND, | ||||
|     CONF_TIME, | ||||
|     CONF_TIME_ID, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_DATE, | ||||
|     CONF_DATETIME, | ||||
|     CONF_TIME, | ||||
|     CONF_YEAR, | ||||
|     CONF_MONTH, | ||||
|     CONF_DAY, | ||||
|     CONF_SECOND, | ||||
|     CONF_HOUR, | ||||
|     CONF_MINUTE, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
|  | ||||
| CODEOWNERS = ["@rfdarter", "@jesserockz"] | ||||
| DEPENDENCIES = ["time"] | ||||
|  | ||||
|   | ||||
| @@ -1,23 +1,26 @@ | ||||
| import re | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from esphome import automation, core | ||||
| from esphome.automation import maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.number import Number | ||||
| from esphome.components.select import Select | ||||
| from esphome.components.switch import Switch | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_TYPE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_ACTIVE, | ||||
|     CONF_COMMAND, | ||||
|     CONF_CUSTOM, | ||||
|     CONF_NUMBER, | ||||
|     CONF_FORMAT, | ||||
|     CONF_ID, | ||||
|     CONF_ITEMS, | ||||
|     CONF_MODE, | ||||
|     CONF_ACTIVE, | ||||
|     CONF_NUMBER, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_TEXT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
| ) | ||||
| from esphome.automation import maybe_simple_id | ||||
| from esphome.components.select import Select | ||||
| from esphome.components.number import Number | ||||
| from esphome.components.switch import Switch | ||||
|  | ||||
| CODEOWNERS = ["@numo68"] | ||||
|  | ||||
| @@ -29,10 +32,8 @@ CONF_JOYSTICK = "joystick" | ||||
| CONF_LABEL = "label" | ||||
| CONF_MENU = "menu" | ||||
| CONF_BACK = "back" | ||||
| CONF_TEXT = "text" | ||||
| CONF_SELECT = "select" | ||||
| CONF_SWITCH = "switch" | ||||
| CONF_ITEMS = "items" | ||||
| CONF_ON_TEXT = "on_text" | ||||
| CONF_OFF_TEXT = "off_text" | ||||
| CONF_VALUE_LAMBDA = "value_lambda" | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import binary_sensor, esp32_ble_server, output | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor, output, esp32_ble_server | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
|  | ||||
| AUTO_LOAD = ["esp32_ble_server"] | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
| DEPENDENCIES = ["wifi", "esp32"] | ||||
| @@ -50,7 +49,7 @@ async def to_code(config): | ||||
|     cg.add(ble_server.register_service_component(var)) | ||||
|  | ||||
|     cg.add_define("USE_IMPROV") | ||||
|     cg.add_library("esphome/Improv", "1.2.3") | ||||
|     cg.add_library("improv/Improv", "1.2.4") | ||||
|  | ||||
|     cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) | ||||
|     cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) | ||||
|   | ||||
| @@ -1,24 +1,24 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_EVENT_TYPE, | ||||
|     CONF_ICON, | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_EVENT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_EVENT_TYPE, | ||||
|     DEVICE_CLASS_BUTTON, | ||||
|     DEVICE_CLASS_DOORBELL, | ||||
|     DEVICE_CLASS_EMPTY, | ||||
|     DEVICE_CLASS_MOTION, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@nohat"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|   | ||||
| @@ -1,31 +1,31 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DIRECTION, | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_OSCILLATING, | ||||
|     CONF_OSCILLATION_COMMAND_TOPIC, | ||||
|     CONF_OSCILLATION_STATE_TOPIC, | ||||
|     CONF_SPEED, | ||||
|     CONF_SPEED_LEVEL_COMMAND_TOPIC, | ||||
|     CONF_SPEED_LEVEL_STATE_TOPIC, | ||||
|     CONF_SPEED_COMMAND_TOPIC, | ||||
|     CONF_SPEED_STATE_TOPIC, | ||||
|     CONF_OFF_SPEED_CYCLE, | ||||
|     CONF_ON_DIRECTION_SET, | ||||
|     CONF_ON_OSCILLATING_SET, | ||||
|     CONF_ON_PRESET_SET, | ||||
|     CONF_ON_SPEED_SET, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_ON_TURN_OFF, | ||||
|     CONF_ON_TURN_ON, | ||||
|     CONF_ON_PRESET_SET, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_DIRECTION, | ||||
|     CONF_OSCILLATING, | ||||
|     CONF_OSCILLATION_COMMAND_TOPIC, | ||||
|     CONF_OSCILLATION_STATE_TOPIC, | ||||
|     CONF_RESTORE_MODE, | ||||
|     CONF_SPEED, | ||||
|     CONF_SPEED_COMMAND_TOPIC, | ||||
|     CONF_SPEED_LEVEL_COMMAND_TOPIC, | ||||
|     CONF_SPEED_LEVEL_STATE_TOPIC, | ||||
|     CONF_SPEED_STATE_TOPIC, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|   | ||||
| @@ -1,19 +1,22 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import display, font, color | ||||
| from esphome.const import CONF_DISPLAY, CONF_ID, CONF_TRIGGER_ID | ||||
| from esphome import automation, core | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import color, display, font | ||||
| from esphome.components.display_menu_base import ( | ||||
|     DISPLAY_MENU_BASE_SCHEMA, | ||||
|     DisplayMenuComponent, | ||||
|     display_menu_to_code, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BACKGROUND_COLOR, | ||||
|     CONF_DISPLAY, | ||||
|     CONF_FONT, | ||||
|     CONF_FOREGROUND_COLOR, | ||||
|     CONF_ID, | ||||
|     CONF_TRIGGER_ID, | ||||
| ) | ||||
|  | ||||
| CONF_FONT = "font" | ||||
| CONF_MENU_ITEM_VALUE = "menu_item_value" | ||||
| CONF_FOREGROUND_COLOR = "foreground_color" | ||||
| CONF_BACKGROUND_COLOR = "background_color" | ||||
| CONF_ON_REDRAW = "on_redraw" | ||||
|  | ||||
| graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") | ||||
|   | ||||
| @@ -16,6 +16,7 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { | ||||
|   void update() override; | ||||
|  | ||||
|   void perform(bool force) override; | ||||
|   void check() override { this->update(); } | ||||
|  | ||||
|   void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } | ||||
|  | ||||
|   | ||||
| @@ -236,7 +236,7 @@ void HydreonRGxxComponent::process_line_() { | ||||
|   } | ||||
|   bool is_data_line = false; | ||||
|   for (int i = 0; i < NUM_SENSORS; i++) { | ||||
|     if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) { | ||||
|     if (this->sensors_[i] != nullptr && this->buffer_.find(PROTOCOL_NAMES[i]) != std::string::npos) { | ||||
|       is_data_line = true; | ||||
|       break; | ||||
|     } | ||||
|   | ||||
| @@ -233,6 +233,7 @@ void I2SAudioSpeaker::loop() { | ||||
|   switch (this->state_) { | ||||
|     case speaker::STATE_STARTING: | ||||
|       this->start_(); | ||||
|       [[fallthrough]]; | ||||
|     case speaker::STATE_RUNNING: | ||||
|     case speaker::STATE_STOPPING: | ||||
|       this->watch_(); | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| import esphome.automation as auto | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, power_supply, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_COLD_WHITE_COLOR_TEMPERATURE, | ||||
|     CONF_COLOR_CORRECT, | ||||
|     CONF_DEFAULT_TRANSITION_LENGTH, | ||||
|     CONF_EFFECTS, | ||||
| @@ -10,36 +11,36 @@ from esphome.const import ( | ||||
|     CONF_GAMMA_CORRECT, | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_POWER_SUPPLY, | ||||
|     CONF_RESTORE_MODE, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_ON_TURN_OFF, | ||||
|     CONF_ON_TURN_ON, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_POWER_SUPPLY, | ||||
|     CONF_RESTORE_MODE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_COLD_WHITE_COLOR_TEMPERATURE, | ||||
|     CONF_WARM_WHITE_COLOR_TEMPERATURE, | ||||
|     CONF_WEB_SERVER_ID, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| from .automation import light_control_to_code  # noqa | ||||
| from .effects import ( | ||||
|     validate_effects, | ||||
|     ADDRESSABLE_EFFECTS, | ||||
|     BINARY_EFFECTS, | ||||
|     EFFECTS_REGISTRY, | ||||
|     MONOCHROMATIC_EFFECTS, | ||||
|     RGB_EFFECTS, | ||||
|     ADDRESSABLE_EFFECTS, | ||||
|     EFFECTS_REGISTRY, | ||||
|     validate_effects, | ||||
| ) | ||||
| from .types import (  # noqa | ||||
|     LightState, | ||||
|     AddressableLightState, | ||||
|     light_ns, | ||||
|     LightOutput, | ||||
|     AddressableLight, | ||||
|     LightTurnOnTrigger, | ||||
|     LightTurnOffTrigger, | ||||
|     AddressableLightState, | ||||
|     LightOutput, | ||||
|     LightState, | ||||
|     LightStateTrigger, | ||||
|     LightTurnOffTrigger, | ||||
|     LightTurnOnTrigger, | ||||
|     light_ns, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import Condition, maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_LOCK, | ||||
|     CONF_ON_UNLOCK, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
|   | ||||
| @@ -1,9 +1,21 @@ | ||||
| import re | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import LambdaAction | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32C6, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
| from esphome.components.libretiny import get_libretiny_component, get_libretiny_family | ||||
| from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ARGS, | ||||
|     CONF_BAUD_RATE, | ||||
| @@ -18,27 +30,12 @@ from esphome.const import ( | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TX_BUFFER_SIZE, | ||||
|     PLATFORM_BK72XX, | ||||
|     PLATFORM_RTL87XX, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_RP2040, | ||||
|     PLATFORM_RTL87XX, | ||||
| ) | ||||
| from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32S3, | ||||
|     VARIANT_ESP32C2, | ||||
|     VARIANT_ESP32C6, | ||||
|     VARIANT_ESP32H2, | ||||
| ) | ||||
| from esphome.components.libretiny import get_libretiny_component, get_libretiny_family | ||||
| from esphome.components.libretiny.const import ( | ||||
|     COMPONENT_BK72XX, | ||||
|     COMPONENT_RTL87XX, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| logger_ns = cg.esphome_ns.namespace("logger") | ||||
|   | ||||
| @@ -15,44 +15,110 @@ from esphome.const import ( | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
| ) | ||||
| from esphome.core import CORE, ID, Lambda | ||||
| from esphome.core import CORE, ID | ||||
| from esphome.cpp_generator import MockObj | ||||
| 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 .lv_validation import lv_images_used | ||||
| from .lvcode import LvContext | ||||
| from .obj import obj_spec | ||||
| from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code | ||||
| from .schemas import any_widget_schema, create_modify_schema, obj_schema | ||||
| from .automation import disp_update, update_to_code | ||||
| from .defines import CONF_SKIP | ||||
| from .encoders import ENCODERS_CONFIG, encoders_to_code | ||||
| from .lv_validation import lv_bool, lv_images_used | ||||
| from .lvcode import LvContext, LvglComponent | ||||
| from .schemas import ( | ||||
|     DISP_BG_SCHEMA, | ||||
|     FLEX_OBJ_SCHEMA, | ||||
|     GRID_CELL_SCHEMA, | ||||
|     LAYOUT_SCHEMAS, | ||||
|     STYLE_SCHEMA, | ||||
|     WIDGET_TYPES, | ||||
|     any_widget_schema, | ||||
|     container_schema, | ||||
|     create_modify_schema, | ||||
|     grid_alignments, | ||||
|     obj_schema, | ||||
| ) | ||||
| from .styles import add_top_layer, styles_to_code, theme_to_code | ||||
| from .touchscreens import touchscreen_schema, touchscreens_to_code | ||||
| from .trigger import generate_triggers | ||||
| from .types import ( | ||||
|     WIDGET_TYPES, | ||||
|     FontEngine, | ||||
|     IdleTrigger, | ||||
|     LvglComponent, | ||||
|     ObjUpdateAction, | ||||
|     lv_font_t, | ||||
|     lv_style_t, | ||||
|     lvgl_ns, | ||||
| ) | ||||
| from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties | ||||
| from .widgets import Widget, add_widgets, lv_scr_act, set_obj_properties | ||||
| from .widgets.animimg import animimg_spec | ||||
| from .widgets.arc import arc_spec | ||||
| from .widgets.button import button_spec | ||||
| from .widgets.buttonmatrix import buttonmatrix_spec | ||||
| from .widgets.checkbox import checkbox_spec | ||||
| from .widgets.dropdown import dropdown_spec | ||||
| from .widgets.img import img_spec | ||||
| from .widgets.keyboard import keyboard_spec | ||||
| from .widgets.label import label_spec | ||||
| from .widgets.led import led_spec | ||||
| from .widgets.line import line_spec | ||||
| from .widgets.lv_bar import bar_spec | ||||
| from .widgets.meter import meter_spec | ||||
| from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code | ||||
| from .widgets.obj import obj_spec | ||||
| from .widgets.page import add_pages, page_spec | ||||
| from .widgets.roller import roller_spec | ||||
| from .widgets.slider import slider_spec | ||||
| from .widgets.spinbox import spinbox_spec | ||||
| from .widgets.spinner import spinner_spec | ||||
| from .widgets.switch import switch_spec | ||||
| from .widgets.tabview import tabview_spec | ||||
| from .widgets.textarea import textarea_spec | ||||
| from .widgets.tileview import tileview_spec | ||||
|  | ||||
| DOMAIN = "lvgl" | ||||
| DEPENDENCIES = ("display",) | ||||
| AUTO_LOAD = ("key_provider",) | ||||
| CODEOWNERS = ("@clydebarrow",) | ||||
| DEPENDENCIES = ["display"] | ||||
| AUTO_LOAD = ["key_provider"] | ||||
| CODEOWNERS = ["@clydebarrow"] | ||||
| LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| for w_type in (label_spec, obj_spec, btn_spec): | ||||
| for w_type in ( | ||||
|     label_spec, | ||||
|     obj_spec, | ||||
|     button_spec, | ||||
|     bar_spec, | ||||
|     slider_spec, | ||||
|     arc_spec, | ||||
|     line_spec, | ||||
|     spinner_spec, | ||||
|     led_spec, | ||||
|     animimg_spec, | ||||
|     checkbox_spec, | ||||
|     img_spec, | ||||
|     switch_spec, | ||||
|     tabview_spec, | ||||
|     buttonmatrix_spec, | ||||
|     meter_spec, | ||||
|     dropdown_spec, | ||||
|     roller_spec, | ||||
|     textarea_spec, | ||||
|     spinbox_spec, | ||||
|     keyboard_spec, | ||||
|     tileview_spec, | ||||
| ): | ||||
|     WIDGET_TYPES[w_type.name] = w_type | ||||
|  | ||||
| WIDGET_SCHEMA = any_widget_schema() | ||||
|  | ||||
| LAYOUT_SCHEMAS[df.TYPE_GRID] = { | ||||
|     cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA)) | ||||
| } | ||||
| LAYOUT_SCHEMAS[df.TYPE_FLEX] = { | ||||
|     cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA)) | ||||
| } | ||||
| LAYOUT_SCHEMAS[df.TYPE_NONE] = { | ||||
|     cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema()) | ||||
| } | ||||
| for w_type in WIDGET_TYPES.values(): | ||||
|     register_action( | ||||
|         f"lvgl.{w_type.name}.update", | ||||
| @@ -61,14 +127,6 @@ for w_type in WIDGET_TYPES.values(): | ||||
|     )(update_to_code) | ||||
|  | ||||
|  | ||||
| async def add_init_lambda(lv_component, init): | ||||
|     if init: | ||||
|         lamb = await cg.process_lambda( | ||||
|             Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")] | ||||
|         ) | ||||
|         cg.add(lv_component.add_init_lambda(lamb)) | ||||
|  | ||||
|  | ||||
| lv_defines = {}  # Dict of #defines to provide as build flags | ||||
|  | ||||
|  | ||||
| @@ -100,6 +158,9 @@ def generate_lv_conf_h(): | ||||
|  | ||||
|  | ||||
| def final_validation(config): | ||||
|     if pages := config.get(CONF_PAGES): | ||||
|         if all(p[CONF_SKIP] for p in pages): | ||||
|             raise cv.Invalid("At least one page must not be skipped") | ||||
|     global_config = full_config.get() | ||||
|     for display_id in config[df.CONF_DISPLAYS]: | ||||
|         path = global_config.get_path_for_id(display_id)[:-1] | ||||
| @@ -193,18 +254,24 @@ async def to_code(config): | ||||
|     else: | ||||
|         add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) | ||||
|  | ||||
|     with LvContext(): | ||||
|     async with LvContext(lv_component): | ||||
|         await touchscreens_to_code(lv_component, config) | ||||
|         await rotary_encoders_to_code(lv_component, config) | ||||
|         await encoders_to_code(lv_component, config) | ||||
|         await theme_to_code(config) | ||||
|         await styles_to_code(config) | ||||
|         await set_obj_properties(lv_scr_act, config) | ||||
|         await add_widgets(lv_scr_act, config) | ||||
|         await add_pages(lv_component, config) | ||||
|         await add_top_layer(config) | ||||
|         await msgboxes_to_code(config) | ||||
|         await disp_update(f"{lv_component}->get_disp()", config) | ||||
|         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()}") | ||||
|     for use in helpers.lv_uses: | ||||
| @@ -239,6 +306,16 @@ CONFIG_SCHEMA = ( | ||||
|             cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( | ||||
|                 "big_endian", "little_endian" | ||||
|             ), | ||||
|             cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( | ||||
|                 cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}) | ||||
|                 .extend(STYLE_SCHEMA) | ||||
|                 .extend( | ||||
|                     { | ||||
|                         cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, | ||||
|                         cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, | ||||
|                     } | ||||
|                 ) | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_IDLE): validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), | ||||
| @@ -247,10 +324,20 @@ CONFIG_SCHEMA = ( | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), | ||||
|             cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA), | ||||
|             cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( | ||||
|                 container_schema(page_spec) | ||||
|             ), | ||||
|             cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), | ||||
|             cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, | ||||
|             cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), | ||||
|             cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, | ||||
|             cv.Optional(df.CONF_THEME): cv.Schema( | ||||
|                 {cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()} | ||||
|             ), | ||||
|             cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, | ||||
|             cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG, | ||||
|             cv.GenerateID(df.CONF_ENCODERS): ENCODERS_CONFIG, | ||||
|         } | ||||
|     ) | ||||
|     .extend(DISP_BG_SCHEMA) | ||||
| ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) | ||||
|   | ||||
| @@ -1,15 +1,26 @@ | ||||
| from collections.abc import Awaitable | ||||
| from typing import Callable | ||||
|  | ||||
| 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 .defines import ( | ||||
|     CONF_DISP_BG_COLOR, | ||||
|     CONF_DISP_BG_IMAGE, | ||||
|     CONF_LVGL_ID, | ||||
|     CONF_SHOW_SNOW, | ||||
|     literal, | ||||
| ) | ||||
| from .lv_validation import lv_bool, lv_color, lv_image | ||||
| from .lvcode import ( | ||||
|     LVGL_COMP_ARG, | ||||
|     LambdaContext, | ||||
|     LocalVariable, | ||||
|     LvConditional, | ||||
|     LvglComponent, | ||||
|     ReturnStatement, | ||||
|     add_line_marks, | ||||
|     lv, | ||||
| @@ -17,46 +28,46 @@ from .lvcode import ( | ||||
|     lv_obj, | ||||
|     lvgl_comp, | ||||
| ) | ||||
| from .schemas import ACTION_SCHEMA, LVGL_SCHEMA | ||||
| from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA | ||||
| from .types import ( | ||||
|     LV_EVENT, | ||||
|     LV_STATE, | ||||
|     LvglAction, | ||||
|     LvglComponent, | ||||
|     LvglComponentPtr, | ||||
|     LvglCondition, | ||||
|     ObjUpdateAction, | ||||
|     lv_disp_t, | ||||
|     lv_obj_t, | ||||
| ) | ||||
| from .widget import Widget, get_widget, lv_scr_act, set_obj_properties | ||||
| from .widgets import Widget, get_widgets, 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) | ||||
| async def action_to_code( | ||||
|     widgets: list[Widget], | ||||
|     action: Callable[[Widget], Awaitable[None]], | ||||
|     action_id, | ||||
|     template_arg, | ||||
|     args, | ||||
| ): | ||||
|     async with LambdaContext(parameters=args, where=action_id) as context: | ||||
|         for widget in widgets: | ||||
|             with LvConditional(widget.obj != nullptr): | ||||
|                 await action(widget) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) | ||||
|     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 | ||||
|         ) | ||||
|     async def do_update(widget: Widget): | ||||
|         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, LV_EVENT.VALUE_CHANGED, nullptr) | ||||
|  | ||||
|     widgets = await get_widgets(config[CONF_ID]) | ||||
|     return await action_to_code(widgets, do_update, action_id, template_arg, args) | ||||
|  | ||||
|  | ||||
| @automation.register_condition( | ||||
| @@ -66,9 +77,7 @@ async def update_to_code(config, action_id, template_arg, args): | ||||
| ) | ||||
| 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: | ||||
|     async with LambdaContext(LVGL_COMP_ARG, 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) | ||||
| @@ -89,15 +98,23 @@ async def lvgl_is_paused(config, condition_id, template_arg, args): | ||||
| 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: | ||||
|     async with LambdaContext(LVGL_COMP_ARG, 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 | ||||
|  | ||||
|  | ||||
| async def disp_update(disp, config: dict): | ||||
|     if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: | ||||
|         return | ||||
|     with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp: | ||||
|         if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: | ||||
|             lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) | ||||
|         if bg_image := config.get(CONF_DISP_BG_IMAGE): | ||||
|             lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.widget.redraw", | ||||
|     ObjUpdateAction, | ||||
| @@ -109,14 +126,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): | ||||
|     ), | ||||
| ) | ||||
| 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) | ||||
|     widgets = await get_widgets(config) or [lv_scr_act] | ||||
|  | ||||
|     async def do_invalidate(widget: Widget): | ||||
|         lv_obj.invalidate(widget.obj) | ||||
|  | ||||
|     return await action_to_code(widgets, do_invalidate, action_id, template_arg, args) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.update", | ||||
|     LvglAction, | ||||
|     DISP_BG_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.use_id(LvglComponent), | ||||
|         } | ||||
|     ).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)), | ||||
| ) | ||||
| async def lvgl_update_to_code(config, action_id, template_arg, args): | ||||
|     widgets = await get_widgets(config) | ||||
|     w = widgets[0] | ||||
|     disp = f"{w.obj}->get_disp()" | ||||
|     async with LambdaContext(parameters=args, where=action_id) as context: | ||||
|         await disp_update(disp, config) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) | ||||
|     await cg.register_parented(var, w.var) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
| @@ -128,8 +163,8 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): | ||||
|     }, | ||||
| ) | ||||
| async def pause_action_to_code(config, action_id, template_arg, args): | ||||
|     with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: | ||||
|         add_line_marks(action_id) | ||||
|     async with LambdaContext(LVGL_COMP_ARG) as context: | ||||
|         add_line_marks(where=action_id) | ||||
|         lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) | ||||
|     await cg.register_parented(var, config[CONF_ID]) | ||||
| @@ -144,45 +179,48 @@ async def pause_action_to_code(config, action_id, template_arg, args): | ||||
|     }, | ||||
| ) | ||||
| async def resume_action_to_code(config, action_id, template_arg, args): | ||||
|     with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: | ||||
|         add_line_marks(action_id) | ||||
|     async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: | ||||
|         lv_add(lvgl_comp.set_paused(False, False)) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) | ||||
|     await cg.register_parented(var, config[CONF_ID]) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action("lvgl.widget.disable", ObjUpdateAction, ACTION_SCHEMA) | ||||
| @automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_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) | ||||
|     async def do_disable(widget: Widget): | ||||
|         widget.add_state(LV_STATE.DISABLED) | ||||
|  | ||||
|     return await action_to_code( | ||||
|         await get_widgets(config), do_disable, action_id, template_arg, args | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA) | ||||
| @automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_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) | ||||
|     async def do_enable(widget: Widget): | ||||
|         widget.clear_state(LV_STATE.DISABLED) | ||||
|  | ||||
|     return await action_to_code( | ||||
|         await get_widgets(config), do_enable, action_id, template_arg, args | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA) | ||||
| @automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_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) | ||||
|     async def do_hide(widget: Widget): | ||||
|         widget.add_flag("LV_OBJ_FLAG_HIDDEN") | ||||
|  | ||||
|     return await action_to_code( | ||||
|         await get_widgets(config), do_hide, action_id, template_arg, args | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA) | ||||
| @automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_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) | ||||
|     async def do_show(widget: Widget): | ||||
|         widget.clear_flag("LV_OBJ_FLAG_HIDDEN") | ||||
|  | ||||
|     return await action_to_code( | ||||
|         await get_widgets(config), do_show, action_id, template_arg, args | ||||
|     ) | ||||
|   | ||||
							
								
								
									
										43
									
								
								esphome/components/lvgl/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/lvgl/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.binary_sensor import ( | ||||
|     BinarySensor, | ||||
|     binary_sensor_schema, | ||||
|     new_binary_sensor, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from ..defines import CONF_LVGL_ID, CONF_WIDGET | ||||
| from ..lvcode import EVENT_ARG, LambdaContext, LvContext | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LV_EVENT, lv_pseudo_button_t | ||||
| from ..widgets import Widget, get_widgets | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     binary_sensor_schema(BinarySensor) | ||||
|     .extend(LVGL_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     sensor = await new_binary_sensor(config) | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     widget = await get_widgets(config, CONF_WIDGET) | ||||
|     widget = widget[0] | ||||
|     assert isinstance(widget, Widget) | ||||
|     async with LambdaContext(EVENT_ARG) as pressed_ctx: | ||||
|         pressed_ctx.add(sensor.publish_state(widget.is_pressed())) | ||||
|     async with LvContext(paren) as ctx: | ||||
|         ctx.add(sensor.publish_initial_state(widget.is_pressed())) | ||||
|         ctx.add( | ||||
|             paren.add_event_cb( | ||||
|                 widget.obj, | ||||
|                 await pressed_ctx.get_lambda(), | ||||
|                 LV_EVENT.PRESSING, | ||||
|                 LV_EVENT.RELEASED, | ||||
|             ) | ||||
|         ) | ||||
| @@ -1,25 +0,0 @@ | ||||
| from esphome.const import CONF_BUTTON | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| from .defines import CONF_MAIN | ||||
| from .types import LvBoolean, WidgetType | ||||
|  | ||||
|  | ||||
| class BtnType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) | ||||
|  | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         """ | ||||
|         LVGL 8 calls buttons `btn` | ||||
|         """ | ||||
|         return f"lv_btn_create({parent})" | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return ("btn",) | ||||
|  | ||||
|     async def to_code(self, w, config): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| btn_spec = BtnType() | ||||
| @@ -4,31 +4,21 @@ 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.const import CONF_ITEMS | ||||
| from esphome.core import ID, Lambda | ||||
| from esphome.cpp_generator import Literal | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.cpp_types import uint32 | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
|  | ||||
| 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 | ||||
| lvgl_ns = cg.esphome_ns.namespace("lvgl") | ||||
|  | ||||
|  | ||||
| def literal(arg: Union[str, ConstantLiteral]): | ||||
| def literal(arg): | ||||
|     if isinstance(arg, str): | ||||
|         return ConstantLiteral(arg) | ||||
|         return MockObj(arg) | ||||
|     return arg | ||||
|  | ||||
|  | ||||
| @@ -93,15 +83,23 @@ class LvConstant(LValidator): | ||||
|             return self.prefix + cv.one_of(*choices, upper=True)(value) | ||||
|  | ||||
|         super().__init__(validator, rtype=uint32) | ||||
|         self.retmapper = self.mapper | ||||
|         self.one_of = LValidator(validator, uint32, retmapper=self.mapper) | ||||
|         self.several_of = LValidator( | ||||
|             cv.ensure_list(self.one_of), uint32, retmapper=self.mapper | ||||
|         ) | ||||
|  | ||||
|     def mapper(self, value, args=()): | ||||
|         if isinstance(value, list): | ||||
|             value = "|".join(value) | ||||
|         return ConstantLiteral(value) | ||||
|         if not isinstance(value, list): | ||||
|             value = [value] | ||||
|         return literal( | ||||
|             "|".join( | ||||
|                 [ | ||||
|                     str(v) if str(v).startswith(self.prefix) else self.prefix + str(v) | ||||
|                     for v in value | ||||
|                 ] | ||||
|             ).upper() | ||||
|         ) | ||||
|  | ||||
|     def extend(self, *choices): | ||||
|         """ | ||||
| @@ -112,21 +110,22 @@ class LvConstant(LValidator): | ||||
|         return LvConstant(self.prefix, *(self.choices + choices)) | ||||
|  | ||||
|  | ||||
| # Widgets | ||||
| CONF_LABEL = "label" | ||||
|  | ||||
| # Parts | ||||
| CONF_MAIN = "main" | ||||
| CONF_SCROLLBAR = "scrollbar" | ||||
| CONF_INDICATOR = "indicator" | ||||
| CONF_KNOB = "knob" | ||||
| CONF_SELECTED = "selected" | ||||
| CONF_ITEMS = "items" | ||||
| CONF_TICKS = "ticks" | ||||
| CONF_TICK_STYLE = "tick_style" | ||||
| CONF_CURSOR = "cursor" | ||||
| CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" | ||||
|  | ||||
| # Layout types | ||||
|  | ||||
| TYPE_FLEX = "flex" | ||||
| TYPE_GRID = "grid" | ||||
| TYPE_NONE = "none" | ||||
|  | ||||
| LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ | ||||
|     "dejavu_16_persian_hebrew", | ||||
|     "simsun_16_cjk", | ||||
| @@ -134,7 +133,7 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ | ||||
|     "unscii_16", | ||||
| ] | ||||
|  | ||||
| LV_EVENT = { | ||||
| LV_EVENT_MAP = { | ||||
|     "PRESS": "PRESSED", | ||||
|     "SHORT_CLICK": "SHORT_CLICKED", | ||||
|     "LONG_PRESS": "LONG_PRESSED", | ||||
| @@ -150,7 +149,7 @@ LV_EVENT = { | ||||
|     "CANCEL": "CANCEL", | ||||
| } | ||||
|  | ||||
| LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT) | ||||
| LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) | ||||
|  | ||||
|  | ||||
| LV_ANIM = LvConstant( | ||||
| @@ -305,7 +304,8 @@ OBJ_FLAGS = ( | ||||
| ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") | ||||
| BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") | ||||
|  | ||||
| BTNMATRIX_CTRLS = ( | ||||
| BUTTONMATRIX_CTRLS = LvConstant( | ||||
|     "LV_BTNMATRIX_CTRL_", | ||||
|     "HIDDEN", | ||||
|     "NO_REPEAT", | ||||
|     "DISABLED", | ||||
| @@ -366,7 +366,6 @@ CONF_ACCEPTED_CHARS = "accepted_chars" | ||||
| CONF_ADJUSTABLE = "adjustable" | ||||
| CONF_ALIGN = "align" | ||||
| CONF_ALIGN_TO = "align_to" | ||||
| CONF_ANGLE_RANGE = "angle_range" | ||||
| CONF_ANIMATED = "animated" | ||||
| CONF_ANIMATION = "animation" | ||||
| CONF_ANTIALIAS = "antialias" | ||||
| @@ -384,13 +383,12 @@ CONF_BYTE_ORDER = "byte_order" | ||||
| CONF_CHANGE_RATE = "change_rate" | ||||
| CONF_CLOSE_BUTTON = "close_button" | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_COLOR_END = "color_end" | ||||
| CONF_COLOR_START = "color_start" | ||||
| CONF_CONTROL = "control" | ||||
| CONF_DEFAULT = "default" | ||||
| CONF_DEFAULT_FONT = "default_font" | ||||
| CONF_DIR = "dir" | ||||
| CONF_DISPLAYS = "displays" | ||||
| CONF_ENCODERS = "encoders" | ||||
| CONF_END_ANGLE = "end_angle" | ||||
| CONF_END_VALUE = "end_value" | ||||
| CONF_ENTER_BUTTON = "enter_button" | ||||
| @@ -414,9 +412,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align" | ||||
| CONF_GRID_ROWS = "grid_rows" | ||||
| CONF_HEADER_MODE = "header_mode" | ||||
| CONF_HOME = "home" | ||||
| CONF_INDICATORS = "indicators" | ||||
| CONF_KEY_CODE = "key_code" | ||||
| CONF_LABEL_GAP = "label_gap" | ||||
| CONF_LAYOUT = "layout" | ||||
| CONF_LEFT_BUTTON = "left_button" | ||||
| CONF_LINE_WIDTH = "line_width" | ||||
| @@ -425,7 +421,6 @@ CONF_LONG_PRESS_TIME = "long_press_time" | ||||
| CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" | ||||
| CONF_LVGL_ID = "lvgl_id" | ||||
| CONF_LONG_MODE = "long_mode" | ||||
| CONF_MAJOR = "major" | ||||
| CONF_MSGBOXES = "msgboxes" | ||||
| CONF_OBJ = "obj" | ||||
| CONF_OFFSET_X = "offset_x" | ||||
| @@ -434,6 +429,7 @@ CONF_ONE_LINE = "one_line" | ||||
| CONF_ON_SELECT = "on_select" | ||||
| CONF_ONE_CHECKED = "one_checked" | ||||
| CONF_NEXT = "next" | ||||
| CONF_PAGE = "page" | ||||
| CONF_PAGE_WRAP = "page_wrap" | ||||
| CONF_PASSWORD_MODE = "password_mode" | ||||
| CONF_PIVOT_X = "pivot_x" | ||||
| @@ -442,14 +438,11 @@ CONF_PLACEHOLDER_TEXT = "placeholder_text" | ||||
| CONF_POINTS = "points" | ||||
| CONF_PREVIOUS = "previous" | ||||
| CONF_REPEAT_COUNT = "repeat_count" | ||||
| CONF_R_MOD = "r_mod" | ||||
| 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" | ||||
| CONF_SCROLLBAR_MODE = "scrollbar_mode" | ||||
| CONF_SELECTED_INDEX = "selected_index" | ||||
| @@ -459,14 +452,14 @@ CONF_SRC = "src" | ||||
| CONF_START_ANGLE = "start_angle" | ||||
| CONF_START_VALUE = "start_value" | ||||
| CONF_STATES = "states" | ||||
| CONF_STRIDE = "stride" | ||||
| CONF_STYLE = "style" | ||||
| CONF_STYLES = "styles" | ||||
| CONF_STYLE_DEFINITIONS = "style_definitions" | ||||
| CONF_STYLE_ID = "style_id" | ||||
| CONF_SKIP = "skip" | ||||
| CONF_SYMBOL = "symbol" | ||||
| CONF_TAB_ID = "tab_id" | ||||
| CONF_TABS = "tabs" | ||||
| CONF_TEXT = "text" | ||||
| CONF_TILE = "tile" | ||||
| CONF_TILE_ID = "tile_id" | ||||
| CONF_TILES = "tiles" | ||||
| @@ -505,4 +498,4 @@ DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" | ||||
|  | ||||
|  | ||||
| def join_enums(enums, prefix=""): | ||||
|     return ConstantLiteral("|".join(f"(int){prefix}{e.upper()}" for e in enums)) | ||||
|     return literal("|".join(f"(int){prefix}{e.upper()}" for e in enums)) | ||||
|   | ||||
| @@ -5,24 +5,26 @@ import esphome.config_validation as cv | ||||
| from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR | ||||
| 
 | ||||
| from .defines import ( | ||||
|     CONF_ENCODERS, | ||||
|     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 .helpers import lvgl_components_required, requires_component | ||||
| from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable | ||||
| from .schemas import ENCODER_SCHEMA | ||||
| from .types import lv_indev_type_t | ||||
| from .types import lv_group_t, lv_indev_type_t | ||||
| 
 | ||||
| ROTARY_ENCODER_CONFIG = cv.ensure_list( | ||||
| ENCODERS_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.All( | ||||
|                     cv.use_id(RotaryEncoderSensor), requires_component("rotary_encoder") | ||||
|                 ), | ||||
|                 cv.Schema( | ||||
|                     { | ||||
|                         cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor), | ||||
| @@ -35,10 +37,9 @@ ROTARY_ENCODER_CONFIG = cv.ensure_list( | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| async def rotary_encoders_to_code(var, config): | ||||
|     for enc_conf in config.get(CONF_ROTARY_ENCODERS, ()): | ||||
| async def encoders_to_code(var, config): | ||||
|     for enc_conf in config.get(CONF_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( | ||||
| @@ -56,7 +57,9 @@ async def rotary_encoders_to_code(var, 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)): | ||||
|         if group := enc_conf.get(CONF_GROUP): | ||||
|             group = lv_Pvariable(lv_group_t, group) | ||||
|             lv_assign(group, lv_expr.group_create()) | ||||
|             lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) | ||||
|         else: | ||||
|             lv.indev_drv_register(listener.get_drv()) | ||||
| @@ -1,10 +1,7 @@ | ||||
| import re | ||||
|  | ||||
| from esphome import config_validation as cv | ||||
| from esphome.config import Config | ||||
| from esphome.const import CONF_ARGS, CONF_FORMAT | ||||
| from esphome.core import CORE, ID | ||||
| from esphome.yaml_util import ESPHomeDataBase | ||||
|  | ||||
| lv_uses = { | ||||
|     "USER_DATA", | ||||
| @@ -44,23 +41,6 @@ def validate_printf(value): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def get_line_marks(value) -> list: | ||||
|     """ | ||||
|     If possible, return a preprocessor directive to identify the line number where the given id was defined. | ||||
|     :param id: The id in question | ||||
|     :return: A list containing zero or more line directives | ||||
|     """ | ||||
|     path = None | ||||
|     if isinstance(value, ESPHomeDataBase): | ||||
|         path = value.esp_range | ||||
|     elif isinstance(value, ID) and isinstance(CORE.config, Config): | ||||
|         path = CORE.config.get_path_for_id(value)[:-1] | ||||
|         path = CORE.config.get_deepest_document_range_for_path(path) | ||||
|     if path is None: | ||||
|         return [] | ||||
|     return [path.start_mark.as_line_directive] | ||||
|  | ||||
|  | ||||
| def requires_component(comp): | ||||
|     def validator(value): | ||||
|         lvgl_components_required.add(comp) | ||||
|   | ||||
							
								
								
									
										32
									
								
								esphome/components/lvgl/light/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								esphome/components/lvgl/light/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import light | ||||
| from esphome.components.light import LightOutput | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_GAMMA_CORRECT, CONF_LED, CONF_OUTPUT_ID | ||||
|  | ||||
| from ..defines import CONF_LVGL_ID | ||||
| from ..lvcode import LvContext | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LvType, lvgl_ns | ||||
| from ..widgets import get_widgets | ||||
|  | ||||
| lv_led_t = LvType("lv_led_t") | ||||
| LVLight = lvgl_ns.class_("LVLight", LightOutput) | ||||
| CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float, | ||||
|         cv.Required(CONF_LED): cv.use_id(lv_led_t), | ||||
|         cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight), | ||||
|     } | ||||
| ).extend(LVGL_SCHEMA) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) | ||||
|     await light.register_light(var, config) | ||||
|  | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     widget = await get_widgets(config, CONF_LED) | ||||
|     widget = widget[0] | ||||
|     async with LvContext(paren) as ctx: | ||||
|         ctx.add(var.set_obj(widget.obj)) | ||||
							
								
								
									
										48
									
								
								esphome/components/lvgl/light/lvgl_light.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								esphome/components/lvgl/light/lvgl_light.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/light/light_output.h" | ||||
| #include "../lvgl_esphome.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| class LVLight : public light::LightOutput { | ||||
|  public: | ||||
|   light::LightTraits get_traits() override { | ||||
|     auto traits = light::LightTraits(); | ||||
|     traits.set_supported_color_modes({light::ColorMode::RGB}); | ||||
|     return traits; | ||||
|   } | ||||
|   void write_state(light::LightState *state) override { | ||||
|     float red, green, blue; | ||||
|     state->current_values_as_rgb(&red, &green, &blue, false); | ||||
|     auto color = lv_color_make(red * 255, green * 255, blue * 255); | ||||
|     if (this->obj_ != nullptr) { | ||||
|       this->set_value_(color); | ||||
|     } else { | ||||
|       this->initial_value_ = color; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_obj(lv_obj_t *obj) { | ||||
|     this->obj_ = obj; | ||||
|     if (this->initial_value_) { | ||||
|       lv_led_set_color(obj, this->initial_value_.value()); | ||||
|       lv_led_on(obj); | ||||
|       this->initial_value_.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void set_value_(lv_color_t value) { | ||||
|     lv_led_set_color(this->obj_, value); | ||||
|     lv_led_on(this->obj_); | ||||
|     lv_event_send(this->obj_, lv_custom_event, nullptr); | ||||
|   } | ||||
|   lv_obj_t *obj_{}; | ||||
|   optional<lv_color_t> initial_value_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
| @@ -1,3 +1,5 @@ | ||||
| from typing import Union | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.binary_sensor import BinarySensor | ||||
| from esphome.components.color import ColorStruct | ||||
| @@ -6,7 +8,7 @@ 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 | ||||
| from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT | ||||
| from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE | ||||
| from esphome.core import HexInt | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.cpp_types import uint32 | ||||
| @@ -14,7 +16,14 @@ 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, ConstantLiteral, LValidator, LvConstant, literal | ||||
| from .defines import ( | ||||
|     CONF_END_VALUE, | ||||
|     CONF_START_VALUE, | ||||
|     LV_FONTS, | ||||
|     LValidator, | ||||
|     LvConstant, | ||||
|     literal, | ||||
| ) | ||||
| from .helpers import ( | ||||
|     esphome_fonts_used, | ||||
|     lv_fonts_used, | ||||
| @@ -60,6 +69,13 @@ def color_retmapper(value): | ||||
|     return lv_expr.color_from(MockObj(value)) | ||||
|  | ||||
|  | ||||
| def option_string(value): | ||||
|     value = cv.string(value).strip() | ||||
|     if value.find("\n") != -1: | ||||
|         raise cv.Invalid("Options strings must not contain newlines") | ||||
|     return value | ||||
|  | ||||
|  | ||||
| lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) | ||||
|  | ||||
|  | ||||
| @@ -156,6 +172,12 @@ lv_bool = LValidator( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def lv_pct(value: Union[int, float]): | ||||
|     if isinstance(value, float): | ||||
|         value = int(value * 100) | ||||
|     return literal(f"lv_pct({value})") | ||||
|  | ||||
|  | ||||
| def lvms_validator_(value): | ||||
|     if value == "never": | ||||
|         value = "2147483647ms" | ||||
| @@ -189,13 +211,16 @@ class TextValidator(LValidator): | ||||
|             args = [str(x) for x in value[CONF_ARGS]] | ||||
|             arg_expr = cg.RawExpression(",".join(args)) | ||||
|             format_str = cpp_string_escape(value[CONF_FORMAT]) | ||||
|             return f"str_sprintf({format_str}, {arg_expr}).c_str()" | ||||
|             return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") | ||||
|         return await super().process(value, args) | ||||
|  | ||||
|  | ||||
| lv_text = TextValidator() | ||||
| lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") | ||||
| lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") | ||||
| lv_brightness = LValidator( | ||||
|     cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255) | ||||
| ) | ||||
|  | ||||
|  | ||||
| def is_lv_font(font): | ||||
| @@ -222,8 +247,33 @@ class LvFont(LValidator): | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|         if is_lv_font(value): | ||||
|             return ConstantLiteral(f"&lv_font_{value}") | ||||
|         return ConstantLiteral(f"{value}_engine->get_lv_font()") | ||||
|             return literal(f"&lv_font_{value}") | ||||
|         return literal(f"{value}_engine->get_lv_font()") | ||||
|  | ||||
|  | ||||
| lv_font = LvFont() | ||||
|  | ||||
|  | ||||
| def animated(value): | ||||
|     if isinstance(value, bool): | ||||
|         value = "ON" if value else "OFF" | ||||
|     return LvConstant("LV_ANIM_", "OFF", "ON").one_of(value) | ||||
|  | ||||
|  | ||||
| def key_code(value): | ||||
|     value = cv.Any(cv.All(cv.string_strict, cv.Length(min=1, max=1)), cv.uint8_t)(value) | ||||
|     if isinstance(value, str): | ||||
|         return ord(value[0]) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| async def get_end_value(config): | ||||
|     return await lv_int.process(config.get(CONF_END_VALUE)) | ||||
|  | ||||
|  | ||||
| async def get_start_value(config): | ||||
|     if CONF_START_VALUE in config: | ||||
|         value = config[CONF_START_VALUE] | ||||
|     else: | ||||
|         value = config.get(CONF_VALUE) | ||||
|     return await lv_int.process(value) | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import abc | ||||
| import logging | ||||
| from typing import Union | ||||
|  | ||||
| from esphome import codegen as cg | ||||
| from esphome.core import ID, Lambda | ||||
| from esphome.config import Config | ||||
| from esphome.core import CORE, ID, Lambda | ||||
| from esphome.cpp_generator import ( | ||||
|     AssignmentExpression, | ||||
|     CallExpression, | ||||
| @@ -18,12 +18,47 @@ from esphome.cpp_generator import ( | ||||
|     VariableDeclarationExpression, | ||||
|     statement, | ||||
| ) | ||||
| from esphome.yaml_util import ESPHomeDataBase | ||||
|  | ||||
| from .defines import ConstantLiteral | ||||
| from .helpers import get_line_marks | ||||
| from .types import lv_group_t | ||||
| from .defines import literal, lvgl_ns | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| LVGL_COMP = "lv_component"  # used as a lambda argument in lvgl_comp() | ||||
|  | ||||
| # Argument tuple for use in lambdas | ||||
| LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) | ||||
| LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)] | ||||
| lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") | ||||
| EVENT_ARG = [(lv_event_t_ptr, "ev")] | ||||
| CUSTOM_EVENT = literal("lvgl::lv_custom_event") | ||||
|  | ||||
|  | ||||
| def get_line_marks(value) -> list: | ||||
|     """ | ||||
|     If possible, return a preprocessor directive to identify the line number where the given id was defined. | ||||
|     :param value: The id or other token to get the line number for | ||||
|     :return: A list containing zero or more line directives | ||||
|     """ | ||||
|     path = None | ||||
|     if isinstance(value, ESPHomeDataBase): | ||||
|         path = value.esp_range | ||||
|     elif isinstance(value, ID) and isinstance(CORE.config, Config): | ||||
|         path = CORE.config.get_path_for_id(value)[:-1] | ||||
|         path = CORE.config.get_deepest_document_range_for_path(path) | ||||
|     if path is None: | ||||
|         return [] | ||||
|     return [path.start_mark.as_line_directive] | ||||
|  | ||||
|  | ||||
| class IndentedStatement(Statement): | ||||
|     def __init__(self, stmt: Statement, indent: int): | ||||
|         self.statement = stmt | ||||
|         self.indent = indent | ||||
|  | ||||
|     def __str__(self): | ||||
|         result = " " * self.indent * 4 + str(self.statement).strip() | ||||
|         if not isinstance(self.statement, RawStatement): | ||||
|             result += ";" | ||||
|         return result | ||||
|  | ||||
|  | ||||
| class CodeContext(abc.ABC): | ||||
| @@ -39,6 +74,16 @@ class CodeContext(abc.ABC): | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         pass | ||||
|  | ||||
|     @staticmethod | ||||
|     def start_block(): | ||||
|         CodeContext.append(RawStatement("{")) | ||||
|         CodeContext.code_context.indent() | ||||
|  | ||||
|     @staticmethod | ||||
|     def end_block(): | ||||
|         CodeContext.code_context.detent() | ||||
|         CodeContext.append(RawStatement("}")) | ||||
|  | ||||
|     @staticmethod | ||||
|     def append(expression: Union[Expression, Statement]): | ||||
|         if CodeContext.code_context is not None: | ||||
| @@ -47,14 +92,25 @@ class CodeContext(abc.ABC): | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.previous: Union[CodeContext | None] = None | ||||
|         self.indent_level = 0 | ||||
|  | ||||
|     def __enter__(self): | ||||
|     async def __aenter__(self): | ||||
|         self.previous = CodeContext.code_context | ||||
|         CodeContext.code_context = self | ||||
|         return self | ||||
|  | ||||
|     def __exit__(self, *args): | ||||
|     async def __aexit__(self, *args): | ||||
|         CodeContext.code_context = self.previous | ||||
|  | ||||
|     def indent(self): | ||||
|         self.indent_level += 1 | ||||
|  | ||||
|     def detent(self): | ||||
|         self.indent_level -= 1 | ||||
|  | ||||
|     def indented_statement(self, stmt): | ||||
|         return IndentedStatement(stmt, self.indent_level) | ||||
|  | ||||
|  | ||||
| class MainContext(CodeContext): | ||||
|     """ | ||||
| @@ -62,42 +118,7 @@ class MainContext(CodeContext): | ||||
|     """ | ||||
|  | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         return cg.add(expression) | ||||
|  | ||||
|  | ||||
| class LvContext(CodeContext): | ||||
|     """ | ||||
|     Code generation into the LVGL initialisation code (called in `setup()`) | ||||
|     """ | ||||
|  | ||||
|     lv_init_code: list["Statement"] = [] | ||||
|  | ||||
|     @staticmethod | ||||
|     def lv_add(expression: Union[Expression, Statement]): | ||||
|         if isinstance(expression, Expression): | ||||
|             expression = statement(expression) | ||||
|         if not isinstance(expression, Statement): | ||||
|             raise ValueError( | ||||
|                 f"Add '{expression}' must be expression or statement, not {type(expression)}" | ||||
|             ) | ||||
|         LvContext.lv_init_code.append(expression) | ||||
|         _LOGGER.debug("LV Adding: %s", expression) | ||||
|         return expression | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_code(): | ||||
|         code = [] | ||||
|         for exp in LvContext.lv_init_code: | ||||
|             text = str(statement(exp)) | ||||
|             text = text.rstrip() | ||||
|             code.append(text) | ||||
|         return "\n".join(code) + "\n\n" | ||||
|  | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         return LvContext.lv_add(expression) | ||||
|  | ||||
|     def set_style(self, prop): | ||||
|         return MockObj("lv_set_style_{prop}", "") | ||||
|         return cg.add(self.indented_statement(expression)) | ||||
|  | ||||
|  | ||||
| class LambdaContext(CodeContext): | ||||
| @@ -110,21 +131,23 @@ class LambdaContext(CodeContext): | ||||
|         parameters: list[tuple[SafeExpType, str]] = None, | ||||
|         return_type: SafeExpType = cg.void, | ||||
|         capture: str = "", | ||||
|         where=None, | ||||
|     ): | ||||
|         super().__init__() | ||||
|         self.code_list: list[Statement] = [] | ||||
|         self.parameters = parameters | ||||
|         self.parameters = parameters or [] | ||||
|         self.return_type = return_type | ||||
|         self.capture = capture | ||||
|         self.where = where | ||||
|  | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         self.code_list.append(expression) | ||||
|         self.code_list.append(self.indented_statement(expression)) | ||||
|         return expression | ||||
|  | ||||
|     async def get_lambda(self) -> LambdaExpression: | ||||
|         code_text = self.get_code() | ||||
|         return await cg.process_lambda( | ||||
|             Lambda("\n".join(code_text) + "\n\n"), | ||||
|             Lambda("\n".join(code_text) + "\n"), | ||||
|             self.parameters, | ||||
|             capture=self.capture, | ||||
|             return_type=self.return_type, | ||||
| @@ -138,33 +161,59 @@ class LambdaContext(CodeContext): | ||||
|             code_text.append(text) | ||||
|         return code_text | ||||
|  | ||||
|     def __enter__(self): | ||||
|         super().__enter__() | ||||
|     async def __aenter__(self): | ||||
|         await super().__aenter__() | ||||
|         add_line_marks(self.where) | ||||
|         return self | ||||
|  | ||||
|  | ||||
| class LvContext(LambdaContext): | ||||
|     """ | ||||
|     Code generation into the LVGL initialisation code (called in `setup()`) | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, lv_component, args=None): | ||||
|         self.args = args or LVGL_COMP_ARG | ||||
|         super().__init__(parameters=self.args) | ||||
|         self.lv_component = lv_component | ||||
|  | ||||
|     async def add_init_lambda(self): | ||||
|         cg.add(self.lv_component.add_init_lambda(await self.get_lambda())) | ||||
|  | ||||
|     async def __aexit__(self, exc_type, exc_val, exc_tb): | ||||
|         await super().__aexit__(exc_type, exc_val, exc_tb) | ||||
|         await self.add_init_lambda() | ||||
|  | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         self.code_list.append(self.indented_statement(expression)) | ||||
|         return expression | ||||
|  | ||||
|     def __call__(self, *args): | ||||
|         return self.add(*args) | ||||
|  | ||||
|  | ||||
| class LocalVariable(MockObj): | ||||
|     """ | ||||
|     Create a local variable and enclose the code using it within a block. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, name, type, modifier=None, rhs=None): | ||||
|         base = ID(name, True, type) | ||||
|     def __init__(self, name, type, rhs=None, modifier="*"): | ||||
|         base = ID(name + "_VAR_", True, type) | ||||
|         super().__init__(base, "") | ||||
|         self.modifier = modifier | ||||
|         self.rhs = rhs | ||||
|  | ||||
|     def __enter__(self): | ||||
|         CodeContext.append(RawStatement("{")) | ||||
|         CodeContext.start_block() | ||||
|         CodeContext.append( | ||||
|             VariableDeclarationExpression(self.base.type, self.modifier, self.base.id) | ||||
|         ) | ||||
|         if self.rhs is not None: | ||||
|             CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs)) | ||||
|         return self.base | ||||
|         return MockObj(self.base) | ||||
|  | ||||
|     def __exit__(self, *args): | ||||
|         CodeContext.append(RawStatement("}")) | ||||
|         CodeContext.end_block() | ||||
|  | ||||
|  | ||||
| class MockLv: | ||||
| @@ -199,14 +248,27 @@ class MockLv: | ||||
|         self.append(result) | ||||
|         return result | ||||
|  | ||||
|     def cond_if(self, expression: Expression): | ||||
|         CodeContext.append(RawStatement(f"if {expression} {{")) | ||||
|  | ||||
|     def cond_else(self): | ||||
| class LvConditional: | ||||
|     def __init__(self, condition): | ||||
|         self.condition = condition | ||||
|  | ||||
|     def __enter__(self): | ||||
|         if self.condition is not None: | ||||
|             CodeContext.append(RawStatement(f"if ({self.condition}) {{")) | ||||
|             CodeContext.code_context.indent() | ||||
|         return self | ||||
|  | ||||
|     def __exit__(self, *args): | ||||
|         if self.condition is not None: | ||||
|             CodeContext.code_context.detent() | ||||
|             CodeContext.append(RawStatement("}")) | ||||
|  | ||||
|     def else_(self): | ||||
|         assert self.condition is not None | ||||
|         CodeContext.code_context.detent() | ||||
|         CodeContext.append(RawStatement("} else {")) | ||||
|  | ||||
|     def cond_endif(self): | ||||
|         CodeContext.append(RawStatement("}")) | ||||
|         CodeContext.code_context.indent() | ||||
|  | ||||
|  | ||||
| class ReturnStatement(ExpressionStatement): | ||||
| @@ -228,36 +290,56 @@ lv = MockLv("lv_") | ||||
| lv_expr = LvExpr("lv_") | ||||
| # Mock for lv_obj_ calls | ||||
| lv_obj = MockLv("lv_obj_") | ||||
| lvgl_comp = MockObj("lvgl_comp", "->") | ||||
| # Operations on the LVGL component | ||||
| lvgl_comp = MockObj(LVGL_COMP, "->") | ||||
|  | ||||
|  | ||||
| # equivalent to cg.add() for the lvgl init context | ||||
| # equivalent to cg.add() for the current code context | ||||
| def lv_add(expression: Union[Expression, Statement]): | ||||
|     return CodeContext.append(expression) | ||||
|  | ||||
|  | ||||
| def add_line_marks(where): | ||||
|     """ | ||||
|     Add line marks for the current code context | ||||
|     :param where: An object to identify the source of the line marks | ||||
|     :return: | ||||
|     """ | ||||
|     for mark in get_line_marks(where): | ||||
|         lv_add(cg.RawStatement(mark)) | ||||
|  | ||||
|  | ||||
| def lv_assign(target, expression): | ||||
|     lv_add(RawExpression(f"{target} = {expression}")) | ||||
|     lv_add(AssignmentExpression("", "", target, expression)) | ||||
|  | ||||
|  | ||||
| lv_groups = {}  # Widget group names | ||||
| def lv_Pvariable(type, name): | ||||
|     """ | ||||
|     Create but do not initialise a pointer variable | ||||
|     :param type: Type of the variable target | ||||
|     :param name: name of the variable, or an ID | ||||
|     :return:  A MockObj of the variable | ||||
|     """ | ||||
|     if isinstance(name, str): | ||||
|         name = ID(name, True, type) | ||||
|     decl = VariableDeclarationExpression(type, "*", name) | ||||
|     CORE.add_global(decl) | ||||
|     var = MockObj(name, "->") | ||||
|     CORE.register_variable(name, var) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| 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] | ||||
| def lv_variable(type, name): | ||||
|     """ | ||||
|     Create but do not initialise a variable | ||||
|     :param type: Type of the variable target | ||||
|     :param name: name of the variable, or an ID | ||||
|     :return:  A MockObj of the variable | ||||
|     """ | ||||
|     if isinstance(name, str): | ||||
|         name = ID(name, True, type) | ||||
|     decl = VariableDeclarationExpression(type, "", name) | ||||
|     CORE.add_global(decl) | ||||
|     var = MockObj(name, ".") | ||||
|     CORE.register_variable(name, var) | ||||
|     return var | ||||
|   | ||||
| @@ -9,8 +9,72 @@ namespace esphome { | ||||
| namespace lvgl { | ||||
| static const char *const TAG = "lvgl"; | ||||
|  | ||||
| #if LV_USE_LOG | ||||
| static void log_cb(const char *buf) { | ||||
|   esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); | ||||
| } | ||||
| #endif  // LV_USE_LOG | ||||
|  | ||||
| static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { | ||||
|   // make sure all coordinates are even | ||||
|   if (area->x1 & 1) | ||||
|     area->x1--; | ||||
|   if (!(area->x2 & 1)) | ||||
|     area->x2++; | ||||
|   if (area->y1 & 1) | ||||
|     area->y1--; | ||||
|   if (!(area->y2 & 1)) | ||||
|     area->y2++; | ||||
| } | ||||
|  | ||||
| lv_event_code_t lv_custom_event;  // NOLINT | ||||
| void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } | ||||
| void LvglComponent::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 LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { | ||||
|   lv_obj_add_event_cb(obj, callback, event, this); | ||||
|   if (event == LV_EVENT_VALUE_CHANGED) { | ||||
|     lv_obj_add_event_cb(obj, callback, lv_custom_event, this); | ||||
|   } | ||||
| } | ||||
| void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, | ||||
|                                  lv_event_code_t event2) { | ||||
|   this->add_event_cb(obj, callback, event1); | ||||
|   this->add_event_cb(obj, callback, event2); | ||||
| } | ||||
| void LvglComponent::add_page(LvPageType *page) { | ||||
|   this->pages_.push_back(page); | ||||
|   page->setup(this->pages_.size() - 1); | ||||
| } | ||||
| void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { | ||||
|   if (index >= this->pages_.size()) | ||||
|     return; | ||||
|   this->current_page_ = index; | ||||
|   lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); | ||||
| } | ||||
| void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { | ||||
|   if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) | ||||
|     return; | ||||
|   do { | ||||
|     this->current_page_ = (this->current_page_ + 1) % this->pages_.size(); | ||||
|   } while (this->pages_[this->current_page_]->skip);  // skip empty pages() | ||||
|   this->show_page(this->current_page_, anim, time); | ||||
| } | ||||
| void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { | ||||
|   if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) | ||||
|     return; | ||||
|   do { | ||||
|     this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size(); | ||||
|   } while (this->pages_[this->current_page_]->skip);  // skip empty pages() | ||||
|   this->show_page(this->current_page_, anim, time); | ||||
| } | ||||
| void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { | ||||
|   for (auto *display : this->displays_) { | ||||
|     display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr, | ||||
| @@ -27,6 +91,116 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv | ||||
|   } | ||||
|   lv_disp_flush_ready(disp_drv); | ||||
| } | ||||
| IdleTrigger::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; | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| #ifdef USE_LVGL_TOUCHSCREEN | ||||
| LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { | ||||
|   lv_indev_drv_init(&this->drv_); | ||||
|   this->drv_.long_press_repeat_time = long_press_repeat_time; | ||||
|   this->drv_.long_press_time = long_press_time; | ||||
|   this->drv_.type = LV_INDEV_TYPE_POINTER; | ||||
|   this->drv_.user_data = this; | ||||
|   this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { | ||||
|     auto *l = static_cast<LVTouchListener *>(d->user_data); | ||||
|     if (l->touch_pressed_) { | ||||
|       data->point.x = l->touch_point_.x; | ||||
|       data->point.y = l->touch_point_.y; | ||||
|       data->state = LV_INDEV_STATE_PRESSED; | ||||
|     } else { | ||||
|       data->state = LV_INDEV_STATE_RELEASED; | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { | ||||
|   this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); | ||||
|   if (this->touch_pressed_) | ||||
|     this->touch_point_ = tpoints[0]; | ||||
| } | ||||
| #endif  // USE_LVGL_TOUCHSCREEN | ||||
|  | ||||
| #ifdef USE_LVGL_KEY_LISTENER | ||||
| LVEncoderListener::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; | ||||
|   }; | ||||
| } | ||||
| #endif  // USE_LVGL_KEY_LISTENER | ||||
|  | ||||
| #ifdef USE_LVGL_BUTTONMATRIX | ||||
| void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { | ||||
|   LvCompound::set_obj(lv_obj); | ||||
|   lv_obj_add_event_cb( | ||||
|       lv_obj, | ||||
|       [](lv_event_t *event) { | ||||
|         auto *self = static_cast<LvButtonMatrixType *>(event->user_data); | ||||
|         if (self->key_callback_.size() == 0) | ||||
|           return; | ||||
|         auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); | ||||
|         if (key_idx == LV_BTNMATRIX_BTN_NONE) | ||||
|           return; | ||||
|         if (self->key_map_.count(key_idx) != 0) { | ||||
|           self->send_key_(self->key_map_[key_idx]); | ||||
|           return; | ||||
|         } | ||||
|         const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx); | ||||
|         auto len = strlen(str); | ||||
|         while (len--) | ||||
|           self->send_key_(*str++); | ||||
|       }, | ||||
|       LV_EVENT_PRESSED, this); | ||||
| } | ||||
| #endif  // USE_LVGL_BUTTONMATRIX | ||||
|  | ||||
| #ifdef USE_LVGL_KEYBOARD | ||||
| static const char *const KB_SPECIAL_KEYS[] = { | ||||
|     "abc", "ABC", "1#", | ||||
|     // maybe add other special keys here | ||||
| }; | ||||
|  | ||||
| void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { | ||||
|   LvCompound::set_obj(lv_obj); | ||||
|   lv_obj_add_event_cb( | ||||
|       lv_obj, | ||||
|       [](lv_event_t *event) { | ||||
|         auto *self = static_cast<LvKeyboardType *>(event->user_data); | ||||
|         if (self->key_callback_.size() == 0) | ||||
|           return; | ||||
|  | ||||
|         auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); | ||||
|         if (key_idx == LV_BTNMATRIX_BTN_NONE) | ||||
|           return; | ||||
|         const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx); | ||||
|         if (txt == nullptr) | ||||
|           return; | ||||
|         for (const auto *kb_special_key : KB_SPECIAL_KEYS) { | ||||
|           if (strcmp(txt, kb_special_key) == 0) | ||||
|             return; | ||||
|         } | ||||
|         while (*txt != 0) | ||||
|           self->send_key_(*txt++); | ||||
|       }, | ||||
|       LV_EVENT_PRESSED, this); | ||||
| } | ||||
| #endif  // USE_LVGL_KEYBOARD | ||||
|  | ||||
| void LvglComponent::write_random_() { | ||||
|   // length of 2 lines in 32 bit units | ||||
| @@ -97,9 +271,24 @@ void LvglComponent::setup() { | ||||
|   this->disp_ = lv_disp_drv_register(&this->disp_drv_); | ||||
|   for (const auto &v : this->init_lambdas_) | ||||
|     v(this); | ||||
|   this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); | ||||
|   lv_disp_trig_activity(this->disp_); | ||||
|   ESP_LOGCONFIG(TAG, "LVGL Setup complete"); | ||||
| } | ||||
| void LvglComponent::update() { | ||||
|   // update indicators | ||||
|   if (this->paused_) { | ||||
|     return; | ||||
|   } | ||||
|   this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); | ||||
| } | ||||
| void LvglComponent::loop() { | ||||
|   if (this->paused_) { | ||||
|     if (this->show_snow_) | ||||
|       this->write_random_(); | ||||
|   } | ||||
|   lv_timer_handler_run_in_period(5); | ||||
| } | ||||
|  | ||||
| #ifdef USE_LVGL_IMAGE | ||||
| lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { | ||||
| @@ -142,7 +331,20 @@ lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { | ||||
|   } | ||||
|   return img_dsc; | ||||
| } | ||||
| #endif  // USE_LVGL_IMAGE | ||||
|  | ||||
| #ifdef USE_LVGL_ANIMIMG | ||||
| void lv_animimg_stop(lv_obj_t *obj) { | ||||
|   auto *animg = (lv_animimg_t *) obj; | ||||
|   int32_t duration = animg->anim.time; | ||||
|   lv_animimg_set_duration(obj, 0); | ||||
|   lv_animimg_start(obj); | ||||
|   lv_animimg_set_duration(obj, duration); | ||||
| } | ||||
| #endif | ||||
| void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { | ||||
|   reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); | ||||
| } | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -18,8 +18,8 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <lvgl.h> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
| #include <map> | ||||
| #ifdef USE_LVGL_IMAGE | ||||
| #include "esphome/components/image/image.h" | ||||
| #endif  // USE_LVGL_IMAGE | ||||
| @@ -31,6 +31,10 @@ | ||||
| #include "esphome/components/touchscreen/touchscreen.h" | ||||
| #endif  // USE_LVGL_TOUCHSCREEN | ||||
|  | ||||
| #if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD) | ||||
| #include "esphome/components/key_provider/key_provider.h" | ||||
| #endif  // USE_LVGL_BUTTONMATRIX | ||||
|  | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| @@ -47,12 +51,25 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT | ||||
| #endif  // LV_COLOR_DEPTH | ||||
|  | ||||
| // Parent class for things that wrap an LVGL object | ||||
| class LvCompound final { | ||||
| class LvCompound { | ||||
|  public: | ||||
|   void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } | ||||
|   virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } | ||||
|   lv_obj_t *obj{}; | ||||
| }; | ||||
|  | ||||
| class LvPageType { | ||||
|  public: | ||||
|   LvPageType(bool skip) : skip(skip) {} | ||||
|  | ||||
|   void setup(size_t index) { | ||||
|     this->index = index; | ||||
|     this->obj = lv_obj_create(nullptr); | ||||
|   } | ||||
|   lv_obj_t *obj{}; | ||||
|   size_t index{}; | ||||
|   bool skip; | ||||
| }; | ||||
|  | ||||
| using LvLambdaType = std::function<void(lv_obj_t *)>; | ||||
| using set_value_lambda_t = std::function<void(float)>; | ||||
| using event_callback_t = void(_lv_event_t *); | ||||
| @@ -89,48 +106,20 @@ class FontEngine { | ||||
| lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr); | ||||
| #endif  // USE_LVGL_IMAGE | ||||
|  | ||||
| #ifdef USE_LVGL_ANIMIMG | ||||
| void lv_animimg_stop(lv_obj_t *obj); | ||||
| #endif  // USE_LVGL_ANIMIMG | ||||
|  | ||||
| class LvglComponent : public PollingComponent { | ||||
|   constexpr static const char *const TAG = "lvgl"; | ||||
|  | ||||
|  public: | ||||
|   static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { | ||||
|     reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); | ||||
|   } | ||||
|   static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::PROCESSOR; } | ||||
|   static void log_cb(const char *buf) { | ||||
|     esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); | ||||
|   } | ||||
|   static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { | ||||
|     // make sure all coordinates are even | ||||
|     if (area->x1 & 1) | ||||
|       area->x1--; | ||||
|     if (!(area->x2 & 1)) | ||||
|       area->x2++; | ||||
|     if (area->y1 & 1) | ||||
|       area->y1--; | ||||
|     if (!(area->y2 & 1)) | ||||
|       area->y2++; | ||||
|   } | ||||
|  | ||||
|   void setup() 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 update() override; | ||||
|   void loop() override; | ||||
|   void add_on_idle_callback(std::function<void(uint32_t)> &&callback) { | ||||
|     this->idle_callbacks_.add(std::move(callback)); | ||||
|   } | ||||
| @@ -141,23 +130,15 @@ class LvglComponent : public PollingComponent { | ||||
|   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); | ||||
|     } | ||||
|   } | ||||
|   void set_paused(bool paused, bool show_snow); | ||||
|   void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); | ||||
|   void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); | ||||
|   bool is_paused() const { return this->paused_; } | ||||
|   void add_page(LvPageType *page); | ||||
|   void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); | ||||
|   void show_next_page(lv_scr_load_anim_t anim, uint32_t time); | ||||
|   void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); | ||||
|   void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } | ||||
|  | ||||
|  protected: | ||||
|   void write_random_(); | ||||
| @@ -168,8 +149,11 @@ class LvglComponent : public PollingComponent { | ||||
|   lv_disp_drv_t disp_drv_{}; | ||||
|   lv_disp_t *disp_{}; | ||||
|   bool paused_{}; | ||||
|   std::vector<LvPageType *> pages_{}; | ||||
|   size_t current_page_{0}; | ||||
|   bool show_snow_{}; | ||||
|   lv_coord_t snow_line_{}; | ||||
|   bool page_wrap_{true}; | ||||
|  | ||||
|   std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_; | ||||
|   CallbackManager<void(uint32_t)> idle_callbacks_{}; | ||||
| @@ -179,16 +163,7 @@ class LvglComponent : public PollingComponent { | ||||
|  | ||||
| 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; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout); | ||||
|  | ||||
|  protected: | ||||
|   TemplatableValue<uint32_t> timeout_; | ||||
| @@ -217,28 +192,8 @@ template<typename... Ts> class LvglCondition : public Condition<Ts...>, public P | ||||
| #ifdef USE_LVGL_TOUCHSCREEN | ||||
| class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> { | ||||
|  public: | ||||
|   LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { | ||||
|     lv_indev_drv_init(&this->drv_); | ||||
|     this->drv_.long_press_repeat_time = long_press_repeat_time; | ||||
|     this->drv_.long_press_time = long_press_time; | ||||
|     this->drv_.type = LV_INDEV_TYPE_POINTER; | ||||
|     this->drv_.user_data = this; | ||||
|     this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { | ||||
|       auto *l = static_cast<LVTouchListener *>(d->user_data); | ||||
|       if (l->touch_pressed_) { | ||||
|         data->point.x = l->touch_point_.x; | ||||
|         data->point.y = l->touch_point_.y; | ||||
|         data->state = LV_INDEV_STATE_PRESSED; | ||||
|       } else { | ||||
|         data->state = LV_INDEV_STATE_RELEASED; | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|   void update(const touchscreen::TouchPoints_t &tpoints) override { | ||||
|     this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); | ||||
|     if (this->touch_pressed_) | ||||
|       this->touch_point_ = tpoints[0]; | ||||
|   } | ||||
|   LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time); | ||||
|   void update(const touchscreen::TouchPoints_t &tpoints) override; | ||||
|   void release() override { touch_pressed_ = false; } | ||||
|   lv_indev_drv_t *get_drv() { return &this->drv_; } | ||||
|  | ||||
| @@ -252,21 +207,7 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC | ||||
| #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; | ||||
|     }; | ||||
|   } | ||||
|   LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); | ||||
|  | ||||
|   void set_left_button(binary_sensor::BinarySensor *left_button) { | ||||
|     left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); | ||||
| @@ -279,9 +220,11 @@ class LVEncoderListener : public Parented<LvglComponent> { | ||||
|     enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_LVGL_ROTARY_ENCODER | ||||
|   void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) { | ||||
|     sensor->register_listener([this](int32_t count) { this->set_count(count); }); | ||||
|   } | ||||
| #endif  // USE_LVGL_ROTARY_ENCODER | ||||
|  | ||||
|   void event(int key, bool pressed) { | ||||
|     if (!this->parent_->is_paused()) { | ||||
| @@ -304,6 +247,25 @@ class LVEncoderListener : public Parented<LvglComponent> { | ||||
|   int32_t last_count_{}; | ||||
|   int key_{}; | ||||
| }; | ||||
| #endif  // USE_LVGL_KEY_LISTENER | ||||
| #endif  //  USE_LVGL_KEY_LISTENER | ||||
|  | ||||
| #ifdef USE_LVGL_BUTTONMATRIX | ||||
| class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { | ||||
|  public: | ||||
|   void set_obj(lv_obj_t *lv_obj) override; | ||||
|   uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } | ||||
|   void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; } | ||||
|  | ||||
|  protected: | ||||
|   std::map<size_t, uint8_t> key_map_{}; | ||||
| }; | ||||
| #endif  // USE_LVGL_BUTTONMATRIX | ||||
|  | ||||
| #ifdef USE_LVGL_KEYBOARD | ||||
| class LvKeyboardType : public key_provider::KeyProvider, public LvCompound { | ||||
|  public: | ||||
|   void set_obj(lv_obj_t *lv_obj) override; | ||||
| }; | ||||
| #endif  // USE_LVGL_KEYBOARD | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
|   | ||||
							
								
								
									
										52
									
								
								esphome/components/lvgl/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/lvgl/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import number | ||||
| import esphome.config_validation as cv | ||||
| from esphome.cpp_generator import MockObj | ||||
|  | ||||
| from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET | ||||
| from ..lv_validation import animated | ||||
| from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LV_EVENT, LvNumber, lvgl_ns | ||||
| from ..widgets import get_widgets | ||||
|  | ||||
| LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     number.number_schema(LVGLNumber) | ||||
|     .extend(LVGL_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_WIDGET): cv.use_id(LvNumber), | ||||
|             cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     widget = await get_widgets(config, CONF_WIDGET) | ||||
|     widget = widget[0] | ||||
|     var = await number.new_number( | ||||
|         config, | ||||
|         max_value=widget.get_max(), | ||||
|         min_value=widget.get_min(), | ||||
|         step=widget.get_step(), | ||||
|     ) | ||||
|  | ||||
|     async with LambdaContext([(cg.float_, "v")]) as control: | ||||
|         await widget.set_property( | ||||
|             "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] | ||||
|         ) | ||||
|         lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) | ||||
|     async with LambdaContext(EVENT_ARG) as event: | ||||
|         event.add(var.publish_state(widget.get_value())) | ||||
|     async with LvContext(paren): | ||||
|         lv_add(var.set_control_lambda(await control.get_lambda())) | ||||
|         lv_add( | ||||
|             paren.add_event_cb( | ||||
|                 widget.obj, await event.get_lambda(), LV_EVENT.VALUE_CHANGED | ||||
|             ) | ||||
|         ) | ||||
|         lv_add(var.publish_state(widget.get_value())) | ||||
							
								
								
									
										33
									
								
								esphome/components/lvgl/number/lvgl_number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/lvgl/number/lvgl_number.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/number/number.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| class LVGLNumber : public number::Number { | ||||
|  public: | ||||
|   void set_control_lambda(std::function<void(float)> control_lambda) { | ||||
|     this->control_lambda_ = control_lambda; | ||||
|     if (this->initial_state_.has_value()) { | ||||
|       this->control_lambda_(this->initial_state_.value()); | ||||
|       this->initial_state_.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void control(float value) { | ||||
|     if (this->control_lambda_ != nullptr) | ||||
|       this->control_lambda_(value); | ||||
|     else | ||||
|       this->initial_state_ = value; | ||||
|   } | ||||
|   std::function<void(float)> control_lambda_{}; | ||||
|   optional<float> initial_state_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
| @@ -7,6 +7,7 @@ from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_STATE, | ||||
|     CONF_TEXT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
| ) | ||||
| @@ -15,13 +16,17 @@ 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 id_name, lv_font | ||||
| from .types import WIDGET_TYPES, WidgetType | ||||
| from .lv_validation import lv_color, lv_font, lv_image | ||||
| from .lvcode import LvglComponent | ||||
| from .types import WidgetType, lv_group_t | ||||
|  | ||||
| # this will be populated later, in __init__.py to avoid circular imports. | ||||
| WIDGET_TYPES: dict = {} | ||||
|  | ||||
| # A schema for text properties | ||||
| TEXT_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(df.CONF_TEXT): cv.Any( | ||||
|         cv.Optional(CONF_TEXT): cv.Any( | ||||
|             cv.All( | ||||
|                 cv.Schema( | ||||
|                     { | ||||
| @@ -38,11 +43,13 @@ 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, | ||||
| LIST_ACTION_SCHEMA = cv.ensure_list( | ||||
|     cv.maybe_simple_value( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), | ||||
|         }, | ||||
|         key=CONF_ID, | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| PRESS_TIME = cv.All( | ||||
| @@ -54,7 +61,7 @@ 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(CONF_GROUP): cv.declare_id(lv_group_t), | ||||
|         cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, | ||||
|         cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, | ||||
|     } | ||||
| @@ -154,6 +161,7 @@ STYLE_REMAP = { | ||||
| # Complete object style schema | ||||
| STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( | ||||
|     { | ||||
|         cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)), | ||||
|         cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( | ||||
|             "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" | ||||
|         ).one_of, | ||||
| @@ -209,7 +217,14 @@ def create_modify_schema(widget_type): | ||||
|         part_schema(widget_type) | ||||
|         .extend( | ||||
|             { | ||||
|                 cv.Required(CONF_ID): cv.use_id(widget_type), | ||||
|                 cv.Required(CONF_ID): cv.ensure_list( | ||||
|                     cv.maybe_simple_value( | ||||
|                         { | ||||
|                             cv.Required(CONF_ID): cv.use_id(widget_type), | ||||
|                         }, | ||||
|                         key=CONF_ID, | ||||
|                     ) | ||||
|                 ), | ||||
|                 cv.Optional(CONF_STATE): SET_STATE_SCHEMA, | ||||
|             } | ||||
|         ) | ||||
| @@ -227,19 +242,22 @@ def obj_schema(widget_type: WidgetType): | ||||
|     return ( | ||||
|         part_schema(widget_type) | ||||
|         .extend(FLAG_SCHEMA) | ||||
|         .extend(LAYOUT_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, | ||||
|                     cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| LAYOUT_SCHEMAS = {} | ||||
|  | ||||
| ALIGN_TO_SCHEMA = { | ||||
|     cv.Optional(df.CONF_ALIGN_TO): cv.Schema( | ||||
|         { | ||||
| @@ -252,21 +270,78 @@ ALIGN_TO_SCHEMA = { | ||||
| } | ||||
|  | ||||
|  | ||||
| def grid_free_space(value): | ||||
|     value = cv.Upper(value) | ||||
|     if value.startswith("FR(") and value.endswith(")"): | ||||
|         value = value.removesuffix(")").removeprefix("FR(") | ||||
|         return f"LV_GRID_FR({cv.positive_int(value)})" | ||||
|     raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") | ||||
|  | ||||
|  | ||||
| grid_spec = cv.Any( | ||||
|     lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space | ||||
| ) | ||||
|  | ||||
| cell_alignments = df.LV_CELL_ALIGNMENTS.one_of | ||||
| grid_alignments = df.LV_GRID_ALIGNMENTS.one_of | ||||
| flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of | ||||
|  | ||||
| LAYOUT_SCHEMA = { | ||||
|     cv.Optional(df.CONF_LAYOUT): cv.typed_schema( | ||||
|         { | ||||
|             df.TYPE_GRID: { | ||||
|                 cv.Required(df.CONF_GRID_ROWS): [grid_spec], | ||||
|                 cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], | ||||
|                 cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, | ||||
|                 cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, | ||||
|             }, | ||||
|             df.TYPE_FLEX: { | ||||
|                 cv.Optional( | ||||
|                     df.CONF_FLEX_FLOW, default="row_wrap" | ||||
|                 ): df.FLEX_FLOWS.one_of, | ||||
|                 cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, | ||||
|                 cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, | ||||
|                 cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, | ||||
|             }, | ||||
|         }, | ||||
|         lower=True, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| GRID_CELL_SCHEMA = { | ||||
|     cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, | ||||
|     cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, | ||||
|     cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, | ||||
| } | ||||
|  | ||||
| FLEX_OBJ_SCHEMA = { | ||||
|     cv.Optional(df.CONF_FLEX_GROW): cv.int_, | ||||
| } | ||||
|  | ||||
| DISP_BG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image, | ||||
|         cv.Optional(df.CONF_DISP_BG_COLOR): lv_color, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| # A style schema that can include text | ||||
| STYLED_TEXT_SCHEMA = cv.maybe_simple_value( | ||||
|     STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT | ||||
|     STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT | ||||
| ) | ||||
|  | ||||
| # For use by platform components | ||||
| LVGL_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent), | ||||
|         cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(LvglComponent), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| ALL_STYLES = { | ||||
|     **STYLE_PROPS, | ||||
| } | ||||
| ALL_STYLES = {**STYLE_PROPS, **GRID_CELL_SCHEMA, **FLEX_OBJ_SCHEMA} | ||||
|  | ||||
|  | ||||
| def container_validator(schema, widget_type: WidgetType): | ||||
| @@ -281,16 +356,17 @@ def container_validator(schema, widget_type: WidgetType): | ||||
|         result = schema | ||||
|         if w_sch := widget_type.schema: | ||||
|             result = result.extend(w_sch) | ||||
|         ltype = df.TYPE_NONE | ||||
|         if value and (layout := value.get(df.CONF_LAYOUT)): | ||||
|             if not isinstance(layout, dict): | ||||
|                 raise cv.Invalid("Layout value must be a dict") | ||||
|             ltype = layout.get(CONF_TYPE) | ||||
|             if not ltype: | ||||
|                 raise (cv.Invalid("Layout schema requires type:")) | ||||
|             add_lv_use(ltype) | ||||
|         result = result.extend( | ||||
|             {cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())} | ||||
|         ) | ||||
|         if value == SCHEMA_EXTRACT: | ||||
|             return result | ||||
|         result = result.extend(LAYOUT_SCHEMAS[ltype.lower()]) | ||||
|         return result(value) | ||||
|  | ||||
|     return validator | ||||
|   | ||||
							
								
								
									
										46
									
								
								esphome/components/lvgl/select/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								esphome/components/lvgl/select/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import select | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_OPTIONS | ||||
|  | ||||
| from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET | ||||
| from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LV_EVENT, LvSelect, lvgl_ns | ||||
| from ..widgets import get_widgets | ||||
|  | ||||
| LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     select.select_schema(LVGLSelect) | ||||
|     .extend(LVGL_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_WIDGET): cv.use_id(LvSelect), | ||||
|             cv.Optional(CONF_ANIMATED, default=False): cv.boolean, | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     widget = await get_widgets(config, CONF_WIDGET) | ||||
|     widget = widget[0] | ||||
|     options = widget.config.get(CONF_OPTIONS, []) | ||||
|     selector = await select.new_select(config, options=options) | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     async with LambdaContext(EVENT_ARG) as pub_ctx: | ||||
|         pub_ctx.add(selector.publish_index(widget.get_value())) | ||||
|     async with LambdaContext([(cg.uint16, "v")]) as control: | ||||
|         await widget.set_property("selected", "v", animated=config[CONF_ANIMATED]) | ||||
|         lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) | ||||
|     async with LvContext(paren) as ctx: | ||||
|         lv_add(selector.set_control_lambda(await control.get_lambda())) | ||||
|         ctx.add( | ||||
|             paren.add_event_cb( | ||||
|                 widget.obj, | ||||
|                 await pub_ctx.get_lambda(), | ||||
|                 LV_EVENT.VALUE_CHANGED, | ||||
|             ) | ||||
|         ) | ||||
|         lv_add(selector.publish_index(widget.get_value())) | ||||
							
								
								
									
										62
									
								
								esphome/components/lvgl/select/lvgl_select.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/lvgl/select/lvgl_select.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/select/select.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| static std::vector<std::string> split_string(const std::string &str) { | ||||
|   std::vector<std::string> strings; | ||||
|   auto delimiter = std::string("\n"); | ||||
|  | ||||
|   std::string::size_type pos; | ||||
|   std::string::size_type prev = 0; | ||||
|   while ((pos = str.find(delimiter, prev)) != std::string::npos) { | ||||
|     strings.push_back(str.substr(prev, pos - prev)); | ||||
|     prev = pos + delimiter.size(); | ||||
|   } | ||||
|  | ||||
|   // To get the last substring (or only, if delimiter is not found) | ||||
|   strings.push_back(str.substr(prev)); | ||||
|  | ||||
|   return strings; | ||||
| } | ||||
|  | ||||
| class LVGLSelect : public select::Select { | ||||
|  public: | ||||
|   void set_control_lambda(std::function<void(size_t)> lambda) { | ||||
|     this->control_lambda_ = lambda; | ||||
|     if (this->initial_state_.has_value()) { | ||||
|       this->control(this->initial_state_.value()); | ||||
|       this->initial_state_.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void publish_index(size_t index) { | ||||
|     auto value = this->at(index); | ||||
|     if (value) | ||||
|       this->publish_state(value.value()); | ||||
|   } | ||||
|  | ||||
|   void set_options(const char *str) { this->traits.set_options(split_string(str)); } | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override { | ||||
|     if (this->control_lambda_ != nullptr) { | ||||
|       auto index = index_of(value); | ||||
|       if (index) | ||||
|         this->control_lambda_(index.value()); | ||||
|     } else { | ||||
|       this->initial_state_ = value.c_str(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   std::function<void(size_t)> control_lambda_{}; | ||||
|   optional<const char *> initial_state_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
							
								
								
									
										35
									
								
								esphome/components/lvgl/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								esphome/components/lvgl/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.sensor import Sensor, new_sensor, sensor_schema | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from ..defines import CONF_LVGL_ID, CONF_WIDGET | ||||
| from ..lvcode import EVENT_ARG, LVGL_COMP_ARG, LambdaContext, LvContext, lv_add | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LV_EVENT, LvNumber | ||||
| from ..widgets import Widget, get_widgets | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor_schema(Sensor) | ||||
|     .extend(LVGL_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_WIDGET): cv.use_id(LvNumber), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     sensor = await new_sensor(config) | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     widget = await get_widgets(config, CONF_WIDGET) | ||||
|     widget = widget[0] | ||||
|     assert isinstance(widget, Widget) | ||||
|     async with LambdaContext(EVENT_ARG) as lamb: | ||||
|         lv_add(sensor.publish_state(widget.get_value())) | ||||
|     async with LvContext(paren, LVGL_COMP_ARG): | ||||
|         lv_add( | ||||
|             paren.add_event_cb( | ||||
|                 widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED | ||||
|             ) | ||||
|         ) | ||||
							
								
								
									
										58
									
								
								esphome/components/lvgl/styles.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/lvgl/styles.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import CONF_ID | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import MockObj | ||||
|  | ||||
| from .defines import ( | ||||
|     CONF_STYLE_DEFINITIONS, | ||||
|     CONF_THEME, | ||||
|     CONF_TOP_LAYER, | ||||
|     LValidator, | ||||
|     literal, | ||||
| ) | ||||
| from .helpers import add_lv_use | ||||
| from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable | ||||
| from .schemas import ALL_STYLES | ||||
| from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr | ||||
| from .widgets import Widget, add_widgets, set_obj_properties, theme_widget_map | ||||
| from .widgets.obj import obj_spec | ||||
|  | ||||
| TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())") | ||||
|  | ||||
|  | ||||
| async def styles_to_code(config): | ||||
|     """Convert styles to C__ code.""" | ||||
|     for style in config.get(CONF_STYLE_DEFINITIONS, ()): | ||||
|         svar = cg.new_Pvariable(style[CONF_ID]) | ||||
|         lv.style_init(svar) | ||||
|         for prop, validator in ALL_STYLES.items(): | ||||
|             if (value := style.get(prop)) is not None: | ||||
|                 if isinstance(validator, LValidator): | ||||
|                     value = await validator.process(value) | ||||
|                 if isinstance(value, list): | ||||
|                     value = "|".join(value) | ||||
|                 lv.call(f"style_set_{prop}", svar, literal(value)) | ||||
|  | ||||
|  | ||||
| async def theme_to_code(config): | ||||
|     if theme := config.get(CONF_THEME): | ||||
|         add_lv_use(CONF_THEME) | ||||
|         for w_name, style in theme.items(): | ||||
|             if not isinstance(style, dict): | ||||
|                 continue | ||||
|  | ||||
|             lname = "lv_theme_apply_" + w_name | ||||
|             apply = lv_variable(lv_lambda_t, lname) | ||||
|             theme_widget_map[w_name] = apply | ||||
|             ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) | ||||
|             async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: | ||||
|                 await set_obj_properties(ow, style) | ||||
|             lv_assign(apply, await context.get_lambda()) | ||||
|  | ||||
|  | ||||
| async def add_top_layer(config): | ||||
|     if top_conf := config.get(CONF_TOP_LAYER): | ||||
|         with LocalVariable("top_layer", lv_obj_t, TOP_LAYER) as top_layer_obj: | ||||
|             top_w = Widget(top_layer_obj, obj_spec, top_conf) | ||||
|             await set_obj_properties(top_w, top_conf) | ||||
|             await add_widgets(top_w, top_conf) | ||||
							
								
								
									
										54
									
								
								esphome/components/lvgl/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								esphome/components/lvgl/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.switch import Switch, new_switch, switch_schema | ||||
| import esphome.config_validation as cv | ||||
| from esphome.cpp_generator import MockObj | ||||
|  | ||||
| from ..defines import CONF_LVGL_ID, CONF_WIDGET | ||||
| from ..lvcode import ( | ||||
|     CUSTOM_EVENT, | ||||
|     EVENT_ARG, | ||||
|     LambdaContext, | ||||
|     LvConditional, | ||||
|     LvContext, | ||||
|     lv, | ||||
|     lv_add, | ||||
| ) | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns | ||||
| from ..widgets import get_widgets | ||||
|  | ||||
| LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) | ||||
| CONFIG_SCHEMA = ( | ||||
|     switch_schema(LVGLSwitch) | ||||
|     .extend(LVGL_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     switch = await new_switch(config) | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     widget = await get_widgets(config, CONF_WIDGET) | ||||
|     widget = widget[0] | ||||
|     async with LambdaContext(EVENT_ARG) as checked_ctx: | ||||
|         checked_ctx.add(switch.publish_state(widget.get_value())) | ||||
|     async with LambdaContext([(cg.bool_, "v")]) as control: | ||||
|         with LvConditional(MockObj("v")) as cond: | ||||
|             widget.add_state(LV_STATE.CHECKED) | ||||
|             cond.else_() | ||||
|             widget.clear_state(LV_STATE.CHECKED) | ||||
|         lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) | ||||
|     async with LvContext(paren) as ctx: | ||||
|         lv_add(switch.set_control_lambda(await control.get_lambda())) | ||||
|         ctx.add( | ||||
|             paren.add_event_cb( | ||||
|                 widget.obj, | ||||
|                 await checked_ctx.get_lambda(), | ||||
|                 LV_EVENT.VALUE_CHANGED, | ||||
|             ) | ||||
|         ) | ||||
|         lv_add(switch.publish_state(widget.get_value())) | ||||
							
								
								
									
										33
									
								
								esphome/components/lvgl/switch/lvgl_switch.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/lvgl/switch/lvgl_switch.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/switch/switch.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| class LVGLSwitch : public switch_::Switch { | ||||
|  public: | ||||
|   void set_control_lambda(std::function<void(bool)> state_lambda) { | ||||
|     this->state_lambda_ = state_lambda; | ||||
|     if (this->initial_state_.has_value()) { | ||||
|       this->state_lambda_(this->initial_state_.value()); | ||||
|       this->initial_state_.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void write_state(bool value) { | ||||
|     if (this->state_lambda_ != nullptr) | ||||
|       this->state_lambda_(value); | ||||
|     else | ||||
|       this->initial_state_ = value; | ||||
|   } | ||||
|   std::function<void(bool)> state_lambda_{}; | ||||
|   optional<bool> initial_state_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
							
								
								
									
										39
									
								
								esphome/components/lvgl/text/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								esphome/components/lvgl/text/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import text | ||||
| from esphome.components.text import new_text | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from ..defines import CONF_LVGL_ID, CONF_WIDGET | ||||
| from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LV_EVENT, LvText, lvgl_ns | ||||
| from ..widgets import get_widgets | ||||
|  | ||||
| LVGLText = lvgl_ns.class_("LVGLText", text.Text) | ||||
|  | ||||
| CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(LVGLText), | ||||
|         cv.Required(CONF_WIDGET): cv.use_id(LvText), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     textvar = await new_text(config) | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     widget = await get_widgets(config, CONF_WIDGET) | ||||
|     widget = widget[0] | ||||
|     async with LambdaContext([(cg.std_string, "text_value")]) as control: | ||||
|         await widget.set_property("text", "text_value.c_str())") | ||||
|         lv.event_send(widget.obj, CUSTOM_EVENT, None) | ||||
|     async with LambdaContext(EVENT_ARG) as lamb: | ||||
|         lv_add(textvar.publish_state(widget.get_value())) | ||||
|     async with LvContext(paren): | ||||
|         widget.var.set_control_lambda(await control.get_lambda()) | ||||
|         lv_add( | ||||
|             paren.add_event_cb( | ||||
|                 widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED | ||||
|             ) | ||||
|         ) | ||||
|         lv_add(textvar.publish_state(widget.get_value())) | ||||
							
								
								
									
										33
									
								
								esphome/components/lvgl/text/lvgl_text.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/lvgl/text/lvgl_text.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/text/text.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| class LVGLText : public text::Text { | ||||
|  public: | ||||
|   void set_control_lambda(std::function<void(const std::string)> control_lambda) { | ||||
|     this->control_lambda_ = control_lambda; | ||||
|     if (this->initial_state_.has_value()) { | ||||
|       this->control_lambda_(this->initial_state_.value()); | ||||
|       this->initial_state_.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) { | ||||
|     if (this->control_lambda_ != nullptr) | ||||
|       this->control_lambda_(value); | ||||
|     else | ||||
|       this->initial_state_ = value; | ||||
|   } | ||||
|   std::function<void(const std::string)> control_lambda_{}; | ||||
|   optional<std::string> initial_state_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
							
								
								
									
										40
									
								
								esphome/components/lvgl/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								esphome/components/lvgl/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.text_sensor import ( | ||||
|     TextSensor, | ||||
|     new_text_sensor, | ||||
|     text_sensor_schema, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from ..defines import CONF_LVGL_ID, CONF_WIDGET | ||||
| from ..lvcode import EVENT_ARG, LambdaContext, LvContext | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LV_EVENT, LvText | ||||
| from ..widgets import get_widgets | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     text_sensor_schema(TextSensor) | ||||
|     .extend(LVGL_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_WIDGET): cv.use_id(LvText), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     sensor = await new_text_sensor(config) | ||||
|     paren = await cg.get_variable(config[CONF_LVGL_ID]) | ||||
|     widget = await get_widgets(config, CONF_WIDGET) | ||||
|     widget = widget[0] | ||||
|     async with LambdaContext(EVENT_ARG) as pressed_ctx: | ||||
|         pressed_ctx.add(sensor.publish_state(widget.get_value())) | ||||
|     async with LvContext(paren) as ctx: | ||||
|         ctx.add( | ||||
|             paren.add_event_cb( | ||||
|                 widget.obj, | ||||
|                 await pressed_ctx.get_lambda(), | ||||
|                 LV_EVENT.VALUE_CHANGED, | ||||
|             ) | ||||
|         ) | ||||
| @@ -34,7 +34,7 @@ def touchscreen_schema(config): | ||||
|  | ||||
|  | ||||
| async def touchscreens_to_code(var, config): | ||||
|     for tconf in config.get(CONF_TOUCHSCREENS) or (): | ||||
|     for tconf in config.get(CONF_TOUCHSCREENS, ()): | ||||
|         lvgl_components_required.add(CONF_TOUCHSCREEN) | ||||
|         touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) | ||||
|         lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds | ||||
|   | ||||
| @@ -7,14 +7,13 @@ from .defines import ( | ||||
|     CONF_ALIGN_TO, | ||||
|     CONF_X, | ||||
|     CONF_Y, | ||||
|     LV_EVENT, | ||||
|     LV_EVENT_MAP, | ||||
|     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") | ||||
| from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add | ||||
| from .types import LV_EVENT | ||||
| from .widgets import widget_map | ||||
|  | ||||
|  | ||||
| async def generate_triggers(lv_component): | ||||
| @@ -34,15 +33,15 @@ async def generate_triggers(lv_component): | ||||
|             }.items(): | ||||
|                 conf = conf[0] | ||||
|                 w.add_flag("LV_OBJ_FLAG_CLICKABLE") | ||||
|                 event = "LV_EVENT_" + LV_EVENT[event[3:].upper()] | ||||
|                 event = literal("LV_EVENT_" + LV_EVENT_MAP[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) | ||||
|                 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] | ||||
|                 align = literal(align_to[CONF_ALIGN]) | ||||
|                 x = align_to[CONF_X] | ||||
|                 y = align_to[CONF_Y] | ||||
|                 lv.obj_align_to(w.obj, target, align, x, y) | ||||
| @@ -50,12 +49,11 @@ async def generate_triggers(lv_component): | ||||
|  | ||||
| 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))) | ||||
|     async with LambdaContext(EVENT_ARG, where=tid) as context: | ||||
|         with LvConditional(w.is_selected()): | ||||
|             lv_add(trigger.trigger(value)) | ||||
|     lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), event)) | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| from esphome import automation, codegen as cg | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| import sys | ||||
|  | ||||
| from .defines import CONF_TEXT | ||||
| from esphome import automation, codegen as cg | ||||
| from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE | ||||
| from esphome.cpp_generator import MockObj, MockObjClass | ||||
|  | ||||
| from .defines import lvgl_ns | ||||
| from .lvcode import lv_expr | ||||
|  | ||||
|  | ||||
| class LvType(cg.MockObjClass): | ||||
| @@ -18,36 +21,48 @@ class LvType(cg.MockObjClass): | ||||
|         return self.args[0][0] if len(self.args) else None | ||||
|  | ||||
|  | ||||
| class LvNumber(LvType): | ||||
|     def __init__(self, *args): | ||||
|         super().__init__( | ||||
|             *args, | ||||
|             largs=[(cg.float_, "x")], | ||||
|             lvalue=lambda w: w.get_number_value(), | ||||
|             has_on_value=True, | ||||
|         ) | ||||
|         self.value_property = CONF_VALUE | ||||
|  | ||||
|  | ||||
| uint16_t_ptr = cg.uint16.operator("ptr") | ||||
| 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_coord_t = cg.global_ns.namespace("lv_coord_t") | ||||
| lv_event_code_t = cg.global_ns.enum("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) | ||||
| lv_lambda_t = lvgl_ns.class_("LvLambdaType") | ||||
| 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") | ||||
| # fake parent class for first class widgets and matrix buttons | ||||
| lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") | ||||
| 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_disp_t = cg.global_ns.struct("lv_disp_t") | ||||
| 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_page_t = cg.global_ns.class_("LvPageType", LvCompound) | ||||
| lv_img_t = LvType("lv_img_t") | ||||
|  | ||||
|  | ||||
| # this will be populated later, in __init__.py to avoid circular imports. | ||||
| WIDGET_TYPES: dict = {} | ||||
| LV_EVENT = MockObj(base="LV_EVENT_", op="") | ||||
| LV_STATE = MockObj(base="LV_STATE_", op="") | ||||
| LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="") | ||||
|  | ||||
|  | ||||
| class LvText(LvType): | ||||
| @@ -55,7 +70,8 @@ class LvText(LvType): | ||||
|         super().__init__( | ||||
|             *args, | ||||
|             largs=[(cg.std_string, "text")], | ||||
|             lvalue=lambda w: w.get_property("text")[0], | ||||
|             lvalue=lambda w: w.get_property("text"), | ||||
|             has_on_value=True, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         self.value_property = CONF_TEXT | ||||
| @@ -66,13 +82,21 @@ class LvBoolean(LvType): | ||||
|         super().__init__( | ||||
|             *args, | ||||
|             largs=[(cg.bool_, "x")], | ||||
|             lvalue=lambda w: w.has_state("LV_STATE_CHECKED"), | ||||
|             lvalue=lambda w: w.is_checked(), | ||||
|             has_on_value=True, | ||||
|             **kwargs, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t) | ||||
| class LvSelect(LvType): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__( | ||||
|             *args, | ||||
|             largs=[(cg.int_, "x")], | ||||
|             lvalue=lambda w: w.get_property("selected"), | ||||
|             has_on_value=True, | ||||
|             **kwargs, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class WidgetType: | ||||
| @@ -80,7 +104,15 @@ class WidgetType: | ||||
|     Describes a type of Widget, e.g. "bar" or "line" | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, name, w_type, parts, schema=None, modify_schema=None): | ||||
|     def __init__( | ||||
|         self, | ||||
|         name: str, | ||||
|         w_type: LvType, | ||||
|         parts: tuple, | ||||
|         schema=None, | ||||
|         modify_schema=None, | ||||
|         lv_name=None, | ||||
|     ): | ||||
|         """ | ||||
|         :param name: The widget name, e.g. "bar" | ||||
|         :param w_type: The C type of the widget | ||||
| @@ -89,6 +121,7 @@ class WidgetType: | ||||
|         :param modify_schema: A schema to update the widget | ||||
|         """ | ||||
|         self.name = name | ||||
|         self.lv_name = lv_name or name | ||||
|         self.w_type = w_type | ||||
|         self.parts = parts | ||||
|         if schema is None: | ||||
| @@ -98,7 +131,8 @@ class WidgetType: | ||||
|         if modify_schema is None: | ||||
|             self.modify_schema = self.schema | ||||
|         else: | ||||
|             self.modify_schema = self.schema | ||||
|             self.modify_schema = modify_schema | ||||
|         self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") | ||||
|  | ||||
|     @property | ||||
|     def animated(self): | ||||
| @@ -118,7 +152,7 @@ class WidgetType: | ||||
|         :param config: Its configuration | ||||
|         :return: Generated code as a list of text lines | ||||
|         """ | ||||
|         raise NotImplementedError(f"No to_code defined for {self.name}") | ||||
|         return [] | ||||
|  | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         """ | ||||
| @@ -127,7 +161,7 @@ class WidgetType: | ||||
|         :param config:  Its configuration | ||||
|         :return: Generated code as a single text line | ||||
|         """ | ||||
|         return f"lv_{self.name}_create({parent})" | ||||
|         return lv_expr.call(f"{self.lv_name}_create", parent) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         """ | ||||
| @@ -135,3 +169,23 @@ class WidgetType: | ||||
|         :return: | ||||
|         """ | ||||
|         return () | ||||
|  | ||||
|     def get_max(self, config: dict): | ||||
|         return sys.maxsize | ||||
|  | ||||
|     def get_min(self, config: dict): | ||||
|         return -sys.maxsize | ||||
|  | ||||
|     def get_step(self, config: dict): | ||||
|         return 1 | ||||
|  | ||||
|     def get_scale(self, config: dict): | ||||
|         return 1.0 | ||||
|  | ||||
|  | ||||
| class NumberType(WidgetType): | ||||
|     def get_max(self, config: dict): | ||||
|         return int(config[CONF_MAX_VALUE] or 100) | ||||
|  | ||||
|     def get_min(self, config: dict): | ||||
|         return int(config[CONF_MIN_VALUE] or 0) | ||||
|   | ||||
| @@ -1,33 +1,55 @@ | ||||
| import sys | ||||
| from typing import Any | ||||
| from typing import Any, Union | ||||
| 
 | ||||
| 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 CORE, TimePeriod | ||||
| from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE | ||||
| from esphome.core import ID, TimePeriod | ||||
| from esphome.coroutine import FakeAwaitable | ||||
| from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression | ||||
| from esphome.cpp_generator import CallExpression, MockObj | ||||
| 
 | ||||
| from .defines import ( | ||||
| from ..defines import ( | ||||
|     CONF_DEFAULT, | ||||
|     CONF_FLEX_ALIGN_CROSS, | ||||
|     CONF_FLEX_ALIGN_MAIN, | ||||
|     CONF_FLEX_ALIGN_TRACK, | ||||
|     CONF_FLEX_FLOW, | ||||
|     CONF_GRID_COLUMN_ALIGN, | ||||
|     CONF_GRID_COLUMNS, | ||||
|     CONF_GRID_ROW_ALIGN, | ||||
|     CONF_GRID_ROWS, | ||||
|     CONF_LAYOUT, | ||||
|     CONF_MAIN, | ||||
|     CONF_SCROLLBAR_MODE, | ||||
|     CONF_STYLES, | ||||
|     CONF_WIDGETS, | ||||
|     OBJ_FLAGS, | ||||
|     PARTS, | ||||
|     STATES, | ||||
|     ConstantLiteral, | ||||
|     TYPE_FLEX, | ||||
|     TYPE_GRID, | ||||
|     LValidator, | ||||
|     join_enums, | ||||
|     literal, | ||||
| ) | ||||
| from .helpers import add_lv_use | ||||
| 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, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr | ||||
| from ..helpers import add_lv_use | ||||
| from ..lvcode import ( | ||||
|     LvConditional, | ||||
|     add_line_marks, | ||||
|     lv, | ||||
|     lv_add, | ||||
|     lv_assign, | ||||
|     lv_expr, | ||||
|     lv_obj, | ||||
|     lv_Pvariable, | ||||
| ) | ||||
| from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES | ||||
| from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr | ||||
| 
 | ||||
| EVENT_LAMB = "event_lamb__" | ||||
| 
 | ||||
| theme_widget_map = {} | ||||
| 
 | ||||
| 
 | ||||
| class LvScrActType(WidgetType): | ||||
|     """ | ||||
| @@ -37,9 +59,6 @@ class LvScrActType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__("lv_scr_act()", lv_obj_t, ()) | ||||
| 
 | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         return [] | ||||
| 
 | ||||
|     async def to_code(self, w, config: dict): | ||||
|         return [] | ||||
| 
 | ||||
| @@ -55,7 +74,7 @@ class Widget: | ||||
|     def set_completed(): | ||||
|         Widget.widgets_completed = True | ||||
| 
 | ||||
|     def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None): | ||||
|     def __init__(self, var, wtype: WidgetType, config: dict = None): | ||||
|         self.var = var | ||||
|         self.type = wtype | ||||
|         self.config = config | ||||
| @@ -63,21 +82,18 @@ class Widget: | ||||
|         self.step = 1.0 | ||||
|         self.range_from = -sys.maxsize | ||||
|         self.range_to = sys.maxsize | ||||
|         self.parent = parent | ||||
|         if wtype.is_compound(): | ||||
|             self.obj = MockObj(f"{self.var}->obj") | ||||
|         else: | ||||
|             self.obj = var | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def create(name, var, wtype: WidgetType, config: dict = None, parent=None): | ||||
|         w = Widget(var, wtype, config, parent) | ||||
|     def create(name, var, wtype: WidgetType, config: dict = None): | ||||
|         w = Widget(var, wtype, config) | ||||
|         if name is not None: | ||||
|             widget_map[name] = w | ||||
|         return w | ||||
| 
 | ||||
|     @property | ||||
|     def obj(self): | ||||
|         if self.type.is_compound(): | ||||
|             return f"{self.var}->obj" | ||||
|         return self.var | ||||
| 
 | ||||
|     def add_state(self, state): | ||||
|         return lv_obj.add_state(self.obj, literal(state)) | ||||
| 
 | ||||
| @@ -85,7 +101,13 @@ class Widget: | ||||
|         return lv_obj.clear_state(self.obj, literal(state)) | ||||
| 
 | ||||
|     def has_state(self, state): | ||||
|         return lv_expr.obj_get_state(self.obj) & literal(state) != 0 | ||||
|         return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0 | ||||
| 
 | ||||
|     def is_pressed(self): | ||||
|         return self.has_state(LV_STATE.PRESSED) | ||||
| 
 | ||||
|     def is_checked(self): | ||||
|         return self.has_state(LV_STATE.CHECKED) | ||||
| 
 | ||||
|     def add_flag(self, flag): | ||||
|         return lv_obj.add_flag(self.obj, literal(flag)) | ||||
| @@ -93,32 +115,37 @@ class Widget: | ||||
|     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): | ||||
|     async def set_property(self, prop, value, animated: bool = None): | ||||
|         if isinstance(value, dict): | ||||
|             value = value.get(prop) | ||||
|             if isinstance(ALL_STYLES.get(prop), LValidator): | ||||
|                 value = await ALL_STYLES[prop].process(value) | ||||
|             else: | ||||
|                 value = literal(value) | ||||
|         if value is None: | ||||
|             return | ||||
|         if isinstance(value, TimePeriod): | ||||
|             value = value.total_milliseconds | ||||
|         ltype = ltype or self.__type_base() | ||||
|         if isinstance(value, str): | ||||
|             value = literal(value) | ||||
|         if animated is None or self.type.animated is not True: | ||||
|             lv.call(f"{ltype}_set_{prop}", self.obj, value) | ||||
|             lv.call(f"{self.type.lv_name}_set_{prop}", self.obj, value) | ||||
|         else: | ||||
|             lv.call( | ||||
|                 f"{ltype}_set_{prop}", | ||||
|                 f"{self.type.lv_name}_set_{prop}", | ||||
|                 self.obj, | ||||
|                 value, | ||||
|                 "LV_ANIM_ON" if animated else "LV_ANIM_OFF", | ||||
|                 literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"), | ||||
|             ) | ||||
| 
 | ||||
|     def get_property(self, prop, ltype=None): | ||||
|         ltype = ltype or self.__type_base() | ||||
|         return f"lv_{ltype}_get_{prop}({self.obj})" | ||||
|         return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") | ||||
| 
 | ||||
|     def set_style(self, prop, value, state): | ||||
|         if value is None: | ||||
|             return [] | ||||
|         return lv.call(f"obj_set_style_{prop}", self.obj, value, state) | ||||
|             return | ||||
|         lv.call(f"obj_set_style_{prop}", self.obj, value, state) | ||||
| 
 | ||||
|     def __type_base(self): | ||||
|         wtype = self.type.w_type | ||||
| @@ -140,6 +167,32 @@ class Widget: | ||||
|             return self.type.w_type.value(self) | ||||
|         return self.obj | ||||
| 
 | ||||
|     def get_number_value(self): | ||||
|         value = self.type.mock_obj.get_value(self.obj) | ||||
|         if self.scale == 1.0: | ||||
|             return value | ||||
|         return value / float(self.scale) | ||||
| 
 | ||||
|     def is_selected(self): | ||||
|         """ | ||||
|         Overridable property to determine if the widget is selected. Will be None except | ||||
|         for matrix buttons | ||||
|         :return: | ||||
|         """ | ||||
|         return None | ||||
| 
 | ||||
|     def get_max(self): | ||||
|         return self.type.get_max(self.config) | ||||
| 
 | ||||
|     def get_min(self): | ||||
|         return self.type.get_min(self.config) | ||||
| 
 | ||||
|     def get_step(self): | ||||
|         return self.type.get_step(self.config) | ||||
| 
 | ||||
|     def get_scale(self): | ||||
|         return self.type.get_scale(self.config) | ||||
| 
 | ||||
| 
 | ||||
| # Map of widgets to their config, used for trigger generation | ||||
| widget_map: dict[Any, Widget] = {} | ||||
| @@ -161,13 +214,20 @@ def get_widget_generator(wid): | ||||
|         yield | ||||
| 
 | ||||
| 
 | ||||
| async def get_widget(config: dict, id: str = CONF_ID) -> Widget: | ||||
|     wid = config[id] | ||||
| async def get_widget_(wid: Widget): | ||||
|     if obj := widget_map.get(wid): | ||||
|         return obj | ||||
|     return await FakeAwaitable(get_widget_generator(wid)) | ||||
| 
 | ||||
| 
 | ||||
| async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: | ||||
|     if not config: | ||||
|         return [] | ||||
|     if not isinstance(config, list): | ||||
|         config = [config] | ||||
|     return [await get_widget_(c[id]) for c in config if id in c] | ||||
| 
 | ||||
| 
 | ||||
| def collect_props(config): | ||||
|     """ | ||||
|     Collect all properties from a configuration | ||||
| @@ -175,7 +235,7 @@ def collect_props(config): | ||||
|     :return: | ||||
|     """ | ||||
|     props = {} | ||||
|     for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]: | ||||
|     for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]: | ||||
|         if prop in config: | ||||
|             props[prop] = config[prop] | ||||
|     return props | ||||
| @@ -209,12 +269,39 @@ def collect_parts(config): | ||||
| 
 | ||||
| async def set_obj_properties(w: Widget, config): | ||||
|     """Generate a list of C++ statements to apply properties to an lv_obj_t""" | ||||
|     if layout := config.get(CONF_LAYOUT): | ||||
|         layout_type: str = layout[CONF_TYPE] | ||||
|         lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) | ||||
|         if layout_type == TYPE_GRID: | ||||
|             wid = config[CONF_ID] | ||||
|             rows = [str(x) for x in layout[CONF_GRID_ROWS]] | ||||
|             rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}" | ||||
|             row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) | ||||
|             row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) | ||||
|             w.set_style("grid_row_dsc_array", row_array, 0) | ||||
|             columns = [str(x) for x in layout[CONF_GRID_COLUMNS]] | ||||
|             columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" | ||||
|             column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) | ||||
|             column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) | ||||
|             w.set_style("grid_column_dsc_array", column_array, 0) | ||||
|             w.set_style( | ||||
|                 CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0 | ||||
|             ) | ||||
|             w.set_style( | ||||
|                 CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0 | ||||
|             ) | ||||
|         if layout_type == TYPE_FLEX: | ||||
|             lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) | ||||
|             main = literal(layout[CONF_FLEX_ALIGN_MAIN]) | ||||
|             cross = literal(layout[CONF_FLEX_ALIGN_CROSS]) | ||||
|             track = literal(layout[CONF_FLEX_ALIGN_TRACK]) | ||||
|             lv_obj.set_flex_align(w.obj, main, cross, track) | ||||
|     parts = collect_parts(config) | ||||
|     for part, states in parts.items(): | ||||
|         for state, props in states.items(): | ||||
|             lv_state = ConstantLiteral( | ||||
|                 f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}" | ||||
|             ) | ||||
|             lv_state = join_enums((f"LV_STATE_{state}", f"LV_PART_{part}")) | ||||
|             for style_id in props.get(CONF_STYLES, ()): | ||||
|                 lv_obj.add_style(w.obj, MockObj(style_id), lv_state) | ||||
|             for prop, value in { | ||||
|                 k: v for k, v in props.items() if k in ALL_STYLES | ||||
|             }.items(): | ||||
| @@ -222,7 +309,8 @@ async def set_obj_properties(w: Widget, config): | ||||
|                     value = await ALL_STYLES[prop].process(value) | ||||
|                 prop_r = STYLE_REMAP.get(prop, prop) | ||||
|                 w.set_style(prop_r, value, lv_state) | ||||
|     if group := add_group(config.get(CONF_GROUP)): | ||||
|     if group := config.get(CONF_GROUP): | ||||
|         group = await cg.get_variable(group) | ||||
|         lv.group_add_obj(group, w.obj) | ||||
|     flag_clr = set() | ||||
|     flag_set = set() | ||||
| @@ -258,14 +346,12 @@ 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 = f"LV_STATE_{key.upper}" | ||||
|             lv.cond_if(lamb) | ||||
|             w.add_state(state) | ||||
|             lv.cond_else() | ||||
|             w.clear_state(state) | ||||
|             lv.cond_endif() | ||||
|     if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE): | ||||
|         lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode) | ||||
|             state = f"LV_STATE_{key.upper()}" | ||||
|             with LvConditional(f"{lamb}()") as cond: | ||||
|                 w.add_state(state) | ||||
|                 cond.else_() | ||||
|                 w.clear_state(state) | ||||
|     await w.set_property(CONF_SCROLLBAR_MODE, config) | ||||
| 
 | ||||
| 
 | ||||
| async def add_widgets(parent: Widget, config: dict): | ||||
| @@ -275,12 +361,12 @@ async def add_widgets(parent: Widget, config: dict): | ||||
|     :param config: The configuration | ||||
|     :return: | ||||
|     """ | ||||
|     for w in config.get(CONF_WIDGETS) or (): | ||||
|     for w in config.get(CONF_WIDGETS, ()): | ||||
|         w_type, w_cnfig = next(iter(w.items())) | ||||
|         await widget_to_code(w_cnfig, w_type, parent.obj) | ||||
| 
 | ||||
| 
 | ||||
| async def widget_to_code(w_cnfig, w_type, parent): | ||||
| async def widget_to_code(w_cnfig, w_type: WidgetType, parent): | ||||
|     """ | ||||
|     Converts a Widget definition to C code. | ||||
|     :param w_cnfig: The widget configuration | ||||
| @@ -298,19 +384,16 @@ async def widget_to_code(w_cnfig, w_type, parent): | ||||
|         var = cg.new_Pvariable(wid) | ||||
|         lv_add(var.set_obj(creator)) | ||||
|     else: | ||||
|         var = MockObj(wid, "->") | ||||
|         decl = VariableDeclarationExpression(lv_obj_t, "*", wid) | ||||
|         CORE.add_global(decl) | ||||
|         CORE.register_variable(wid, var) | ||||
|         var = lv_Pvariable(lv_obj_t, wid) | ||||
|         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) | ||||
|     w = Widget.create(wid, var, spec, w_cnfig) | ||||
|     if theme := theme_widget_map.get(w_type): | ||||
|         lv_add(CallExpression(theme, w.obj)) | ||||
|     await set_obj_properties(w, w_cnfig) | ||||
|     await add_widgets(w, w_cnfig) | ||||
|     await spec.to_code(w, w_cnfig) | ||||
| 
 | ||||
| 
 | ||||
| lv_scr_act_spec = LvScrActType() | ||||
| lv_scr_act = Widget.create( | ||||
|     None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None | ||||
| ) | ||||
| lv_scr_act = Widget.create(None, literal("lv_scr_act()"), lv_scr_act_spec, {}) | ||||
							
								
								
									
										117
									
								
								esphome/components/lvgl/widgets/animimg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								esphome/components/lvgl/widgets/animimg.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_DURATION, CONF_ID | ||||
| from esphome.cpp_generator import MockObj | ||||
|  | ||||
| from ..automation import action_to_code | ||||
| from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC | ||||
| from ..helpers import lvgl_components_required | ||||
| from ..lv_validation import lv_image, lv_milliseconds | ||||
| from ..lvcode import lv, lv_expr | ||||
| from ..types import LvType, ObjUpdateAction, void_ptr | ||||
| from . import Widget, WidgetType, get_widgets | ||||
| from .img import CONF_IMAGE | ||||
| from .label import CONF_LABEL | ||||
|  | ||||
| CONF_ANIMIMG = "animimg" | ||||
| CONF_SRC_LIST_ID = "src_list_id" | ||||
|  | ||||
|  | ||||
| def lv_repeat_count(value): | ||||
|     if isinstance(value, str) and value.lower() in ("forever", "infinite"): | ||||
|         value = 0xFFFF | ||||
|     return cv.int_range(min=0, max=0xFFFF)(value) | ||||
|  | ||||
|  | ||||
| ANIMIMG_BASE_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_REPEAT_COUNT, default="forever"): lv_repeat_count, | ||||
|         cv.Optional(CONF_AUTO_START, default=True): cv.boolean, | ||||
|     } | ||||
| ) | ||||
| ANIMIMG_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Required(CONF_DURATION): lv_milliseconds, | ||||
|         cv.Required(CONF_SRC): cv.ensure_list(lv_image), | ||||
|         cv.GenerateID(CONF_SRC_LIST_ID): cv.declare_id(void_ptr), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| ANIMIMG_MODIFY_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(CONF_DURATION): lv_milliseconds, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| lv_animimg_t = LvType("lv_animimg_t") | ||||
|  | ||||
|  | ||||
| class AnimimgType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_ANIMIMG, | ||||
|             lv_animimg_t, | ||||
|             (CONF_MAIN,), | ||||
|             ANIMIMG_SCHEMA, | ||||
|             ANIMIMG_MODIFY_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         lvgl_components_required.add(CONF_IMAGE) | ||||
|         lvgl_components_required.add(CONF_ANIMIMG) | ||||
|         if CONF_SRC in config: | ||||
|             for x in config[CONF_SRC]: | ||||
|                 await cg.get_variable(x) | ||||
|             srcs = [lv_expr.img_from(MockObj(x)) for x in config[CONF_SRC]] | ||||
|             src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs) | ||||
|             count = len(config[CONF_SRC]) | ||||
|             lv.animimg_set_src(w.obj, src_id, count) | ||||
|         lv.animimg_set_repeat_count(w.obj, config[CONF_REPEAT_COUNT]) | ||||
|         lv.animimg_set_duration(w.obj, config[CONF_DURATION]) | ||||
|         if config.get(CONF_AUTO_START): | ||||
|             lv.animimg_start(w.obj) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return CONF_IMAGE, CONF_LABEL | ||||
|  | ||||
|  | ||||
| animimg_spec = AnimimgType() | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.animimg.start", | ||||
|     ObjUpdateAction, | ||||
|     cv.maybe_simple_value( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(lv_animimg_t), | ||||
|         }, | ||||
|         key=CONF_ID, | ||||
|     ), | ||||
| ) | ||||
| async def animimg_start(config, action_id, template_arg, args): | ||||
|     widget = await get_widgets(config) | ||||
|  | ||||
|     async def do_start(w: Widget): | ||||
|         lv.animimg_start(w.obj) | ||||
|  | ||||
|     return await action_to_code(widget, do_start, action_id, template_arg, args) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.animimg.stop", | ||||
|     ObjUpdateAction, | ||||
|     cv.maybe_simple_value( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(lv_animimg_t), | ||||
|         }, | ||||
|         key=CONF_ID, | ||||
|     ), | ||||
| ) | ||||
| async def animimg_stop(config, action_id, template_arg, args): | ||||
|     widget = await get_widgets(config) | ||||
|  | ||||
|     async def do_stop(w: Widget): | ||||
|         lv.animimg_stop(w.obj) | ||||
|  | ||||
|     return await action_to_code(widget, do_stop, action_id, template_arg, args) | ||||
							
								
								
									
										78
									
								
								esphome/components/lvgl/widgets/arc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/lvgl/widgets/arc.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_MAX_VALUE, | ||||
|     CONF_MIN_VALUE, | ||||
|     CONF_MODE, | ||||
|     CONF_ROTATION, | ||||
|     CONF_VALUE, | ||||
| ) | ||||
| from esphome.cpp_types import nullptr | ||||
|  | ||||
| from ..defines import ( | ||||
|     ARC_MODES, | ||||
|     CONF_ADJUSTABLE, | ||||
|     CONF_CHANGE_RATE, | ||||
|     CONF_END_ANGLE, | ||||
|     CONF_INDICATOR, | ||||
|     CONF_KNOB, | ||||
|     CONF_MAIN, | ||||
|     CONF_START_ANGLE, | ||||
|     literal, | ||||
| ) | ||||
| from ..lv_validation import angle, get_start_value, lv_float | ||||
| from ..lvcode import lv, lv_obj | ||||
| from ..types import LvNumber, NumberType | ||||
| from . import Widget | ||||
|  | ||||
| CONF_ARC = "arc" | ||||
| ARC_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, | ||||
|         cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, | ||||
|         cv.Optional(CONF_START_ANGLE, default=135): angle, | ||||
|         cv.Optional(CONF_END_ANGLE, default=45): angle, | ||||
|         cv.Optional(CONF_ROTATION, default=0.0): angle, | ||||
|         cv.Optional(CONF_ADJUSTABLE, default=False): bool, | ||||
|         cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, | ||||
|         cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| ARC_MODIFY_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class ArcType(NumberType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_ARC, | ||||
|             LvNumber("lv_arc_t"), | ||||
|             parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), | ||||
|             schema=ARC_SCHEMA, | ||||
|             modify_schema=ARC_MODIFY_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if CONF_MIN_VALUE in config: | ||||
|             lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) | ||||
|             lv.arc_set_bg_angles( | ||||
|                 w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 | ||||
|             ) | ||||
|             lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) | ||||
|             lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) | ||||
|             lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) | ||||
|  | ||||
|         if config.get(CONF_ADJUSTABLE) is False: | ||||
|             lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) | ||||
|             w.clear_flag("LV_OBJ_FLAG_CLICKABLE") | ||||
|  | ||||
|         value = await get_start_value(config) | ||||
|         if value is not None: | ||||
|             lv.arc_set_value(w.obj, value) | ||||
|  | ||||
|  | ||||
| arc_spec = ArcType() | ||||
							
								
								
									
										20
									
								
								esphome/components/lvgl/widgets/button.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/lvgl/widgets/button.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| from esphome.const import CONF_BUTTON | ||||
|  | ||||
| from ..defines import CONF_MAIN | ||||
| from ..types import LvBoolean, WidgetType | ||||
|  | ||||
| lv_button_t = LvBoolean("lv_btn_t") | ||||
|  | ||||
|  | ||||
| class ButtonType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn") | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return ("btn",) | ||||
|  | ||||
|     async def to_code(self, w, config): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| button_spec = ButtonType() | ||||
							
								
								
									
										275
									
								
								esphome/components/lvgl/widgets/buttonmatrix.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								esphome/components/lvgl/widgets/buttonmatrix.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.key_provider import KeyProvider | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH | ||||
| from esphome.cpp_generator import MockObj | ||||
|  | ||||
| from ..automation import action_to_code | ||||
| from ..defines import ( | ||||
|     BUTTONMATRIX_CTRLS, | ||||
|     CONF_BUTTONS, | ||||
|     CONF_CONTROL, | ||||
|     CONF_KEY_CODE, | ||||
|     CONF_MAIN, | ||||
|     CONF_ONE_CHECKED, | ||||
|     CONF_ROWS, | ||||
|     CONF_SELECTED, | ||||
| ) | ||||
| from ..helpers import lvgl_components_required | ||||
| from ..lv_validation import key_code, lv_bool | ||||
| from ..lvcode import lv, lv_add, lv_expr | ||||
| from ..schemas import automation_schema | ||||
| from ..types import ( | ||||
|     LV_BTNMATRIX_CTRL, | ||||
|     LV_STATE, | ||||
|     LvBoolean, | ||||
|     LvCompound, | ||||
|     LvType, | ||||
|     ObjUpdateAction, | ||||
|     char_ptr, | ||||
|     lv_pseudo_button_t, | ||||
| ) | ||||
| from . import Widget, WidgetType, get_widgets, widget_map | ||||
| from .button import lv_button_t | ||||
|  | ||||
| CONF_BUTTONMATRIX = "buttonmatrix" | ||||
| CONF_BUTTON_TEXT_LIST_ID = "button_text_list_id" | ||||
|  | ||||
| LvButtonMatrixButton = LvBoolean( | ||||
|     str(cg.uint16), | ||||
|     parents=(lv_pseudo_button_t,), | ||||
| ) | ||||
| BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_TEXT): cv.string, | ||||
|         cv.Optional(CONF_KEY_CODE): key_code, | ||||
|         cv.GenerateID(): cv.declare_id(LvButtonMatrixButton), | ||||
|         cv.Optional(CONF_WIDTH, default=1): cv.positive_int, | ||||
|         cv.Optional(CONF_CONTROL): cv.ensure_list( | ||||
|             cv.Schema( | ||||
|                 {cv.Optional(k.lower()): cv.boolean for k in BUTTONMATRIX_CTRLS.choices} | ||||
|             ) | ||||
|         ), | ||||
|     } | ||||
| ).extend(automation_schema(lv_button_t)) | ||||
|  | ||||
| BUTTONMATRIX_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool, | ||||
|         cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), | ||||
|         cv.Required(CONF_ROWS): cv.ensure_list( | ||||
|             cv.Schema( | ||||
|                 { | ||||
|                     cv.Required(CONF_BUTTONS): cv.ensure_list( | ||||
|                         BUTTONMATRIX_BUTTON_SCHEMA | ||||
|                     ), | ||||
|                 } | ||||
|             ) | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class ButtonmatrixButtonType(WidgetType): | ||||
|     """ | ||||
|     A pseudo-widget for the matrix buttons | ||||
|     """ | ||||
|  | ||||
|     def __init__(self): | ||||
|         super().__init__("btnmatrix_btn", LvButtonMatrixButton, (), {}, {}) | ||||
|  | ||||
|     async def to_code(self, w, config: dict): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| btn_btn_spec = ButtonmatrixButtonType() | ||||
|  | ||||
|  | ||||
| class MatrixButton(Widget): | ||||
|     """ | ||||
|     Describes a button within a button matrix. | ||||
|     """ | ||||
|  | ||||
|     @staticmethod | ||||
|     def create_button(id, parent, config: dict, index): | ||||
|         w = MatrixButton(id, parent, config, index) | ||||
|         widget_map[id] = w | ||||
|         return w | ||||
|  | ||||
|     def __init__(self, id, parent: Widget, config, index): | ||||
|         super().__init__(id, btn_btn_spec, config) | ||||
|         self.parent = parent | ||||
|         self.index = index | ||||
|         self.obj = parent.obj | ||||
|  | ||||
|     def is_selected(self): | ||||
|         return self.parent.var.get_selected() == MockObj(self.var) | ||||
|  | ||||
|     @staticmethod | ||||
|     def map_ctrls(state): | ||||
|         state = str(state).upper().removeprefix("LV_STATE_") | ||||
|         assert state in BUTTONMATRIX_CTRLS.choices | ||||
|         return getattr(LV_BTNMATRIX_CTRL, state) | ||||
|  | ||||
|     def has_state(self, state): | ||||
|         state = self.map_ctrls(state) | ||||
|         return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state) | ||||
|  | ||||
|     def add_state(self, state): | ||||
|         state = self.map_ctrls(state) | ||||
|         return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state) | ||||
|  | ||||
|     def clear_state(self, state): | ||||
|         state = self.map_ctrls(state) | ||||
|         return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state) | ||||
|  | ||||
|     def is_pressed(self): | ||||
|         return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED) | ||||
|  | ||||
|     def is_checked(self): | ||||
|         return self.has_state(LV_STATE.CHECKED) | ||||
|  | ||||
|     def get_value(self): | ||||
|         return self.is_checked() | ||||
|  | ||||
|     def check_null(self): | ||||
|         return None | ||||
|  | ||||
|  | ||||
| async def get_button_data(config, buttonmatrix: Widget): | ||||
|     """ | ||||
|     Process a button matrix button list | ||||
|     :param config: The row list | ||||
|     :param buttonmatrix: The parent variable | ||||
|     :return: text array id, control list, width list | ||||
|     """ | ||||
|     text_list = [] | ||||
|     ctrl_list = [] | ||||
|     width_list = [] | ||||
|     key_list = [] | ||||
|     for row in config: | ||||
|         for button_conf in row.get(CONF_BUTTONS, ()): | ||||
|             bid = button_conf[CONF_ID] | ||||
|             index = len(width_list) | ||||
|             MatrixButton.create_button(bid, buttonmatrix, button_conf, index) | ||||
|             cg.new_variable(bid, index) | ||||
|             text_list.append(button_conf.get(CONF_TEXT) or "") | ||||
|             key_list.append(button_conf.get(CONF_KEY_CODE) or 0) | ||||
|             width_list.append(button_conf[CONF_WIDTH]) | ||||
|             ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"] | ||||
|             for item in button_conf.get(CONF_CONTROL, ()): | ||||
|                 ctrl.extend([k for k, v in item.items() if v]) | ||||
|             ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl)) | ||||
|         text_list.append("\n") | ||||
|     text_list = text_list[:-1] | ||||
|     text_list.append(cg.nullptr) | ||||
|     return text_list, ctrl_list, width_list, key_list | ||||
|  | ||||
|  | ||||
| lv_buttonmatrix_t = LvType( | ||||
|     "LvButtonMatrixType", | ||||
|     parents=(KeyProvider, LvCompound), | ||||
|     largs=[(cg.uint16, "x")], | ||||
|     lvalue=lambda w: w.var.get_selected(), | ||||
| ) | ||||
|  | ||||
|  | ||||
| class ButtonMatrixType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_BUTTONMATRIX, | ||||
|             lv_buttonmatrix_t, | ||||
|             (CONF_MAIN, CONF_ITEMS), | ||||
|             BUTTONMATRIX_SCHEMA, | ||||
|             {}, | ||||
|             lv_name="btnmatrix", | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         lvgl_components_required.add("BUTTONMATRIX") | ||||
|         if CONF_ROWS not in config: | ||||
|             return [] | ||||
|         text_list, ctrl_list, width_list, key_list = await get_button_data( | ||||
|             config[CONF_ROWS], w | ||||
|         ) | ||||
|         text_id = config[CONF_BUTTON_TEXT_LIST_ID] | ||||
|         text_id = cg.static_const_array(text_id, text_list) | ||||
|         lv.btnmatrix_set_map(w.obj, text_id) | ||||
|         set_btn_data(w.obj, ctrl_list, width_list) | ||||
|         lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) | ||||
|         for index, key in enumerate(key_list): | ||||
|             if key != 0: | ||||
|                 lv_add(w.var.set_key(index, key)) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return ("btnmatrix",) | ||||
|  | ||||
|  | ||||
| def set_btn_data(obj, ctrl_list, width_list): | ||||
|     for index, ctrl in enumerate(ctrl_list): | ||||
|         lv.btnmatrix_set_btn_ctrl(obj, index, ctrl) | ||||
|     for index, width in enumerate(width_list): | ||||
|         lv.btnmatrix_set_btn_width(obj, index, width) | ||||
|  | ||||
|  | ||||
| buttonmatrix_spec = ButtonMatrixType() | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.matrix.button.update", | ||||
|     ObjUpdateAction, | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Optional(CONF_WIDTH): cv.positive_int, | ||||
|             cv.Optional(CONF_CONTROL): cv.ensure_list( | ||||
|                 cv.Schema( | ||||
|                     { | ||||
|                         cv.Optional(k.lower()): cv.boolean | ||||
|                         for k in BUTTONMATRIX_CTRLS.choices | ||||
|                     } | ||||
|                 ), | ||||
|             ), | ||||
|             cv.Required(CONF_ID): cv.ensure_list( | ||||
|                 cv.maybe_simple_value( | ||||
|                     { | ||||
|                         cv.Required(CONF_ID): cv.use_id(LvButtonMatrixButton), | ||||
|                     }, | ||||
|                     key=CONF_ID, | ||||
|                 ) | ||||
|             ), | ||||
|             cv.Optional(CONF_SELECTED): lv_bool, | ||||
|         } | ||||
|     ), | ||||
| ) | ||||
| async def button_update_to_code(config, action_id, template_arg, args): | ||||
|     widgets = await get_widgets(config[CONF_ID]) | ||||
|     assert all(isinstance(w, MatrixButton) for w in widgets) | ||||
|  | ||||
|     async def do_button_update(w: MatrixButton): | ||||
|         if (width := config.get(CONF_WIDTH)) is not None: | ||||
|             lv.btnmatrix_set_btn_width(w.obj, w.index, width) | ||||
|         if config.get(CONF_SELECTED): | ||||
|             lv.btnmatrix_set_selected_btn(w.obj, w.index) | ||||
|         if controls := config.get(CONF_CONTROL): | ||||
|             adds = [] | ||||
|             clrs = [] | ||||
|             for item in controls: | ||||
|                 adds.extend( | ||||
|                     [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v] | ||||
|                 ) | ||||
|                 clrs.extend( | ||||
|                     [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v] | ||||
|                 ) | ||||
|             if adds: | ||||
|                 lv.btnmatrix_set_btn_ctrl( | ||||
|                     w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds) | ||||
|                 ) | ||||
|             if clrs: | ||||
|                 lv.btnmatrix_clear_btn_ctrl( | ||||
|                     w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs) | ||||
|                 ) | ||||
|  | ||||
|     return await action_to_code( | ||||
|         widgets, do_button_update, action_id, template_arg, args | ||||
|     ) | ||||
							
								
								
									
										27
									
								
								esphome/components/lvgl/widgets/checkbox.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/lvgl/widgets/checkbox.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| from esphome.const import CONF_TEXT | ||||
|  | ||||
| from ..defines import CONF_INDICATOR, CONF_MAIN | ||||
| from ..lv_validation import lv_text | ||||
| from ..lvcode import lv | ||||
| from ..schemas import TEXT_SCHEMA | ||||
| from ..types import LvBoolean | ||||
| from . import Widget, WidgetType | ||||
|  | ||||
| CONF_CHECKBOX = "checkbox" | ||||
|  | ||||
|  | ||||
| class CheckboxType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_CHECKBOX, | ||||
|             LvBoolean("lv_checkbox_t"), | ||||
|             (CONF_MAIN, CONF_INDICATOR), | ||||
|             TEXT_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if (value := config.get(CONF_TEXT)) is not None: | ||||
|             lv.checkbox_set_text(w.obj, await lv_text.process(value)) | ||||
|  | ||||
|  | ||||
| checkbox_spec = CheckboxType() | ||||
							
								
								
									
										76
									
								
								esphome/components/lvgl/widgets/dropdown.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/lvgl/widgets/dropdown.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_OPTIONS | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_DIR, | ||||
|     CONF_INDICATOR, | ||||
|     CONF_MAIN, | ||||
|     CONF_SELECTED_INDEX, | ||||
|     CONF_SYMBOL, | ||||
|     DIRECTIONS, | ||||
|     literal, | ||||
| ) | ||||
| from ..lv_validation import lv_int, lv_text, option_string | ||||
| from ..lvcode import LocalVariable, lv, lv_expr | ||||
| from ..schemas import part_schema | ||||
| from ..types import LvSelect, LvType, lv_obj_t | ||||
| from . import Widget, WidgetType, set_obj_properties | ||||
| from .label import CONF_LABEL | ||||
|  | ||||
| CONF_DROPDOWN = "dropdown" | ||||
| CONF_DROPDOWN_LIST = "dropdown_list" | ||||
|  | ||||
| lv_dropdown_t = LvSelect("lv_dropdown_t") | ||||
| lv_dropdown_list_t = LvType("lv_dropdown_list_t") | ||||
| dropdown_list_spec = WidgetType(CONF_DROPDOWN_LIST, lv_dropdown_list_t, (CONF_MAIN,)) | ||||
|  | ||||
| DROPDOWN_BASE_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_SYMBOL): lv_text, | ||||
|         cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_), | ||||
|         cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, | ||||
|         cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class DropdownType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_DROPDOWN, | ||||
|             lv_dropdown_t, | ||||
|             (CONF_MAIN, CONF_INDICATOR), | ||||
|             DROPDOWN_SCHEMA, | ||||
|             DROPDOWN_BASE_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if options := config.get(CONF_OPTIONS): | ||||
|             text = cg.safe_exp("\n".join(options)) | ||||
|             lv.dropdown_set_options(w.obj, text) | ||||
|         if symbol := config.get(CONF_SYMBOL): | ||||
|             lv.dropdown_set_symbol(w.obj, await lv_text.process(symbol)) | ||||
|         if (selected := config.get(CONF_SELECTED_INDEX)) is not None: | ||||
|             value = await lv_int.process(selected) | ||||
|             lv.dropdown_set_selected(w.obj, value) | ||||
|         if dirn := config.get(CONF_DIR): | ||||
|             lv.dropdown_set_dir(w.obj, literal(dirn)) | ||||
|         if dlist := config.get(CONF_DROPDOWN_LIST): | ||||
|             with LocalVariable( | ||||
|                 "dropdown_list", lv_obj_t, lv_expr.dropdown_get_list(w.obj) | ||||
|             ) as dlist_obj: | ||||
|                 dwid = Widget(dlist_obj, dropdown_list_spec, dlist) | ||||
|                 await set_obj_properties(dwid, dlist) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return (CONF_LABEL,) | ||||
|  | ||||
|  | ||||
| dropdown_spec = DropdownType() | ||||
							
								
								
									
										85
									
								
								esphome/components/lvgl/widgets/img.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								esphome/components/lvgl/widgets/img.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ANGLE, CONF_MODE | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_ANTIALIAS, | ||||
|     CONF_MAIN, | ||||
|     CONF_OFFSET_X, | ||||
|     CONF_OFFSET_Y, | ||||
|     CONF_PIVOT_X, | ||||
|     CONF_PIVOT_Y, | ||||
|     CONF_SRC, | ||||
|     CONF_ZOOM, | ||||
|     LvConstant, | ||||
| ) | ||||
| from ..lv_validation import angle, lv_bool, lv_image, size, zoom | ||||
| from ..lvcode import lv | ||||
| from ..types import lv_img_t | ||||
| from . import Widget, WidgetType | ||||
| from .label import CONF_LABEL | ||||
|  | ||||
| CONF_IMAGE = "image" | ||||
|  | ||||
| BASE_IMG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_PIVOT_X, default="50%"): size, | ||||
|         cv.Optional(CONF_PIVOT_Y, default="50%"): size, | ||||
|         cv.Optional(CONF_ANGLE): angle, | ||||
|         cv.Optional(CONF_ZOOM): zoom, | ||||
|         cv.Optional(CONF_OFFSET_X): size, | ||||
|         cv.Optional(CONF_OFFSET_Y): size, | ||||
|         cv.Optional(CONF_ANTIALIAS): lv_bool, | ||||
|         cv.Optional(CONF_MODE): LvConstant( | ||||
|             "LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL" | ||||
|         ).one_of, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| IMG_SCHEMA = BASE_IMG_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Required(CONF_SRC): lv_image, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| IMG_MODIFY_SCHEMA = BASE_IMG_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(CONF_SRC): lv_image, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class ImgType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_IMAGE, | ||||
|             lv_img_t, | ||||
|             (CONF_MAIN,), | ||||
|             IMG_SCHEMA, | ||||
|             IMG_MODIFY_SCHEMA, | ||||
|             lv_name="img", | ||||
|         ) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return "img", CONF_LABEL | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if src := config.get(CONF_SRC): | ||||
|             lv.img_set_src(w.obj, await lv_image.process(src)) | ||||
|         if (cf_angle := config.get(CONF_ANGLE)) is not None: | ||||
|             pivot_x = config[CONF_PIVOT_X] | ||||
|             pivot_y = config[CONF_PIVOT_Y] | ||||
|             lv.img_set_pivot(w.obj, pivot_x, pivot_y) | ||||
|             lv.img_set_angle(w.obj, cf_angle) | ||||
|         if (img_zoom := config.get(CONF_ZOOM)) is not None: | ||||
|             lv.img_set_zoom(w.obj, img_zoom) | ||||
|         if (offset := config.get(CONF_OFFSET_X)) is not None: | ||||
|             lv.img_set_offset_x(w.obj, offset) | ||||
|         if (offset := config.get(CONF_OFFSET_Y)) is not None: | ||||
|             lv.img_set_offset_y(w.obj, offset) | ||||
|         if CONF_ANTIALIAS in config: | ||||
|             lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) | ||||
|         if mode := config.get(CONF_MODE): | ||||
|             lv.img_set_mode(w.obj, mode) | ||||
|  | ||||
|  | ||||
| img_spec = ImgType() | ||||
							
								
								
									
										49
									
								
								esphome/components/lvgl/widgets/keyboard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								esphome/components/lvgl/widgets/keyboard.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| from esphome.components.key_provider import KeyProvider | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ITEMS, CONF_MODE | ||||
| from esphome.cpp_types import std_string | ||||
|  | ||||
| from ..defines import CONF_MAIN, KEYBOARD_MODES, literal | ||||
| from ..helpers import add_lv_use, lvgl_components_required | ||||
| from ..types import LvCompound, LvType | ||||
| from . import Widget, WidgetType, get_widgets | ||||
| from .textarea import CONF_TEXTAREA, lv_textarea_t | ||||
|  | ||||
| CONF_KEYBOARD = "keyboard" | ||||
|  | ||||
| KEYBOARD_SCHEMA = { | ||||
|     cv.Optional(CONF_MODE, default="TEXT_UPPER"): KEYBOARD_MODES.one_of, | ||||
|     cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), | ||||
| } | ||||
|  | ||||
| lv_keyboard_t = LvType( | ||||
|     "LvKeyboardType", | ||||
|     parents=(KeyProvider, LvCompound), | ||||
|     largs=[(std_string, "text")], | ||||
|     has_on_value=True, | ||||
|     lvalue=lambda w: literal(f"lv_textarea_get_text({w.obj})"), | ||||
| ) | ||||
|  | ||||
|  | ||||
| class KeyboardType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_KEYBOARD, | ||||
|             lv_keyboard_t, | ||||
|             (CONF_MAIN, CONF_ITEMS), | ||||
|             KEYBOARD_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return CONF_KEYBOARD, CONF_TEXTAREA | ||||
|  | ||||
|     async def to_code(self, w: Widget, config: dict): | ||||
|         lvgl_components_required.add("KEY_LISTENER") | ||||
|         lvgl_components_required.add(CONF_KEYBOARD) | ||||
|         add_lv_use("btnmatrix") | ||||
|         await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE])) | ||||
|         if ta := await get_widgets(config, CONF_TEXTAREA): | ||||
|             await w.set_property(CONF_TEXTAREA, ta[0].obj) | ||||
|  | ||||
|  | ||||
| keyboard_spec = KeyboardType() | ||||
| @@ -1,19 +1,20 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_TEXT | ||||
| 
 | ||||
| from .defines import ( | ||||
|     CONF_LABEL, | ||||
| from ..defines import ( | ||||
|     CONF_LONG_MODE, | ||||
|     CONF_MAIN, | ||||
|     CONF_RECOLOR, | ||||
|     CONF_SCROLLBAR, | ||||
|     CONF_SELECTED, | ||||
|     CONF_TEXT, | ||||
|     LV_LONG_MODES, | ||||
| ) | ||||
| from .lv_validation import lv_bool, lv_text | ||||
| from .schemas import TEXT_SCHEMA | ||||
| from .types import LvText, WidgetType | ||||
| from .widget import Widget | ||||
| from ..lv_validation import lv_bool, lv_text | ||||
| from ..schemas import TEXT_SCHEMA | ||||
| from ..types import LvText, WidgetType | ||||
| from . import Widget | ||||
| 
 | ||||
| CONF_LABEL = "label" | ||||
| 
 | ||||
| 
 | ||||
| class LabelType(WidgetType): | ||||
| @@ -33,9 +34,9 @@ class LabelType(WidgetType): | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         """For a text object, create and set text""" | ||||
|         if value := config.get(CONF_TEXT): | ||||
|             w.set_property(CONF_TEXT, await lv_text.process(value)) | ||||
|         w.set_property(CONF_LONG_MODE, config) | ||||
|         w.set_property(CONF_RECOLOR, config) | ||||
|             await w.set_property(CONF_TEXT, await lv_text.process(value)) | ||||
|         await w.set_property(CONF_LONG_MODE, config) | ||||
|         await w.set_property(CONF_RECOLOR, config) | ||||
| 
 | ||||
| 
 | ||||
| label_spec = LabelType() | ||||
							
								
								
									
										29
									
								
								esphome/components/lvgl/widgets/led.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/lvgl/widgets/led.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED | ||||
|  | ||||
| from ..defines import CONF_MAIN | ||||
| from ..lv_validation import lv_brightness, lv_color | ||||
| from ..lvcode import lv | ||||
| from ..types import LvType | ||||
| from . import Widget, WidgetType | ||||
|  | ||||
| LED_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_COLOR): lv_color, | ||||
|         cv.Optional(CONF_BRIGHTNESS): lv_brightness, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class LedType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if (color := config.get(CONF_COLOR)) is not None: | ||||
|             lv.led_set_color(w.obj, await lv_color.process(color)) | ||||
|         if (brightness := config.get(CONF_BRIGHTNESS)) is not None: | ||||
|             lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) | ||||
|  | ||||
|  | ||||
| led_spec = LedType() | ||||
							
								
								
									
										50
									
								
								esphome/components/lvgl/widgets/line.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								esphome/components/lvgl/widgets/line.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import functools | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from ..defines import CONF_MAIN, literal | ||||
| from ..lvcode import lv | ||||
| from ..types import LvType | ||||
| from . import Widget, WidgetType | ||||
|  | ||||
| CONF_LINE = "line" | ||||
| CONF_POINTS = "points" | ||||
| CONF_POINT_LIST_ID = "point_list_id" | ||||
|  | ||||
| lv_point_t = cg.global_ns.struct("lv_point_t") | ||||
|  | ||||
|  | ||||
| def point_list(il): | ||||
|     il = cv.string(il) | ||||
|     nl = il.replace(" ", "").split(",") | ||||
|     return [int(n) for n in nl] | ||||
|  | ||||
|  | ||||
| def cv_point_list(value): | ||||
|     if not isinstance(value, list): | ||||
|         raise cv.Invalid("List of points required") | ||||
|     values = [point_list(v) for v in value] | ||||
|     if not functools.reduce(lambda f, v: f and len(v) == 2, values, True): | ||||
|         raise cv.Invalid("Points must be a list of x,y integer pairs") | ||||
|     return values | ||||
|  | ||||
|  | ||||
| LINE_SCHEMA = { | ||||
|     cv.Required(CONF_POINTS): cv_point_list, | ||||
|     cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t), | ||||
| } | ||||
|  | ||||
|  | ||||
| class LineType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__(CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         """For a line object, create and add the points""" | ||||
|         data = literal(config[CONF_POINTS]) | ||||
|         points = cg.static_const_array(config[CONF_POINT_LIST_ID], data) | ||||
|         lv.line_set_points(w.obj, points, len(data)) | ||||
|  | ||||
|  | ||||
| line_spec = LineType() | ||||
							
								
								
									
										55
									
								
								esphome/components/lvgl/widgets/lv_bar.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								esphome/components/lvgl/widgets/lv_bar.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE | ||||
|  | ||||
| from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal | ||||
| from ..lv_validation import animated, get_start_value, lv_float | ||||
| from ..lvcode import lv | ||||
| from ..types import LvNumber, NumberType | ||||
| from . import Widget | ||||
|  | ||||
| # Note this file cannot be called "bar.py" because that name is disallowed. | ||||
|  | ||||
| CONF_BAR = "bar" | ||||
| BAR_MODIFY_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| BAR_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, | ||||
|         cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, | ||||
|         cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, | ||||
|         cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class BarType(NumberType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_BAR, | ||||
|             LvNumber("lv_bar_t"), | ||||
|             parts=(CONF_MAIN, CONF_INDICATOR), | ||||
|             schema=BAR_SCHEMA, | ||||
|             modify_schema=BAR_MODIFY_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         var = w.obj | ||||
|         if CONF_MIN_VALUE in config: | ||||
|             lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) | ||||
|             lv.bar_set_mode(var, literal(config[CONF_MODE])) | ||||
|         value = await get_start_value(config) | ||||
|         if value is not None: | ||||
|             lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) | ||||
|  | ||||
|     @property | ||||
|     def animated(self): | ||||
|         return True | ||||
|  | ||||
|  | ||||
| bar_spec = BarType() | ||||
							
								
								
									
										302
									
								
								esphome/components/lvgl/widgets/meter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								esphome/components/lvgl/widgets/meter.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,302 @@ | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_COLOR, | ||||
|     CONF_COUNT, | ||||
|     CONF_ID, | ||||
|     CONF_LENGTH, | ||||
|     CONF_LOCAL, | ||||
|     CONF_RANGE_FROM, | ||||
|     CONF_RANGE_TO, | ||||
|     CONF_ROTATION, | ||||
|     CONF_VALUE, | ||||
|     CONF_WIDTH, | ||||
| ) | ||||
|  | ||||
| from ..automation import action_to_code | ||||
| from ..defines import ( | ||||
|     CONF_END_VALUE, | ||||
|     CONF_MAIN, | ||||
|     CONF_PIVOT_X, | ||||
|     CONF_PIVOT_Y, | ||||
|     CONF_SRC, | ||||
|     CONF_START_VALUE, | ||||
|     CONF_TICKS, | ||||
| ) | ||||
| from ..helpers import add_lv_use | ||||
| from ..lv_validation import ( | ||||
|     angle, | ||||
|     get_end_value, | ||||
|     get_start_value, | ||||
|     lv_bool, | ||||
|     lv_color, | ||||
|     lv_float, | ||||
|     lv_image, | ||||
|     requires_component, | ||||
|     size, | ||||
| ) | ||||
| from ..lvcode import LocalVariable, lv, lv_assign, lv_expr | ||||
| from ..types import LvType, ObjUpdateAction | ||||
| from . import Widget, WidgetType, get_widgets | ||||
| from .arc import CONF_ARC | ||||
| from .img import CONF_IMAGE | ||||
| from .line import CONF_LINE | ||||
| from .obj import obj_spec | ||||
|  | ||||
| CONF_ANGLE_RANGE = "angle_range" | ||||
| CONF_COLOR_END = "color_end" | ||||
| CONF_COLOR_START = "color_start" | ||||
| CONF_INDICATORS = "indicators" | ||||
| CONF_LABEL_GAP = "label_gap" | ||||
| CONF_MAJOR = "major" | ||||
| CONF_METER = "meter" | ||||
| CONF_R_MOD = "r_mod" | ||||
| CONF_SCALES = "scales" | ||||
| CONF_STRIDE = "stride" | ||||
| CONF_TICK_STYLE = "tick_style" | ||||
|  | ||||
| lv_meter_t = LvType("lv_meter_t") | ||||
| lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t") | ||||
| lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr") | ||||
|  | ||||
|  | ||||
| def pixels(value): | ||||
|     """A size in one axis in pixels""" | ||||
|     if isinstance(value, str) and value.lower().endswith("px"): | ||||
|         return cv.int_(value[:-2]) | ||||
|     return cv.int_(value) | ||||
|  | ||||
|  | ||||
| INDICATOR_LINE_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_WIDTH, default=4): size, | ||||
|         cv.Optional(CONF_COLOR, default=0): lv_color, | ||||
|         cv.Optional(CONF_R_MOD, default=0): size, | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|     } | ||||
| ) | ||||
| INDICATOR_IMG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_SRC): lv_image, | ||||
|         cv.Required(CONF_PIVOT_X): pixels, | ||||
|         cv.Required(CONF_PIVOT_Y): pixels, | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|     } | ||||
| ) | ||||
| INDICATOR_ARC_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_WIDTH, default=4): size, | ||||
|         cv.Optional(CONF_COLOR, default=0): lv_color, | ||||
|         cv.Optional(CONF_R_MOD, default=0): size, | ||||
|         cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, | ||||
|         cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_END_VALUE): lv_float, | ||||
|     } | ||||
| ) | ||||
| INDICATOR_TICKS_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_WIDTH, default=4): size, | ||||
|         cv.Optional(CONF_COLOR_START, default=0): lv_color, | ||||
|         cv.Optional(CONF_COLOR_END): lv_color, | ||||
|         cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, | ||||
|         cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_END_VALUE): lv_float, | ||||
|         cv.Optional(CONF_LOCAL, default=False): lv_bool, | ||||
|     } | ||||
| ) | ||||
| INDICATOR_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), | ||||
|             } | ||||
|         ), | ||||
|         cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All( | ||||
|             INDICATOR_IMG_SCHEMA.extend( | ||||
|                 { | ||||
|                     cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), | ||||
|                 } | ||||
|             ), | ||||
|             requires_component("image"), | ||||
|         ), | ||||
|         cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), | ||||
|             } | ||||
|         ), | ||||
|         cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| SCALE_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_TICKS): cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_COUNT, default=12): cv.positive_int, | ||||
|                 cv.Optional(CONF_WIDTH, default=2): size, | ||||
|                 cv.Optional(CONF_LENGTH, default=10): size, | ||||
|                 cv.Optional(CONF_COLOR, default=0x808080): lv_color, | ||||
|                 cv.Optional(CONF_MAJOR): cv.Schema( | ||||
|                     { | ||||
|                         cv.Optional(CONF_STRIDE, default=3): cv.positive_int, | ||||
|                         cv.Optional(CONF_WIDTH, default=5): size, | ||||
|                         cv.Optional(CONF_LENGTH, default="15%"): size, | ||||
|                         cv.Optional(CONF_COLOR, default=0): lv_color, | ||||
|                         cv.Optional(CONF_LABEL_GAP, default=4): size, | ||||
|                     } | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, | ||||
|         cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, | ||||
|         cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), | ||||
|         cv.Optional(CONF_ROTATION): angle, | ||||
|         cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)} | ||||
|  | ||||
|  | ||||
| class MeterType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__(CONF_METER, lv_meter_t, (CONF_MAIN,), METER_SCHEMA) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         """For a meter object, create and set parameters""" | ||||
|  | ||||
|         var = w.obj | ||||
|         for scale_conf in config.get(CONF_SCALES, ()): | ||||
|             rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 | ||||
|             if CONF_ROTATION in scale_conf: | ||||
|                 rotation = scale_conf[CONF_ROTATION] // 10 | ||||
|             with LocalVariable( | ||||
|                 "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) | ||||
|             ) as meter_var: | ||||
|                 lv.meter_set_scale_range( | ||||
|                     var, | ||||
|                     meter_var, | ||||
|                     scale_conf[CONF_RANGE_FROM], | ||||
|                     scale_conf[CONF_RANGE_TO], | ||||
|                     scale_conf[CONF_ANGLE_RANGE], | ||||
|                     rotation, | ||||
|                 ) | ||||
|                 if ticks := scale_conf.get(CONF_TICKS): | ||||
|                     color = await lv_color.process(ticks[CONF_COLOR]) | ||||
|                     lv.meter_set_scale_ticks( | ||||
|                         var, | ||||
|                         meter_var, | ||||
|                         ticks[CONF_COUNT], | ||||
|                         ticks[CONF_WIDTH], | ||||
|                         ticks[CONF_LENGTH], | ||||
|                         color, | ||||
|                     ) | ||||
|                     if CONF_MAJOR in ticks: | ||||
|                         major = ticks[CONF_MAJOR] | ||||
|                         color = await lv_color.process(major[CONF_COLOR]) | ||||
|                         lv.meter_set_scale_major_ticks( | ||||
|                             var, | ||||
|                             meter_var, | ||||
|                             major[CONF_STRIDE], | ||||
|                             major[CONF_WIDTH], | ||||
|                             major[CONF_LENGTH], | ||||
|                             color, | ||||
|                             major[CONF_LABEL_GAP], | ||||
|                         ) | ||||
|                 for indicator in scale_conf.get(CONF_INDICATORS, ()): | ||||
|                     (t, v) = next(iter(indicator.items())) | ||||
|                     iid = v[CONF_ID] | ||||
|                     ivar = cg.new_variable( | ||||
|                         iid, cg.nullptr, type_=lv_meter_indicator_t_ptr | ||||
|                     ) | ||||
|                     # Enable getting the meter to which this belongs. | ||||
|                     wid = Widget.create(iid, var, obj_spec, v) | ||||
|                     wid.obj = ivar | ||||
|                     if t == CONF_LINE: | ||||
|                         color = await lv_color.process(v[CONF_COLOR]) | ||||
|                         lv_assign( | ||||
|                             ivar, | ||||
|                             lv_expr.meter_add_needle_line( | ||||
|                                 var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] | ||||
|                             ), | ||||
|                         ) | ||||
|                     if t == CONF_ARC: | ||||
|                         color = await lv_color.process(v[CONF_COLOR]) | ||||
|                         lv_assign( | ||||
|                             ivar, | ||||
|                             lv_expr.meter_add_arc( | ||||
|                                 var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] | ||||
|                             ), | ||||
|                         ) | ||||
|                     if t == CONF_TICK_STYLE: | ||||
|                         color_start = await lv_color.process(v[CONF_COLOR_START]) | ||||
|                         color_end = await lv_color.process( | ||||
|                             v.get(CONF_COLOR_END) or color_start | ||||
|                         ) | ||||
|                         lv_assign( | ||||
|                             ivar, | ||||
|                             lv_expr.meter_add_scale_lines( | ||||
|                                 var, | ||||
|                                 meter_var, | ||||
|                                 color_start, | ||||
|                                 color_end, | ||||
|                                 v[CONF_LOCAL], | ||||
|                                 v[CONF_WIDTH], | ||||
|                             ), | ||||
|                         ) | ||||
|                     if t == CONF_IMAGE: | ||||
|                         add_lv_use("img") | ||||
|                         lv_assign( | ||||
|                             ivar, | ||||
|                             lv_expr.meter_add_needle_img( | ||||
|                                 var, | ||||
|                                 meter_var, | ||||
|                                 await lv_image.process(v[CONF_SRC]), | ||||
|                                 v[CONF_PIVOT_X], | ||||
|                                 v[CONF_PIVOT_Y], | ||||
|                             ), | ||||
|                         ) | ||||
|                     start_value = await get_start_value(v) | ||||
|                     end_value = await get_end_value(v) | ||||
|                     set_indicator_values(var, ivar, start_value, end_value) | ||||
|  | ||||
|  | ||||
| meter_spec = MeterType() | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.indicator.update", | ||||
|     ObjUpdateAction, | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(lv_meter_indicator_t), | ||||
|             cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, | ||||
|             cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, | ||||
|             cv.Optional(CONF_END_VALUE): lv_float, | ||||
|         } | ||||
|     ), | ||||
| ) | ||||
| async def indicator_update_to_code(config, action_id, template_arg, args): | ||||
|     widget = await get_widgets(config) | ||||
|     start_value = await get_start_value(config) | ||||
|     end_value = await get_end_value(config) | ||||
|  | ||||
|     async def set_value(w: Widget): | ||||
|         set_indicator_values(w.var, w.obj, start_value, end_value) | ||||
|  | ||||
|     return await action_to_code(widget, set_value, action_id, template_arg, args) | ||||
|  | ||||
|  | ||||
| def set_indicator_values(meter, indicator, start_value, end_value): | ||||
|     if start_value is not None: | ||||
|         if end_value is None: | ||||
|             lv.meter_set_indicator_value(meter, indicator, start_value) | ||||
|         else: | ||||
|             lv.meter_set_indicator_start_value(meter, indicator, start_value) | ||||
|     if end_value is not None: | ||||
|         lv.meter_set_indicator_end_value(meter, indicator, end_value) | ||||
							
								
								
									
										134
									
								
								esphome/components/lvgl/widgets/msgbox.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								esphome/components/lvgl/widgets/msgbox.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| from esphome import config_validation as cv | ||||
| from esphome.const import CONF_BUTTON, CONF_ID, CONF_TEXT | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import new_Pvariable, static_const_array | ||||
| from esphome.cpp_types import nullptr | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_BODY, | ||||
|     CONF_BUTTONS, | ||||
|     CONF_CLOSE_BUTTON, | ||||
|     CONF_MSGBOXES, | ||||
|     CONF_TITLE, | ||||
|     TYPE_FLEX, | ||||
|     literal, | ||||
| ) | ||||
| from ..helpers import add_lv_use | ||||
| from ..lv_validation import lv_bool, lv_pct, lv_text | ||||
| from ..lvcode import ( | ||||
|     EVENT_ARG, | ||||
|     LambdaContext, | ||||
|     LocalVariable, | ||||
|     lv_add, | ||||
|     lv_assign, | ||||
|     lv_expr, | ||||
|     lv_obj, | ||||
|     lv_Pvariable, | ||||
| ) | ||||
| from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema | ||||
| from ..styles import TOP_LAYER | ||||
| from ..types import LV_EVENT, char_ptr, lv_obj_t | ||||
| from . import Widget, set_obj_properties | ||||
| from .button import button_spec | ||||
| from .buttonmatrix import ( | ||||
|     BUTTONMATRIX_BUTTON_SCHEMA, | ||||
|     CONF_BUTTON_TEXT_LIST_ID, | ||||
|     buttonmatrix_spec, | ||||
|     get_button_data, | ||||
|     lv_buttonmatrix_t, | ||||
|     set_btn_data, | ||||
| ) | ||||
| from .label import CONF_LABEL | ||||
| from .obj import obj_spec | ||||
|  | ||||
| CONF_MSGBOX = "msgbox" | ||||
| MSGBOX_SCHEMA = container_schema( | ||||
|     obj_spec, | ||||
|     STYLE_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t), | ||||
|             cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, | ||||
|             cv.Optional(CONF_BODY): STYLED_TEXT_SCHEMA, | ||||
|             cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), | ||||
|             cv.Optional(CONF_CLOSE_BUTTON): lv_bool, | ||||
|             cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), | ||||
|         } | ||||
|     ), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def msgbox_to_code(conf): | ||||
|     """ | ||||
|     Construct a message box. This consists of a full-screen translucent background enclosing a centered container | ||||
|     with an optional title, body, close button and a button matrix. And any other widgets the user cares to add | ||||
|     :param conf: The config data | ||||
|     :return: code to add to the init lambda | ||||
|     """ | ||||
|     add_lv_use( | ||||
|         TYPE_FLEX, | ||||
|         CONF_BUTTON, | ||||
|         CONF_LABEL, | ||||
|         CONF_MSGBOX, | ||||
|         *buttonmatrix_spec.get_uses(), | ||||
|         *button_spec.get_uses(), | ||||
|     ) | ||||
|     messagebox_id = conf[CONF_ID] | ||||
|     outer = lv_Pvariable(lv_obj_t, messagebox_id.id) | ||||
|     buttonmatrix = new_Pvariable( | ||||
|         ID( | ||||
|             f"{messagebox_id.id}_buttonmatrix_", | ||||
|             is_declaration=True, | ||||
|             type=lv_buttonmatrix_t, | ||||
|         ) | ||||
|     ) | ||||
|     msgbox = lv_Pvariable(lv_obj_t, f"{messagebox_id.id}_msgbox") | ||||
|     outer_widget = Widget.create(messagebox_id, outer, obj_spec, conf) | ||||
|     buttonmatrix_widget = Widget.create( | ||||
|         str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf | ||||
|     ) | ||||
|     text_list, ctrl_list, width_list, _ = await get_button_data( | ||||
|         (conf,), buttonmatrix_widget | ||||
|     ) | ||||
|     text_id = conf[CONF_BUTTON_TEXT_LIST_ID] | ||||
|     text_list = static_const_array(text_id, text_list) | ||||
|     if (text := conf.get(CONF_BODY)) is not None: | ||||
|         text = await lv_text.process(text.get(CONF_TEXT)) | ||||
|     if (title := conf.get(CONF_TITLE)) is not None: | ||||
|         title = await lv_text.process(title.get(CONF_TEXT)) | ||||
|     close_button = conf[CONF_CLOSE_BUTTON] | ||||
|     lv_assign(outer, lv_expr.obj_create(TOP_LAYER)) | ||||
|     lv_obj.set_width(outer, lv_pct(100)) | ||||
|     lv_obj.set_height(outer, lv_pct(100)) | ||||
|     lv_obj.set_style_bg_opa(outer, 128, 0) | ||||
|     lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0) | ||||
|     lv_obj.set_style_border_width(outer, 0, 0) | ||||
|     lv_obj.set_style_pad_all(outer, 0, 0) | ||||
|     lv_obj.set_style_radius(outer, 0, 0) | ||||
|     outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") | ||||
|     lv_assign( | ||||
|         msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button) | ||||
|     ) | ||||
|     lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) | ||||
|     lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox))) | ||||
|     await set_obj_properties(outer_widget, conf) | ||||
|     if close_button: | ||||
|         async with LambdaContext(EVENT_ARG, where=messagebox_id) as context: | ||||
|             outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") | ||||
|         with LocalVariable( | ||||
|             "close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox) | ||||
|         ) as close_btn: | ||||
|             lv_obj.remove_event_cb(close_btn, nullptr) | ||||
|             lv_obj.add_event_cb( | ||||
|                 close_btn, | ||||
|                 await context.get_lambda(), | ||||
|                 LV_EVENT.CLICKED, | ||||
|                 nullptr, | ||||
|             ) | ||||
|  | ||||
|     if len(ctrl_list) != 0 or len(width_list) != 0: | ||||
|         set_btn_data(buttonmatrix.obj, ctrl_list, width_list) | ||||
|  | ||||
|  | ||||
| async def msgboxes_to_code(config): | ||||
|     for conf in config.get(CONF_MSGBOXES, ()): | ||||
|         await msgbox_to_code(conf) | ||||
| @@ -1,9 +1,9 @@ | ||||
| from esphome import automation | ||||
| 
 | ||||
| from .automation import update_to_code | ||||
| from .defines import CONF_MAIN, CONF_OBJ | ||||
| from .schemas import create_modify_schema | ||||
| from .types import ObjUpdateAction, WidgetType, lv_obj_t | ||||
| from ..automation import update_to_code | ||||
| from ..defines import CONF_MAIN, CONF_OBJ | ||||
| from ..schemas import create_modify_schema | ||||
| from ..types import ObjUpdateAction, WidgetType, lv_obj_t | ||||
| 
 | ||||
| 
 | ||||
| class ObjType(WidgetType): | ||||
							
								
								
									
										113
									
								
								esphome/components/lvgl/widgets/page.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								esphome/components/lvgl/widgets/page.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| from esphome import automation, codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_ANIMATION, | ||||
|     CONF_LVGL_ID, | ||||
|     CONF_PAGE, | ||||
|     CONF_PAGE_WRAP, | ||||
|     CONF_SKIP, | ||||
|     LV_ANIM, | ||||
| ) | ||||
| from ..lv_validation import lv_bool, lv_milliseconds | ||||
| from ..lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp | ||||
| from ..schemas import LVGL_SCHEMA | ||||
| from ..types import LvglAction, lv_page_t | ||||
| from . import Widget, WidgetType, add_widgets, set_obj_properties | ||||
|  | ||||
|  | ||||
| class PageType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_PAGE, | ||||
|             lv_page_t, | ||||
|             (), | ||||
|             { | ||||
|                 cv.Optional(CONF_SKIP, default=False): lv_bool, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config: dict): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| SHOW_SCHEMA = LVGL_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(CONF_ANIMATION, default="NONE"): LV_ANIM.one_of, | ||||
|         cv.Optional(CONF_TIME, default="50ms"): lv_milliseconds, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| page_spec = PageType() | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.page.next", | ||||
|     LvglAction, | ||||
|     SHOW_SCHEMA, | ||||
| ) | ||||
| async def page_next_to_code(config, action_id, template_arg, args): | ||||
|     animation = await LV_ANIM.process(config[CONF_ANIMATION]) | ||||
|     time = await lv_milliseconds.process(config[CONF_TIME]) | ||||
|     async with LambdaContext(LVGL_COMP_ARG) as context: | ||||
|         add_line_marks(action_id) | ||||
|         lv_add(lvgl_comp.show_next_page(animation, time)) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) | ||||
|     await cg.register_parented(var, config[CONF_LVGL_ID]) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.page.previous", | ||||
|     LvglAction, | ||||
|     SHOW_SCHEMA, | ||||
| ) | ||||
| async def page_previous_to_code(config, action_id, template_arg, args): | ||||
|     animation = await LV_ANIM.process(config[CONF_ANIMATION]) | ||||
|     time = await lv_milliseconds.process(config[CONF_TIME]) | ||||
|     async with LambdaContext(LVGL_COMP_ARG) as context: | ||||
|         add_line_marks(action_id) | ||||
|         lv_add(lvgl_comp.show_prev_page(animation, time)) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) | ||||
|     await cg.register_parented(var, config[CONF_LVGL_ID]) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.page.show", | ||||
|     LvglAction, | ||||
|     cv.maybe_simple_value( | ||||
|         SHOW_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.Required(CONF_ID): cv.use_id(lv_page_t), | ||||
|             } | ||||
|         ), | ||||
|         key=CONF_ID, | ||||
|     ), | ||||
| ) | ||||
| async def page_show_to_code(config, action_id, template_arg, args): | ||||
|     widget = await cg.get_variable(config[CONF_ID]) | ||||
|     animation = await LV_ANIM.process(config[CONF_ANIMATION]) | ||||
|     time = await lv_milliseconds.process(config[CONF_TIME]) | ||||
|     async with LambdaContext(LVGL_COMP_ARG) as context: | ||||
|         add_line_marks(action_id) | ||||
|         lv_add(lvgl_comp.show_page(widget.index, animation, time)) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) | ||||
|     await cg.register_parented(var, config[CONF_LVGL_ID]) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def add_pages(lv_component, config): | ||||
|     lv_add(lv_component.set_page_wrap(config[CONF_PAGE_WRAP])) | ||||
|     for pconf in config.get(CONF_PAGES, ()): | ||||
|         id = pconf[CONF_ID] | ||||
|         skip = pconf[CONF_SKIP] | ||||
|         var = cg.new_Pvariable(id, skip) | ||||
|         page = Widget.create(id, var, page_spec, pconf) | ||||
|         lv_add(lv_component.add_page(var)) | ||||
|         # Set outer config first | ||||
|         await set_obj_properties(page, config) | ||||
|         await set_obj_properties(page, pconf) | ||||
|         await add_widgets(page, pconf) | ||||
							
								
								
									
										77
									
								
								esphome/components/lvgl/widgets/roller.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/lvgl/widgets/roller.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_MODE, CONF_OPTIONS | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_ANIMATED, | ||||
|     CONF_MAIN, | ||||
|     CONF_SELECTED, | ||||
|     CONF_SELECTED_INDEX, | ||||
|     CONF_VISIBLE_ROW_COUNT, | ||||
|     ROLLER_MODES, | ||||
|     literal, | ||||
| ) | ||||
| from ..lv_validation import animated, lv_int, option_string | ||||
| from ..lvcode import lv | ||||
| from ..types import LvSelect | ||||
| from . import WidgetType | ||||
| from .label import CONF_LABEL | ||||
|  | ||||
| CONF_ROLLER = "roller" | ||||
| lv_roller_t = LvSelect("lv_roller_t") | ||||
|  | ||||
| ROLLER_BASE_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_), | ||||
|         cv.Optional(CONF_VISIBLE_ROW_COUNT): lv_int, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| ROLLER_SCHEMA = ROLLER_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), | ||||
|         cv.Optional(CONF_MODE, default="NORMAL"): ROLLER_MODES.one_of, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| ROLLER_MODIFY_SCHEMA = ROLLER_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class RollerType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_ROLLER, | ||||
|             lv_roller_t, | ||||
|             (CONF_MAIN, CONF_SELECTED), | ||||
|             ROLLER_SCHEMA, | ||||
|             ROLLER_MODIFY_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w, config): | ||||
|         if options := config.get(CONF_OPTIONS): | ||||
|             mode = await ROLLER_MODES.process(config[CONF_MODE]) | ||||
|             text = cg.safe_exp("\n".join(options)) | ||||
|             lv.roller_set_options(w.obj, text, mode) | ||||
|         animopt = literal(config.get(CONF_ANIMATED) or "LV_ANIM_OFF") | ||||
|         if CONF_SELECTED_INDEX in config: | ||||
|             if selected := config[CONF_SELECTED_INDEX]: | ||||
|                 value = await lv_int.process(selected) | ||||
|                 lv.roller_set_selected(w.obj, value, animopt) | ||||
|         await w.set_property( | ||||
|             CONF_VISIBLE_ROW_COUNT, | ||||
|             await lv_int.process(config.get(CONF_VISIBLE_ROW_COUNT)), | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def animated(self): | ||||
|         return True | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return (CONF_LABEL,) | ||||
|  | ||||
|  | ||||
| roller_spec = RollerType() | ||||
							
								
								
									
										63
									
								
								esphome/components/lvgl/widgets/slider.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								esphome/components/lvgl/widgets/slider.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE | ||||
|  | ||||
| from ..defines import ( | ||||
|     BAR_MODES, | ||||
|     CONF_ANIMATED, | ||||
|     CONF_INDICATOR, | ||||
|     CONF_KNOB, | ||||
|     CONF_MAIN, | ||||
|     literal, | ||||
| ) | ||||
| from ..helpers import add_lv_use | ||||
| from ..lv_validation import animated, get_start_value, lv_float | ||||
| from ..lvcode import lv | ||||
| from ..types import LvNumber, NumberType | ||||
| from . import Widget | ||||
| from .lv_bar import CONF_BAR | ||||
|  | ||||
| CONF_SLIDER = "slider" | ||||
| SLIDER_MODIFY_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| SLIDER_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, | ||||
|         cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, | ||||
|         cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, | ||||
|         cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class SliderType(NumberType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_SLIDER, | ||||
|             LvNumber("lv_slider_t"), | ||||
|             parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), | ||||
|             schema=SLIDER_SCHEMA, | ||||
|             modify_schema=SLIDER_MODIFY_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def animated(self): | ||||
|         return True | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         add_lv_use(CONF_BAR) | ||||
|         if CONF_MIN_VALUE in config: | ||||
|             # not modify case | ||||
|             lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) | ||||
|             lv.slider_set_mode(w.obj, literal(config[CONF_MODE])) | ||||
|         value = await get_start_value(config) | ||||
|         if value is not None: | ||||
|             lv.slider_set_value(w.obj, value, literal(config[CONF_ANIMATED])) | ||||
|  | ||||
|  | ||||
| slider_spec = SliderType() | ||||
							
								
								
									
										178
									
								
								esphome/components/lvgl/widgets/spinbox.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								esphome/components/lvgl/widgets/spinbox.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| from esphome import automation | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE | ||||
|  | ||||
| from ..automation import action_to_code, update_to_code | ||||
| from ..defines import ( | ||||
|     CONF_CURSOR, | ||||
|     CONF_DECIMAL_PLACES, | ||||
|     CONF_DIGITS, | ||||
|     CONF_MAIN, | ||||
|     CONF_ROLLOVER, | ||||
|     CONF_SCROLLBAR, | ||||
|     CONF_SELECTED, | ||||
|     CONF_TEXTAREA_PLACEHOLDER, | ||||
| ) | ||||
| from ..lv_validation import lv_bool, lv_float | ||||
| from ..lvcode import lv | ||||
| from ..types import LvNumber, ObjUpdateAction | ||||
| from . import Widget, WidgetType, get_widgets | ||||
| from .label import CONF_LABEL | ||||
| from .textarea import CONF_TEXTAREA | ||||
|  | ||||
| CONF_SPINBOX = "spinbox" | ||||
|  | ||||
| lv_spinbox_t = LvNumber("lv_spinbox_t") | ||||
|  | ||||
| SPIN_ACTIONS = ( | ||||
|     "INCREMENT", | ||||
|     "DECREMENT", | ||||
|     "STEP_NEXT", | ||||
|     "STEP_PREV", | ||||
|     "CLEAR", | ||||
| ) | ||||
|  | ||||
|  | ||||
| def validate_spinbox(config): | ||||
|     max_val = 2**31 - 1 | ||||
|     min_val = -1 - max_val | ||||
|     range_from = int(config[CONF_RANGE_FROM]) | ||||
|     range_to = int(config[CONF_RANGE_TO]) | ||||
|     step = int(config[CONF_STEP]) | ||||
|     if ( | ||||
|         range_from > max_val | ||||
|         or range_from < min_val | ||||
|         or range_to > max_val | ||||
|         or range_to < min_val | ||||
|     ): | ||||
|         raise cv.Invalid("Range outside allowed limits") | ||||
|     if step <= 0 or step >= (range_to - range_from) / 2: | ||||
|         raise cv.Invalid("Invalid step value") | ||||
|     if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]: | ||||
|         raise cv.Invalid("Number of digits must exceed number of decimal places") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| SPINBOX_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_RANGE_FROM, default=0): cv.float_, | ||||
|         cv.Optional(CONF_RANGE_TO, default=100): cv.float_, | ||||
|         cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10), | ||||
|         cv.Optional(CONF_STEP, default=1.0): cv.positive_float, | ||||
|         cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6), | ||||
|         cv.Optional(CONF_ROLLOVER, default=False): lv_bool, | ||||
|     } | ||||
| ).add_extra(validate_spinbox) | ||||
|  | ||||
|  | ||||
| SPINBOX_MODIFY_SCHEMA = { | ||||
|     cv.Required(CONF_VALUE): lv_float, | ||||
| } | ||||
|  | ||||
|  | ||||
| class SpinboxType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_SPINBOX, | ||||
|             lv_spinbox_t, | ||||
|             ( | ||||
|                 CONF_MAIN, | ||||
|                 CONF_SCROLLBAR, | ||||
|                 CONF_SELECTED, | ||||
|                 CONF_CURSOR, | ||||
|                 CONF_TEXTAREA_PLACEHOLDER, | ||||
|             ), | ||||
|             SPINBOX_SCHEMA, | ||||
|             SPINBOX_MODIFY_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if CONF_DIGITS in config: | ||||
|             digits = config[CONF_DIGITS] | ||||
|             scale = 10 ** config[CONF_DECIMAL_PLACES] | ||||
|             range_from = int(config[CONF_RANGE_FROM]) * scale | ||||
|             range_to = int(config[CONF_RANGE_TO]) * scale | ||||
|             step = int(config[CONF_STEP]) * scale | ||||
|             w.scale = scale | ||||
|             w.step = step | ||||
|             w.range_to = range_to | ||||
|             w.range_from = range_from | ||||
|             lv.spinbox_set_range(w.obj, range_from, range_to) | ||||
|             await w.set_property(CONF_STEP, step) | ||||
|             await w.set_property(CONF_ROLLOVER, config) | ||||
|             lv.spinbox_set_digit_format( | ||||
|                 w.obj, digits, digits - config[CONF_DECIMAL_PLACES] | ||||
|             ) | ||||
|         if (value := config.get(CONF_VALUE)) is not None: | ||||
|             lv.spinbox_set_value(w.obj, await lv_float.process(value)) | ||||
|  | ||||
|     def get_scale(self, config): | ||||
|         return 10 ** config[CONF_DECIMAL_PLACES] | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return CONF_TEXTAREA, CONF_LABEL | ||||
|  | ||||
|     def get_max(self, config: dict): | ||||
|         return config[CONF_RANGE_TO] | ||||
|  | ||||
|     def get_min(self, config: dict): | ||||
|         return config[CONF_RANGE_FROM] | ||||
|  | ||||
|     def get_step(self, config: dict): | ||||
|         return config[CONF_STEP] | ||||
|  | ||||
|  | ||||
| spinbox_spec = SpinboxType() | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.spinbox.increment", | ||||
|     ObjUpdateAction, | ||||
|     cv.maybe_simple_value( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), | ||||
|         }, | ||||
|         key=CONF_ID, | ||||
|     ), | ||||
| ) | ||||
| async def spinbox_increment(config, action_id, template_arg, args): | ||||
|     widgets = await get_widgets(config) | ||||
|  | ||||
|     async def do_increment(w: Widget): | ||||
|         lv.spinbox_increment(w.obj) | ||||
|  | ||||
|     return await action_to_code(widgets, do_increment, action_id, template_arg, args) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.spinbox.decrement", | ||||
|     ObjUpdateAction, | ||||
|     cv.maybe_simple_value( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), | ||||
|         }, | ||||
|         key=CONF_ID, | ||||
|     ), | ||||
| ) | ||||
| async def spinbox_decrement(config, action_id, template_arg, args): | ||||
|     widgets = await get_widgets(config) | ||||
|  | ||||
|     async def do_increment(w: Widget): | ||||
|         lv.spinbox_decrement(w.obj) | ||||
|  | ||||
|     return await action_to_code(widgets, do_increment, action_id, template_arg, args) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.spinbox.update", | ||||
|     ObjUpdateAction, | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), | ||||
|             cv.Required(CONF_VALUE): lv_float, | ||||
|         } | ||||
|     ), | ||||
| ) | ||||
| async def spinbox_update_to_code(config, action_id, template_arg, args): | ||||
|     return await update_to_code(config, action_id, template_arg, args) | ||||
							
								
								
									
										43
									
								
								esphome/components/lvgl/widgets/spinner.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/lvgl/widgets/spinner.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME | ||||
| from ..lv_validation import angle | ||||
| from ..lvcode import lv_expr | ||||
| from ..types import LvType | ||||
| from . import Widget, WidgetType | ||||
| from .arc import CONF_ARC | ||||
|  | ||||
| CONF_SPINNER = "spinner" | ||||
|  | ||||
| SPINNER_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_ARC_LENGTH): angle, | ||||
|         cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class SpinnerType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_SPINNER, | ||||
|             LvType("lv_spinner_t"), | ||||
|             (CONF_MAIN, CONF_INDICATOR), | ||||
|             SPINNER_SCHEMA, | ||||
|             {}, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         return [] | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return (CONF_ARC,) | ||||
|  | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         spin_time = config[CONF_SPIN_TIME].total_milliseconds | ||||
|         arc_length = config[CONF_ARC_LENGTH] // 10 | ||||
|         return lv_expr.call("spinner_create", parent, spin_time, arc_length) | ||||
|  | ||||
|  | ||||
| spinner_spec = SpinnerType() | ||||
							
								
								
									
										20
									
								
								esphome/components/lvgl/widgets/switch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/lvgl/widgets/switch.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN | ||||
| from ..types import LvBoolean | ||||
| from . import WidgetType | ||||
|  | ||||
| CONF_SWITCH = "switch" | ||||
|  | ||||
|  | ||||
| class SwitchType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_SWITCH, | ||||
|             LvBoolean("lv_switch_t"), | ||||
|             (CONF_MAIN, CONF_INDICATOR, CONF_KNOB), | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w, config): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| switch_spec = SwitchType() | ||||
							
								
								
									
										114
									
								
								esphome/components/lvgl/widgets/tabview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								esphome/components/lvgl/widgets/tabview.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| from ..automation import action_to_code | ||||
| from ..defines import ( | ||||
|     CONF_ANIMATED, | ||||
|     CONF_MAIN, | ||||
|     CONF_TAB_ID, | ||||
|     CONF_TABS, | ||||
|     DIRECTIONS, | ||||
|     TYPE_FLEX, | ||||
|     literal, | ||||
| ) | ||||
| from ..lv_validation import animated, lv_int, size | ||||
| from ..lvcode import LocalVariable, lv, lv_assign, lv_expr | ||||
| from ..schemas import container_schema, part_schema | ||||
| from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr | ||||
| from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties | ||||
| from .buttonmatrix import buttonmatrix_spec | ||||
| from .obj import obj_spec | ||||
|  | ||||
| CONF_TABVIEW = "tabview" | ||||
| CONF_TAB_STYLE = "tab_style" | ||||
|  | ||||
| lv_tab_t = LvType("lv_obj_t") | ||||
|  | ||||
| TABVIEW_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_TABS): cv.ensure_list( | ||||
|             container_schema( | ||||
|                 obj_spec, | ||||
|                 { | ||||
|                     cv.Required(CONF_NAME): cv.string, | ||||
|                     cv.GenerateID(): cv.declare_id(lv_tab_t), | ||||
|                 }, | ||||
|             ) | ||||
|         ), | ||||
|         cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec), | ||||
|         cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of, | ||||
|         cv.Optional(CONF_SIZE, default="10%"): size, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TabviewType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_TABVIEW, | ||||
|             LvType( | ||||
|                 "lv_tabview_t", | ||||
|                 largs=[(lv_obj_t_ptr, "tab")], | ||||
|                 lvalue=lambda w: lv_expr.obj_get_child( | ||||
|                     lv_expr.tabview_get_content(w.obj), | ||||
|                     lv_expr.tabview_get_tab_act(w.obj), | ||||
|                 ), | ||||
|                 has_on_value=True, | ||||
|             ), | ||||
|             parts=(CONF_MAIN,), | ||||
|             schema=TABVIEW_SCHEMA, | ||||
|             modify_schema={}, | ||||
|         ) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return "btnmatrix", TYPE_FLEX | ||||
|  | ||||
|     async def to_code(self, w: Widget, config: dict): | ||||
|         for tab_conf in config[CONF_TABS]: | ||||
|             w_id = tab_conf[CONF_ID] | ||||
|             tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) | ||||
|             tab_widget = Widget.create(w_id, tab_obj, obj_spec) | ||||
|             lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) | ||||
|             await set_obj_properties(tab_widget, tab_conf) | ||||
|             await add_widgets(tab_widget, tab_conf) | ||||
|         if button_style := config.get(CONF_TAB_STYLE): | ||||
|             with LocalVariable( | ||||
|                 "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj) | ||||
|             ) as btnmatrix_obj: | ||||
|                 await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style) | ||||
|  | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         return lv_expr.call( | ||||
|             "tabview_create", | ||||
|             parent, | ||||
|             literal(config[CONF_POSITION]), | ||||
|             literal(config[CONF_SIZE]), | ||||
|         ) | ||||
|  | ||||
|  | ||||
| tabview_spec = TabviewType() | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.tabview.select", | ||||
|     ObjUpdateAction, | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(tabview_spec.w_type), | ||||
|             cv.Optional(CONF_ANIMATED, default=False): animated, | ||||
|             cv.Required(CONF_INDEX): lv_int, | ||||
|         }, | ||||
|     ).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)), | ||||
| ) | ||||
| async def tabview_select(config, action_id, template_arg, args): | ||||
|     widget = await get_widgets(config) | ||||
|     index = config[CONF_INDEX] | ||||
|  | ||||
|     async def do_select(w: Widget): | ||||
|         lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED])) | ||||
|         lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) | ||||
|  | ||||
|     return await action_to_code(widget, do_select, action_id, template_arg, args) | ||||
							
								
								
									
										66
									
								
								esphome/components/lvgl/widgets/textarea.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								esphome/components/lvgl/widgets/textarea.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_MAX_LENGTH, CONF_TEXT | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_ACCEPTED_CHARS, | ||||
|     CONF_CURSOR, | ||||
|     CONF_MAIN, | ||||
|     CONF_ONE_LINE, | ||||
|     CONF_PASSWORD_MODE, | ||||
|     CONF_PLACEHOLDER_TEXT, | ||||
|     CONF_SCROLLBAR, | ||||
|     CONF_SELECTED, | ||||
|     CONF_TEXTAREA_PLACEHOLDER, | ||||
| ) | ||||
| from ..lv_validation import lv_bool, lv_int, lv_text | ||||
| from ..schemas import TEXT_SCHEMA | ||||
| from ..types import LvText | ||||
| from . import Widget, WidgetType | ||||
|  | ||||
| CONF_TEXTAREA = "textarea" | ||||
|  | ||||
| lv_textarea_t = LvText("lv_textarea_t") | ||||
|  | ||||
| TEXTAREA_SCHEMA = TEXT_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, | ||||
|         cv.Optional(CONF_ACCEPTED_CHARS): lv_text, | ||||
|         cv.Optional(CONF_ONE_LINE): lv_bool, | ||||
|         cv.Optional(CONF_PASSWORD_MODE): lv_bool, | ||||
|         cv.Optional(CONF_MAX_LENGTH): lv_int, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TextareaType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_TEXTAREA, | ||||
|             lv_textarea_t, | ||||
|             ( | ||||
|                 CONF_MAIN, | ||||
|                 CONF_SCROLLBAR, | ||||
|                 CONF_SELECTED, | ||||
|                 CONF_CURSOR, | ||||
|                 CONF_TEXTAREA_PLACEHOLDER, | ||||
|             ), | ||||
|             TEXTAREA_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config: dict): | ||||
|         for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS): | ||||
|             if (value := config.get(prop)) is not None: | ||||
|                 await w.set_property(prop, await lv_text.process(value)) | ||||
|         await w.set_property( | ||||
|             CONF_MAX_LENGTH, await lv_int.process(config.get(CONF_MAX_LENGTH)) | ||||
|         ) | ||||
|         await w.set_property( | ||||
|             CONF_PASSWORD_MODE, | ||||
|             await lv_bool.process(config.get(CONF_PASSWORD_MODE)), | ||||
|         ) | ||||
|         await w.set_property( | ||||
|             CONF_ONE_LINE, await lv_bool.process(config.get(CONF_ONE_LINE)) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| textarea_spec = TextareaType() | ||||
							
								
								
									
										128
									
								
								esphome/components/lvgl/widgets/tileview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								esphome/components/lvgl/widgets/tileview.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_ROW, CONF_TRIGGER_ID | ||||
|  | ||||
| from ..automation import action_to_code | ||||
| from ..defines import ( | ||||
|     CONF_ANIMATED, | ||||
|     CONF_COLUMN, | ||||
|     CONF_DIR, | ||||
|     CONF_MAIN, | ||||
|     CONF_TILE_ID, | ||||
|     CONF_TILES, | ||||
|     TILE_DIRECTIONS, | ||||
|     literal, | ||||
| ) | ||||
| from ..lv_validation import animated, lv_int | ||||
| from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable | ||||
| from ..schemas import container_schema | ||||
| from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr | ||||
| from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties | ||||
| from .obj import obj_spec | ||||
|  | ||||
| CONF_TILEVIEW = "tileview" | ||||
|  | ||||
| lv_tile_t = LvType("lv_tileview_tile_t") | ||||
|  | ||||
| lv_tileview_t = LvType( | ||||
|     "lv_tileview_t", | ||||
|     largs=[(lv_obj_t_ptr, "tile")], | ||||
|     lvalue=lambda w: w.get_property("tile_act"), | ||||
| ) | ||||
|  | ||||
| tile_spec = WidgetType("lv_tileview_tile_t", lv_tile_t, (CONF_MAIN,), {}) | ||||
|  | ||||
| TILEVIEW_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_TILES): cv.ensure_list( | ||||
|             container_schema( | ||||
|                 obj_spec, | ||||
|                 { | ||||
|                     cv.Required(CONF_ROW): lv_int, | ||||
|                     cv.Required(CONF_COLUMN): lv_int, | ||||
|                     cv.GenerateID(): cv.declare_id(lv_tile_t), | ||||
|                     cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of, | ||||
|                 }, | ||||
|             ) | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_VALUE): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                     automation.Trigger.template(lv_obj_t_ptr) | ||||
|                 ) | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TileviewType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_TILEVIEW, | ||||
|             lv_tileview_t, | ||||
|             (CONF_MAIN,), | ||||
|             schema=TILEVIEW_SCHEMA, | ||||
|             modify_schema={}, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config: dict): | ||||
|         for tile_conf in config.get(CONF_TILES, ()): | ||||
|             w_id = tile_conf[CONF_ID] | ||||
|             tile_obj = lv_Pvariable(lv_obj_t, w_id) | ||||
|             tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) | ||||
|             dirs = tile_conf[CONF_DIR] | ||||
|             if isinstance(dirs, list): | ||||
|                 dirs = "|".join(dirs) | ||||
|             lv_assign( | ||||
|                 tile_obj, | ||||
|                 lv_expr.tileview_add_tile( | ||||
|                     w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs) | ||||
|                 ), | ||||
|             ) | ||||
|             await set_obj_properties(tile, tile_conf) | ||||
|             await add_widgets(tile, tile_conf) | ||||
|  | ||||
|  | ||||
| tileview_spec = TileviewType() | ||||
|  | ||||
|  | ||||
| def tile_select_validate(config): | ||||
|     row = CONF_ROW in config | ||||
|     column = CONF_COLUMN in config | ||||
|     tile = CONF_TILE_ID in config | ||||
|     if tile and (row or column) or not tile and not (row and column): | ||||
|         raise cv.Invalid("Specify either a tile id, or both a row and a column") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "lvgl.tileview.select", | ||||
|     ObjUpdateAction, | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(lv_tileview_t), | ||||
|             cv.Optional(CONF_ANIMATED, default=False): animated, | ||||
|             cv.Optional(CONF_ROW): lv_int, | ||||
|             cv.Optional(CONF_COLUMN): lv_int, | ||||
|             cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t), | ||||
|         }, | ||||
|     ).add_extra(tile_select_validate), | ||||
| ) | ||||
| async def tileview_select(config, action_id, template_arg, args): | ||||
|     widgets = await get_widgets(config) | ||||
|  | ||||
|     async def do_select(w: Widget): | ||||
|         if tile := config.get(CONF_TILE_ID): | ||||
|             tile = await cg.get_variable(tile) | ||||
|             lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) | ||||
|         else: | ||||
|             row = await lv_int.process(config[CONF_ROW]) | ||||
|             column = await lv_int.process(config[CONF_COLUMN]) | ||||
|             lv_obj.set_tile_id( | ||||
|                 widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) | ||||
|             ) | ||||
|         lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) | ||||
|  | ||||
|     return await action_to_code(widgets, do_select, action_id, template_arg, args) | ||||
| @@ -1,6 +1,6 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, spi | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_MAINS_FILTER, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
| @@ -15,8 +15,8 @@ MAX31856Sensor = max31856_ns.class_( | ||||
|  | ||||
| MAX31865ConfigFilter = max31856_ns.enum("MAX31856ConfigFilter") | ||||
| FILTER = { | ||||
|     "50HZ": MAX31865ConfigFilter.FILTER_50HZ, | ||||
|     "60HZ": MAX31865ConfigFilter.FILTER_60HZ, | ||||
|     50: MAX31865ConfigFilter.FILTER_50HZ, | ||||
|     60: MAX31865ConfigFilter.FILTER_60HZ, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
| @@ -29,8 +29,8 @@ CONFIG_SCHEMA = ( | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Optional(CONF_MAINS_FILTER, default="60HZ"): cv.enum( | ||||
|                 FILTER, upper=True, space="" | ||||
|             cv.Optional(CONF_MAINS_FILTER, default="60Hz"): cv.All( | ||||
|                 cv.frequency, cv.enum(FILTER, int=True) | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|   | ||||
| @@ -1,20 +1,18 @@ | ||||
| from esphome import automation | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| from esphome.automation import maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_ON_IDLE, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VOLUME, | ||||
|     CONF_ON_IDLE, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.coroutine import coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
|  | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
|  | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import re | ||||
|  | ||||
| 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 | ||||
| from esphome.components import logger | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_AVAILABILITY, | ||||
|     CONF_BIRTH_MESSAGE, | ||||
| @@ -13,21 +14,21 @@ from esphome.const import ( | ||||
|     CONF_CLIENT_CERTIFICATE, | ||||
|     CONF_CLIENT_CERTIFICATE_KEY, | ||||
|     CONF_CLIENT_ID, | ||||
|     CONF_COMMAND_TOPIC, | ||||
|     CONF_COMMAND_RETAIN, | ||||
|     CONF_COMMAND_TOPIC, | ||||
|     CONF_DISCOVERY, | ||||
|     CONF_DISCOVERY_OBJECT_ID_GENERATOR, | ||||
|     CONF_DISCOVERY_PREFIX, | ||||
|     CONF_DISCOVERY_RETAIN, | ||||
|     CONF_DISCOVERY_UNIQUE_ID_GENERATOR, | ||||
|     CONF_DISCOVERY_OBJECT_ID_GENERATOR, | ||||
|     CONF_ID, | ||||
|     CONF_KEEPALIVE, | ||||
|     CONF_LEVEL, | ||||
|     CONF_LOG_TOPIC, | ||||
|     CONF_ON_JSON_MESSAGE, | ||||
|     CONF_ON_MESSAGE, | ||||
|     CONF_ON_CONNECT, | ||||
|     CONF_ON_DISCONNECT, | ||||
|     CONF_ON_JSON_MESSAGE, | ||||
|     CONF_ON_MESSAGE, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PAYLOAD, | ||||
|     CONF_PAYLOAD_AVAILABLE, | ||||
| @@ -45,12 +46,11 @@ from esphome.const import ( | ||||
|     CONF_USE_ABBREVIATIONS, | ||||
|     CONF_USERNAME, | ||||
|     CONF_WILL_MESSAGE, | ||||
|     PLATFORM_BK72XX, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_BK72XX, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority, CORE | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
|  | ||||
| DEPENDENCIES = ["network"] | ||||
|  | ||||
| @@ -110,6 +110,9 @@ MQTTDisconnectTrigger = mqtt_ns.class_( | ||||
| MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) | ||||
| MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) | ||||
|  | ||||
| MQTTAlarmControlPanelComponent = mqtt_ns.class_( | ||||
|     "MQTTAlarmControlPanelComponent", MQTTComponent | ||||
| ) | ||||
| MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent) | ||||
| MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent) | ||||
| MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent) | ||||
|   | ||||
							
								
								
									
										128
									
								
								esphome/components/mqtt/mqtt_alarm_control_panel.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								esphome/components/mqtt/mqtt_alarm_control_panel.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| #include "mqtt_alarm_control_panel.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include "mqtt_const.h" | ||||
|  | ||||
| #ifdef USE_MQTT | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|  | ||||
| namespace esphome { | ||||
| namespace mqtt { | ||||
|  | ||||
| static const char *const TAG = "mqtt.alarm_control_panel"; | ||||
|  | ||||
| using namespace esphome::alarm_control_panel; | ||||
|  | ||||
| MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) | ||||
|     : alarm_control_panel_(alarm_control_panel) {} | ||||
| void MQTTAlarmControlPanelComponent::setup() { | ||||
|   this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); | ||||
|   this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { | ||||
|     auto call = this->alarm_control_panel_->make_call(); | ||||
|     if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { | ||||
|       call.arm_away(); | ||||
|     } else if (strcasecmp(payload.c_str(), "ARM_HOME") == 0) { | ||||
|       call.arm_home(); | ||||
|     } else if (strcasecmp(payload.c_str(), "ARM_NIGHT") == 0) { | ||||
|       call.arm_night(); | ||||
|     } else if (strcasecmp(payload.c_str(), "ARM_VACATION") == 0) { | ||||
|       call.arm_vacation(); | ||||
|     } else if (strcasecmp(payload.c_str(), "ARM_CUSTOM_BYPASS") == 0) { | ||||
|       call.arm_custom_bypass(); | ||||
|     } else if (strcasecmp(payload.c_str(), "DISARM") == 0) { | ||||
|       call.disarm(); | ||||
|     } else if (strcasecmp(payload.c_str(), "PENDING") == 0) { | ||||
|       call.pending(); | ||||
|     } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { | ||||
|       call.triggered(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); | ||||
|     } | ||||
|     call.perform(); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void MQTTAlarmControlPanelComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str()); | ||||
|   LOG_MQTT_COMPONENT(true, true) | ||||
|   ESP_LOGCONFIG(TAG, "  Supported Features: %" PRIu32, this->alarm_control_panel_->get_supported_features()); | ||||
|   ESP_LOGCONFIG(TAG, "  Requires Code to Disarm: %s", YESNO(this->alarm_control_panel_->get_requires_code())); | ||||
|   ESP_LOGCONFIG(TAG, "  Requires Code To Arm: %s", YESNO(this->alarm_control_panel_->get_requires_code_to_arm())); | ||||
| } | ||||
|  | ||||
| void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||
|   JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); | ||||
|   const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); | ||||
|   if (acp_supported_features & ACP_FEAT_ARM_AWAY) { | ||||
|     supported_features.add("arm_away"); | ||||
|   } | ||||
|   if (acp_supported_features & ACP_FEAT_ARM_HOME) { | ||||
|     supported_features.add("arm_home"); | ||||
|   } | ||||
|   if (acp_supported_features & ACP_FEAT_ARM_NIGHT) { | ||||
|     supported_features.add("arm_night"); | ||||
|   } | ||||
|   if (acp_supported_features & ACP_FEAT_ARM_VACATION) { | ||||
|     supported_features.add("arm_vacation"); | ||||
|   } | ||||
|   if (acp_supported_features & ACP_FEAT_ARM_CUSTOM_BYPASS) { | ||||
|     supported_features.add("arm_custom_bypass"); | ||||
|   } | ||||
|   if (acp_supported_features & ACP_FEAT_TRIGGER) { | ||||
|     supported_features.add("trigger"); | ||||
|   } | ||||
|   root[MQTT_CODE_DISARM_REQUIRED] = this->alarm_control_panel_->get_requires_code(); | ||||
|   root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm(); | ||||
| } | ||||
|  | ||||
| std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; } | ||||
| const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; } | ||||
|  | ||||
| bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } | ||||
| bool MQTTAlarmControlPanelComponent::publish_state() { | ||||
|   bool success = true; | ||||
|   const char *state_s = ""; | ||||
|   switch (this->alarm_control_panel_->get_state()) { | ||||
|     case ACP_STATE_DISARMED: | ||||
|       state_s = "disarmed"; | ||||
|       break; | ||||
|     case ACP_STATE_ARMED_HOME: | ||||
|       state_s = "armed_home"; | ||||
|       break; | ||||
|     case ACP_STATE_ARMED_AWAY: | ||||
|       state_s = "armed_away"; | ||||
|       break; | ||||
|     case ACP_STATE_ARMED_NIGHT: | ||||
|       state_s = "armed_night"; | ||||
|       break; | ||||
|     case ACP_STATE_ARMED_VACATION: | ||||
|       state_s = "armed_vacation"; | ||||
|       break; | ||||
|     case ACP_STATE_ARMED_CUSTOM_BYPASS: | ||||
|       state_s = "armed_custom_bypass"; | ||||
|       break; | ||||
|     case ACP_STATE_PENDING: | ||||
|       state_s = "pending"; | ||||
|       break; | ||||
|     case ACP_STATE_ARMING: | ||||
|       state_s = "arming"; | ||||
|       break; | ||||
|     case ACP_STATE_DISARMING: | ||||
|       state_s = "disarming"; | ||||
|       break; | ||||
|     case ACP_STATE_TRIGGERED: | ||||
|       state_s = "triggered"; | ||||
|       break; | ||||
|     default: | ||||
|       state_s = "unknown"; | ||||
|   } | ||||
|   if (!this->publish(this->get_state_topic_(), state_s)) | ||||
|     success = false; | ||||
|   return success; | ||||
| } | ||||
|  | ||||
| }  // namespace mqtt | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| #endif  // USE_MQTT | ||||
							
								
								
									
										39
									
								
								esphome/components/mqtt/mqtt_alarm_control_panel.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								esphome/components/mqtt/mqtt_alarm_control_panel.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| #ifdef USE_MQTT | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|  | ||||
| #include "mqtt_component.h" | ||||
| #include "esphome/components/alarm_control_panel/alarm_control_panel.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace mqtt { | ||||
|  | ||||
| class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { | ||||
|  public: | ||||
|   explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); | ||||
|  | ||||
|   void setup() override; | ||||
|  | ||||
|   void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; | ||||
|  | ||||
|   bool send_initial_state() override; | ||||
|  | ||||
|   bool publish_state(); | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   std::string component_type() const override; | ||||
|   const EntityBase *get_entity() const override; | ||||
|  | ||||
|   alarm_control_panel::AlarmControlPanel *alarm_control_panel_; | ||||
| }; | ||||
|  | ||||
| }  // namespace mqtt | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| #endif  // USE_MQTT | ||||
| @@ -1,12 +1,11 @@ | ||||
| from string import ascii_letters, digits | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import color | ||||
| from esphome.const import ( | ||||
|     CONF_VISIBLE, | ||||
| ) | ||||
| from . import CONF_NEXTION_ID | ||||
| from . import Nextion | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_BACKGROUND_COLOR, CONF_FOREGROUND_COLOR, CONF_VISIBLE | ||||
|  | ||||
| from . import CONF_NEXTION_ID, Nextion | ||||
|  | ||||
| CONF_VARIABLE_NAME = "variable_name" | ||||
| CONF_COMPONENT_NAME = "component_name" | ||||
| @@ -24,9 +23,7 @@ CONF_WAKE_UP_PAGE = "wake_up_page" | ||||
| CONF_START_UP_PAGE = "start_up_page" | ||||
| CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" | ||||
| CONF_WAVE_MAX_LENGTH = "wave_max_length" | ||||
| CONF_BACKGROUND_COLOR = "background_color" | ||||
| CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" | ||||
| CONF_FOREGROUND_COLOR = "foreground_color" | ||||
| CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" | ||||
| CONF_FONT_ID = "font_id" | ||||
| CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" | ||||
|   | ||||
| @@ -1,24 +1,23 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.components import mqtt | ||||
| from esphome.components import web_server | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ABOVE, | ||||
|     CONF_BELOW, | ||||
|     CONF_CYCLE, | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_ID, | ||||
|     CONF_ICON, | ||||
|     CONF_ID, | ||||
|     CONF_MODE, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_ON_VALUE_RANGE, | ||||
|     CONF_OPERATION, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_UNIT_OF_MEASUREMENT, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_VALUE, | ||||
|     CONF_OPERATION, | ||||
|     CONF_CYCLE, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     DEVICE_CLASS_APPARENT_POWER, | ||||
|     DEVICE_CLASS_AQI, | ||||
| @@ -72,8 +71,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_WIND_SPEED, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| DEVICE_CLASSES = [ | ||||
|   | ||||
							
								
								
									
										161
									
								
								esphome/components/online_image/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								esphome/components/online_image/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent | ||||
| from esphome.components.image import ( | ||||
|     CONF_USE_TRANSPARENCY, | ||||
|     IMAGE_TYPE, | ||||
|     Image_, | ||||
|     validate_cross_dependencies, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BUFFER_SIZE, | ||||
|     CONF_FORMAT, | ||||
|     CONF_ID, | ||||
|     CONF_ON_ERROR, | ||||
|     CONF_RESIZE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
|     CONF_URL, | ||||
| ) | ||||
|  | ||||
| AUTO_LOAD = ["image"] | ||||
| DEPENDENCIES = ["display", "http_request"] | ||||
| CODEOWNERS = ["@guillempages"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| online_image_ns = cg.esphome_ns.namespace("online_image") | ||||
|  | ||||
| ImageFormat = online_image_ns.enum("ImageFormat") | ||||
|  | ||||
| FORMAT_PNG = "PNG" | ||||
|  | ||||
| IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG}  # Add new supported formats here | ||||
|  | ||||
| OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) | ||||
|  | ||||
| # Actions | ||||
| SetUrlAction = online_image_ns.class_( | ||||
|     "OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage) | ||||
| ) | ||||
| ReleaseImageAction = online_image_ns.class_( | ||||
|     "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) | ||||
| ) | ||||
|  | ||||
| # Triggers | ||||
| DownloadFinishedTrigger = online_image_ns.class_( | ||||
|     "DownloadFinishedTrigger", automation.Trigger.template() | ||||
| ) | ||||
| DownloadErrorTrigger = online_image_ns.class_( | ||||
|     "DownloadErrorTrigger", automation.Trigger.template() | ||||
| ) | ||||
|  | ||||
| ONLINE_IMAGE_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_ID): cv.declare_id(OnlineImage), | ||||
|         cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), | ||||
|         # | ||||
|         # Common image options | ||||
|         # | ||||
|         cv.Optional(CONF_RESIZE): cv.dimensions, | ||||
|         cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), | ||||
|         # Not setting default here on purpose; the default depends on the image type, | ||||
|         # and thus will be set in the "validate_cross_dependencies" validator. | ||||
|         cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, | ||||
|         # | ||||
|         # Online Image specific options | ||||
|         # | ||||
|         cv.Required(CONF_URL): cv.url, | ||||
|         cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), | ||||
|         cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), | ||||
|         cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ERROR): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.polling_component_schema("never")) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     cv.All( | ||||
|         ONLINE_IMAGE_SCHEMA, | ||||
|         validate_cross_dependencies, | ||||
|         cv.require_framework_version( | ||||
|             # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed | ||||
|             # esp8266_arduino=cv.Version(2, 7, 0), | ||||
|             esp32_arduino=cv.Version(0, 0, 0), | ||||
|             esp_idf=cv.Version(4, 0, 0), | ||||
|         ), | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| SET_URL_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(OnlineImage), | ||||
|         cv.Required(CONF_URL): cv.templatable(cv.url), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(OnlineImage), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| @automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA) | ||||
| @automation.register_action( | ||||
|     "online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA | ||||
| ) | ||||
| async def online_image_action_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|  | ||||
|     if CONF_URL in config: | ||||
|         template_ = await cg.templatable(config[CONF_URL], args, cg.const_char_ptr) | ||||
|         cg.add(var.set_url(template_)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     format = config[CONF_FORMAT] | ||||
|     if format in [FORMAT_PNG]: | ||||
|         cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") | ||||
|         cg.add_library("pngle", "1.0.2") | ||||
|  | ||||
|     url = config[CONF_URL] | ||||
|     width, height = config.get(CONF_RESIZE, (0, 0)) | ||||
|     transparent = config[CONF_USE_TRANSPARENCY] | ||||
|  | ||||
|     var = cg.new_Pvariable( | ||||
|         config[CONF_ID], | ||||
|         url, | ||||
|         width, | ||||
|         height, | ||||
|         format, | ||||
|         config[CONF_TYPE], | ||||
|         config[CONF_BUFFER_SIZE], | ||||
|     ) | ||||
|     await cg.register_component(var, config) | ||||
|     await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) | ||||
|  | ||||
|     cg.add(var.set_transparency(transparent)) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_ERROR, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
							
								
								
									
										44
									
								
								esphome/components/online_image/image_decoder.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/online_image/image_decoder.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| #include "image_decoder.h" | ||||
| #include "online_image.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace online_image { | ||||
|  | ||||
| static const char *const TAG = "online_image.decoder"; | ||||
|  | ||||
| void ImageDecoder::set_size(int width, int height) { | ||||
|   this->image_->resize_(width, height); | ||||
|   this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width; | ||||
|   this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height; | ||||
| } | ||||
|  | ||||
| void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { | ||||
|   auto width = std::min(this->image_->buffer_width_, static_cast<int>(std::ceil((x + w) * this->x_scale_))); | ||||
|   auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_))); | ||||
|   for (int i = x * this->x_scale_; i < width; i++) { | ||||
|     for (int j = y * this->y_scale_; j < height; j++) { | ||||
|       this->image_->draw_pixel_(i, j, color); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint8_t *DownloadBuffer::data(size_t offset) { | ||||
|   if (offset > this->size_) { | ||||
|     ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); | ||||
|     return this->buffer_; | ||||
|   } | ||||
|   return this->buffer_ + offset; | ||||
| } | ||||
|  | ||||
| size_t DownloadBuffer::read(size_t len) { | ||||
|   this->unread_ -= len; | ||||
|   if (this->unread_ > 0) { | ||||
|     memmove(this->data(), this->data(len), this->unread_); | ||||
|   } | ||||
|   return this->unread_; | ||||
| } | ||||
|  | ||||
| }  // namespace online_image | ||||
| }  // namespace esphome | ||||
							
								
								
									
										112
									
								
								esphome/components/online_image/image_decoder.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								esphome/components/online_image/image_decoder.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| #pragma once | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/color.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace online_image { | ||||
|  | ||||
| class OnlineImage; | ||||
|  | ||||
| /** | ||||
|  * @brief Class to abstract decoding different image formats. | ||||
|  */ | ||||
| class ImageDecoder { | ||||
|  public: | ||||
|   /** | ||||
|    * @brief Construct a new Image Decoder object | ||||
|    * | ||||
|    * @param image The image to decode the stream into. | ||||
|    */ | ||||
|   ImageDecoder(OnlineImage *image) : image_(image) {} | ||||
|   virtual ~ImageDecoder() = default; | ||||
|  | ||||
|   /** | ||||
|    * @brief Initialize the decoder. | ||||
|    * | ||||
|    * @param download_size The total number of bytes that need to be download for the image. | ||||
|    */ | ||||
|   virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } | ||||
|  | ||||
|   /** | ||||
|    * @brief Decode a part of the image. It will try reading from the buffer. | ||||
|    * There is no guarantee that the whole available buffer will be read/decoded; | ||||
|    * the method will return the amount of bytes actually decoded, so that the | ||||
|    * unread content can be moved to the beginning. | ||||
|    * | ||||
|    * @param buffer The buffer to read from. | ||||
|    * @param size   The maximum amount of bytes that can be read from the buffer. | ||||
|    * @return int   The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully | ||||
|    *               decode anything, or negative in case of a decoding error. | ||||
|    */ | ||||
|   virtual int decode(uint8_t *buffer, size_t size); | ||||
|  | ||||
|   /** | ||||
|    * @brief Request the image to be resized once the actual dimensions are known. | ||||
|    * Called by the callback functions, to be able to access the parent Image class. | ||||
|    * | ||||
|    * @param width The image's width. | ||||
|    * @param height The image's height. | ||||
|    */ | ||||
|   void set_size(int width, int height); | ||||
|  | ||||
|   /** | ||||
|    * @brief Draw a rectangle on the display_buffer using the defined color. | ||||
|    * Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly. | ||||
|    * In case of binary displays, the color will be converted to binary as well. | ||||
|    * Called by the callback functions, to be able to access the parent Image class. | ||||
|    * | ||||
|    * @param x The left-most coordinate of the rectangle. | ||||
|    * @param y The top-most coordinate of the rectangle. | ||||
|    * @param w The width of the rectangle. | ||||
|    * @param h The height of the rectangle. | ||||
|    * @param color The color to draw the rectangle with. | ||||
|    */ | ||||
|   void draw(int x, int y, int w, int h, const Color &color); | ||||
|  | ||||
|   bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } | ||||
|  | ||||
|  protected: | ||||
|   OnlineImage *image_; | ||||
|   // Initializing to 1, to ensure it is different than initial "decoded_bytes_". | ||||
|   // Will be overwritten anyway once the download size is known. | ||||
|   uint32_t download_size_ = 1; | ||||
|   uint32_t decoded_bytes_ = 0; | ||||
|   double x_scale_ = 1.0; | ||||
|   double y_scale_ = 1.0; | ||||
| }; | ||||
|  | ||||
| class DownloadBuffer { | ||||
|  public: | ||||
|   DownloadBuffer(size_t size) : size_(size) { | ||||
|     this->buffer_ = this->allocator_.allocate(size); | ||||
|     this->reset(); | ||||
|   } | ||||
|  | ||||
|   virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } | ||||
|  | ||||
|   uint8_t *data(size_t offset = 0); | ||||
|  | ||||
|   uint8_t *append() { return this->data(this->unread_); } | ||||
|  | ||||
|   size_t unread() const { return this->unread_; } | ||||
|   size_t size() const { return this->size_; } | ||||
|   size_t free_capacity() const { return this->size_ - this->unread_; } | ||||
|  | ||||
|   size_t read(size_t len); | ||||
|   size_t write(size_t len) { | ||||
|     this->unread_ += len; | ||||
|     return this->unread_; | ||||
|   } | ||||
|  | ||||
|   void reset() { this->unread_ = 0; } | ||||
|  | ||||
|  protected: | ||||
|   ExternalRAMAllocator<uint8_t> allocator_; | ||||
|   uint8_t *buffer_; | ||||
|   size_t size_; | ||||
|   /** Total number of downloaded bytes not yet read. */ | ||||
|   size_t unread_; | ||||
| }; | ||||
|  | ||||
| }  // namespace online_image | ||||
| }  // namespace esphome | ||||
							
								
								
									
										275
									
								
								esphome/components/online_image/online_image.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								esphome/components/online_image/online_image.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | ||||
| #include "online_image.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| static const char *const TAG = "online_image"; | ||||
|  | ||||
| #include "image_decoder.h" | ||||
|  | ||||
| #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
| #include "png_image.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace online_image { | ||||
|  | ||||
| using image::ImageType; | ||||
|  | ||||
| inline bool is_color_on(const Color &color) { | ||||
|   // This produces the most accurate monochrome conversion, but is slightly slower. | ||||
|   //  return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; | ||||
|  | ||||
|   // Approximation using fast integer computations; produces acceptable results | ||||
|   // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B | ||||
|   return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; | ||||
| } | ||||
|  | ||||
| OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, | ||||
|                          uint32_t download_buffer_size) | ||||
|     : Image(nullptr, 0, 0, type), | ||||
|       buffer_(nullptr), | ||||
|       download_buffer_(download_buffer_size), | ||||
|       format_(format), | ||||
|       fixed_width_(width), | ||||
|       fixed_height_(height) { | ||||
|   this->set_url(url); | ||||
| } | ||||
|  | ||||
| void OnlineImage::release() { | ||||
|   if (this->buffer_) { | ||||
|     ESP_LOGD(TAG, "Deallocating old buffer..."); | ||||
|     this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); | ||||
|     this->data_start_ = nullptr; | ||||
|     this->buffer_ = nullptr; | ||||
|     this->width_ = 0; | ||||
|     this->height_ = 0; | ||||
|     this->buffer_width_ = 0; | ||||
|     this->buffer_height_ = 0; | ||||
|     this->end_connection_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool OnlineImage::resize_(int width_in, int height_in) { | ||||
|   int width = this->fixed_width_; | ||||
|   int height = this->fixed_height_; | ||||
|   if (this->auto_resize_()) { | ||||
|     width = width_in; | ||||
|     height = height_in; | ||||
|     if (this->width_ != width && this->height_ != height) { | ||||
|       this->release(); | ||||
|     } | ||||
|   } | ||||
|   if (this->buffer_) { | ||||
|     return false; | ||||
|   } | ||||
|   auto new_size = this->get_buffer_size_(width, height); | ||||
|   ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); | ||||
|   delay_microseconds_safe(2000); | ||||
|   this->buffer_ = this->allocator_.allocate(new_size); | ||||
|   if (this->buffer_) { | ||||
|     this->buffer_width_ = width; | ||||
|     this->buffer_height_ = height; | ||||
|     this->width_ = width; | ||||
|     ESP_LOGD(TAG, "New size: (%d, %d)", width, height); | ||||
|   } else { | ||||
| #if defined(USE_ESP8266) | ||||
|     // NOLINTNEXTLINE(readability-static-accessed-through-instance) | ||||
|     int max_block = ESP.getMaxFreeBlockSize(); | ||||
| #elif defined(USE_ESP32) | ||||
|     int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); | ||||
| #else | ||||
|     int max_block = -1; | ||||
| #endif | ||||
|     ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block); | ||||
|     this->end_connection_(); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void OnlineImage::update() { | ||||
|   if (this->decoder_) { | ||||
|     ESP_LOGW(TAG, "Image already being updated."); | ||||
|     return; | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "Updating image"); | ||||
|   } | ||||
|  | ||||
|   this->downloader_ = this->parent_->get(this->url_); | ||||
|  | ||||
|   if (this->downloader_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Download failed."); | ||||
|     this->end_connection_(); | ||||
|     this->download_error_callback_.call(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   int http_code = this->downloader_->status_code; | ||||
|   if (http_code == HTTP_CODE_NOT_MODIFIED) { | ||||
|     // Image hasn't changed on server. Skip download. | ||||
|     this->end_connection_(); | ||||
|     return; | ||||
|   } | ||||
|   if (http_code != HTTP_CODE_OK) { | ||||
|     ESP_LOGE(TAG, "HTTP result: %d", http_code); | ||||
|     this->end_connection_(); | ||||
|     this->download_error_callback_.call(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Starting download"); | ||||
|   size_t total_size = this->downloader_->content_length; | ||||
|  | ||||
| #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
|   if (this->format_ == ImageFormat::PNG) { | ||||
|     this->decoder_ = esphome::make_unique<PngDecoder>(this); | ||||
|   } | ||||
| #endif  // ONLINE_IMAGE_PNG_SUPPORT | ||||
|  | ||||
|   if (!this->decoder_) { | ||||
|     ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); | ||||
|     this->end_connection_(); | ||||
|     this->download_error_callback_.call(); | ||||
|     return; | ||||
|   } | ||||
|   this->decoder_->prepare(total_size); | ||||
|   ESP_LOGI(TAG, "Downloading image"); | ||||
| } | ||||
|  | ||||
| void OnlineImage::loop() { | ||||
|   if (!this->decoder_) { | ||||
|     // Not decoding at the moment => nothing to do. | ||||
|     return; | ||||
|   } | ||||
|   if (!this->downloader_ || this->decoder_->is_finished()) { | ||||
|     ESP_LOGD(TAG, "Image fully downloaded"); | ||||
|     this->data_start_ = buffer_; | ||||
|     this->width_ = buffer_width_; | ||||
|     this->height_ = buffer_height_; | ||||
|     this->end_connection_(); | ||||
|     this->download_finished_callback_.call(); | ||||
|     return; | ||||
|   } | ||||
|   if (this->downloader_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); | ||||
|     return; | ||||
|   } | ||||
|   size_t available = this->download_buffer_.free_capacity(); | ||||
|   if (available) { | ||||
|     auto len = this->downloader_->read(this->download_buffer_.append(), available); | ||||
|     if (len > 0) { | ||||
|       this->download_buffer_.write(len); | ||||
|       auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); | ||||
|       if (fed < 0) { | ||||
|         ESP_LOGE(TAG, "Error when decoding image."); | ||||
|         this->end_connection_(); | ||||
|         this->download_error_callback_.call(); | ||||
|         return; | ||||
|       } | ||||
|       this->download_buffer_.read(fed); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void OnlineImage::draw_pixel_(int x, int y, Color color) { | ||||
|   if (!this->buffer_) { | ||||
|     ESP_LOGE(TAG, "Buffer not allocated!"); | ||||
|     return; | ||||
|   } | ||||
|   if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { | ||||
|     ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); | ||||
|     return; | ||||
|   } | ||||
|   uint32_t pos = this->get_position_(x, y); | ||||
|   switch (this->type_) { | ||||
|     case ImageType::IMAGE_TYPE_BINARY: { | ||||
|       const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; | ||||
|       const uint32_t pos = x + y * width_8; | ||||
|       if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { | ||||
|         this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); | ||||
|       } else { | ||||
|         this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ImageType::IMAGE_TYPE_GRAYSCALE: { | ||||
|       uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); | ||||
|       if (this->has_transparency()) { | ||||
|         if (gray == 1) { | ||||
|           gray = 0; | ||||
|         } | ||||
|         if (color.w < 0x80) { | ||||
|           gray = 1; | ||||
|         } | ||||
|       } | ||||
|       this->buffer_[pos] = gray; | ||||
|       break; | ||||
|     } | ||||
|     case ImageType::IMAGE_TYPE_RGB565: { | ||||
|       uint16_t col565 = display::ColorUtil::color_to_565(color); | ||||
|       if (this->has_transparency()) { | ||||
|         if (col565 == 0x0020) { | ||||
|           col565 = 0; | ||||
|         } | ||||
|         if (color.w < 0x80) { | ||||
|           col565 = 0x0020; | ||||
|         } | ||||
|       } | ||||
|       this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); | ||||
|       this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); | ||||
|       break; | ||||
|     } | ||||
|     case ImageType::IMAGE_TYPE_RGBA: { | ||||
|       this->buffer_[pos + 0] = color.r; | ||||
|       this->buffer_[pos + 1] = color.g; | ||||
|       this->buffer_[pos + 2] = color.b; | ||||
|       this->buffer_[pos + 3] = color.w; | ||||
|       break; | ||||
|     } | ||||
|     case ImageType::IMAGE_TYPE_RGB24: | ||||
|     default: { | ||||
|       if (this->has_transparency()) { | ||||
|         if (color.b == 1 && color.r == 0 && color.g == 0) { | ||||
|           color.b = 0; | ||||
|         } | ||||
|         if (color.w < 0x80) { | ||||
|           color.r = 0; | ||||
|           color.g = 0; | ||||
|           color.b = 1; | ||||
|         } | ||||
|       } | ||||
|       this->buffer_[pos + 0] = color.r; | ||||
|       this->buffer_[pos + 1] = color.g; | ||||
|       this->buffer_[pos + 2] = color.b; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void OnlineImage::end_connection_() { | ||||
|   if (this->downloader_) { | ||||
|     this->downloader_->end(); | ||||
|     this->downloader_ = nullptr; | ||||
|   } | ||||
|   this->decoder_.reset(); | ||||
|   this->download_buffer_.reset(); | ||||
| } | ||||
|  | ||||
| bool OnlineImage::validate_url_(const std::string &url) { | ||||
|   if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { | ||||
|     ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void OnlineImage::add_on_finished_callback(std::function<void()> &&callback) { | ||||
|   this->download_finished_callback_.add(std::move(callback)); | ||||
| } | ||||
|  | ||||
| void OnlineImage::add_on_error_callback(std::function<void()> &&callback) { | ||||
|   this->download_error_callback_.add(std::move(callback)); | ||||
| } | ||||
|  | ||||
| }  // namespace online_image | ||||
| }  // namespace esphome | ||||
							
								
								
									
										184
									
								
								esphome/components/online_image/online_image.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								esphome/components/online_image/online_image.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/http_request/http_request.h" | ||||
| #include "esphome/components/image/image.h" | ||||
|  | ||||
| #include "image_decoder.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace online_image { | ||||
|  | ||||
| using t_http_codes = enum { | ||||
|   HTTP_CODE_OK = 200, | ||||
|   HTTP_CODE_NOT_MODIFIED = 304, | ||||
|   HTTP_CODE_NOT_FOUND = 404, | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @brief Format that the image is encoded with. | ||||
|  */ | ||||
| enum ImageFormat { | ||||
|   /** Automatically detect from MIME type. Not supported yet. */ | ||||
|   AUTO, | ||||
|   /** JPEG format. Not supported yet. */ | ||||
|   JPEG, | ||||
|   /** PNG format. */ | ||||
|   PNG, | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @brief Download an image from a given URL, and decode it using the specified decoder. | ||||
|  * The image will then be stored in a buffer, so that it can be re-displayed without the | ||||
|  * need to re-download or re-decode. | ||||
|  */ | ||||
| class OnlineImage : public PollingComponent, | ||||
|                     public image::Image, | ||||
|                     public Parented<esphome::http_request::HttpRequestComponent> { | ||||
|  public: | ||||
|   /** | ||||
|    * @brief Construct a new OnlineImage object. | ||||
|    * | ||||
|    * @param url URL to download the image from. | ||||
|    * @param width Desired width of the target image area. | ||||
|    * @param height Desired height of the target image area. | ||||
|    * @param format Format that the image is encoded in (@see ImageFormat). | ||||
|    * @param buffer_size Size of the buffer used to download the image. | ||||
|    */ | ||||
|   OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, | ||||
|               uint32_t buffer_size); | ||||
|  | ||||
|   void update() override; | ||||
|   void loop() override; | ||||
|  | ||||
|   /** Set the URL to download the image from. */ | ||||
|   void set_url(const std::string &url) { | ||||
|     if (this->validate_url_(url)) { | ||||
|       this->url_ = url; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Release the buffer storing the image. The image will need to be downloaded again | ||||
|    * to be able to be displayed. | ||||
|    */ | ||||
|   void release(); | ||||
|  | ||||
|   void add_on_finished_callback(std::function<void()> &&callback); | ||||
|   void add_on_error_callback(std::function<void()> &&callback); | ||||
|  | ||||
|  protected: | ||||
|   bool validate_url_(const std::string &url); | ||||
|  | ||||
|   using Allocator = ExternalRAMAllocator<uint8_t>; | ||||
|   Allocator allocator_{Allocator::Flags::ALLOW_FAILURE}; | ||||
|  | ||||
|   uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } | ||||
|   int get_buffer_size_(int width, int height) const { | ||||
|     return std::ceil(image::image_type_to_bpp(this->type_) * width * height / 8.0); | ||||
|   } | ||||
|  | ||||
|   int get_position_(int x, int y) const { | ||||
|     return ((x + y * this->buffer_width_) * image::image_type_to_bpp(this->type_)) / 8; | ||||
|   } | ||||
|  | ||||
|   ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } | ||||
|  | ||||
|   bool resize_(int width, int height); | ||||
|  | ||||
|   /** | ||||
|    * @brief Draw a pixel into the buffer. | ||||
|    * | ||||
|    * This is used by the decoder to fill the buffer that will later be displayed | ||||
|    * by the `draw` method. This will internally convert the supplied 32 bit RGBA | ||||
|    * color into the requested image storage format. | ||||
|    * | ||||
|    * @param x Horizontal pixel position. | ||||
|    * @param y Vertical pixel position. | ||||
|    * @param color 32 bit color to put into the pixel. | ||||
|    */ | ||||
|   void draw_pixel_(int x, int y, Color color); | ||||
|  | ||||
|   void end_connection_(); | ||||
|  | ||||
|   CallbackManager<void()> download_finished_callback_{}; | ||||
|   CallbackManager<void()> download_error_callback_{}; | ||||
|  | ||||
|   std::shared_ptr<http_request::HttpContainer> downloader_{nullptr}; | ||||
|   std::unique_ptr<ImageDecoder> decoder_{nullptr}; | ||||
|  | ||||
|   uint8_t *buffer_; | ||||
|   DownloadBuffer download_buffer_; | ||||
|  | ||||
|   const ImageFormat format_; | ||||
|  | ||||
|   std::string url_{""}; | ||||
|  | ||||
|   /** width requested on configuration, or 0 if non specified. */ | ||||
|   const int fixed_width_; | ||||
|   /** height requested on configuration, or 0 if non specified. */ | ||||
|   const int fixed_height_; | ||||
|   /** | ||||
|    * Actual width of the current image. If fixed_width_ is specified, | ||||
|    * this will be equal to it; otherwise it will be set once the decoding | ||||
|    * starts and the original size is known. | ||||
|    * This needs to be separate from "BaseImage::get_width()" because the latter | ||||
|    * must return 0 until the image has been decoded (to avoid showing partially | ||||
|    * decoded images). | ||||
|    */ | ||||
|   int buffer_width_; | ||||
|   /** | ||||
|    * Actual height of the current image. If fixed_height_ is specified, | ||||
|    * this will be equal to it; otherwise it will be set once the decoding | ||||
|    * starts and the original size is known. | ||||
|    * This needs to be separate from "BaseImage::get_height()" because the latter | ||||
|    * must return 0 until the image has been decoded (to avoid showing partially | ||||
|    * decoded images). | ||||
|    */ | ||||
|   int buffer_height_; | ||||
|  | ||||
|   friend void ImageDecoder::set_size(int width, int height); | ||||
|   friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> { | ||||
|  public: | ||||
|   OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} | ||||
|   TEMPLATABLE_VALUE(const char *, url) | ||||
|   void play(Ts... x) override { | ||||
|     this->parent_->set_url(this->url_.value(x...)); | ||||
|     this->parent_->update(); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   OnlineImage *parent_; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class OnlineImageReleaseAction : public Action<Ts...> { | ||||
|  public: | ||||
|   OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {} | ||||
|   TEMPLATABLE_VALUE(const char *, url) | ||||
|   void play(Ts... x) override { this->parent_->release(); } | ||||
|  | ||||
|  protected: | ||||
|   OnlineImage *parent_; | ||||
| }; | ||||
|  | ||||
| class DownloadFinishedTrigger : public Trigger<> { | ||||
|  public: | ||||
|   explicit DownloadFinishedTrigger(OnlineImage *parent) { | ||||
|     parent->add_on_finished_callback([this]() { this->trigger(); }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class DownloadErrorTrigger : public Trigger<> { | ||||
|  public: | ||||
|   explicit DownloadErrorTrigger(OnlineImage *parent) { | ||||
|     parent->add_on_error_callback([this]() { this->trigger(); }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace online_image | ||||
| }  // namespace esphome | ||||
							
								
								
									
										68
									
								
								esphome/components/online_image/png_image.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								esphome/components/online_image/png_image.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| #include "png_image.h" | ||||
| #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
|  | ||||
| #include "esphome/components/display/display_buffer.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| static const char *const TAG = "online_image.png"; | ||||
|  | ||||
| namespace esphome { | ||||
| namespace online_image { | ||||
|  | ||||
| /** | ||||
|  * @brief Callback method that will be called by the PNGLE engine when the basic | ||||
|  * data of the image is received (i.e. width and height); | ||||
|  * | ||||
|  * @param pngle The PNGLE object, including the context data. | ||||
|  * @param w The width of the image. | ||||
|  * @param h The height of the image. | ||||
|  */ | ||||
| static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) { | ||||
|   PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); | ||||
|   decoder->set_size(w, h); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @brief Callback method that will be called by the PNGLE engine when a chunk | ||||
|  * of the image is decoded. | ||||
|  * | ||||
|  * @param pngle The PNGLE object, including the context data. | ||||
|  * @param x The X coordinate to draw the rectangle on. | ||||
|  * @param y The Y coordinate to draw the rectangle on. | ||||
|  * @param w The width of the rectangle to draw. | ||||
|  * @param h The height of the rectangle to draw. | ||||
|  * @param rgba The color to paint the rectangle in. | ||||
|  */ | ||||
| static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) { | ||||
|   PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); | ||||
|   Color color(rgba[0], rgba[1], rgba[2], rgba[3]); | ||||
|   decoder->draw(x, y, w, h, color); | ||||
| } | ||||
|  | ||||
| void PngDecoder::prepare(uint32_t download_size) { | ||||
|   ImageDecoder::prepare(download_size); | ||||
|   pngle_set_user_data(this->pngle_, this); | ||||
|   pngle_set_init_callback(this->pngle_, init_callback); | ||||
|   pngle_set_draw_callback(this->pngle_, draw_callback); | ||||
| } | ||||
|  | ||||
| int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { | ||||
|   if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { | ||||
|     ESP_LOGD(TAG, "Waiting for data"); | ||||
|     return 0; | ||||
|   } | ||||
|   auto fed = pngle_feed(this->pngle_, buffer, size); | ||||
|   if (fed < 0) { | ||||
|     ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_)); | ||||
|   } else { | ||||
|     this->decoded_bytes_ += fed; | ||||
|   } | ||||
|   return fed; | ||||
| } | ||||
|  | ||||
| }  // namespace online_image | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
							
								
								
									
										33
									
								
								esphome/components/online_image/png_image.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/online_image/png_image.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "image_decoder.h" | ||||
| #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
| #include <pngle.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace online_image { | ||||
|  | ||||
| /** | ||||
|  * @brief Image decoder specialization for PNG images. | ||||
|  */ | ||||
| class PngDecoder : public ImageDecoder { | ||||
|  public: | ||||
|   /** | ||||
|    * @brief Construct a new PNG Decoder object. | ||||
|    * | ||||
|    * @param display The image to decode the stream into. | ||||
|    */ | ||||
|   PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} | ||||
|   ~PngDecoder() override { pngle_destroy(this->pngle_); } | ||||
|  | ||||
|   void prepare(uint32_t download_size) override; | ||||
|   int HOT decode(uint8_t *buffer, size_t size) override; | ||||
|  | ||||
|  protected: | ||||
|   pngle_t *pngle_; | ||||
| }; | ||||
|  | ||||
| }  // namespace online_image | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
| @@ -49,7 +49,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
| #ifdef USE_ESP32 | ||||
|   void configure_rmt_(); | ||||
|  | ||||
|   uint32_t current_carrier_frequency_{UINT32_MAX}; | ||||
|   uint32_t current_carrier_frequency_{38000}; | ||||
|   bool initialized_{false}; | ||||
|   std::vector<rmt_item32_t> rmt_temp_; | ||||
|   esp_err_t error_code_{ESP_OK}; | ||||
|   | ||||
| @@ -1,20 +1,20 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_CYCLE, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_ICON, | ||||
|     CONF_ID, | ||||
|     CONF_INDEX, | ||||
|     CONF_MODE, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_OPERATION, | ||||
|     CONF_OPTION, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_CYCLE, | ||||
|     CONF_MODE, | ||||
|     CONF_OPERATION, | ||||
|     CONF_INDEX, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|   | ||||
| @@ -1,22 +1,27 @@ | ||||
| import math | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ABOVE, | ||||
|     CONF_ACCURACY_DECIMALS, | ||||
|     CONF_ALPHA, | ||||
|     CONF_BELOW, | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_EXPIRE_AFTER, | ||||
|     CONF_FILTERS, | ||||
|     CONF_FORCE_UPDATE, | ||||
|     CONF_FROM, | ||||
|     CONF_ICON, | ||||
|     CONF_ID, | ||||
|     CONF_IGNORE_OUT_OF_RANGE, | ||||
|     CONF_MAX_VALUE, | ||||
|     CONF_METHOD, | ||||
|     CONF_MIN_VALUE, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_MULTIPLE, | ||||
|     CONF_ON_RAW_VALUE, | ||||
|     CONF_ON_VALUE, | ||||
| @@ -30,14 +35,9 @@ from esphome.const import ( | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
|     CONF_UNIT_OF_MEASUREMENT, | ||||
|     CONF_WINDOW_SIZE, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_FORCE_UPDATE, | ||||
|     CONF_VALUE, | ||||
|     CONF_MIN_VALUE, | ||||
|     CONF_MAX_VALUE, | ||||
|     CONF_METHOD, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_WINDOW_SIZE, | ||||
|     DEVICE_CLASS_APPARENT_POWER, | ||||
|     DEVICE_CLASS_AQI, | ||||
|     DEVICE_CLASS_ATMOSPHERIC_PRESSURE, | ||||
|   | ||||
| @@ -19,24 +19,22 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) { | ||||
|  | ||||
| socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { | ||||
| #if USE_NETWORK_IPV6 | ||||
|   if (addrlen < sizeof(sockaddr_in6)) { | ||||
|     errno = EINVAL; | ||||
|     return 0; | ||||
|   } | ||||
|   auto *server = reinterpret_cast<sockaddr_in6 *>(addr); | ||||
|   memset(server, 0, sizeof(sockaddr_in6)); | ||||
|   server->sin6_family = AF_INET6; | ||||
|   server->sin6_port = htons(port); | ||||
|   if (ip_address.find(':') != std::string::npos) { | ||||
|     if (addrlen < sizeof(sockaddr_in6)) { | ||||
|       errno = EINVAL; | ||||
|       return 0; | ||||
|     } | ||||
|     auto *server = reinterpret_cast<sockaddr_in6 *>(addr); | ||||
|     memset(server, 0, sizeof(sockaddr_in6)); | ||||
|     server->sin6_family = AF_INET6; | ||||
|     server->sin6_port = htons(port); | ||||
|  | ||||
|   if (ip_address.find('.') != std::string::npos) { | ||||
|     server->sin6_addr.un.u32_addr[3] = inet_addr(ip_address.c_str()); | ||||
|   } else { | ||||
|     ip6_addr_t ip6; | ||||
|     inet6_aton(ip_address.c_str(), &ip6); | ||||
|     memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); | ||||
|     return sizeof(sockaddr_in6); | ||||
|   } | ||||
|   return sizeof(sockaddr_in6); | ||||
| #else | ||||
| #endif /* USE_NETWORK_IPV6 */ | ||||
|   if (addrlen < sizeof(sockaddr_in)) { | ||||
|     errno = EINVAL; | ||||
|     return 0; | ||||
| @@ -47,7 +45,6 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri | ||||
|   server->sin_addr.s_addr = inet_addr(ip_address.c_str()); | ||||
|   server->sin_port = htons(port); | ||||
|   return sizeof(sockaddr_in); | ||||
| #endif /* USE_NETWORK_IPV6 */ | ||||
| } | ||||
|  | ||||
| socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { | ||||
|   | ||||
| @@ -7,10 +7,6 @@ namespace spi { | ||||
|  | ||||
| const char *const TAG = "spi"; | ||||
|  | ||||
| SPIDelegate *const SPIDelegate::NULL_DELEGATE =  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|     new SPIDelegateDummy(); | ||||
| // https://bugs.llvm.org/show_bug.cgi?id=48040 | ||||
|  | ||||
| bool SPIDelegate::is_ready() { return true; } | ||||
|  | ||||
| GPIOPin *const NullPin::NULL_PIN = new NullPin();  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| @@ -79,8 +75,6 @@ void SPIComponent::dump_config() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); } | ||||
|  | ||||
| uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); } | ||||
|  | ||||
| void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); } | ||||
|   | ||||
| @@ -163,8 +163,6 @@ class Utility { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class SPIDelegateDummy; | ||||
|  | ||||
| // represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is | ||||
| // a thin wrapper over SPIClass. | ||||
| class SPIDelegate { | ||||
| @@ -250,21 +248,6 @@ class SPIDelegate { | ||||
|   uint32_t data_rate_{1000000}; | ||||
|   SPIMode mode_{MODE0}; | ||||
|   GPIOPin *cs_pin_{NullPin::NULL_PIN}; | ||||
|   static SPIDelegate *const NULL_DELEGATE;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * A dummy SPIDelegate that complains if it's used. | ||||
|  */ | ||||
|  | ||||
| class SPIDelegateDummy : public SPIDelegate { | ||||
|  public: | ||||
|   SPIDelegateDummy() = default; | ||||
|  | ||||
|   uint8_t transfer(uint8_t data) override { return 0; } | ||||
|   void end_transaction() override{}; | ||||
|  | ||||
|   void begin_transaction() override; | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -382,7 +365,7 @@ class SPIClient { | ||||
|  | ||||
|   virtual void spi_teardown() { | ||||
|     this->parent_->unregister_device(this); | ||||
|     this->delegate_ = SPIDelegate::NULL_DELEGATE; | ||||
|     this->delegate_ = nullptr; | ||||
|   } | ||||
|  | ||||
|   bool spi_is_ready() { return this->delegate_->is_ready(); } | ||||
| @@ -393,7 +376,7 @@ class SPIClient { | ||||
|   uint32_t data_rate_{1000000}; | ||||
|   SPIComponent *parent_{nullptr}; | ||||
|   GPIOPin *cs_{nullptr}; | ||||
|   SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; | ||||
|   SPIDelegate *delegate_{nullptr}; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import Condition, maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
| @@ -10,11 +10,11 @@ from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_INVERTED, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_ON_TURN_OFF, | ||||
|     CONF_ON_TURN_ON, | ||||
|     CONF_RESTORE_MODE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     DEVICE_CLASS_EMPTY, | ||||
|     DEVICE_CLASS_OUTLET, | ||||
|     DEVICE_CLASS_SWITCH, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user