Merge branch 'dev' into gsm

This commit is contained in:
Olivier ARCHER 2024-08-03 02:37:37 +02:00 committed by GitHub
commit 5b4ccabc17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1148 additions and 189 deletions

2
.gitignore vendored
View file

@ -138,3 +138,5 @@ sdkconfig.*
.tests/
/components
/managed_components

View file

@ -60,7 +60,7 @@ bool AdE7953Spi::ade_read_16(uint16_t reg, uint16_t *value) {
this->write_byte16(reg);
this->transfer_byte(0x80);
uint8_t recv[2];
this->read_array(recv, 4);
this->read_array(recv, 2);
*value = encode_uint16(recv[0], recv[1]);
this->disable();
return false;

View file

@ -1,25 +1,27 @@
import base64
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION,
CONF_ACTIONS,
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_EVENT,
CONF_ID,
CONF_KEY,
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
CONF_PASSWORD,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
CONF_SERVICE,
CONF_VARIABLES,
CONF_SERVICES,
CONF_TRIGGER_ID,
CONF_EVENT,
CONF_TAG,
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
CONF_TRIGGER_ID,
CONF_VARIABLES,
)
from esphome.core import coroutine_with_priority
@ -63,40 +65,51 @@ def validate_encryption_key(value):
return value
CONFIG_SCHEMA = cv.Schema(
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port,
cv.Optional(CONF_PASSWORD, default=""): cv.string_strict,
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_SERVICES): automation.validate_automation(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Required(CONF_SERVICE): cv.valid_name,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.validate_id_name: cv.one_of(
*SERVICE_ARG_NATIVE_TYPES, lower=True
),
}
),
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
}
),
cv.Optional(CONF_ENCRYPTION): cv.Schema(
{
cv.Required(CONF_KEY): validate_encryption_key,
}
),
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
single=True
),
}
).extend(cv.COMPONENT_SCHEMA)
},
cv.All(
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
),
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port,
cv.Optional(CONF_PASSWORD, default=""): cv.string_strict,
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
cv.Exclusive(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): cv.Schema(
{
cv.Required(CONF_KEY): validate_encryption_key,
}
),
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
single=True
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
)
@coroutine_with_priority(40.0)
@ -108,7 +121,7 @@ async def to_code(config):
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
for conf in config.get(CONF_SERVICES, []):
for conf in config.get(CONF_ACTIONS, []):
template_args = []
func_args = []
service_arg_names = []
@ -119,7 +132,7 @@ async def to_code(config):
service_arg_names.append(name)
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
)
cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf)
@ -152,28 +165,43 @@ async def to_code(config):
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Required(CONF_SERVICE): cv.templatable(cv.string),
cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{cv.string: cv.returning_lambda}
),
}
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.templatable(
cv.string
),
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.templatable(
cv.string
),
cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{cv.string: cv.returning_lambda}
),
}
),
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
)
@automation.register_action(
"homeassistant.action",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
)
@automation.register_action(
"homeassistant.service",
HomeAssistantServiceCallAction,
HOMEASSISTANT_SERVICE_ACTION_SCHEMA,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
)
async def homeassistant_service_to_code(config, action_id, template_arg, args):
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_SERVICE], args, None)
templ = await cg.templatable(config[CONF_ACTION], args, None)
cg.add(var.set_service(templ))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)

View file

@ -138,8 +138,8 @@ void HttpRequestUpdate::update() {
this->publish_state();
}
void HttpRequestUpdate::perform() {
if (this->state_ != update::UPDATE_STATE_AVAILABLE) {
void HttpRequestUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
return;
}

View file

@ -15,7 +15,7 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
void setup() override;
void update() override;
void perform() override;
void perform(bool force) override;
void set_source_url(const std::string &source_url) { this->source_url_ = source_url; }

View file

@ -1,8 +1,7 @@
import re
import esphome.config_validation as cv
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import __version__
CODEOWNERS = ["@esphome/core"]
@ -39,4 +38,4 @@ def _process_next_url(url: str):
async def setup_improv_core(var, config):
if CONF_NEXT_URL in config:
cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL])))
cg.add_library("esphome/Improv", "1.2.3")
cg.add_library("improv/Improv", "1.2.4")

View file

@ -1,5 +1,6 @@
import logging
from esphome.automation import build_automation, register_action, validate_automation
import esphome.codegen as cg
from esphome.components.display import Display
import esphome.config_validation as cv
@ -8,7 +9,11 @@ from esphome.const import (
CONF_BUFFER_SIZE,
CONF_ID,
CONF_LAMBDA,
CONF_ON_IDLE,
CONF_PAGES,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_TYPE,
)
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
@ -16,21 +21,26 @@ from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .automation import update_to_code
from .btn import btn_spec
from .label import label_spec
from .lvcode import ConstantLiteral, LvContext
from .lv_validation import lv_images_used
from .lvcode import LvContext
from .obj import obj_spec
from .schemas import any_widget_schema, obj_schema
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
from .schemas import any_widget_schema, create_modify_schema, obj_schema
from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import generate_triggers
from .types import (
WIDGET_TYPES,
FontEngine,
IdleTrigger,
LvglComponent,
lv_disp_t_ptr,
ObjUpdateAction,
lv_font_t,
lvgl_ns,
)
from .widget import LvScrActType, Widget, add_widgets, set_obj_properties
from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties
DOMAIN = "lvgl"
DEPENDENCIES = ("display",)
@ -41,17 +51,21 @@ LOGGER = logging.getLogger(__name__)
for w_type in (label_spec, obj_spec, btn_spec):
WIDGET_TYPES[w_type.name] = w_type
lv_scr_act_spec = LvScrActType()
lv_scr_act = Widget.create(
None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None
)
WIDGET_SCHEMA = any_widget_schema()
for w_type in WIDGET_TYPES.values():
register_action(
f"lvgl.{w_type.name}.update",
ObjUpdateAction,
create_modify_schema(w_type),
)(update_to_code)
async def add_init_lambda(lv_component, init):
if init:
lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")])
lamb = await cg.process_lambda(
Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")]
)
cg.add(lv_component.add_init_lambda(lamb))
@ -99,6 +113,13 @@ def final_validation(config):
buffer_frac = config[CONF_BUFFER_SIZE]
if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config:
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
for image_id in lv_images_used:
path = global_config.get_path_for_id(image_id)[:-1]
image_conf = global_config.get_config_for_path(path)
if image_conf[CONF_TYPE] in ("RGBA", "RGB24"):
raise cv.Invalid(
"Using RGBA or RGB24 in image config not compatible with LVGL", path
)
async def to_code(config):
@ -174,9 +195,15 @@ async def to_code(config):
with LvContext():
await touchscreens_to_code(lv_component, config)
await rotary_encoders_to_code(lv_component, config)
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
Widget.set_completed()
Widget.set_completed()
await generate_triggers(lv_component)
for conf in config.get(CONF_ON_IDLE, ()):
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)
await build_automation(idle_trigger, [], conf)
await add_init_lambda(lv_component, LvContext.get_code())
for comp in helpers.lvgl_components_required:
CORE.add_define(f"USE_LVGL_{comp.upper()}")
@ -212,9 +239,18 @@ CONFIG_SCHEMA = (
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian"
),
cv.Optional(CONF_ON_IDLE): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
cv.Required(CONF_TIMEOUT): cv.templatable(
cv.positive_time_period_milliseconds
),
}
),
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema,
cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG,
}
)
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))

