Merge branch 'dev' into gsm

This commit is contained in:
oarcher 2024-08-07 22:46:09 +02:00
commit 51aca893da
138 changed files with 5917 additions and 1042 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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;
}

View file

@ -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

View file

@ -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("}");
}

View file

@ -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;

View file

@ -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"]

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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() {

View file

@ -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_{};
};

View file

@ -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"]

View file

@ -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"

View file

@ -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]))

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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; }

View file

@ -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;
}

View file

@ -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_();

View file

@ -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"]

View file

@ -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

View file

@ -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")

View file

@ -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))

View file

@ -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
)

View 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,
)
)

View file

@ -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()

View file

@ -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))

View file

@ -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())

View file

@ -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)

View 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))

View 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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View 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()))

View 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

View file

@ -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

View 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()))

View 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

View 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
)
)

View 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)

View 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()))

View 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

View 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()))

View 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

View 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,
)
)

View file

@ -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

View file

@ -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))

View file

@ -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)

View file

@ -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, {})

View 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)

View 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()

View 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()

View 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
)

View 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()

View 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()

View 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()

View 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()

View file

@ -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()

View 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()

View 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()

View 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()

View 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)

View 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)

View file

@ -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):

View 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)

View 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()

View 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()

View 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)

View 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()

View 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()

View 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)

View 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()

View 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)

View file

@ -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)
),
}
)

View file

@ -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

View file

@ -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)

View 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

View 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

View file

@ -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"

View file

@ -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 = [

View 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)

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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};

View file

@ -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

View file

@ -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,

View file

@ -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) {

View file

@ -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); }

View file

@ -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};
};
/**

View file

@ -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