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