View file

@ -0,0 +1,188 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TIMEOUT
from esphome.core import Lambda
from esphome.cpp_generator import RawStatement
from esphome.cpp_types import nullptr
from .defines import CONF_LVGL_ID, CONF_SHOW_SNOW, literal
from .lv_validation import lv_bool
from .lvcode import (
LambdaContext,
ReturnStatement,
add_line_marks,
lv,
lv_add,
lv_obj,
lvgl_comp,
)
from .schemas import ACTION_SCHEMA, LVGL_SCHEMA
from .types import (
LvglAction,
LvglComponent,
LvglComponentPtr,
LvglCondition,
ObjUpdateAction,
lv_obj_t,
)
from .widget import Widget, get_widget, lv_scr_act, set_obj_properties
async def action_to_code(action: list, action_id, widget: Widget, template_arg, args):
with LambdaContext() as context:
lv.cond_if(widget.obj == nullptr)
lv_add(RawStatement(" return;"))
lv.cond_endif()
code = context.get_code()
code.extend(action)
action = "\n".join(code) + "\n\n"
lamb = await cg.process_lambda(Lambda(action), args)
var = cg.new_Pvariable(action_id, template_arg, lamb)
return var
async def update_to_code(config, action_id, template_arg, args):
if config is not None:
widget = await get_widget(config)
with LambdaContext() as context:
add_line_marks(action_id)
await set_obj_properties(widget, config)
await widget.type.to_code(widget, config)
if (
widget.type.w_type.value_property is not None
and widget.type.w_type.value_property in config
):
lv.event_send(widget.obj, literal("LV_EVENT_VALUE_CHANGED"), nullptr)
return await action_to_code(
context.get_code(), action_id, widget, template_arg, args
)
@automation.register_condition(
"lvgl.is_paused",
LvglCondition,
LVGL_SCHEMA,
)
async def lvgl_is_paused(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID]
with LambdaContext(
[(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_
) as context:
lv_add(ReturnStatement(lvgl_comp.is_paused()))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
await cg.register_parented(var, lvgl)
return var
@automation.register_condition(
"lvgl.is_idle",
LvglCondition,
LVGL_SCHEMA.extend(
{
cv.Required(CONF_TIMEOUT): cv.templatable(
cv.positive_time_period_milliseconds
)
}
),
)
async def lvgl_is_idle(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID]
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
with LambdaContext(
[(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_
) as context:
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
await cg.register_parented(var, lvgl)
return var
@automation.register_action(
"lvgl.widget.redraw",
ObjUpdateAction,
cv.Schema(
{
cv.Optional(CONF_ID): cv.use_id(lv_obj_t),
cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent),
}
),
)
async def obj_invalidate_to_code(config, action_id, template_arg, args):
if CONF_ID in config:
w = await get_widget(config)
else:
w = lv_scr_act
with LambdaContext() as context:
add_line_marks(action_id)
lv_obj.invalidate(w.obj)
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
@automation.register_action(
"lvgl.pause",
LvglAction,
{
cv.GenerateID(): cv.use_id(LvglComponent),
cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool,
},
)
async def pause_action_to_code(config, action_id, template_arg, args):
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context:
add_line_marks(action_id)
lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW]))
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"lvgl.resume",
LvglAction,
{
cv.GenerateID(): cv.use_id(LvglComponent),
},
)
async def resume_action_to_code(config, action_id, template_arg, args):
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context:
add_line_marks(action_id)
lv_add(lvgl_comp.set_paused(False, False))
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action("lvgl.widget.disable", ObjUpdateAction, ACTION_SCHEMA)
async def obj_disable_to_code(config, action_id, template_arg, args):
w = await get_widget(config)
with LambdaContext() as context:
add_line_marks(action_id)
w.add_state("LV_STATE_DISABLED")
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
@automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA)
async def obj_enable_to_code(config, action_id, template_arg, args):
w = await get_widget(config)
with LambdaContext() as context:
add_line_marks(action_id)
w.clear_state("LV_STATE_DISABLED")
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
@automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA)
async def obj_hide_to_code(config, action_id, template_arg, args):
w = await get_widget(config)
with LambdaContext() as context:
add_line_marks(action_id)
w.add_flag("LV_OBJ_FLAG_HIDDEN")
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
@automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA)
async def obj_show_to_code(config, action_id, template_arg, args):
w = await get_widget(config)
with LambdaContext() as context:
add_line_marks(action_id)
w.clear_flag("LV_OBJ_FLAG_HIDDEN")
return await action_to_code(context.get_code(), action_id, w, template_arg, args)

View file

