From 17659e9783d96a19d76bf2b0b95caef208c2be22 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Mon, 8 Jan 2024 21:45:24 +1100 Subject: [PATCH] Add support for time based Text Panel and Text Run Panels Easy way to print times and dates in different formats --- .../components/graphical_layout/__init__.py | 2 +- .../graphical_layout/text_panel.cpp | 18 +++++++++++ .../components/graphical_layout/text_panel.h | 7 +++++ .../components/graphical_layout/text_panel.py | 28 +++++++++++++++-- .../graphical_layout/text_run_panel.h | 30 ++++++++++++++++++- .../graphical_layout/text_run_panel.py | 26 +++++++++++++++- 6 files changed, 105 insertions(+), 6 deletions(-) diff --git a/esphome/components/graphical_layout/__init__.py b/esphome/components/graphical_layout/__init__.py index 66a54333ac..700a5d6755 100644 --- a/esphome/components/graphical_layout/__init__.py +++ b/esphome/components/graphical_layout/__init__.py @@ -16,7 +16,7 @@ ContainerLayoutItem = graphical_layout_ns.class_("ContainerLayoutItem", LayoutIt CODEOWNERS = ["@MrMDavidson"] -AUTO_LOAD = ["display", "sensor", "text_sensor"] +AUTO_LOAD = ["display", "sensor", "text_sensor", "time"] MULTI_CONF = True diff --git a/esphome/components/graphical_layout/text_panel.cpp b/esphome/components/graphical_layout/text_panel.cpp index b95c886494..c6b9e257b5 100644 --- a/esphome/components/graphical_layout/text_panel.cpp +++ b/esphome/components/graphical_layout/text_panel.cpp @@ -26,6 +26,11 @@ void TextPanel::dump_config(int indent_depth, int additional_level_depth) { if (this->text_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, "%*sText Sensor: %s", indent_depth, "", this->text_sensor_->get_name().c_str()); } + if (this->time_ != nullptr) { + ESP_LOGCONFIG(TAG, "%*sTime: YES", indent_depth, ""); + ESP_LOGCONFIG(TAG, "%*sUse UTC Time: %s", indent_depth, "", YESNO(this->use_utc_time_)); + ESP_LOGCONFIG(TAG, "%*sTime Format: %s", indent_depth, "", this->time_format_.value().c_str()); + } ESP_LOGCONFIG(TAG, "%*sHas Text Formatter: %s", indent_depth, "", YESNO(!this->text_formatter_.has_value())); } @@ -48,6 +53,19 @@ void TextPanel::setup_complete() { this->text_ = [this]() { return this->text_formatter_.value(this->text_sensor_->get_state()); }; } + if (this->time_ != nullptr) { + this->text_ = [this]() { + ESPTime time; + if (this->use_utc_time_) { + time = this->time_->utcnow(); + } else { + time = this->time_->now(); + } + + return this->text_formatter_.value(time.strftime(this->time_format_.value())); + }; + } + if (this->text_input_.has_value()) { this->text_ = [this]() { return this->text_formatter_.value(this->text_input_.value()); }; } diff --git a/esphome/components/graphical_layout/text_panel.h b/esphome/components/graphical_layout/text_panel.h index 6598ef976b..15de2a8403 100644 --- a/esphome/components/graphical_layout/text_panel.h +++ b/esphome/components/graphical_layout/text_panel.h @@ -7,6 +7,7 @@ #include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/time/real_time_clock.h" namespace esphome { namespace graphical_layout { @@ -22,6 +23,9 @@ class TextPanel : public LayoutItem { template void set_text(V text) { this->text_input_ = text; }; void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }; void set_text_sensor(text_sensor::TextSensor *text_sensor) { this->text_sensor_ = text_sensor; }; + void set_time(time::RealTimeClock *time) { this->time_ = time; }; + void set_use_utc_time(bool use_utc_time) { this->use_utc_time_ = use_utc_time; }; + template void set_time_format(V time_format) { this->time_format_ = time_format; }; template void set_text_formatter(V text_formatter) { this->text_formatter_ = text_formatter; }; void set_font(display::BaseFont *font) { this->font_ = font; }; void set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }; @@ -32,6 +36,9 @@ class TextPanel : public LayoutItem { TemplatableValue text_{}; sensor::Sensor *sensor_{nullptr}; text_sensor::TextSensor *text_sensor_{nullptr}; + time::RealTimeClock *time_{nullptr}; + bool use_utc_time_{false}; + TemplatableValue time_format_{}; TemplatableValue text_formatter_{}; TemplatableValue text_input_{}; display::BaseFont *font_{nullptr}; diff --git a/esphome/components/graphical_layout/text_panel.py b/esphome/components/graphical_layout/text_panel.py index bc149aad92..655d2a3727 100644 --- a/esphome/components/graphical_layout/text_panel.py +++ b/esphome/components/graphical_layout/text_panel.py @@ -1,8 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import font, color, sensor, text_sensor +from esphome.components import font, color, sensor, text_sensor, time from esphome.components.display import display_ns -from esphome.const import CONF_FOREGROUND_COLOR, CONF_BACKGROUND_COLOR, CONF_SENSOR +from esphome.const import ( + CONF_FOREGROUND_COLOR, + CONF_BACKGROUND_COLOR, + CONF_SENSOR, + CONF_TIME_ID, +) + graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") TextPanel = graphical_layout_ns.class_("TextPanel") @@ -14,6 +20,8 @@ CONF_TEXT = "text" CONF_TEXT_ALIGN = "text_align" CONF_TEXT_SENSOR = "text_sensor" CONF_TEXT_FORMATTER = "text_formatter" +CONF_TIME_FORMAT = "time_format" +CONF_USE_UTC_TIME = "use_utc_time" TEXT_ALIGN = { "TOP_LEFT": TextAlign.TOP_LEFT, @@ -43,10 +51,15 @@ def get_config_schema(base_item_schema, item_type_schema): cv.Optional(CONF_TEXT_ALIGN): cv.enum(TEXT_ALIGN, upper=True), cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_TEXT_SENSOR): cv.use_id(text_sensor.TextSensor), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional(CONF_TIME_FORMAT, default="%H:%M"): cv.templatable( + cv.string + ), + cv.Optional(CONF_USE_UTC_TIME, default=False): cv.boolean, cv.Optional(CONF_TEXT_FORMATTER): cv.returning_lambda, } ), - cv.has_exactly_one_key(CONF_TEXT, CONF_SENSOR, CONF_TEXT_SENSOR), + cv.has_exactly_one_key(CONF_TEXT, CONF_SENSOR, CONF_TEXT_SENSOR, CONF_TIME_ID), ) @@ -70,6 +83,15 @@ async def config_to_layout_item(pvariable_builder, item_config, child_item_build elif text_sensor_config := item_config.get(CONF_TEXT_SENSOR): text_sens = await cg.get_variable(text_sensor_config) cg.add(var.set_text_sensor(text_sens)) + elif time_id_config := item_config.get(CONF_TIME_ID): + time_sens = await cg.get_variable(time_id_config) + time_format = await cg.templatable( + item_config[CONF_TIME_FORMAT], args=[], output_type=cg.std_string + ) + use_utc_time = item_config[CONF_USE_UTC_TIME] + cg.add(var.set_time(time_sens)) + cg.add(var.set_time_format(time_format)) + cg.add(var.set_use_utc_time(use_utc_time)) else: text = await cg.templatable( item_config[CONF_TEXT], args=[], output_type=cg.std_string diff --git a/esphome/components/graphical_layout/text_run_panel.h b/esphome/components/graphical_layout/text_run_panel.h index d421b2e4b9..f607ca271b 100644 --- a/esphome/components/graphical_layout/text_run_panel.h +++ b/esphome/components/graphical_layout/text_run_panel.h @@ -9,6 +9,7 @@ #include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/time/real_time_clock.h" namespace esphome { namespace graphical_layout { @@ -64,7 +65,7 @@ class TextRun : public TextRunBase, public FormattableTextRun { this->text_ = std::move(text); } - std::string get_text() override { return this->format_text(text_.value()); } + std::string get_text() override { return this->format_text(this->text_.value()); } protected: TemplatableValue text_{}; @@ -96,6 +97,33 @@ class TextSensorTextRun : public TextRunBase, public FormattableTextRun { text_sensor::TextSensor *text_sensor_{nullptr}; }; +class TimeTextRun : public TextRunBase, public FormattableTextRun { + public: + TimeTextRun(time::RealTimeClock *time, TemplatableValue time_format, bool use_utc_time, + display::BaseFont *font) + : TextRunBase(font) { + this->time_ = time; + this->time_format_ = std::move(time_format); + this->use_utc_time_ = use_utc_time; + } + + std::string get_text() override { + ESPTime time; + if (this->use_utc_time_) { + time = this->time_->utcnow(); + } else { + time = this->time_->now(); + } + + return this->format_text(time.strftime(this->time_format_.value())); + } + + protected: + time::RealTimeClock *time_{nullptr}; + TemplatableValue time_format_{""}; + bool use_utc_time_{false}; +}; + class CalculatedTextRun { public: CalculatedTextRun(TextRunBase *run, std::string text) { diff --git a/esphome/components/graphical_layout/text_run_panel.py b/esphome/components/graphical_layout/text_run_panel.py index 2d97cd5e8d..c1c1c41317 100644 --- a/esphome/components/graphical_layout/text_run_panel.py +++ b/esphome/components/graphical_layout/text_run_panel.py @@ -1,12 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import font, color, sensor, text_sensor +from esphome.components import font, color, sensor, text_sensor, time from esphome.components.display import display_ns from esphome.const import ( CONF_ID, CONF_FOREGROUND_COLOR, CONF_BACKGROUND_COLOR, CONF_SENSOR, + CONF_TIME_ID, ) graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") @@ -16,6 +17,7 @@ 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) +TimeTextRun = graphical_layout_ns.class_("TimeTextRun", TextRunBase) CanWrapAtCharacterArguments = graphical_layout_ns.struct("CanWrapAtCharacterArguments") CanWrapAtCharacterArgumentsConstRef = CanWrapAtCharacterArguments.operator( "const" @@ -32,6 +34,8 @@ 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" +CONF_TIME_FORMAT = "time_format" +CONF_USE_UTC_TIME = "use_utc_time" TEXT_ALIGN = { "TOP_LEFT": TextAlign.TOP_LEFT, @@ -80,10 +84,21 @@ TEXT_SENSOR_TEXT_RUN_SCHEMA = BASE_RUN_SCHEMA.extend( } ) +TIME_TEXT_RUN_SCHEMA = BASE_RUN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TimeTextRun), + cv.Required(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional(CONF_TIME_FORMAT, default="%H:%M"): cv.templatable(cv.string), + cv.Optional(CONF_USE_UTC_TIME, default=False): cv.boolean, + cv.Optional(CONF_TEXT_FORMATTER): cv.returning_lambda, + } +) + RUN_SCHEMA = cv.Any( SENSOR_TEXT_RUN_SCHEMA, TEXT_RUN_SCHEMA, TEXT_SENSOR_TEXT_RUN_SCHEMA, + TIME_TEXT_RUN_SCHEMA, ) @@ -136,6 +151,15 @@ async def config_to_layout_item(pvariable_builder, item_config, child_item_build 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) + elif run_time_id_config := run_config.get(CONF_TIME_ID): + time_sens = await cg.get_variable(run_time_id_config) + time_format = await cg.templatable( + run_config[CONF_TIME_FORMAT], args=[], output_type=cg.std_string + ) + use_utc_time = run_config[CONF_USE_UTC_TIME] + run = cg.new_Pvariable( + run_config[CONF_ID], time_sens, time_format, use_utc_time, run_font + ) else: run_text = await cg.templatable( run_config[CONF_TEXT], args=[], output_type=cg.std_string