diff --git a/CODEOWNERS b/CODEOWNERS index 1236c8d842..9159f5f843 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode +esphome/components/hmac_md5/* @dwmw2 esphome/components/homeassistant/* @OttoWinter @esphome/core esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/switch/* @Links2004 diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 630e00f0b7..d1960e9a93 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -1,4 +1,5 @@ #include "captive_portal.h" +#ifdef USE_CAPTIVE_PORTAL #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/wifi/wifi_component.h" @@ -91,3 +92,4 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo } // namespace captive_portal } // namespace esphome +#endif diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index df45d40d12..24d1295e6a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_CAPTIVE_PORTAL #include #ifdef USE_ARDUINO #include @@ -71,3 +72,4 @@ extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid- } // namespace captive_portal } // namespace esphome +#endif diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0a5dd46478..b630c7638e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -172,6 +172,19 @@ def add_idf_component( KEY_COMPONENTS: components, KEY_SUBMODULES: submodules, } + else: + component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] + if components is not None: + component_config[KEY_COMPONENTS] = list( + set(component_config[KEY_COMPONENTS] + components) + ) + if submodules is not None: + if component_config[KEY_SUBMODULES] is None: + component_config[KEY_SUBMODULES] = submodules + else: + component_config[KEY_SUBMODULES] = list( + set(component_config[KEY_SUBMODULES] + submodules) + ) def add_extra_script(stage: str, filename: str, path: str): diff --git a/esphome/components/hmac_md5/__init__.py b/esphome/components/hmac_md5/__init__.py new file mode 100644 index 0000000000..fe245c0cfd --- /dev/null +++ b/esphome/components/hmac_md5/__init__.py @@ -0,0 +1,2 @@ +AUTO_LOAD = ["md5"] +CODEOWNERS = ["@dwmw2"] diff --git a/esphome/components/hmac_md5/hmac_md5.cpp b/esphome/components/hmac_md5/hmac_md5.cpp new file mode 100644 index 0000000000..90bf91882f --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.cpp @@ -0,0 +1,56 @@ +#include +#include +#include "hmac_md5.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace hmac_md5 { +void HmacMD5::init(const uint8_t *key, size_t len) { + uint8_t ipad[64], opad[64]; + + memset(ipad, 0, sizeof(ipad)); + if (len > 64) { + md5::MD5Digest keymd5; + keymd5.init(); + keymd5.add(key, len); + keymd5.calculate(); + keymd5.get_bytes(ipad); + } else { + memcpy(ipad, key, len); + } + memcpy(opad, ipad, sizeof(opad)); + + for (int i = 0; i < 64; i++) { + ipad[i] ^= 0x36; + opad[i] ^= 0x5c; + } + + this->ihash_.init(); + this->ihash_.add(ipad, sizeof(ipad)); + + this->ohash_.init(); + this->ohash_.add(opad, sizeof(opad)); +} + +void HmacMD5::add(const uint8_t *data, size_t len) { this->ihash_.add(data, len); } + +void HmacMD5::calculate() { + uint8_t ibytes[16]; + + this->ihash_.calculate(); + this->ihash_.get_bytes(ibytes); + + this->ohash_.add(ibytes, sizeof(ibytes)); + this->ohash_.calculate(); +} + +void HmacMD5::get_bytes(uint8_t *output) { this->ohash_.get_bytes(output); } + +void HmacMD5::get_hex(char *output) { this->ohash_.get_hex(output); } + +bool HmacMD5::equals_bytes(const uint8_t *expected) { return this->ohash_.equals_bytes(expected); } + +bool HmacMD5::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } + +} // namespace hmac_md5 +} // namespace esphome diff --git a/esphome/components/hmac_md5/hmac_md5.h b/esphome/components/hmac_md5/hmac_md5.h new file mode 100644 index 0000000000..e6a97ad2e3 --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/components/md5/md5.h" + +#include + +namespace esphome { +namespace hmac_md5 { + +class HmacMD5 { + public: + HmacMD5() = default; + ~HmacMD5() = default; + + /// Initialize a new MD5 digest computation. + void init(const uint8_t *key, size_t len); + void init(const char *key, size_t len) { this->init((const uint8_t *) key, len); } + void init(const std::string &key) { this->init(key.c_str(), key.length()); } + + /// Add bytes of data for the digest. + void add(const uint8_t *data, size_t len); + void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the digest, based on the provided data. + void calculate(); + + /// Retrieve the HMAC-MD5 digest as bytes. + /// The output must be able to hold 16 bytes or more. + void get_bytes(uint8_t *output); + + /// Retrieve the HMAC-MD5 digest as hex characters. + /// The output must be able to hold 32 bytes or more. + void get_hex(char *output); + + /// Compare the digest against a provided byte-encoded digest (16 bytes). + bool equals_bytes(const uint8_t *expected); + + /// Compare the digest against a provided hex-encoded digest (32 bytes). + bool equals_hex(const char *expected); + + protected: + md5::MD5Digest ihash_; + md5::MD5Digest ohash_; +}; + +} // namespace hmac_md5 +} // namespace esphome diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index eedd71ab27..979a1acb07 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -41,29 +41,29 @@ class ESPColorCorrection { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } protected: diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 8f7a973722..6a8b20b505 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -6,8 +6,8 @@ Constants already defined in esphome.const are not duplicated here and must be i from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS -from esphome.core import ID, Lambda -from esphome.cpp_generator import MockObj +from esphome.core import Lambda +from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -22,19 +22,22 @@ def literal(arg): return arg +def call_lambda(lamb: LambdaExpression): + expr = lamb.content.strip() + if expr.startswith("return") and expr.endswith(";"): + return expr[7:][:-1] + return f"{lamb}()" + + class LValidator: """ A validator for a particular type used in LVGL. Usable in configs as a validator, also has `process()` to convert a value during code generation """ - def __init__( - self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None - ): + def __init__(self, validator, rtype, retmapper=None, requires=None): self.validator = validator self.rtype = rtype - self.idtype = idtype - self.idexpr = idexpr self.retmapper = retmapper self.requires = requires @@ -43,8 +46,6 @@ class LValidator: 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): - return cv.use_id(self.idtype)(value) return self.validator(value) async def process(self, value, args=()): @@ -52,10 +53,10 @@ class LValidator: return None if isinstance(value, Lambda): return cg.RawExpression( - f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" + call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) ) - if self.idtype is not None and isinstance(value, ID): - return cg.RawExpression(f"{value}->{self.idexpr}") if self.retmapper is not None: return self.retmapper(value) return cg.safe_exp(value) @@ -89,7 +90,7 @@ class LvConstant(LValidator): cv.ensure_list(self.one_of), uint32, retmapper=self.mapper ) - def mapper(self, value, args=()): + def mapper(self, value): if not isinstance(value, list): value = [value] return literal( @@ -103,7 +104,7 @@ class LvConstant(LValidator): def extend(self, *choices): """ - Extend an LVCconstant with additional choices. + Extend an LVconstant with additional choices. :param choices: The extra choices :return: A new LVConstant instance """ @@ -431,6 +432,8 @@ CONF_ONE_LINE = "one_line" CONF_ON_SELECT = "on_select" CONF_ONE_CHECKED = "one_checked" CONF_NEXT = "next" +CONF_PAD_ROW = "pad_row" +CONF_PAD_COLUMN = "pad_column" CONF_PAGE = "page" CONF_PAGE_WRAP = "page_wrap" CONF_PASSWORD_MODE = "password_mode" @@ -462,6 +465,7 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" +CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" CONF_TILE_ID = "tile_id" CONF_TILES = "tiles" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index b351b84af6..a2be4a2abe 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,17 +1,14 @@ from typing import Union 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 -from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE -from esphome.core import HexInt +from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE +from esphome.core import HexInt, Lambda from esphome.cpp_generator import MockObj -from esphome.cpp_types import uint32 +from esphome.cpp_types import ESPTime, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -19,9 +16,11 @@ from . import types as ty from .defines import ( CONF_END_VALUE, CONF_START_VALUE, + CONF_TIME_FORMAT, LV_FONTS, LValidator, LvConstant, + call_lambda, literal, ) from .helpers import ( @@ -110,13 +109,13 @@ def angle(value): def size_validator(value): """A size in one axis - one of "size_content", a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: - return ["size_content", "pixels", "..%"] + return ["SIZE_CONTENT", "number of pixels", "percentage"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) if isinstance(value, str) and not value.endswith("%"): if value.upper() == "SIZE_CONTENT": return "LV_SIZE_CONTENT" - raise cv.Invalid("must be 'size_content', a pixel position or a percentage") + raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)") if isinstance(value, int): return cv.int_(value) # Will throw an exception if not a percentage. @@ -125,6 +124,15 @@ def size_validator(value): size = LValidator(size_validator, uint32, retmapper=literal) + +def pixels_validator(value): + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +pixels = LValidator(pixels_validator, uint32, retmapper=literal) + radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") @@ -167,9 +175,7 @@ lv_image = LValidator( retmapper=lambda x: lv_expr.img_from(MockObj(x)), requires="image", ) -lv_bool = LValidator( - cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal -) +lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) def lv_pct(value: Union[int, float]): @@ -185,42 +191,60 @@ def lvms_validator_(value): lv_milliseconds = LValidator( - lvms_validator_, - cg.int32, - retmapper=lambda x: x.total_milliseconds, + lvms_validator_, cg.int32, retmapper=lambda x: x.total_milliseconds ) class TextValidator(LValidator): def __init__(self): - super().__init__( - cv.string, - cg.const_char_ptr, - TextSensor, - "get_state().c_str()", - lambda s: cg.safe_exp(f"{s}"), - ) + super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}")) def __call__(self, value): - if isinstance(value, dict): + if isinstance(value, dict) and CONF_FORMAT in value: return value return super().__call__(value) async def process(self, value, args=()): if isinstance(value, dict): - args = [str(x) for x in value[CONF_ARGS]] - arg_expr = cg.RawExpression(",".join(args)) - format_str = cpp_string_escape(value[CONF_FORMAT]) - return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if format_str := value.get(CONF_FORMAT): + args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(args)) + format_str = cpp_string_escape(format_str) + return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if time_format := value.get(CONF_TIME_FORMAT): + source = value[CONF_TIME] + if isinstance(source, Lambda): + time_format = cpp_string_escape(time_format) + return cg.RawExpression( + call_lambda( + await cg.process_lambda(source, args, return_type=ESPTime) + ) + + f".strftime({time_format}).c_str()" + ) + # must be an ID + source = await cg.get_variable(source) + return source.now().strftime(time_format).c_str() + if isinstance(value, Lambda): + value = call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) + + # Was the lambda call reduced to a string? + if value.endswith("c_str()") or ( + value.endswith('"') and value.startswith('"') + ): + pass + else: + # Either a std::string or a lambda call returning that. We need const char* + value = f"({value}).c_str()" + return cg.RawExpression(value) return await super().process(value, args) lv_text = TextValidator() -lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") -lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") -lv_brightness = LValidator( - cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255) -) +lv_float = LValidator(cv.float_, cg.float_) +lv_int = LValidator(cv.int_, cg.int_) +lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) def is_lv_font(font): diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index f1c7ff4df6..e9714e3b1a 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,5 +1,6 @@ from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation +from esphome.components.time import RealTimeClock from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -8,6 +9,7 @@ from esphome.const import ( CONF_ON_VALUE, CONF_STATE, CONF_TEXT, + CONF_TIME, CONF_TRIGGER_ID, CONF_TYPE, ) @@ -15,6 +17,7 @@ from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid +from .defines import CONF_TIME_FORMAT from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_image from .lvcode import LvglComponent @@ -46,7 +49,13 @@ TEXT_SCHEMA = cv.Schema( ), validate_printf, ), - lvalid.lv_text, + cv.Schema( + { + cv.Required(CONF_TIME_FORMAT): cv.string, + cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), + } + ), + cv.templatable(cv.string), ) } ) @@ -116,15 +125,13 @@ STYLE_PROPS = { "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, - "outline_pad": lvalid.size, - "outline_width": lvalid.size, - "pad_all": lvalid.size, - "pad_bottom": lvalid.size, - "pad_column": lvalid.size, - "pad_left": lvalid.size, - "pad_right": lvalid.size, - "pad_row": lvalid.size, - "pad_top": lvalid.size, + "outline_pad": lvalid.pixels, + "outline_width": lvalid.pixels, + "pad_all": lvalid.pixels, + "pad_bottom": lvalid.pixels, + "pad_left": lvalid.pixels, + "pad_right": lvalid.pixels, + "pad_top": lvalid.pixels, "shadow_color": lvalid.lv_color, "shadow_ofs_x": cv.int_, "shadow_ofs_y": cv.int_, @@ -304,6 +311,8 @@ LAYOUT_SCHEMA = { cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, }, df.TYPE_FLEX: { cv.Optional( @@ -312,6 +321,8 @@ LAYOUT_SCHEMA = { cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, }, }, lower=True, @@ -338,7 +349,6 @@ DISP_BG_SCHEMA = cv.Schema( } ) - # A style schema that can include text STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 603de6aa3e..4abb25c61d 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -20,6 +20,8 @@ from ..defines import ( CONF_GRID_ROWS, CONF_LAYOUT, CONF_MAIN, + CONF_PAD_COLUMN, + CONF_PAD_ROW, CONF_SCROLLBAR_MODE, CONF_STYLES, CONF_WIDGETS, @@ -29,6 +31,7 @@ from ..defines import ( TYPE_FLEX, TYPE_GRID, LValidator, + call_lambda, join_enums, literal, ) @@ -273,6 +276,10 @@ async def set_obj_properties(w: Widget, config): layout_type: str = layout[CONF_TYPE] add_lv_use(layout_type) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) + if (pad_row := layout.get(CONF_PAD_ROW)) is not None: + w.set_style(CONF_PAD_ROW, pad_row, 0) + if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: + w.set_style(CONF_PAD_COLUMN, pad_column, 0) if layout_type == TYPE_GRID: wid = config[CONF_ID] rows = [str(x) for x in layout[CONF_GRID_ROWS]] @@ -316,8 +323,13 @@ async def set_obj_properties(w: Widget, config): flag_clr = set() flag_set = set() props = parts[CONF_MAIN][CONF_DEFAULT] + lambs = {} + flag_set = set() + flag_clr = set() for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): - if value: + if isinstance(value, cv.Lambda): + lambs[prop] = value + elif value: flag_set.add(prop) else: flag_clr.add(prop) @@ -327,6 +339,13 @@ async def set_obj_properties(w: Widget, config): if flag_clr: 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_) + flag = f"LV_OBJ_FLAG_{key.upper()}" + with LvConditional(call_lambda(lamb)) as cond: + w.add_flag(flag) + cond.else_() + w.clear_flag(flag) if states := config.get(CONF_STATE): adds = set() @@ -348,7 +367,7 @@ async def set_obj_properties(w: Widget, config): for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) state = f"LV_STATE_{key.upper()}" - with LvConditional(f"{lamb}()") as cond: + with LvConditional(call_lambda(lamb)) as cond: w.add_state(state) cond.else_() w.clear_state(state) diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index e01a10e15c..914ad80bea 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -1,6 +1,9 @@ #pragma once -#include "esphome/core/entity_base.h" +#include +#include +#include +#include #include "esphome/core/helpers.h" namespace esphome { diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 96db322bde..caa873a746 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -24,7 +24,11 @@ CONFIG_SCHEMA = cv.Schema( esp32=False, rp2040=False, ): cv.All( - cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]) + cv.boolean, + cv.Any( + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.boolean_false, + ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, } diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index 3e5e82898d..2aaa2ceb19 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -7,8 +7,10 @@ #include #include +#include #include #include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -23,6 +25,19 @@ static std::map conf_count_ = { {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false}, {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false}, }; +static bool dma_chan_active_[12]; +static struct semaphore dma_write_complete_sem_[12]; + +// DMA interrupt service routine +void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() { + uint32_t channel = dma_hw->ints0; + for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) { + if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) { + dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt + sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt + } + } +} void RP2040PIOLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip..."); @@ -57,22 +72,22 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; } // keep track of how many instances of the PIO program are running on each PIO - num_instance_[this->pio_ == pio0 ? 0 : 1]++; + RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++; // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space if (this->conf_count_[this->chipset_]) { - offset = chipset_offsets_[this->chipset_]; + offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_]; } else { // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it offset = pio_add_program(this->pio_, this->program_); - chipset_offsets_[this->chipset_] = offset; - conf_count_[this->chipset_] = true; + RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset; + RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true; } // Configure the state machine's PIO, and start it @@ -93,6 +108,9 @@ void RP2040PIOLEDStripLightOutput::setup() { return; } + // Mark the DMA channel as active + RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true; + this->dma_config_ = dma_channel_get_default_config(this->dma_chan_); channel_config_set_transfer_data_size( &this->dma_config_, @@ -109,6 +127,13 @@ void RP2040PIOLEDStripLightOutput::setup() { false // don't start yet ); + // Initialize the semaphore for this DMA channel + sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1); + + irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt + dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt + irq_set_enabled(DMA_IRQ_0, true); // enable interrupt + this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); } @@ -126,6 +151,7 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { } // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA + sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]); dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_()); } diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index 9976842f02..7b62648974 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace esphome { @@ -95,6 +96,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + static void dma_write_complete_handler_(); + uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -120,6 +123,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { inline static int num_instance_[2]; inline static std::map conf_count_; inline static std::map chipset_offsets_; + inline static bool dma_chan_active_[12]; + inline static struct semaphore dma_write_complete_sem_[12]; }; } // namespace rp2040_pio_led_strip diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index a97120499d..495b5c1c8a 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -32,7 +32,7 @@ void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { int pos = this->rtttl_.find(':'); auto name = this->rtttl_.substr(0, pos); - ESP_LOGW(TAG, "RTTL Component is already playing: %s", name.c_str()); + ESP_LOGW(TAG, "RTTTL Component is already playing: %s", name.c_str()); return; } @@ -122,6 +122,7 @@ void Rtttl::stop() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); + this->set_state_(STATE_STOPPED); } #endif #ifdef USE_SPEAKER @@ -129,10 +130,10 @@ void Rtttl::stop() { if (this->speaker_->is_running()) { this->speaker_->stop(); } + this->set_state_(STATE_STOPPING); } #endif this->note_duration_ = 0; - this->set_state_(STATE_STOPPING); } void Rtttl::loop() { @@ -342,6 +343,7 @@ void Rtttl::finish_() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); } #endif #ifdef USE_SPEAKER @@ -354,9 +356,9 @@ void Rtttl::finish_() { this->speaker_->play((uint8_t *) (&sample), 8); this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); } #endif - this->set_state_(State::STATE_STOPPING); this->note_duration_ = 0; this->on_finished_playback_callback_.call(); ESP_LOGD(TAG, "Playback finished"); diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 5d3528dad8..e260fce05e 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -1,4 +1,5 @@ #include "socket.h" +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) #include #include #include @@ -74,3 +75,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po } } // namespace socket } // namespace esphome +#endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 5c12210d15..cefdb51e0d 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -5,6 +5,7 @@ #include "esphome/core/optional.h" #include "headers.h" +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) namespace esphome { namespace socket { @@ -57,3 +58,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po } // namespace socket } // namespace esphome +#endif diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 142231881c..375ccc4e8c 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + namespace esphome { namespace speaker { diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 24df428e6f..7c1d436673 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -480,7 +480,7 @@ void HOT WaveshareEPaperTypeA::display() { this->start_data_(); switch (this->model_) { case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations - int16_t wb = ((this->get_width_internal()) >> 3); + int16_t wb = ((this->get_width_controller()) >> 3); for (int i = 0; i < this->get_height_internal(); i++) { for (int j = 0; j < wb; j++) { int idx = j + (this->get_height_internal() - 1 - i) * wb; @@ -766,7 +766,7 @@ void WaveshareEPaper2P7InV2::initialize() { // XRAM_START_AND_END_POSITION this->command(0x44); this->data(0x00); - this->data(((get_width_internal() - 1) >> 3) & 0xFF); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); // YRAM_START_AND_END_POSITION this->command(0x45); this->data(0x00); @@ -928,8 +928,8 @@ void HOT WaveshareEPaper2P7InB::display() { // TCON_RESOLUTION this->command(0x61); - this->data(this->get_width_internal() >> 8); - this->data(this->get_width_internal() & 0xff); // 176 + this->data(this->get_width_controller() >> 8); + this->data(this->get_width_controller() & 0xff); // 176 this->data(this->get_height_internal() >> 8); this->data(this->get_height_internal() & 0xff); // 264 @@ -994,7 +994,7 @@ void WaveshareEPaper2P7InBV2::initialize() { // self.SetWindows(0, 0, self.width-1, self.height-1) // SetWindows(self, Xstart, Ystart, Xend, Yend): - uint32_t xend = this->get_width_internal() - 1; + uint32_t xend = this->get_width_controller() - 1; uint32_t yend = this->get_height_internal() - 1; this->command(0x44); this->data(0x00); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ef60d6e0d6..719cc43b31 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -370,6 +370,20 @@ def boolean(value): ) +def boolean_false(value): + """Validate the given config option to be a boolean, set to False. + + This option allows a bunch of different ways of expressing boolean values: + - instance of boolean + - 'true'/'false' + - 'yes'/'no' + - 'enable'/disable + """ + if boolean(value): + raise Invalid("Expected boolean value to be false") + return False + + @schema_extractor_list def ensure_list(*validators): """Validate this configuration option to be a list. diff --git a/esphome/const.py b/esphome/const.py index 6157ce32f7..b9c37a53a8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1042,6 +1042,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" +UNIT_LITRE = "L" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 002c7a118d..7ef7772ac9 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -127,3 +127,11 @@ binary_sensor: - platform: lvgl name: LVGL checkbox widget: checkbox_id + +wifi: + ssid: SSID + password: PASSWORD123 + +time: + platform: sntp + id: time_id diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 54022354f5..800d6eff27 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -16,8 +16,6 @@ lvgl: border_width: 0 radius: 0 pad_all: 0 - pad_row: 0 - pad_column: 0 border_color: 0x0077b3 text_color: 0xFFFFFF width: 100% @@ -55,6 +53,13 @@ lvgl: pages: - id: page1 skip: true + layout: + type: flex + pad_row: 4 + pad_column: 4px + flex_align_main: center + flex_align_cross: start + flex_align_track: end widgets: - animimg: height: 60 @@ -118,10 +123,8 @@ lvgl: outline_width: 10px pad_all: 10px pad_bottom: 10px - pad_column: 10px pad_left: 10px pad_right: 10px - pad_row: 10px pad_top: 10px shadow_color: light_blue shadow_ofs_x: 5 @@ -221,10 +224,47 @@ lvgl: - label: text: Button on_click: - lvgl.label.update: - id: hello_label - bg_color: 0x123456 - text: clicked + - lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + - lvgl.label.update: + id: hello_label + text: !lambda return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda 'return str_sprintf("Hello space");' + - lvgl.label.update: + id: hello_label + text: + format: "sprintf format %s" + args: ['x ? "checked" : "unchecked"'] + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: time_id + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda return id(time_id).now(); + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return id(time_id).now(); on_value: logger.log: format: "state now %d" @@ -396,6 +436,8 @@ lvgl: grid_row_align: end grid_rows: [25px, fr(1), content] grid_columns: [40, fr(1), fr(1)] + pad_row: 6px + pad_column: 0 widgets: - image: grid_cell_row_pos: 0 diff --git a/tests/components/network/test-ipv6.bk72xx-ard.yaml b/tests/components/network/test-ipv6.bk72xx-ard.yaml new file mode 100644 index 0000000000..361ca09977 --- /dev/null +++ b/tests/components/network/test-ipv6.bk72xx-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "false" + +<<: !include common.yaml