@ -9,9 +9,6 @@ class BtnType(WidgetType):
def __init__(self):
super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,))
async def to_code(self, w, config):
return []
def obj_creator(self, parent: MockObjClass, config: dict):
"""
LVGL 8 calls buttons `btn`
@ -21,5 +18,8 @@ class BtnType(WidgetType):
def get_uses(self):
return ("btn",)
async def to_code(self, w, config):
return []
btn_spec = BtnType()

View file

@ -4,12 +4,32 @@ Constants already defined in esphome.const are not duplicated here and must be i
"""
from typing import Union
from esphome import codegen as cg, config_validation as cv
from esphome.core import ID, Lambda
from esphome.cpp_generator import Literal
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from .lvcode import ConstantLiteral
from .helpers import requires_component
class ConstantLiteral(Literal):
__slots__ = ("constant",)
def __init__(self, constant: str):
super().__init__()
self.constant = constant
def __str__(self):
return self.constant
def literal(arg: Union[str, ConstantLiteral]):
if isinstance(arg, str):
return ConstantLiteral(arg)
return arg
class LValidator:
@ -18,14 +38,19 @@ class LValidator:
has `process()` to convert a value during code generation
"""
def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None):
def __init__(
self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None
):
self.validator = validator
self.rtype = rtype
self.idtype = idtype
self.idexpr = idexpr
self.retmapper = retmapper
self.requires = requires
def __call__(self, value):
if self.requires:
value = requires_component(self.requires)(value)
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if self.idtype is not None and isinstance(value, ID):
@ -422,6 +447,7 @@ CONF_RECOLOR = "recolor"
CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROTARY_ENCODERS = "rotary_encoders"
CONF_ROWS = "rows"
CONF_SCALES = "scales"
CONF_SCALE_LINES = "scale_lines"

View file

@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor
from esphome.components.color import ColorStruct
from esphome.components.font import Font
from esphome.components.image import Image_
from esphome.components.sensor import Sensor
from esphome.components.text_sensor import TextSensor
import esphome.config_validation as cv
@ -13,22 +14,15 @@ from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from . import types as ty
from .defines import LV_FONTS, LValidator, LvConstant
from .defines import LV_FONTS, ConstantLiteral, LValidator, LvConstant, literal
from .helpers import (
esphome_fonts_used,
lv_fonts_used,
lvgl_components_required,
requires_component,
)
from .lvcode import ConstantLiteral, lv_expr
from .types import lv_font_t
def literal_mapper(value, args=()):
if isinstance(value, str):
return ConstantLiteral(value)
return value
from .lvcode import lv_expr
from .types import lv_font_t, lv_img_t
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
@ -43,7 +37,7 @@ def opacity_validator(value):
return value
opacity = LValidator(opacity_validator, uint32, retmapper=literal_mapper)
opacity = LValidator(opacity_validator, uint32, retmapper=literal)
@schema_extractor("one_of")
@ -79,9 +73,7 @@ def pixels_or_percent_validator(value):
return f"lv_pct({int(cv.percentage(value) * 100)})"
pixels_or_percent = LValidator(
pixels_or_percent_validator, uint32, retmapper=literal_mapper
)
pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal)
def zoom(value):
@ -115,7 +107,7 @@ def size_validator(value):
return f"lv_pct({int(cv.percentage(value) * 100)})"
size = LValidator(size_validator, uint32, retmapper=literal_mapper)
size = LValidator(size_validator, uint32, retmapper=literal)
radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
@ -130,21 +122,37 @@ def radius_validator(value):
return value
radius = LValidator(radius_validator, uint32, retmapper=literal)
def id_name(value):
if value == SCHEMA_EXTRACT:
return "id"
return cv.validate_id_name(value)
radius = LValidator(radius_validator, uint32, retmapper=literal_mapper)
def stop_value(value):
return cv.int_range(0, 255)(value)
lv_images_used = set()
def image_validator(value):
value = requires_component("image")(value)
value = cv.use_id(Image_)(value)
lv_images_used.add(value)
return value
lv_image = LValidator(
image_validator,
lv_img_t,
retmapper=lambda x: lv_expr.img_from(MockObj(x)),
requires="image",
)
lv_bool = LValidator(
cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper
cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal
)

View file

@ -8,8 +8,8 @@ from esphome.cpp_generator import (
AssignmentExpression,
CallExpression,
Expression,
ExpressionStatement,
LambdaExpression,
Literal,
MockObj,
RawExpression,
RawStatement,
@ -19,7 +19,9 @@ from esphome.cpp_generator import (
statement,
)
from .defines import ConstantLiteral
from .helpers import get_line_marks
from .types import lv_group_t
_LOGGER = logging.getLogger(__name__)
@ -105,29 +107,40 @@ class LambdaContext(CodeContext):
def __init__(
self,
parameters: list[tuple[SafeExpType, str]],
return_type: SafeExpType = None,
parameters: list[tuple[SafeExpType, str]] = None,
return_type: SafeExpType = cg.void,
capture: str = "",
):
super().__init__()
self.code_list: list[Statement] = []
self.parameters = parameters
self.return_type = return_type
self.capture = capture
def add(self, expression: Union[Expression, Statement]):
self.code_list.append(expression)
return expression
async def code(self) -> LambdaExpression:
async def get_lambda(self) -> LambdaExpression:
code_text = self.get_code()
return await cg.process_lambda(
Lambda("\n".join(code_text) + "\n\n"),
self.parameters,
capture=self.capture,
return_type=self.return_type,
)
def get_code(self):
code_text = []
for exp in self.code_list:
text = str(statement(exp))
text = text.rstrip()
code_text.append(text)
return await cg.process_lambda(
Lambda("\n".join(code_text) + "\n\n"),
self.parameters,
return_type=self.return_type,
)
return code_text
def __enter__(self):
super().__enter__()
return self
class LocalVariable(MockObj):
@ -187,13 +200,18 @@ class MockLv:
return result
def cond_if(self, expression: Expression):
CodeContext.append(RawExpression(f"if({expression}) {{"))
CodeContext.append(RawStatement(f"if {expression} {{"))
def cond_else(self):
CodeContext.append(RawExpression("} else {"))
CodeContext.append(RawStatement("} else {"))
def cond_endif(self):
CodeContext.append(RawExpression("}"))
CodeContext.append(RawStatement("}"))
class ReturnStatement(ExpressionStatement):
def __str__(self):
return f"return {self.expression};"
class LvExpr(MockLv):
@ -210,6 +228,7 @@ lv = MockLv("lv_")
lv_expr = LvExpr("lv_")
# Mock for lv_obj_ calls
lv_obj = MockLv("lv_obj_")
lvgl_comp = MockObj("lvgl_comp", "->")
# equivalent to cg.add() for the lvgl init context
@ -226,12 +245,19 @@ def lv_assign(target, expression):
lv_add(RawExpression(f"{target} = {expression}"))
class ConstantLiteral(Literal):
__slots__ = ("constant",)
lv_groups = {} # Widget group names
def __init__(self, constant: str):
super().__init__()
self.constant = constant
def __str__(self):
return self.constant
def add_group(name):
if name is None:
return None
fullname = f"lv_esp_group_{name}"
if name not in lv_groups:
gid = ID(fullname, True, type=lv_group_t.operator("ptr"))
lv_add(
AssignmentExpression(
type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create()
)
)
lv_groups[name] = ConstantLiteral(fullname)
return lv_groups[name]

View file

@ -19,13 +19,35 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) {
}
void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
auto now = millis();
this->draw_buffer_(area, (const uint8_t *) color_p);
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
lv_area_get_height(area), (int) (millis() - now));
if (!this->paused_) {
auto now = millis();
this->draw_buffer_(area, (const uint8_t *) color_p);
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
lv_area_get_height(area), (int) (millis() - now));
}
lv_disp_flush_ready(disp_drv);
}
void LvglComponent::write_random_() {
// length of 2 lines in 32 bit units
// we write 2 lines for the benefit of displays that won't write one line at a time.
size_t line_len = this->disp_drv_.hor_res * LV_COLOR_DEPTH / 8 / 4 * 2;
for (size_t i = 0; i != line_len; i++) {
((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32();
}
lv_area_t area;
area.x1 = 0;
area.x2 = this->disp_drv_.hor_res - 1;
if (this->snow_line_ == this->disp_drv_.ver_res / 2) {
area.y1 = static_cast<lv_coord_t>(random_uint32() % (this->disp_drv_.ver_res / 2) * 2);
} else {
area.y1 = this->snow_line_++ * 2;
}
// write 2 lines
area.y2 = area.y1 + 1;
this->draw_buffer_(&area, (const uint8_t *) this->draw_buf_.buf1);
}
void LvglComponent::setup() {
ESP_LOGCONFIG(TAG, "LVGL Setup starts");
#if LV_USE_LOG
@ -74,10 +96,53 @@ void LvglComponent::setup() {
ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated);
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
for (const auto &v : this->init_lambdas_)
v(this->disp_);
v(this);
lv_disp_trig_activity(this->disp_);
ESP_LOGCONFIG(TAG, "LVGL Setup complete");
}
#ifdef USE_LVGL_IMAGE
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) {
if (img_dsc == nullptr)
img_dsc = new lv_img_dsc_t(); // NOLINT
img_dsc->header.always_zero = 0;
img_dsc->header.reserved = 0;
img_dsc->header.w = src->get_width();
img_dsc->header.h = src->get_height();
img_dsc->data = src->get_data_start();
img_dsc->data_size = image_type_to_width_stride(img_dsc->header.w * img_dsc->header.h, src->get_type());
switch (src->get_type()) {
case image::IMAGE_TYPE_BINARY:
img_dsc->header.cf = LV_IMG_CF_ALPHA_1BIT;
break;
case image::IMAGE_TYPE_GRAYSCALE:
img_dsc->header.cf = LV_IMG_CF_ALPHA_8BIT;
break;
case image::IMAGE_TYPE_RGB24:
img_dsc->header.cf = LV_IMG_CF_RGB888;
break;
case image::IMAGE_TYPE_RGB565:
#if LV_COLOR_DEPTH == 16
img_dsc->header.cf = src->has_transparency() ? LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED : LV_IMG_CF_TRUE_COLOR;
#else
img_dsc->header.cf = LV_IMG_CF_RGB565;
#endif
break;
case image::IMAGE_TYPE_RGBA:
#if LV_COLOR_DEPTH == 32
img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR;
#else
img_dsc->header.cf = LV_IMG_CF_RGBA8888;
#endif
break;
}
return img_dsc;
}
#endif
} // namespace lvgl
} // namespace esphome

View file

@ -1,23 +1,32 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_LVGL
#ifdef USE_LVGL_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif // USE_LVGL_BINARY_SENSOR
#ifdef USE_LVGL_ROTARY_ENCODER
#include "esphome/components/rotary_encoder/rotary_encoder.h"
#endif // USE_LVGL_ROTARY_ENCODER
// required for clang-tidy
#ifndef LV_CONF_H
#define LV_CONF_SKIP 1 // NOLINT
#endif
#endif // LV_CONF_H
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <lvgl.h>
#include <utility>
#include <vector>
#ifdef USE_LVGL_IMAGE
#include "esphome/components/image/image.h"
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_FONT
#include "esphome/components/font/font.h"
#endif
#endif // USE_LVGL_FONT
#ifdef USE_LVGL_TOUCHSCREEN
#include "esphome/components/touchscreen/touchscreen.h"
#endif // USE_LVGL_TOUCHSCREEN
@ -40,7 +49,7 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
// Parent class for things that wrap an LVGL object
class LvCompound final {
public:
virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
lv_obj_t *obj{};
};
@ -49,6 +58,15 @@ using set_value_lambda_t = std::function<void(float)>;
using event_callback_t = void(_lv_event_t *);
using text_lambda_t = std::function<const char *()>;
template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
public:
explicit ObjUpdateAction(std::function<void(Ts...)> &&lamb) : lamb_(std::move(lamb)) {}
void play(Ts... x) override { this->lamb_(x...); }
protected:
std::function<void(Ts...)> lamb_;
};
#ifdef USE_LVGL_FONT
class FontEngine {
public:
@ -67,6 +85,9 @@ class FontEngine {
lv_font_t lv_font_{};
};
#endif // USE_LVGL_FONT
#ifdef USE_LVGL_IMAGE
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr);
#endif // USE_LVGL_IMAGE
class LvglComponent : public PollingComponent {
constexpr static const char *const TAG = "lvgl";
@ -92,27 +113,54 @@ class LvglComponent : public PollingComponent {
area->y2++;
}
void loop() override { lv_timer_handler_run_in_period(5); }
void setup() override;
void update() override {}
void update() override {
// update indicators
if (this->paused_) {
return;
}
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
}
void loop() override {
if (this->paused_) {
if (this->show_snow_)
this->write_random_();
}
lv_timer_handler_run_in_period(5);
}
void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
this->idle_callbacks_.add(std::move(callback));
}
void add_display(display::Display *display) { this->displays_.push_back(display); }
void add_init_lambda(const std::function<void(lv_disp_t *)> &lamb) { this->init_lambdas_.push_back(lamb); }
void add_init_lambda(const std::function<void(LvglComponent *)> &lamb) { this->init_lambdas_.push_back(lamb); }
void dump_config() override;
void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; }
bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; }
void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
lv_disp_t *get_disp() { return this->disp_; }
void set_paused(bool paused, bool show_snow) {
this->paused_ = paused;
this->show_snow_ = show_snow;
this->snow_line_ = 0;
if (!paused && lv_scr_act() != nullptr) {
lv_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act());
}
}
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
lv_obj_add_event_cb(obj, callback, event, this);
if (event == LV_EVENT_VALUE_CHANGED) {
lv_obj_add_event_cb(obj, callback, lv_custom_event, this);
}
}
bool is_paused() const { return this->paused_; }
protected:
void write_random_();
void draw_buffer_(const lv_area_t *area, const uint8_t *ptr);
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
std::vector<display::Display *> displays_{};
@ -120,12 +168,52 @@ class LvglComponent : public PollingComponent {
lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{};
bool paused_{};
bool show_snow_{};
lv_coord_t snow_line_{};
std::vector<std::function<void(lv_disp_t *)>> init_lambdas_;
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
CallbackManager<void(uint32_t)> idle_callbacks_{};
size_t buffer_frac_{1};
bool full_refresh_{};
};
class IdleTrigger : public Trigger<> {
public:
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) {
parent->add_on_idle_callback([this](uint32_t idle_time) {
if (!this->is_idle_ && idle_time > this->timeout_.value()) {
this->is_idle_ = true;
this->trigger();
} else if (this->is_idle_ && idle_time < this->timeout_.value()) {
this->is_idle_ = false;
}
});
}
protected:
TemplatableValue<uint32_t> timeout_;
bool is_idle_{};
};
template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> {
public:
explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {}
void play(Ts... x) override { this->action_(this->parent_); }
protected:
std::function<void(LvglComponent *)> action_{};
};
template<typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<LvglComponent> {
public:
LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda)
: condition_lambda_(std::move(condition_lambda)) {}
bool check(Ts... x) override { return this->condition_lambda_(this->parent_); }
protected:
std::function<bool(LvglComponent *)> condition_lambda_{};
};
#ifdef USE_LVGL_TOUCHSCREEN
class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> {
public:
@ -160,7 +248,62 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC
bool touch_pressed_{};
};
#endif // USE_LVGL_TOUCHSCREEN
#ifdef USE_LVGL_KEY_LISTENER
class LVEncoderListener : public Parented<LvglComponent> {
public:
LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) {
lv_indev_drv_init(&this->drv_);
this->drv_.type = type;
this->drv_.user_data = this;
this->drv_.long_press_time = lpt;
this->drv_.long_press_repeat_time = lprt;
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVEncoderListener *>(d->user_data);
data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
data->key = l->key_;
data->enc_diff = (int16_t) (l->count_ - l->last_count_);
l->last_count_ = l->count_;
data->continue_reading = false;
};
}
void set_left_button(binary_sensor::BinarySensor *left_button) {
left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); });
}
void set_right_button(binary_sensor::BinarySensor *right_button) {
right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); });
}
void set_enter_button(binary_sensor::BinarySensor *enter_button) {
enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); });
}
void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) {
sensor->register_listener([this](int32_t count) { this->set_count(count); });
}
void event(int key, bool pressed) {
if (!this->parent_->is_paused()) {
this->pressed_ = pressed;
this->key_ = key;
}
}
void set_count(int32_t count) {
if (!this->parent_->is_paused())
this->count_ = count;
}
lv_indev_drv_t *get_drv() { return &this->drv_; }
protected:
lv_indev_drv_t drv_{};
bool pressed_{};
int32_t count_{};
int32_t last_count_{};
int key_{};
};
#endif // USE_LVGL_KEY_LISTENER
} // namespace lvgl
} // namespace esphome
#endif // USE_LVGL

View file

@ -1,5 +1,9 @@
from esphome import automation
from .automation import update_to_code
from .defines import CONF_MAIN, CONF_OBJ
from .types import WidgetType, lv_obj_t
from .schemas import create_modify_schema
from .types import ObjUpdateAction, WidgetType, lv_obj_t
class ObjType(WidgetType):
@ -15,3 +19,10 @@ class ObjType(WidgetType):
obj_spec = ObjType()
@automation.register_action(
"lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec)
)
async def obj_update_to_code(config, action_id, template_arg, args):
return await update_to_code(config, action_id, template_arg, args)

View file

@ -0,0 +1,62 @@
import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor
from esphome.components.rotary_encoder.sensor import RotaryEncoderSensor
import esphome.config_validation as cv
from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR
from .defines import (
CONF_ENTER_BUTTON,
CONF_LEFT_BUTTON,
CONF_LONG_PRESS_REPEAT_TIME,
CONF_LONG_PRESS_TIME,
CONF_RIGHT_BUTTON,
CONF_ROTARY_ENCODERS,
)
from .helpers import lvgl_components_required
from .lvcode import add_group, lv, lv_add, lv_expr
from .schemas import ENCODER_SCHEMA
from .types import lv_indev_type_t
ROTARY_ENCODER_CONFIG = cv.ensure_list(
ENCODER_SCHEMA.extend(
{
cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor),
cv.Required(CONF_SENSOR): cv.Any(
cv.use_id(RotaryEncoderSensor),
cv.Schema(
{
cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor),
cv.Required(CONF_RIGHT_BUTTON): cv.use_id(BinarySensor),
}
),
),
}
)
)
async def rotary_encoders_to_code(var, config):
for enc_conf in config.get(CONF_ROTARY_ENCODERS, ()):
lvgl_components_required.add("KEY_LISTENER")
lvgl_components_required.add("ROTARY_ENCODER")
lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds
lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
listener = cg.new_Pvariable(
enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_ENCODER, lpt, lprt
)
await cg.register_parented(listener, var)
if sensor_config := enc_conf.get(CONF_SENSOR):
if isinstance(sensor_config, dict):
b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON])
cg.add(listener.set_left_button(b_sensor))
b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON])
cg.add(listener.set_right_button(b_sensor))
else:
sensor_config = await cg.get_variable(sensor_config)
lv_add(listener.set_sensor(sensor_config))
b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON])
cg.add(listener.set_enter_button(b_sensor))
if group := add_group(enc_conf.get(CONF_GROUP)):
lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group)
else:
lv.indev_drv_register(listener.get_drv())

View file

@ -1,10 +1,21 @@
from esphome import config_validation as cv
from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE
from esphome.automation import Trigger, validate_automation
from esphome.const import (
CONF_ARGS,
CONF_FORMAT,
CONF_GROUP,
CONF_ID,
CONF_ON_VALUE,
CONF_STATE,
CONF_TRIGGER_ID,
CONF_TYPE,
)
from esphome.core import TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid, types as ty
from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_font
from .lv_validation import id_name, lv_font
from .types import WIDGET_TYPES, WidgetType
# A schema for text properties
@ -27,6 +38,28 @@ TEXT_SCHEMA = cv.Schema(
}
)
ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t),
},
key=CONF_ID,
)
PRESS_TIME = cv.All(
lvalid.lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535))
)
ENCODER_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.All(
cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor")
),
cv.Optional(CONF_GROUP): lvalid.id_name,
cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
}
)
# All LVGL styles and their validators
STYLE_PROPS = {
"align": df.CHILD_ALIGNMENTS.one_of,
@ -43,6 +76,7 @@ STYLE_PROPS = {
"bg_image_opa": lvalid.opacity,
"bg_image_recolor": lvalid.lv_color,
"bg_image_recolor_opa": lvalid.opacity,
"bg_image_src": lvalid.lv_image,
"bg_main_stop": lvalid.stop_value,
"bg_opa": lvalid.opacity,
"border_color": lvalid.lv_color,
@ -151,6 +185,39 @@ def part_schema(widget_type: WidgetType):
)
def automation_schema(typ: ty.LvType):
if typ.has_on_value:
events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,)
else:
events = df.LV_EVENT_TRIGGERS
if isinstance(typ, ty.LvType):
template = Trigger.template(typ.get_arg_type())
else:
template = Trigger.template()
return {
cv.Optional(event): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(template),
}
)
for event in events
}
def create_modify_schema(widget_type):
return (
part_schema(widget_type)
.extend(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
}
)
.extend(FLAG_SCHEMA)
.extend(widget_type.modify_schema)
)
def obj_schema(widget_type: WidgetType):
"""
Create a schema for a widget type itself i.e. no allowance for children
@ -161,10 +228,12 @@ def obj_schema(widget_type: WidgetType):
part_schema(widget_type)
.extend(FLAG_SCHEMA)
.extend(ALIGN_TO_SCHEMA)
.extend(automation_schema(widget_type.w_type))
.extend(
cv.Schema(
{
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
cv.Optional(CONF_GROUP): id_name,
}
)
)
@ -188,6 +257,13 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT
)
# For use by platform components
LVGL_SCHEMA = cv.Schema(
{
cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent),
}
)
ALL_STYLES = {
**STYLE_PROPS,
}

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, TimePeriod
from esphome.core import CORE
from .defines import (
CONF_LONG_PRESS_REPEAT_TIME,
@ -10,11 +10,10 @@ from .defines import (
CONF_TOUCHSCREENS,
)
from .helpers import lvgl_components_required
from .lv_validation import lv_milliseconds
from .lvcode import lv
from .schemas import PRESS_TIME
from .types import LVTouchListener
PRESS_TIME = cv.All(lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535)))
CONF_TOUCHSCREEN = "touchscreen"
TOUCHSCREENS_CONFIG = cv.maybe_simple_value(
{

View file

@ -0,0 +1,61 @@
from esphome import automation
import esphome.codegen as cg
from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_TRIGGER_ID
from .defines import (
CONF_ALIGN,
CONF_ALIGN_TO,
CONF_X,
CONF_Y,
LV_EVENT,
LV_EVENT_TRIGGERS,
literal,
)
from .lvcode import LambdaContext, add_line_marks, lv, lv_add
from .widget import widget_map
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
async def generate_triggers(lv_component):
"""
Generate LVGL triggers for all defined widgets
Must be done after all widgets completed
:param lv_component: The parent component
:return:
"""
for w in widget_map.values():
if w.config:
for event, conf in {
event: conf
for event, conf in w.config.items()
if event in LV_EVENT_TRIGGERS
}.items():
conf = conf[0]
w.add_flag("LV_OBJ_FLAG_CLICKABLE")
event = "LV_EVENT_" + LV_EVENT[event[3:].upper()]
await add_trigger(conf, event, lv_component, w)
for conf in w.config.get(CONF_ON_VALUE, ()):
await add_trigger(conf, "LV_EVENT_VALUE_CHANGED", lv_component, w)
# Generate align to directives while we're here
if align_to := w.config.get(CONF_ALIGN_TO):
target = widget_map[align_to[CONF_ID]].obj
align = align_to[CONF_ALIGN]
x = align_to[CONF_X]
y = align_to[CONF_Y]
lv.obj_align_to(w.obj, target, align, x, y)
async def add_trigger(conf, event, lv_component, w):
tid = conf[CONF_TRIGGER_ID]
add_line_marks(tid)
trigger = cg.new_Pvariable(tid)
args = w.get_args()
value = w.get_value()
await automation.build_automation(trigger, args, conf)
with LambdaContext([(lv_event_t_ptr, "event_data")]) as context:
add_line_marks(tid)
lv_add(trigger.trigger(value))
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), literal(event)))

