From c5ed675be54ebb72ac3bf4753eb8e0ef0f6f8993 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Tue, 2 Jan 2024 17:37:36 +1100 Subject: [PATCH] Add support for Text Run Panel grabbing run text from a Sensor or Text Sensor --- .../graphical_layout/text_run_panel.cpp | 8 +- .../graphical_layout/text_run_panel.h | 80 +++++++++++++++++-- .../graphical_layout/text_run_panel.py | 68 +++++++++++++--- 3 files changed, 134 insertions(+), 22 deletions(-) diff --git a/esphome/components/graphical_layout/text_run_panel.cpp b/esphome/components/graphical_layout/text_run_panel.cpp index ed0c765e32..7fe61b74bf 100644 --- a/esphome/components/graphical_layout/text_run_panel.cpp +++ b/esphome/components/graphical_layout/text_run_panel.cpp @@ -19,8 +19,8 @@ void TextRunPanel::dump_config(int indent_depth, int additional_level_depth) { ESP_LOGCONFIG(TAG, "%*sText Align: %s", indent_depth, "", LOG_STR_ARG(display::text_align_to_string(this->text_align_))); ESP_LOGCONFIG(TAG, "%*sText Runs: %i", indent_depth, "", this->text_runs_.size()); - for (TextRun *run : this->text_runs_) { - std::string text = run->text_.value(); + for (TextRunBase *run : this->text_runs_) { + std::string text = run->get_text(); ESP_LOGCONFIG(TAG, "%*sText: %s", indent_depth + additional_level_depth, "", text.c_str()); } } @@ -72,12 +72,12 @@ CalculatedLayout TextRunPanel::determine_layout(display::Display *display, displ int widest_line = 0; int line_number = 0; - for (TextRun *run : this->text_runs_) { + for (TextRunBase *run : this->text_runs_) { int x1; int width; int height; int baseline; - std::string text = run->text_.value(); + std::string text = run->get_text(); run->font_->measure(text.c_str(), &width, &x1, &baseline, &height); diff --git a/esphome/components/graphical_layout/text_run_panel.h b/esphome/components/graphical_layout/text_run_panel.h index 43c37c5dcb..2ef4e45751 100644 --- a/esphome/components/graphical_layout/text_run_panel.h +++ b/esphome/components/graphical_layout/text_run_panel.h @@ -1,10 +1,14 @@ #pragma once #include +#include +#include #include "esphome/components/graphical_layout/graphical_layout.h" #include "esphome/components/font/font.h" #include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" namespace esphome { namespace graphical_layout { @@ -25,25 +29,85 @@ struct CanWrapAtCharacterArguments { char character; }; -class TextRun { +class TextRunBase { public: - TextRun(TemplatableValue text, display::BaseFont *font) { - this->text_ = std::move(text); + TextRunBase(display::BaseFont *font) { this->font_ = font; } void set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; } void set_background_color(Color background_color) { this->background_color_ = background_color; } + virtual std::string get_text() = 0; + - TemplatableValue text_{}; display::BaseFont *font_{nullptr}; Color foreground_color_{COLOR_ON}; Color background_color_{COLOR_OFF}; }; +class FormattableTextRun { + public: + template void set_text_formatter(V text_formatter) { this->text_formatter_ = text_formatter; }; + + std::string format_text(std::string string) { + if (this->text_formatter_.has_value()) { + return this->text_formatter_.value(string); + } + + return string; + } + + protected: + TemplatableValue text_formatter_{}; +}; + +class TextRun : public TextRunBase, public FormattableTextRun { + public: + TextRun(TemplatableValue text, display::BaseFont *font) : TextRunBase(font) { + this->text_ = std::move(text); + } + + std::string get_text() override { + return this->format_text(text_.value()); + } + + protected: + TemplatableValue text_{}; +}; + +class SensorTextRun : public TextRunBase, public FormattableTextRun { + public: + SensorTextRun(sensor::Sensor *sensor, display::BaseFont *font) : TextRunBase(font) { + this->sensor_ = sensor; + } + + std::string get_text() override { + std::stringstream stream; + stream << std::fixed << std::setprecision(this->sensor_->get_accuracy_decimals()) << this->sensor_->get_state(); + return this->format_text(stream.str()); + } + + protected: + sensor::Sensor *sensor_{nullptr}; +}; + +class TextSensorTextRun : public TextRunBase, public FormattableTextRun { + public: + TextSensorTextRun(text_sensor::TextSensor *text_sensor, display::BaseFont *font) : TextRunBase(font) { + this->text_sensor_ = text_sensor; + } + + std::string get_text() override { + return this->format_text(this->text_sensor_->get_state()); + } + + protected: + text_sensor::TextSensor *text_sensor_{nullptr}; +}; + class CalculatedTextRun { public: - CalculatedTextRun(TextRun *run, std::string text, display::Rect bounds, int16_t baseline, int16_t line_number) { + CalculatedTextRun(TextRunBase *run, std::string text, display::Rect bounds, int16_t baseline, int16_t line_number) { this->run = run; this->text_ = std::move(text); this->bounds = bounds; @@ -53,7 +117,7 @@ class CalculatedTextRun { std::string text_; display::Rect bounds; - TextRun *run; + TextRunBase *run; int16_t line_number_; int16_t baseline; }; @@ -81,14 +145,14 @@ class TextRunPanel : public LayoutItem { this->can_wrap_at_character_ = can_wrap_at_character; }; - void add_text_run(TextRun *text_run) { this->text_runs_.push_back(text_run); }; + void add_text_run(TextRunBase *text_run) { this->text_runs_.push_back(text_run); }; void set_text_align(display::TextAlign text_align) { this->text_align_ = text_align; }; void set_min_width(int min_width) { this->min_width_ = min_width; }; void set_max_width(int max_width) { this->max_width_ = max_width; }; void set_debug_outline_runs(bool debug_outline_runs) { this->debug_outline_runs_ = debug_outline_runs; }; protected: - std::vector text_runs_; + std::vector text_runs_; display::TextAlign text_align_{display::TextAlign::TOP_LEFT}; int min_width_{0}; int max_width_{0}; diff --git a/esphome/components/graphical_layout/text_run_panel.py b/esphome/components/graphical_layout/text_run_panel.py index 47ef3c1497..f09c304bdd 100644 --- a/esphome/components/graphical_layout/text_run_panel.py +++ b/esphome/components/graphical_layout/text_run_panel.py @@ -1,13 +1,16 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import font, color +from esphome.components import font, color, sensor, text_sensor from esphome.components.display import display_ns -from esphome.const import CONF_ID, CONF_FOREGROUND_COLOR, CONF_BACKGROUND_COLOR +from esphome.const import CONF_ID, CONF_FOREGROUND_COLOR, CONF_BACKGROUND_COLOR, CONF_SENSOR graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") TextRunPanel = graphical_layout_ns.class_("TextRunPanel") TextAlign = display_ns.enum("TextAlign", is_class=True) -TextRun = graphical_layout_ns.class_("TextRun") +TextRunBase = graphical_layout_ns.class_("TextRunBase") +TextRun = graphical_layout_ns.class_("TextRun", TextRunBase) +SensorTextRun = graphical_layout_ns.class_("SensorTextRun", TextRunBase) +TextSensorTextRun = graphical_layout_ns.class_("TextSensorTextRun", TextRunBase) CanWrapAtCharacterArguments = graphical_layout_ns.struct("CanWrapAtCharacterArguments") CanWrapAtCharacterArgumentsConstRef = CanWrapAtCharacterArguments.operator( "const" @@ -22,6 +25,8 @@ CONF_MIN_WIDTH = "min_width" CONF_RUNS = "runs" CONF_CAN_WRAP_AT_CHARACTER = "can_wrap_at_character" CONF_DEBUG_OUTLINE_RUNS = "debug_outline_runs" +CONF_TEXT_SENSOR = "text_sensor" +CONF_TEXT_FORMATTER = "text_formatter" TEXT_ALIGN = { "TOP_LEFT": TextAlign.TOP_LEFT, @@ -38,16 +43,44 @@ TEXT_ALIGN = { "BOTTOM_RIGHT": TextAlign.BOTTOM_RIGHT, } -RUN_SCHEMA = cv.Schema( +BASE_RUN_SCHEMA = cv.Schema( { - cv.GenerateID(): cv.declare_id(TextRun), cv.Required(CONF_FONT): cv.use_id(font.Font), cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), - cv.Required(CONF_TEXT): cv.templatable(cv.string), } ) +SENSOR_TEXT_RUN_SCHEMA = BASE_RUN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SensorTextRun), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_TEXT_FORMATTER): cv.returning_lambda, + } +) + +TEXT_RUN_SCHEMA = BASE_RUN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TextRun), + cv.Required(CONF_TEXT): cv.templatable(cv.string), + cv.Optional(CONF_TEXT_FORMATTER): cv.returning_lambda, + } +) + +TEXT_SENSOR_TEXT_RUN_SCHEMA = BASE_RUN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TextSensorTextRun), + cv.Required(CONF_TEXT_SENSOR): cv.use_id(text_sensor.TextSensor), + cv.Optional(CONF_TEXT_FORMATTER): cv.returning_lambda, + } +) + +RUN_SCHEMA = cv.Any( + SENSOR_TEXT_RUN_SCHEMA, + TEXT_RUN_SCHEMA, + TEXT_SENSOR_TEXT_RUN_SCHEMA, +) + def get_config_schema(base_item_schema, item_type_schema): return base_item_schema.extend( @@ -90,12 +123,27 @@ async def config_to_layout_item(pvariable_builder, item_config, child_item_build cg.add(var.set_debug_outline_runs(debug_outline_runs)) for run_config in item_config[CONF_RUNS]: - run_text = await cg.templatable( - run_config[CONF_TEXT], args=[], output_type=cg.std_string - ) + run = None run_font = await cg.get_variable(run_config[CONF_FONT]) + if run_sensor_config := run_config.get(CONF_SENSOR): + sens = await cg.get_variable(run_sensor_config) + run = cg.new_Pvariable(run_config[CONF_ID], sens, run_font) + elif run_text_sensor_config := run_config.get(CONF_TEXT_SENSOR): + text_sens = await cg.get_variable(run_text_sensor_config) + run = cg.new_Pvariable(run_config[CONF_ID], text_sens, run_font) + else: + run_text = await cg.templatable( + run_config[CONF_TEXT], args=[], output_type=cg.std_string + ) + run = cg.new_Pvariable(run_config[CONF_ID], run_text, run_font) - run = cg.new_Pvariable(run_config[CONF_ID], run_text, run_font) + if run_text_formatter_config := run_config.get(CONF_TEXT_FORMATTER): + run_text_formatter = await cg.process_lambda( + run_text_formatter_config, + [(cg.std_string, "it")], + return_type=cg.std_string, + ) + cg.add(run.set_text_formatter(run_text_formatter)) if run_background_color_config := run_config.get(CONF_BACKGROUND_COLOR): run_background_color = await cg.get_variable(run_background_color_config)