Merge branch 'dev' into integration

This commit is contained in:
J. Nick Koston
2025-11-26 17:06:58 -06:00
16 changed files with 284 additions and 132 deletions

View File

@@ -108,7 +108,7 @@ LV_CONF_H_FORMAT = """\
def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in df.lv_defines.items()]
definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()]
definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions))
@@ -140,11 +140,11 @@ def multi_conf_validate(configs: list[dict]):
)
def final_validation(configs):
if len(configs) != 1:
multi_conf_validate(configs)
def final_validation(config_list):
if len(config_list) != 1:
multi_conf_validate(config_list)
global_config = full_config.get()
for config in configs:
for config in config_list:
if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
for display_id in config[df.CONF_DISPLAYS]:
@@ -190,6 +190,14 @@ def final_validation(configs):
raise cv.Invalid(
f"Widget '{w}' does not have any dynamic properties to refresh",
)
# Do per-widget type final validation for update actions
for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items():
for conf in update_configs:
for id_conf in conf.get(CONF_ID, ()):
name = id_conf[CONF_ID]
path = global_config.get_path_for_id(name)
widget_conf = global_config.get_config_for_path(path[:-1])
widget_type.final_validate(name, conf, widget_conf, path[1:])
async def to_code(configs):
@@ -276,6 +284,7 @@ async def to_code(configs):
config[df.CONF_FULL_REFRESH],
config[CONF_DRAW_ROUNDING],
config[df.CONF_RESUME_ON_INPUT],
config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE],
)
await cg.register_component(lv_component, config)
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
@@ -373,6 +382,9 @@ LVGL_SCHEMA = cv.All(
df.CONF_DEFAULT_FONT, default="montserrat_14"
): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(
df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False
): cv.boolean,
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(

View File

@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import ID, Lambda
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@@ -20,11 +20,27 @@ from .helpers import requires_component
LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl")
lv_defines = {} # Dict of #defines to provide as build flags
DOMAIN = "lvgl"
KEY_LV_DEFINES = "lv_defines"
KEY_UPDATED_WIDGETS = "updated_widgets"
def get_data(key, default=None):
"""
Get a data structure from the global data store by key
:param key: A key for the data
:param default: The default data - the default is an empty dict
:return:
"""
return CORE.data.setdefault(DOMAIN, {}).setdefault(
key, default if default is not None else {}
)
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
lv_defines = get_data(KEY_LV_DEFINES)
value = str(value)
if lv_defines.setdefault(macro, value) != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
@@ -542,6 +558,7 @@ CONF_TOUCHSCREENS = "touchscreens"
CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme"
CONF_UPDATE_ON_RELEASE = "update_on_release"
CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle"
CONF_VISIBLE_ROW_COUNT = "visible_row_count"
CONF_WIDGET = "widget"
CONF_WIDGETS = "widgets"

View File

@@ -106,6 +106,7 @@ void LvglComponent::dump_config() {
this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation,
(int) this->draw_rounding);
}
void LvglComponent::set_paused(bool paused, bool show_snow) {
this->paused_ = paused;
this->show_snow_ = show_snow;
@@ -124,32 +125,38 @@ void LvglComponent::esphome_lvgl_init() {
lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
}
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, nullptr);
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
lv_event_code_t event2) {
add_event_cb(obj, callback, event1);
add_event_cb(obj, callback, event2);
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
lv_event_code_t event2, lv_event_code_t event3) {
add_event_cb(obj, callback, event1);
add_event_cb(obj, callback, event2);
add_event_cb(obj, callback, event3);
}
void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page);
page->set_parent(this);
lv_disp_set_default(this->disp_);
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;
@@ -158,6 +165,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) {
} 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;
@@ -166,8 +174,10 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area);
@@ -222,7 +232,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
}
void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
if (!this->paused_) {
if (!this->is_paused()) {
auto now = millis();
this->draw_buffer_(area, color_p);
ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
@@ -230,6 +240,7 @@ 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()) {
@@ -377,6 +388,27 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
}
#endif // USE_LVGL_KEYBOARD
void LvglComponent::draw_end_() {
if (this->draw_end_callback_ != nullptr)
this->draw_end_callback_->trigger();
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_)
disp->update();
}
}
bool LvglComponent::is_paused() const {
if (this->paused_)
return true;
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_) {
if (!disp->is_idle())
return true;
}
}
return false;
}
void LvglComponent::write_random_() {
int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000;
if (iterations <= 0)
@@ -426,12 +458,13 @@ void LvglComponent::write_random_() {
* presses a key or clicks on the screen.
*/
LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh,
int draw_rounding, bool resume_on_input)
int draw_rounding, bool resume_on_input, bool update_when_display_idle)
: draw_rounding(draw_rounding),
displays_(std::move(displays)),
buffer_frac_(buffer_frac),
full_refresh_(full_refresh),
resume_on_input_(resume_on_input) {
resume_on_input_(resume_on_input),
update_when_display_idle_(update_when_display_idle) {
lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0);
lv_disp_drv_init(&this->disp_drv_);
this->disp_drv_.draw_buf = &this->draw_buf_;
@@ -487,7 +520,7 @@ void LvglComponent::setup() {
if (this->draw_start_callback_ != nullptr) {
this->disp_drv_.render_start_cb = render_start_cb;
}
if (this->draw_end_callback_ != nullptr) {
if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
this->disp_drv_.monitor_cb = monitor_cb;
}
#if LV_USE_LOG
@@ -509,14 +542,15 @@ void LvglComponent::setup() {
void LvglComponent::update() {
// update indicators
if (this->paused_) {
if (this->is_paused()) {
return;
}
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
}
void LvglComponent::loop() {
if (this->paused_) {
if (this->show_snow_)
if (this->is_paused()) {
if (this->paused_ && this->show_snow_)
this->write_random_();
} else {
lv_timer_handler_run_in_period(5);

View File

@@ -151,7 +151,7 @@ class LvglComponent : public PollingComponent {
public:
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
bool resume_on_input);
bool resume_on_input, bool update_when_display_idle);
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; }
@@ -171,7 +171,9 @@ class LvglComponent : public PollingComponent {
// @param paused If true, pause the display. If false, resume the display.
// @param show_snow If true, show the snow effect when paused.
void set_paused(bool paused, bool show_snow);
bool is_paused() const { return this->paused_; }
// Returns true if the display is explicitly paused, or a blocking display update is in progress.
bool is_paused() const;
// If the display is paused and we have resume_on_input_ set to true, resume the display.
void maybe_wakeup() {
if (this->paused_ && this->resume_on_input_) {
@@ -210,10 +212,10 @@ class LvglComponent : public PollingComponent {
void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; }
protected:
// these functions are never called unless the callbacks are non-null since the
// LVGL callbacks that call them are not set unless the start/end callbacks are non-null
void draw_end_();
// Not checking for non-null callback since the
// LVGL callback that calls it is not set in that case
void draw_start_() const { this->draw_start_callback_->trigger(); }
void draw_end_() const { this->draw_end_callback_->trigger(); }
void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr);
@@ -222,6 +224,7 @@ class LvglComponent : public PollingComponent {
size_t buffer_frac_{1};
bool full_refresh_{};
bool resume_on_input_{};
bool update_when_display_idle_{};
lv_disp_draw_buf_t draw_buf_{};
lv_disp_drv_t disp_drv_{};

View File

@@ -1,3 +1,5 @@
from collections.abc import Callable
from esphome import config_validation as cv
from esphome.automation import Trigger, validate_automation
from esphome.components.time import RealTimeClock
@@ -311,19 +313,36 @@ def automation_schema(typ: LvType):
}
def base_update_schema(widget_type, parts):
def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]:
"""
Create a schema for updating a widgets style properties, states and flags
During validation of update actions, create a map of action types to affected widgets
for use in final validation.
:param widget_type:
:return:
"""
def validator(value: dict) -> dict:
df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value)
return value
return validator
def base_update_schema(widget_type: WidgetType | LvType, parts):
"""
Create a schema for updating a widget's style properties, states and flags.
:param widget_type: The type of the ID
:param parts: The allowable parts to specify
:return:
"""
return part_schema(parts).extend(
w_type = widget_type.w_type if isinstance(widget_type, WidgetType) else widget_type
schema = part_schema(parts).extend(
{
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
cv.Required(CONF_ID): cv.use_id(w_type),
},
key=CONF_ID,
)
@@ -332,11 +351,9 @@ def base_update_schema(widget_type, parts):
}
)
def create_modify_schema(widget_type):
return base_update_schema(widget_type.w_type, widget_type.parts).extend(
widget_type.modify_schema
)
if isinstance(widget_type, WidgetType):
schema.add_extra(_update_widget(widget_type))
return schema
def obj_schema(widget_type: WidgetType):

View File

@@ -152,18 +152,18 @@ class WidgetType:
# Local import to avoid circular import
from .automation import update_to_code
from .schemas import WIDGET_TYPES, create_modify_schema
from .schemas import WIDGET_TYPES, base_update_schema
if not is_mock:
if self.name in WIDGET_TYPES:
raise EsphomeError(f"Duplicate definition of widget type '{self.name}'")
WIDGET_TYPES[self.name] = self
# Register the update action automatically
# Register the update action automatically, adding widget-specific properties
register_action(
f"lvgl.{self.name}.update",
ObjUpdateAction,
create_modify_schema(self),
base_update_schema(self, self.parts).extend(self.modify_schema),
)(update_to_code)
@property
@@ -182,7 +182,6 @@ class WidgetType:
Generate code for a given widget
:param w: The widget
:param config: Its configuration
:return: Generated code as a list of text lines
"""
async def obj_creator(self, parent: MockObjClass, config: dict):
@@ -228,6 +227,15 @@ class WidgetType:
"""
return value
def final_validate(self, widget, update_config, widget_config, path):
"""
Allow final validation for a given widget type update action
:param widget: A widget
:param update_config: The configuration for the update action
:param widget_config: The configuration for the widget itself
:param path: The path to the widget, for error reporting
"""
class NumberType(WidgetType):
def get_max(self, config: dict):

View File

@@ -382,7 +382,7 @@ async def set_obj_properties(w: Widget, config):
clrs = join_enums(flag_clr, "LV_OBJ_FLAG_")
w.clear_flag(clrs)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
flag = f"LV_OBJ_FLAG_{key.upper()}"
with LvConditional(call_lambda(lamb)) as cond:
w.add_flag(flag)
@@ -407,7 +407,7 @@ async def set_obj_properties(w: Widget, config):
clears = join_enums(clears, "LV_STATE_")
w.clear_state(clears)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
state = f"LV_STATE_{key.upper()}"
with LvConditional(call_lambda(lamb)) as cond:
w.add_state(state)

View File

@@ -1,20 +1,52 @@
from esphome.const import CONF_BUTTON
from esphome import config_validation as cv
from esphome.const import CONF_BUTTON, CONF_TEXT
from esphome.cpp_generator import MockObj
from ..defines import CONF_MAIN
from ..defines import CONF_MAIN, CONF_WIDGETS
from ..helpers import add_lv_use
from ..lv_validation import lv_text
from ..lvcode import lv, lv_expr
from ..schemas import TEXT_SCHEMA
from ..types import LvBoolean, WidgetType
from . import Widget
from .label import label_spec
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")
super().__init__(
CONF_BUTTON, lv_button_t, (CONF_MAIN,), schema=TEXT_SCHEMA, lv_name="btn"
)
def validate(self, value):
if CONF_TEXT in value:
if CONF_WIDGETS in value:
raise cv.Invalid("Cannot use both text and widgets in a button")
add_lv_use("label")
return value
def get_uses(self):
return ("btn",)
async def to_code(self, w, config):
return []
def on_create(self, var: MockObj, config: dict):
if CONF_TEXT in config:
lv.label_create(var)
return var
async def to_code(self, w: Widget, config):
if text := config.get(CONF_TEXT):
label_widget = Widget.create(
None, lv_expr.obj_get_child(w.obj, 0), label_spec
)
await label_widget.set_property(CONF_TEXT, await lv_text.process(text))
def final_validate(self, widget, update_config, widget_config, path):
if CONF_TEXT in update_config and CONF_TEXT not in widget_config:
raise cv.Invalid(
"Button must have 'text:' configured to allow updating text", path
)
button_spec = ButtonType()

View File

@@ -141,6 +141,24 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st
}
}
#ifdef USE_ESP8266
void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name,
EntityBase *obj, std::string &area, std::string &node,
std::string &friendly_name) {
#else
void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj,
std::string &area, std::string &node, std::string &friendly_name) {
#endif
stream->print(metric_name);
stream->print(ESPHOME_F("{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
}
// Type-specific implementation
#ifdef USE_SENSOR
void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) {
@@ -303,13 +321,7 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
if (obj->is_internal() && !this->include_internal_)
return;
// State
stream->print(ESPHOME_F("esphome_light_state{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
print_metric_labels_(stream, ESPHOME_F("esphome_light_state"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\"} "));
stream->print(obj->remote_values.is_on());
stream->print(ESPHOME_F("\n"));
@@ -318,78 +330,45 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
float brightness, r, g, b, w;
color.as_brightness(&brightness);
color.as_rgbw(&r, &g, &b, &w);
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"brightness\"} "));
stream->print(brightness);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"r\"} "));
stream->print(r);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"g\"} "));
stream->print(g);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"b\"} "));
stream->print(b);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"w\"} "));
stream->print(w);
stream->print(ESPHOME_F("\n"));
// Effect
std::string effect = obj->get_effect_name();
if (effect == "None") {
stream->print(ESPHOME_F("esphome_light_effect_active{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",effect=\"None\"} 0\n"));
} else {
stream->print(ESPHOME_F("esphome_light_effect_active{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
if (obj->get_traits().supports_color_capability(light::ColorCapability::BRIGHTNESS)) {
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"brightness\"} "));
stream->print(brightness);
stream->print(ESPHOME_F("\n"));
}
if (obj->get_traits().supports_color_capability(light::ColorCapability::RGB)) {
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"r\"} "));
stream->print(r);
stream->print(ESPHOME_F("\n"));
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"g\"} "));
stream->print(g);
stream->print(ESPHOME_F("\n"));
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"b\"} "));
stream->print(b);
stream->print(ESPHOME_F("\n"));
}
if (obj->get_traits().supports_color_capability(light::ColorCapability::WHITE)) {
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"w\"} "));
stream->print(w);
stream->print(ESPHOME_F("\n"));
}
// Skip effect metrics if light has no effects
if (!obj->get_effects().empty()) {
// Effect
std::string effect = obj->get_effect_name();
print_metric_labels_(stream, ESPHOME_F("esphome_light_effect_active"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",effect=\""));
stream->print(effect.c_str());
stream->print(ESPHOME_F("\"} 1\n"));
// Only vary based on effect
if (effect == "None") {
stream->print(ESPHOME_F("None\"} 0\n"));
} else {
stream->print(effect.c_str());
stream->print(ESPHOME_F("\"} 1\n"));
}
}
}
#endif

View File

@@ -66,6 +66,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
void add_area_label_(AsyncResponseStream *stream, std::string &area);
void add_node_label_(AsyncResponseStream *stream, std::string &node);
void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name);
/// Print metric name and common labels (id, area, node, friendly_name, name)
#ifdef USE_ESP8266
void print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, EntityBase *obj,
std::string &area, std::string &node, std::string &friendly_name);
#else
void print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, std::string &area,
std::string &node, std::string &friendly_name);
#endif
#ifdef USE_SENSOR
/// Return the type for prometheus

View File

@@ -654,7 +654,7 @@ void ThermostatClimate::trigger_supplemental_action_() {
void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((action == this->humidification_action_) && this->setup_complete_) {
if ((action == this->humidification_action) && this->setup_complete_) {
// already in target mode
return;
}
@@ -683,7 +683,7 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction
this->prev_humidity_control_trigger_->stop_action();
this->prev_humidity_control_trigger_ = nullptr;
}
this->humidification_action_ = action;
this->humidification_action = action;
this->prev_humidity_control_trigger_ = trig;
if (trig != nullptr) {
trig->trigger();
@@ -1114,7 +1114,7 @@ bool ThermostatClimate::dehumidification_required_() {
}
// if we get here, the current humidity is between target + hysteresis and target - hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
}
bool ThermostatClimate::humidification_required_() {
@@ -1127,7 +1127,7 @@ bool ThermostatClimate::humidification_required_() {
}
// if we get here, the current humidity is between target - hysteresis and target + hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
}
void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) {

View File

@@ -207,6 +207,9 @@ class ThermostatClimate : public climate::Climate, public Component {
void validate_target_temperature_high();
void validate_target_humidity();
/// The current humidification action
HumidificationAction humidification_action{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE};
protected:
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
@@ -301,9 +304,6 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The current supplemental action
climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF};
/// The current humidification action
HumidificationAction humidification_action_{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE};
/// Default standard preset to use on start up
climate::ClimatePreset default_preset_{};

View File

@@ -659,7 +659,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
async def process_lambda(
value: Lambda,
parameters: TemplateArgsType,
capture: str = "=",
capture: str = "",
return_type: SafeExpType = None,
) -> LambdaExpression | None:
"""Process the given lambda value into a LambdaExpression.
@@ -702,12 +702,6 @@ async def process_lambda(
parts[i * 3 + 1] = var
parts[i * 3 + 2] = ""
# All id() references are global variables in generated C++ code.
# Global variables should not be captured - they're accessible everywhere.
# Use empty capture instead of capture-by-value.
if capture == "=":
capture = ""
if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
location = value.esp_range.start_mark
location.line += value.content_offset

View File

@@ -16,6 +16,18 @@ binary_sensor:
platform: template
- id: left_sensor
platform: template
- platform: lvgl
id: button_checker
name: LVGL button
widget: button_button
on_state:
then:
- lvgl.checkbox.update:
id: checkbox_id
state:
checked: !lambda |-
auto y = x; // block inlining of one line return
return y;
lvgl:
log_level: debug
@@ -414,6 +426,14 @@ lvgl:
logger.log: Long pressed repeated
- buttons:
- id: button_e
- button:
id: button_with_text
text: Button
on_click:
lvgl.button.update:
id: button_with_text
text: Clicked
- button:
layout: 2x1
id: button_button

View File

@@ -60,6 +60,7 @@ display:
update_interval: never
lvgl:
update_when_display_idle: true
displays:
- tft_display
- second_display

View File

@@ -112,6 +112,25 @@ cover:
}
return COVER_CLOSED;
light:
- platform: binary
name: "Binary Light"
output: test_output
- platform: monochromatic
name: "Brightness Light"
output: test_output
- platform: rgb
name: "RGB Light"
red: test_output
green: test_output
blue: test_output
- platform: rgbw
name: "RGBW Light"
red: test_output
green: test_output
blue: test_output
white: test_output
lock:
- platform: template
id: template_lock1
@@ -122,6 +141,14 @@ lock:
return LOCK_STATE_UNLOCKED;
optimistic: true
output:
- platform: template
id: test_output
type: float
write_action:
- lambda: |-
// no-op for CI/build tests
(void)state;
select:
- platform: template
id: template_select1