View file

@ -1,4 +1,4 @@
from esphome import codegen as cg
from esphome import automation, codegen as cg
from esphome.core import ID
from esphome.cpp_generator import MockObjClass
@ -23,8 +23,14 @@ lvgl_ns = cg.esphome_ns.namespace("lvgl")
char_ptr = cg.global_ns.namespace("char").operator("ptr")
void_ptr = cg.void.operator("ptr")
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
LvglComponentPtr = LvglComponent.operator("ptr")
lv_event_code_t = cg.global_ns.namespace("lv_event_code_t")
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
FontEngine = lvgl_ns.class_("FontEngine")
IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action)
LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition)
LvglAction = lvgl_ns.class_("LvglAction", automation.Action)
LvCompound = lvgl_ns.class_("LvCompound")
lv_font_t = cg.global_ns.class_("lv_font_t")
lv_style_t = cg.global_ns.struct("lv_style_t")
@ -33,9 +39,11 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t)
lv_obj_t_ptr = lv_obj_base_t.operator("ptr")
lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr")
lv_color_t = cg.global_ns.struct("lv_color_t")
lv_group_t = cg.global_ns.struct("lv_group_t")
LVTouchListener = lvgl_ns.class_("LVTouchListener")
LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
lv_obj_t = LvType("lv_obj_t")
lv_img_t = LvType("lv_img_t")
# this will be populated later, in __init__.py to avoid circular imports.
@ -58,7 +66,7 @@ class LvBoolean(LvType):
super().__init__(
*args,
largs=[(cg.bool_, "x")],
lvalue=lambda w: w.is_checked(),
lvalue=lambda w: w.has_state("LV_STATE_CHECKED"),
has_on_value=True,
**kwargs,
)
@ -83,11 +91,14 @@ class WidgetType:
self.name = name
self.w_type = w_type
self.parts = parts
self.schema = schema or {}
if modify_schema is None:
self.modify_schema = schema
if schema is None:
self.schema = {}
else:
self.modify_schema = modify_schema
self.schema = schema
if modify_schema is None:
self.modify_schema = self.schema
else:
self.modify_schema = self.schema
@property
def animated(self):

View file

@ -4,9 +4,9 @@ from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.config_validation import Invalid
from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE
from esphome.core import ID, TimePeriod
from esphome.core import CORE, TimePeriod
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression
from .defines import (
CONF_DEFAULT,
@ -16,13 +16,15 @@ from .defines import (
OBJ_FLAGS,
PARTS,
STATES,
ConstantLiteral,
LValidator,
join_enums,
literal,
)
from .helpers import add_lv_use
from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj
from .lvcode import add_group, add_line_marks, lv, lv_add, lv_assign, lv_expr, lv_obj
from .schemas import ALL_STYLES, STYLE_REMAP
from .types import WIDGET_TYPES, WidgetType, lv_obj_t
from .types import WIDGET_TYPES, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr
EVENT_LAMB = "event_lamb__"
@ -76,17 +78,20 @@ class Widget:
return f"{self.var}->obj"
return self.var
def add_state(self, *args):
return lv_obj.add_state(self.obj, *args)
def add_state(self, state):
return lv_obj.add_state(self.obj, literal(state))
def clear_state(self, *args):
return lv_obj.clear_state(self.obj, *args)
def clear_state(self, state):
return lv_obj.clear_state(self.obj, literal(state))
def add_flag(self, *args):
return lv_obj.add_flag(self.obj, *args)
def has_state(self, state):
return lv_expr.obj_get_state(self.obj) & literal(state) != 0
def clear_flag(self, *args):
return lv_obj.clear_flag(self.obj, *args)
def add_flag(self, flag):
return lv_obj.add_flag(self.obj, literal(flag))
def clear_flag(self, flag):
return lv_obj.clear_flag(self.obj, literal(flag))
def set_property(self, prop, value, animated: bool = None, ltype=None):
if isinstance(value, dict):
@ -125,6 +130,16 @@ class Widget:
def __str__(self):
return f"({self.var}, {self.type})"
def get_args(self):
if isinstance(self.type.w_type, LvType):
return self.type.w_type.args
return [(lv_obj_t_ptr, "obj")]
def get_value(self):
if isinstance(self.type.w_type, LvType):
return self.type.w_type.value(self)
return self.obj
# Map of widgets to their config, used for trigger generation
widget_map: dict[Any, Widget] = {}
@ -146,7 +161,8 @@ def get_widget_generator(wid):
yield
async def get_widget(wid: ID) -> Widget:
async def get_widget(config: dict, id: str = CONF_ID) -> Widget:
wid = config[id]
if obj := widget_map.get(wid):
return obj
return await FakeAwaitable(get_widget_generator(wid))
@ -204,9 +220,10 @@ async def set_obj_properties(w: Widget, config):
}.items():
if isinstance(ALL_STYLES[prop], LValidator):
value = await ALL_STYLES[prop].process(value)
# Remapping for backwards compatibility of style names
prop_r = STYLE_REMAP.get(prop, prop)
w.set_style(prop_r, value, lv_state)
if group := add_group(config.get(CONF_GROUP)):
lv.group_add_obj(group, w.obj)
flag_clr = set()
flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT]
@ -241,7 +258,7 @@ async def set_obj_properties(w: Widget, config):
w.clear_state(clears)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
state = ConstantLiteral(f"LV_STATE_{key.upper}")
state = f"LV_STATE_{key.upper}"
lv.cond_if(lamb)
w.add_state(state)
lv.cond_else()
@ -281,10 +298,19 @@ async def widget_to_code(w_cnfig, w_type, parent):
var = cg.new_Pvariable(wid)
lv_add(var.set_obj(creator))
else:
var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t)
var = MockObj(wid, "->")
decl = VariableDeclarationExpression(lv_obj_t, "*", wid)
CORE.add_global(decl)
CORE.register_variable(wid, var)
lv_assign(var, creator)
widget = Widget.create(wid, var, spec, w_cnfig, parent)
await set_obj_properties(widget, w_cnfig)
await add_widgets(widget, w_cnfig)
await spec.to_code(widget, w_cnfig)
lv_scr_act_spec = LvScrActType()
lv_scr_act = Widget.create(
None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None
)

View file

@ -8,6 +8,7 @@ static const char *const TAG = "matrix_keypad";
void MatrixKeypad::setup() {
for (auto *pin : this->rows_) {
pin->setup();
if (!has_diodes_) {
pin->pin_mode(gpio::FLAG_INPUT);
} else {
@ -15,6 +16,7 @@ void MatrixKeypad::setup() {
}
}
for (auto *pin : this->columns_) {
pin->setup();
if (has_pulldowns_) {
pin->pin_mode(gpio::FLAG_INPUT);
} else {

View file

@ -148,7 +148,7 @@ WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutof
};
bool WakeWordModel::determine_detected() {
int32_t sum = 0;
uint32_t sum = 0;
for (auto &prob : this->recent_streaming_probabilities_) {
sum += prob;
}
@ -175,12 +175,14 @@ VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t
};
bool VADModel::determine_detected() {
uint8_t max = 0;
uint32_t sum = 0;
for (auto &prob : this->recent_streaming_probabilities_) {
max = std::max(prob, max);
sum += prob;
}
return max > this->probability_cutoff_;
float sliding_window_average = static_cast<float>(sum) / static_cast<float>(255 * this->sliding_window_size_);
return sliding_window_average > this->probability_cutoff_;
}
} // namespace micro_wake_word

View file

@ -17,6 +17,7 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_IGNORE_OUT_OF_RANGE,
CONF_MULTIPLE,
CONF_ON_RAW_VALUE,
CONF_ON_VALUE,
CONF_ON_VALUE_RANGE,
@ -249,6 +250,7 @@ CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
RoundFilter = sensor_ns.class_("RoundFilter", Filter)
RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
validate_unit_of_measurement = cv.string_strict
validate_accuracy_decimals = cv.int_
@ -734,6 +736,23 @@ async def round_filter_to_code(config, filter_id):
)
@FILTER_REGISTRY.register(
"round_to_multiple_of",
RoundMultipleFilter,
cv.maybe_simple_value(
{
cv.Required(CONF_MULTIPLE): cv.positive_not_null_float,
},
key=CONF_MULTIPLE,
),
)
async def round_multiple_filter_to_code(config, filter_id):
return cg.new_Pvariable(
filter_id,
config[CONF_MULTIPLE],
)
async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)

View file

@ -472,5 +472,13 @@ optional<float> RoundFilter::new_value(float value) {
return value;
}
RoundMultipleFilter::RoundMultipleFilter(float multiple) : multiple_(multiple) {}
optional<float> RoundMultipleFilter::new_value(float value) {
if (std::isfinite(value)) {
return value - remainderf(value, this->multiple_);
}
return value;
}
} // namespace sensor
} // namespace esphome

View file

@ -431,5 +431,14 @@ class RoundFilter : public Filter {
uint8_t precision_;
};
class RoundMultipleFilter : public Filter {
public:
explicit RoundMultipleFilter(float multiple);
optional<float> new_value(float value) override;
protected:
float multiple_;
};
} // namespace sensor
} // namespace esphome

View file

@ -1,10 +1,11 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY,
CONF_FORCE_UPDATE,
CONF_ID,
CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
@ -23,8 +24,12 @@ UpdateEntity = update_ns.class_("UpdateEntity", cg.EntityBase)
UpdateInfo = update_ns.struct("UpdateInfo")
PerformAction = update_ns.class_("PerformAction", automation.Action)
IsAvailableCondition = update_ns.class_("IsAvailableCondition", automation.Condition)
PerformAction = update_ns.class_(
"PerformAction", automation.Action, cg.Parented.template(UpdateEntity)
)
IsAvailableCondition = update_ns.class_(
"IsAvailableCondition", automation.Condition, cg.Parented.template(UpdateEntity)
)
DEVICE_CLASSES = [
DEVICE_CLASS_EMPTY,
@ -92,24 +97,37 @@ async def to_code(config):
cg.add_global(update_ns.using)
UPDATE_AUTOMATION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(UpdateEntity),
}
@automation.register_action(
"update.perform",
PerformAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(UpdateEntity),
cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean),
}
),
)
@automation.register_action("update.perform", PerformAction, UPDATE_AUTOMATION_SCHEMA)
async def update_perform_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, paren, paren)
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
force = await cg.templatable(config[CONF_FORCE_UPDATE], args, cg.bool_)
cg.add(var.set_force(force))
return var
@automation.register_condition(
"update.is_available", IsAvailableCondition, UPDATE_AUTOMATION_SCHEMA
"update.is_available",
IsAvailableCondition,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(UpdateEntity),
}
),
)
async def update_is_available_condition_to_code(
config, condition_id, template_arg, args
):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, paren, paren)
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View file

@ -0,0 +1,23 @@
#pragma once
#include "update_entity.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace update {
template<typename... Ts> class PerformAction : public Action<Ts...>, public Parented<UpdateEntity> {
TEMPLATABLE_VALUE(bool, force)
public:
void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); }
};
template<typename... Ts> class IsAvailableCondition : public Condition<Ts...>, public Parented<UpdateEntity> {
public:
bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; }
};
} // namespace update
} // namespace esphome

View file

@ -32,7 +32,9 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
void publish_state();
virtual void perform() = 0;
void perform() { this->perform(false); }
virtual void perform(bool force) = 0;
const UpdateInfo &update_info = update_info_;
const UpdateState &state = state_;

View file

@ -464,6 +464,7 @@ zero_to_one_float = float_range(min=0, max=1)
negative_one_to_one_float = float_range(min=-1, max=1)
positive_int = int_range(min=0)
positive_not_null_int = int_range(min=0, min_included=False)
positive_not_null_float = float_range(min=0, min_included=False)
def validate_id_name(value):
@ -2181,3 +2182,13 @@ SOURCE_SCHEMA = Any(
}
),
)
def rename_key(old_key, new_key):
def validator(config: dict) -> dict:
config = config.copy()
if old_key in config:
config[new_key] = config.pop(old_key)
return config
return validator

View file

@ -37,8 +37,10 @@ CONF_ACCELERATION_Y = "acceleration_y"
CONF_ACCELERATION_Z = "acceleration_z"
CONF_ACCURACY = "accuracy"
CONF_ACCURACY_DECIMALS = "accuracy_decimals"
CONF_ACTION = "action"
CONF_ACTION_ID = "action_id"
CONF_ACTION_STATE_TOPIC = "action_state_topic"
CONF_ACTIONS = "actions"
CONF_ACTIVE = "active"
CONF_ACTIVE_POWER = "active_power"
CONF_ACTUAL_GAIN = "actual_gain"
@ -501,6 +503,7 @@ CONF_MOTION = "motion"
CONF_MOVEMENT_COUNTER = "movement_counter"
CONF_MQTT = "mqtt"
CONF_MQTT_ID = "mqtt_id"
CONF_MULTIPLE = "multiple"
CONF_MULTIPLEXER = "multiplexer"
CONF_MULTIPLY = "multiply"
CONF_NAME = "name"

View file

@ -39,9 +39,12 @@
#define USE_LOCK
#define USE_LOGGER
#define USE_LVGL
#define USE_LVGL_BINARY_SENSOR
#define USE_LVGL_FONT
#define USE_LVGL_IMAGE
#define USE_LVGL_KEY_LISTENER
#define USE_LVGL_TOUCHSCREEN
#define USE_LVGL_ROTARY_ENCODER
#define USE_MDNS
#define USE_MEDIA_PLAYER
#define USE_MQTT

View file

@ -35,7 +35,7 @@ build_flags =
lib_deps =
esphome/noise-c@0.1.4 ; api
makuna/NeoPixelBus@2.7.3 ; neopixelbus
esphome/Improv@1.2.3 ; improv_serial / esp32_improv
improv/Improv@1.2.4 ; improv_serial / esp32_improv
bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393

View file

@ -5,8 +5,8 @@ esphome:
event: esphome.button_pressed
data:
message: Button was pressed
- homeassistant.service:
service: notify.html5
- homeassistant.action:
action: notify.html5
data:
message: Button was pressed
- homeassistant.tag_scanned: pulse
@ -21,8 +21,8 @@ api:
reboot_timeout: 0min
encryption:
key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=
services:
- service: hello_world
actions:
- action: hello_world
variables:
name: string
then:
@ -30,10 +30,10 @@ api:
format: Hello World %s!
args:
- name.c_str()
- service: empty_service
- action: empty_action
then:
- logger.log: Service Called
- service: all_types
- logger.log: Action Called
- action: all_types
variables:
bool_: bool
int_: int
@ -41,7 +41,7 @@ api:
string_: string
then:
- logger.log: Something happened
- service: array_types
- action: array_types
variables:
bool_arr: bool[]
int_arr: int[]

View file

@ -13,12 +13,12 @@ esphome:
message: The humidity is {{ my_variable }}%.
variables:
my_variable: "return id(ha_hello_world_temperature).state;"
- homeassistant.service:
service: notify.html5
- homeassistant.action:
action: notify.html5
data:
message: Button was pressed
- homeassistant.service:
service: notify.html5
- homeassistant.action:
action: notify.html5
data:
title: New Humidity
data_template:

View file

@ -7,6 +7,7 @@ lvgl:
long_press_time: 500ms
widgets:
- label:
id: hello_label
text: Hello world
text_color: 0xFF8000
align: center
@ -95,9 +96,43 @@ lvgl:
height: 10%
pressed:
bg_color: light_blue
checkable: true
checked:
bg_color: 0x000000
widgets:
- label:
text: Button
on_click:
lvgl.label.update:
id: hello_label
bg_color: 0x123456
text: clicked
on_value:
logger.log:
format: "state now %d"
args: [x]
on_short_click:
lvgl.widget.hide: hello_label
on_long_press:
lvgl.widget.show: hello_label
on_cancel:
lvgl.widget.enable: hello_label
on_ready:
lvgl.widget.disable: hello_label
on_defocus:
lvgl.widget.hide: hello_label
on_focus:
logger.log: Button clicked
on_scroll:
logger.log: Button clicked
on_scroll_end:
logger.log: Button clicked
on_scroll_begin:
logger.log: Button clicked
on_release:
logger.log: Button clicked
on_long_press_repeat:
logger.log: Button clicked
font:
- file: "gfonts://Roboto"

View file

@ -6,6 +6,23 @@ i2c:
sda: GPIO18
scl: GPIO19
sensor:
- platform: rotary_encoder
name: "Rotary Encoder"
id: encoder
pin_a: 2
pin_b: 1
internal: true
binary_sensor:
- platform: gpio
id: pushbutton
name: Pushbutton
pin:
number: 0
inverted: true
ignore_strapping_warning: true
display:
- platform: ili9xxx
model: st7789v
@ -50,5 +67,9 @@ lvgl:
displays:
- tft_display
- second_display
rotary_encoders:
sensor: encoder
enter_button: pushbutton
group: general
<<: !include common.yaml

View file

@ -1 +1,28 @@
substitutions:
verify_ssl: "true"
esphome:
on_boot:
then:
- if:
condition:
update.is_available:
then:
- logger.log: "Update available"
- update.perform:
force_update: true
wifi:
ssid: MySSID
password: password1
http_request:
verify_ssl: ${verify_ssl}
ota:
- platform: http_request
update:
- platform: http_request
name: Firmware Update
source: http://example.com/manifest.json

View file

@ -1 +1,4 @@
substitutions:
verify_ssl: "false"
<<: !include common.yaml

View file

@ -1 +1,4 @@
substitutions:
verify_ssl: "false"
<<: !include common.yaml

View file

@ -1 +1,4 @@
substitutions:
verify_ssl: "false"
<<: !include common.yaml