mirror of
https://github.com/esphome/esphome.git
synced 2025-11-17 15:26:01 +00:00
Compare commits
2 Commits
dev
...
combine_na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ad2da6562 | ||
|
|
8997fb3443 |
@@ -4,7 +4,8 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cover.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
template<typename... Ts> class OpenAction : public Action<Ts...> {
|
||||
public:
|
||||
@@ -130,4 +131,5 @@ class CoverClosedTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
static const char *const TAG = "cover";
|
||||
|
||||
@@ -211,4 +212,5 @@ void CoverRestoreState::apply(Cover *cover) {
|
||||
cover->publish_state();
|
||||
}
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
#include "cover_traits.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
const extern float COVER_OPEN;
|
||||
const extern float COVER_CLOSED;
|
||||
@@ -156,4 +157,5 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
|
||||
ESPPreferenceObject rtc_;
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace esphome::cover {
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
|
||||
class CoverTraits {
|
||||
public:
|
||||
@@ -25,4 +26,5 @@ class CoverTraits {
|
||||
bool supports_stop_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
|
||||
@@ -486,8 +486,6 @@ class GlyphInfo:
|
||||
|
||||
|
||||
def glyph_to_glyphinfo(glyph, font, size, bpp):
|
||||
# Convert to 32 bit unicode codepoint
|
||||
glyph = ord(glyph)
|
||||
scale = 256 // (1 << bpp)
|
||||
if not font.is_scalable:
|
||||
sizes = [pt_to_px(x.size) for x in font.available_sizes]
|
||||
|
||||
@@ -6,147 +6,42 @@
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
|
||||
static const char *const TAG = "font";
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
|
||||
auto *fe = (Font *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data_(unicode_letter);
|
||||
if (gd == nullptr) {
|
||||
return nullptr;
|
||||
// Compare the char at the string position with this char.
|
||||
// Return true if this char is less than or equal the other.
|
||||
bool Glyph::compare_to(const uint8_t *str) const {
|
||||
// 1 -> this->char_
|
||||
// 2 -> str
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->a_char[i] == '\0')
|
||||
return true;
|
||||
if (str[i] == '\0')
|
||||
return false;
|
||||
if (this->a_char[i] > str[i])
|
||||
return false;
|
||||
if (this->a_char[i] < str[i])
|
||||
return true;
|
||||
}
|
||||
return gd->data;
|
||||
// this should not happen
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
|
||||
auto *fe = (Font *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data_(unicode_letter);
|
||||
if (gd == nullptr) {
|
||||
return false;
|
||||
int Glyph::match_length(const uint8_t *str) const {
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->a_char[i] == '\0')
|
||||
return i;
|
||||
if (str[i] != this->a_char[i])
|
||||
return 0;
|
||||
}
|
||||
dsc->adv_w = gd->advance;
|
||||
dsc->ofs_x = gd->offset_x;
|
||||
dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line;
|
||||
dsc->box_w = gd->width;
|
||||
dsc->box_h = gd->height;
|
||||
dsc->is_placeholder = 0;
|
||||
dsc->bpp = fe->get_bpp();
|
||||
return true;
|
||||
// this should not happen
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) {
|
||||
if (unicode_letter == this->last_letter_ && this->last_letter_ != 0)
|
||||
return this->last_data_;
|
||||
auto *glyph = this->find_glyph(unicode_letter);
|
||||
if (glyph == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
this->last_data_ = glyph;
|
||||
this->last_letter_ = unicode_letter;
|
||||
return glyph;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string.
|
||||
* If successful, return the codepoint and set the length to the number of bytes read.
|
||||
* If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to
|
||||
* 0.
|
||||
*
|
||||
* @param utf8_str The input string
|
||||
* @param length Pointer to length storage
|
||||
* @return The extracted code point
|
||||
*/
|
||||
static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
|
||||
// Safely cast to uint8_t* for correct bitwise operations on bytes
|
||||
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
uint32_t code_point = 0;
|
||||
uint8_t c1 = *current++;
|
||||
|
||||
// check for end of string
|
||||
if (c1 == 0) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
|
||||
if (c1 < 0x80) {
|
||||
// Valid ASCII byte.
|
||||
code_point = c1;
|
||||
// Optimization: No need to check for continuation bytes.
|
||||
}
|
||||
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xE0) == 0xC0) {
|
||||
uint8_t c2 = *current++;
|
||||
|
||||
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
|
||||
if ((c2 & 0xC0) != 0x80) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x1F) << 6;
|
||||
code_point |= (c2 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (2-byte must be > 0x7F)
|
||||
if (code_point <= 0x7F) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF0) == 0xE0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x0F) << 12;
|
||||
code_point |= (c2 & 0x3F) << 6;
|
||||
code_point |= (c3 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
|
||||
// Also check for surrogates (0xD800-0xDFFF)
|
||||
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF8) == 0xF0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
uint8_t c4 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x07) << 18;
|
||||
code_point |= (c2 & 0x3F) << 12;
|
||||
code_point |= (c3 & 0x3F) << 6;
|
||||
code_point |= (c4 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
|
||||
// Also check for valid Unicode range (must be <= 0x10FFFF)
|
||||
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
|
||||
else {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
return code_point;
|
||||
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
|
||||
*x1 = this->offset_x;
|
||||
*y1 = this->offset_y;
|
||||
*width = this->width;
|
||||
*height = this->height;
|
||||
}
|
||||
|
||||
Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
@@ -158,93 +53,82 @@ Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descend
|
||||
linegap_(height - baseline - descender),
|
||||
xheight_(xheight),
|
||||
capheight_(capheight),
|
||||
bpp_(bpp) {
|
||||
#ifdef USE_LVGL_FONT
|
||||
this->lv_font_.dsc = this;
|
||||
this->lv_font_.line_height = this->get_height();
|
||||
this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline();
|
||||
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
|
||||
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
|
||||
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
|
||||
this->lv_font_.underline_position = -1;
|
||||
this->lv_font_.underline_thickness = 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
const Glyph *Font::find_glyph(uint32_t codepoint) const {
|
||||
bpp_(bpp) {}
|
||||
int Font::match_next_glyph(const uint8_t *str, int *match_length) const {
|
||||
int lo = 0;
|
||||
int hi = this->glyphs_.size() - 1;
|
||||
while (lo != hi) {
|
||||
int mid = (lo + hi + 1) / 2;
|
||||
if (this->glyphs_[mid].is_less_or_equal(codepoint)) {
|
||||
if (this->glyphs_[mid].compare_to(str)) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
auto *result = &this->glyphs_[lo];
|
||||
if (result->code_point == codepoint)
|
||||
return result;
|
||||
return nullptr;
|
||||
*match_length = this->glyphs_[lo].match_length(str);
|
||||
if (*match_length <= 0)
|
||||
return -1;
|
||||
return lo;
|
||||
}
|
||||
|
||||
#ifdef USE_DISPLAY
|
||||
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
|
||||
*baseline = this->baseline_;
|
||||
*height = this->height_;
|
||||
int i = 0;
|
||||
int min_x = 0;
|
||||
bool has_char = false;
|
||||
int x = 0;
|
||||
for (;;) {
|
||||
size_t length;
|
||||
auto code_point = extract_unicode_codepoint(str, &length);
|
||||
if (length == 0)
|
||||
break;
|
||||
str += length;
|
||||
auto *glyph = this->find_glyph(code_point);
|
||||
if (glyph == nullptr) {
|
||||
while (str[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
if (!this->glyphs_.empty())
|
||||
x += this->glyphs_[0].advance;
|
||||
if (!this->get_glyphs().empty())
|
||||
x += this->get_glyphs()[0].advance;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->glyphs_[glyph_n];
|
||||
if (!has_char) {
|
||||
min_x = glyph->offset_x;
|
||||
min_x = glyph.offset_x;
|
||||
} else {
|
||||
min_x = std::min(min_x, x + glyph->offset_x);
|
||||
min_x = std::min(min_x, x + glyph.offset_x);
|
||||
}
|
||||
x += glyph->advance;
|
||||
x += glyph.advance;
|
||||
|
||||
i += match_length;
|
||||
has_char = true;
|
||||
}
|
||||
*x_offset = min_x;
|
||||
*width = x - min_x;
|
||||
}
|
||||
|
||||
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) {
|
||||
int i = 0;
|
||||
int x_at = x_start;
|
||||
for (;;) {
|
||||
size_t length;
|
||||
auto code_point = extract_unicode_codepoint(text, &length);
|
||||
if (length == 0)
|
||||
break;
|
||||
text += length;
|
||||
auto *glyph = this->find_glyph(code_point);
|
||||
if (glyph == nullptr) {
|
||||
int scan_x1, scan_y1, scan_width, scan_height;
|
||||
while (text[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
|
||||
if (!this->glyphs_.empty()) {
|
||||
uint8_t glyph_width = this->glyphs_[0].advance;
|
||||
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
|
||||
if (!this->get_glyphs().empty()) {
|
||||
uint8_t glyph_width = this->get_glyphs()[0].advance;
|
||||
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
x_at += glyph_width;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t *data = glyph->data;
|
||||
const int max_x = x_at + glyph->offset_x + glyph->width;
|
||||
const int max_y = y_start + glyph->offset_y + glyph->height;
|
||||
const Glyph &glyph = this->get_glyphs()[glyph_n];
|
||||
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
|
||||
|
||||
const uint8_t *data = glyph.data;
|
||||
const int max_x = x_at + scan_x1 + scan_width;
|
||||
const int max_y = y_start + scan_y1 + scan_height;
|
||||
|
||||
uint8_t bitmask = 0;
|
||||
uint8_t pixel_data = 0;
|
||||
@@ -257,10 +141,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
auto b_g = (float) background.g;
|
||||
auto b_b = (float) background.b;
|
||||
auto b_w = (float) background.w;
|
||||
for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) {
|
||||
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
|
||||
uint8_t pixel = 0;
|
||||
for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) {
|
||||
for (int bit_num = 0; bit_num != this->bpp_; bit_num++) {
|
||||
if (bitmask == 0) {
|
||||
pixel_data = progmem_read_byte(data++);
|
||||
bitmask = 0x80;
|
||||
@@ -280,9 +164,12 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
}
|
||||
}
|
||||
}
|
||||
x_at += glyph->advance;
|
||||
x_at += glyph.advance;
|
||||
|
||||
i += match_length;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace font
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
#ifdef USE_DISPLAY
|
||||
#include "esphome/components/display/display.h"
|
||||
#endif
|
||||
#ifdef USE_LVGL_FONT
|
||||
#include <lvgl.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
@@ -17,9 +14,9 @@ class Font;
|
||||
|
||||
class Glyph {
|
||||
public:
|
||||
constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width,
|
||||
constexpr Glyph(const char *a_char, const uint8_t *data, int advance, int offset_x, int offset_y, int width,
|
||||
int height)
|
||||
: code_point(code_point),
|
||||
: a_char(a_char),
|
||||
data(data),
|
||||
advance(advance),
|
||||
offset_x(offset_x),
|
||||
@@ -27,15 +24,24 @@ class Glyph {
|
||||
width(width),
|
||||
height(height) {}
|
||||
|
||||
bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; }
|
||||
const uint8_t *get_char() const { return reinterpret_cast<const uint8_t *>(this->a_char); }
|
||||
|
||||
const uint32_t code_point;
|
||||
bool compare_to(const uint8_t *str) const;
|
||||
|
||||
int match_length(const uint8_t *str) const;
|
||||
|
||||
void scan_area(int *x1, int *y1, int *width, int *height) const;
|
||||
|
||||
const char *a_char;
|
||||
const uint8_t *data;
|
||||
int advance;
|
||||
int offset_x;
|
||||
int offset_y;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
protected:
|
||||
friend Font;
|
||||
};
|
||||
|
||||
class Font
|
||||
@@ -58,7 +64,7 @@ class Font
|
||||
Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
uint8_t bpp = 1);
|
||||
|
||||
const Glyph *find_glyph(uint32_t codepoint) const;
|
||||
int match_next_glyph(const uint8_t *str, int *match_length) const;
|
||||
|
||||
#ifdef USE_DISPLAY
|
||||
void print(int x_start, int y_start, display::Display *display, Color color, const char *text,
|
||||
@@ -73,9 +79,6 @@ class Font
|
||||
inline int get_xheight() { return this->xheight_; }
|
||||
inline int get_capheight() { return this->capheight_; }
|
||||
inline int get_bpp() { return this->bpp_; }
|
||||
#ifdef USE_LVGL_FONT
|
||||
const lv_font_t *get_lv_font() const { return &this->lv_font_; }
|
||||
#endif
|
||||
|
||||
const ConstVector<Glyph> &get_glyphs() const { return glyphs_; }
|
||||
|
||||
@@ -88,14 +91,6 @@ class Font
|
||||
int xheight_;
|
||||
int capheight_;
|
||||
uint8_t bpp_; // bits per pixel
|
||||
#ifdef USE_LVGL_FONT
|
||||
lv_font_t lv_font_{};
|
||||
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter);
|
||||
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next);
|
||||
const Glyph *get_glyph_data_(uint32_t unicode_letter);
|
||||
uint32_t last_letter_{};
|
||||
const Glyph *last_data_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace font
|
||||
|
||||
@@ -52,7 +52,15 @@ from .schemas import (
|
||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||
from .touchscreens import touchscreen_schema, touchscreens_to_code
|
||||
from .trigger import add_on_boot_triggers, generate_triggers
|
||||
from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns
|
||||
from .types import (
|
||||
FontEngine,
|
||||
IdleTrigger,
|
||||
PlainTrigger,
|
||||
lv_font_t,
|
||||
lv_group_t,
|
||||
lv_style_t,
|
||||
lvgl_ns,
|
||||
)
|
||||
from .widgets import (
|
||||
LvScrActType,
|
||||
Widget,
|
||||
@@ -236,6 +244,7 @@ async def to_code(configs):
|
||||
cg.add_global(lvgl_ns.using)
|
||||
for font in helpers.esphome_fonts_used:
|
||||
await cg.get_variable(font)
|
||||
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
|
||||
default_font = config_0[df.CONF_DEFAULT_FONT]
|
||||
if not lvalid.is_lv_font(default_font):
|
||||
add_define(
|
||||
@@ -247,8 +256,7 @@ async def to_code(configs):
|
||||
type=lv_font_t.operator("ptr").operator("const"),
|
||||
)
|
||||
cg.new_variable(
|
||||
globfont_id,
|
||||
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
|
||||
globfont_id, MockObj(await lvalid.lv_font.process(default_font))
|
||||
)
|
||||
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
|
||||
else:
|
||||
|
||||
76
esphome/components/lvgl/font.cpp
Normal file
76
esphome/components/lvgl/font.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "lvgl_esphome.h"
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
namespace esphome {
|
||||
namespace lvgl {
|
||||
|
||||
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
|
||||
auto *fe = (FontEngine *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data(unicode_letter);
|
||||
if (gd == nullptr)
|
||||
return nullptr;
|
||||
// esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data);
|
||||
|
||||
return gd->data;
|
||||
}
|
||||
|
||||
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
|
||||
auto *fe = (FontEngine *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data(unicode_letter);
|
||||
if (gd == nullptr)
|
||||
return false;
|
||||
dsc->adv_w = gd->advance;
|
||||
dsc->ofs_x = gd->offset_x;
|
||||
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
|
||||
dsc->box_w = gd->width;
|
||||
dsc->box_h = gd->height;
|
||||
dsc->is_placeholder = 0;
|
||||
dsc->bpp = fe->bpp;
|
||||
return true;
|
||||
}
|
||||
|
||||
FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
|
||||
this->bpp = esp_font->get_bpp();
|
||||
this->lv_font_.dsc = this;
|
||||
this->lv_font_.line_height = this->height = esp_font->get_height();
|
||||
this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline();
|
||||
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
|
||||
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
|
||||
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
|
||||
this->lv_font_.underline_position = -1;
|
||||
this->lv_font_.underline_thickness = 1;
|
||||
}
|
||||
|
||||
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
|
||||
|
||||
const font::Glyph *FontEngine::get_glyph_data(uint32_t unicode_letter) {
|
||||
if (unicode_letter == last_letter_)
|
||||
return this->last_data_;
|
||||
uint8_t unicode[5];
|
||||
memset(unicode, 0, sizeof unicode);
|
||||
if (unicode_letter > 0xFFFF) {
|
||||
unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7);
|
||||
unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F);
|
||||
unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F);
|
||||
unicode[3] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else if (unicode_letter > 0x7FF) {
|
||||
unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF);
|
||||
unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F);
|
||||
unicode[2] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else if (unicode_letter > 0x7F) {
|
||||
unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F);
|
||||
unicode[1] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else {
|
||||
unicode[0] = unicode_letter;
|
||||
}
|
||||
int match_length;
|
||||
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
|
||||
if (glyph_n < 0)
|
||||
return nullptr;
|
||||
this->last_data_ = &this->font_->get_glyphs()[glyph_n];
|
||||
this->last_letter_ = unicode_letter;
|
||||
return this->last_data_;
|
||||
}
|
||||
} // namespace lvgl
|
||||
} // namespace esphome
|
||||
#endif // USES_LVGL_FONT
|
||||
@@ -493,7 +493,6 @@ class LvFont(LValidator):
|
||||
return LV_FONTS
|
||||
if is_lv_font(value):
|
||||
return lv_builtin_font(value)
|
||||
add_lv_use("font")
|
||||
fontval = cv.use_id(Font)(value)
|
||||
esphome_fonts_used.add(fontval)
|
||||
return requires_component("font")(fontval)
|
||||
@@ -503,9 +502,7 @@ class LvFont(LValidator):
|
||||
async def process(self, value, args=()):
|
||||
if is_lv_font(value):
|
||||
return literal(f"&lv_font_{value}")
|
||||
if isinstance(value, str):
|
||||
return literal(f"{value}")
|
||||
return await super().process(value, args)
|
||||
return literal(f"{value}_engine->get_lv_font()")
|
||||
|
||||
|
||||
lv_font = LvFont()
|
||||
|
||||
@@ -50,14 +50,6 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
|
||||
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
|
||||
#endif // LV_COLOR_DEPTH
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) {
|
||||
lv_obj_set_style_text_font(obj, font->get_lv_font(), part);
|
||||
}
|
||||
inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
lv_style_set_text_font(style, font->get_lv_font());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
// Shortcut / overload, so that the source of an image can easily be updated
|
||||
// from within a lambda.
|
||||
@@ -142,6 +134,24 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
|
||||
protected:
|
||||
std::function<void(Ts...)> lamb_;
|
||||
};
|
||||
#ifdef USE_LVGL_FONT
|
||||
class FontEngine {
|
||||
public:
|
||||
FontEngine(font::Font *esp_font);
|
||||
const lv_font_t *get_lv_font();
|
||||
|
||||
const font::Glyph *get_glyph_data(uint32_t unicode_letter);
|
||||
uint16_t baseline{};
|
||||
uint16_t height{};
|
||||
uint8_t bpp{};
|
||||
|
||||
protected:
|
||||
font::Font *font_{};
|
||||
uint32_t last_letter_{};
|
||||
const font::Glyph *last_data_{};
|
||||
lv_font_t lv_font_{};
|
||||
};
|
||||
#endif // USE_LVGL_FONT
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
void lv_animimg_stop(lv_obj_t *obj);
|
||||
#endif // USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -45,6 +45,7 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t")
|
||||
lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
|
||||
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
|
||||
lv_key_t = cg.global_ns.enum("lv_key_t")
|
||||
FontEngine = lvgl_ns.class_("FontEngine")
|
||||
PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template())
|
||||
DrawEndTrigger = esphome_ns.class_(
|
||||
"Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32)
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
from esphome.config_helpers import merge_config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SERVERS,
|
||||
CONF_TIME,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
@@ -17,74 +12,13 @@ from esphome.const import (
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CONF_SNTP = "sntp"
|
||||
|
||||
sntp_ns = cg.esphome_ns.namespace("sntp")
|
||||
SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock)
|
||||
|
||||
DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"]
|
||||
|
||||
|
||||
def _sntp_final_validate(config: ConfigType) -> None:
|
||||
"""Merge multiple SNTP instances into one, similar to OTA merging behavior."""
|
||||
full_conf = fv.full_config.get()
|
||||
time_confs = full_conf.get(CONF_TIME, [])
|
||||
|
||||
sntp_configs: list[ConfigType] = []
|
||||
other_time_configs: list[ConfigType] = []
|
||||
|
||||
for time_conf in time_confs:
|
||||
if time_conf.get(CONF_PLATFORM) == CONF_SNTP:
|
||||
sntp_configs.append(time_conf)
|
||||
else:
|
||||
other_time_configs.append(time_conf)
|
||||
|
||||
if len(sntp_configs) <= 1:
|
||||
return
|
||||
|
||||
# Merge all SNTP configs into the first one
|
||||
merged = sntp_configs[0]
|
||||
for sntp_conf in sntp_configs[1:]:
|
||||
# Validate that IDs are consistent if manually specified
|
||||
if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual:
|
||||
raise cv.Invalid(
|
||||
f"Found multiple SNTP configurations but {CONF_ID} is inconsistent"
|
||||
)
|
||||
merged = merge_config(merged, sntp_conf)
|
||||
|
||||
# Deduplicate servers while preserving order
|
||||
servers = merged[CONF_SERVERS]
|
||||
unique_servers = list(dict.fromkeys(servers))
|
||||
|
||||
# Warn if we're dropping servers due to 3-server limit
|
||||
if len(unique_servers) > 3:
|
||||
dropped = unique_servers[3:]
|
||||
unique_servers = unique_servers[:3]
|
||||
_LOGGER.warning(
|
||||
"SNTP supports maximum 3 servers. Dropped excess server(s): %s",
|
||||
dropped,
|
||||
)
|
||||
|
||||
merged[CONF_SERVERS] = unique_servers
|
||||
|
||||
_LOGGER.warning(
|
||||
"Found and merged %d SNTP time configurations into one instance",
|
||||
len(sntp_configs),
|
||||
)
|
||||
|
||||
# Replace time configs with merged SNTP + other time platforms
|
||||
other_time_configs.append(merged)
|
||||
full_conf[CONF_TIME] = other_time_configs
|
||||
fv.full_config.set(full_conf)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
time_.TIME_SCHEMA.extend(
|
||||
{
|
||||
@@ -106,8 +40,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _sntp_final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
servers = config[CONF_SERVERS]
|
||||
|
||||
@@ -56,19 +56,11 @@ uint32_t ESP8266UartComponent::get_config() {
|
||||
}
|
||||
|
||||
void ESP8266UartComponent::setup() {
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
}
|
||||
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
|
||||
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
|
||||
setup_pin_if_needed(this->rx_pin_);
|
||||
if (this->rx_pin_ != this->tx_pin_) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
if (this->rx_pin_) {
|
||||
this->rx_pin_->setup();
|
||||
}
|
||||
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||
this->tx_pin_->setup();
|
||||
}
|
||||
|
||||
// Use Arduino HardwareSerial UARTs if all used pins match the ones
|
||||
|
||||
@@ -133,19 +133,11 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
}
|
||||
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
|
||||
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
|
||||
setup_pin_if_needed(this->rx_pin_);
|
||||
if (this->rx_pin_ != this->tx_pin_) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
if (this->rx_pin_) {
|
||||
this->rx_pin_->setup();
|
||||
}
|
||||
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||
this->tx_pin_->setup();
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
|
||||
@@ -53,7 +53,7 @@ void LibreTinyUARTComponent::setup() {
|
||||
|
||||
auto shouldFallbackToSoftwareSerial = [&]() -> bool {
|
||||
auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool {
|
||||
return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE;
|
||||
return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE;
|
||||
};
|
||||
if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) ||
|
||||
hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) {
|
||||
|
||||
@@ -52,19 +52,11 @@ uint16_t RP2040UartComponent::get_config() {
|
||||
}
|
||||
|
||||
void RP2040UartComponent::setup() {
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
}
|
||||
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
|
||||
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
|
||||
setup_pin_if_needed(this->rx_pin_);
|
||||
if (this->rx_pin_ != this->tx_pin_) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
if (this->rx_pin_) {
|
||||
this->rx_pin_->setup();
|
||||
}
|
||||
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||
this->tx_pin_->setup();
|
||||
}
|
||||
|
||||
uint16_t config = get_config();
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
||||
from esphome.config_helpers import merge_config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network", "web_server_base"]
|
||||
@@ -19,53 +12,6 @@ DEPENDENCIES = ["network", "web_server_base"]
|
||||
web_server_ns = cg.esphome_ns.namespace("web_server")
|
||||
WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent)
|
||||
|
||||
|
||||
def _web_server_ota_final_validate(config: ConfigType) -> None:
|
||||
"""Merge multiple web_server OTA instances into one.
|
||||
|
||||
Multiple web_server OTA instances register duplicate HTTP handlers for /update,
|
||||
causing undefined behavior. Merge them into a single instance.
|
||||
"""
|
||||
full_conf = fv.full_config.get()
|
||||
ota_confs = full_conf.get(CONF_OTA, [])
|
||||
|
||||
web_server_ota_configs: list[ConfigType] = []
|
||||
other_ota_configs: list[ConfigType] = []
|
||||
|
||||
for ota_conf in ota_confs:
|
||||
if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER:
|
||||
web_server_ota_configs.append(ota_conf)
|
||||
else:
|
||||
other_ota_configs.append(ota_conf)
|
||||
|
||||
if len(web_server_ota_configs) <= 1:
|
||||
return
|
||||
|
||||
# Merge all web_server OTA configs into the first one
|
||||
merged = web_server_ota_configs[0]
|
||||
for ota_conf in web_server_ota_configs[1:]:
|
||||
# Validate that IDs are consistent if manually specified
|
||||
if (
|
||||
merged[CONF_ID].is_manual
|
||||
and ota_conf[CONF_ID].is_manual
|
||||
and merged[CONF_ID] != ota_conf[CONF_ID]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent"
|
||||
)
|
||||
merged = merge_config(merged, ota_conf)
|
||||
|
||||
_LOGGER.warning(
|
||||
"Found and merged %d web_server OTA configurations into one instance",
|
||||
len(web_server_ota_configs),
|
||||
)
|
||||
|
||||
# Replace OTA configs with merged web_server + other OTA platforms
|
||||
other_ota_configs.append(merged)
|
||||
full_conf[CONF_OTA] = other_ota_configs
|
||||
fv.full_config.set(full_conf)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -76,8 +22,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WEB_SERVER_OTA)
|
||||
async def to_code(config):
|
||||
|
||||
@@ -489,18 +489,10 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
|
||||
void AsyncEventSourceResponse::destroy(void *ptr) {
|
||||
auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
|
||||
int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
|
||||
|
||||
if (fd > 0) {
|
||||
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
|
||||
// Immediately shut down the socket to prevent lwIP from delivering more data
|
||||
// This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack
|
||||
// tries to deliver queued data after the session is marked as dead
|
||||
// See: https://github.com/esphome/esphome/issues/11936
|
||||
shutdown(fd, SHUT_RDWR);
|
||||
// Note: We don't close() the socket - httpd owns it and will close it
|
||||
}
|
||||
// Session will be cleaned up in the main loop to avoid race conditions
|
||||
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load());
|
||||
// Mark as dead by setting fd to 0 - will be cleaned up in the main loop
|
||||
rsp->fd_.store(0);
|
||||
// Note: We don't delete or remove from set here to avoid race conditions
|
||||
}
|
||||
|
||||
// helper for allowing only unique entries in the queue
|
||||
|
||||
@@ -338,44 +338,21 @@ def check_replaceme(value):
|
||||
)
|
||||
|
||||
|
||||
def _get_item_id(item: Any) -> str | Extend | Remove | None:
|
||||
"""Attempts to get a list item's ID"""
|
||||
if not isinstance(item, dict):
|
||||
return None # not a dict, can't have ID
|
||||
# 1.- Check regular case:
|
||||
# - id: my_id
|
||||
item_id = item.get(CONF_ID)
|
||||
if item_id is None and len(item) == 1:
|
||||
# 2.- Check single-key dict case:
|
||||
# - obj:
|
||||
# id: my_id
|
||||
item = next(iter(item.values()))
|
||||
if isinstance(item, dict):
|
||||
item_id = item.get(CONF_ID)
|
||||
if isinstance(item_id, Extend):
|
||||
# Remove instances of Extend so they don't overwrite the original item when merging:
|
||||
del item[CONF_ID]
|
||||
return item_id
|
||||
|
||||
|
||||
def _build_list_index(
|
||||
lst: list[Any],
|
||||
) -> tuple[
|
||||
OrderedDict[str | Extend | Remove, Any], list[tuple[int, str, Any]], set[str]
|
||||
]:
|
||||
def _build_list_index(lst):
|
||||
index = OrderedDict()
|
||||
extensions, removals = [], set()
|
||||
for pos, item in enumerate(lst):
|
||||
for item in lst:
|
||||
if item is None:
|
||||
removals.add(None)
|
||||
continue
|
||||
item_id = _get_item_id(item)
|
||||
if isinstance(item_id, Extend):
|
||||
extensions.append((pos, item_id.value, item))
|
||||
continue
|
||||
if isinstance(item_id, Remove):
|
||||
removals.add(item_id.value)
|
||||
continue
|
||||
item_id = None
|
||||
if isinstance(item, dict) and (item_id := item.get(CONF_ID)):
|
||||
if isinstance(item_id, Extend):
|
||||
extensions.append(item)
|
||||
continue
|
||||
if isinstance(item_id, Remove):
|
||||
removals.add(item_id.value)
|
||||
continue
|
||||
if not item_id or item_id in index:
|
||||
# no id or duplicate -> pass through with identity-based key
|
||||
item_id = id(item)
|
||||
@@ -383,7 +360,7 @@ def _build_list_index(
|
||||
return index, extensions, removals
|
||||
|
||||
|
||||
def resolve_extend_remove(value: Any, is_key: bool = False) -> None:
|
||||
def resolve_extend_remove(value, is_key=None):
|
||||
if isinstance(value, ESPLiteralValue):
|
||||
return # do not check inside literal blocks
|
||||
if isinstance(value, list):
|
||||
@@ -391,16 +368,26 @@ def resolve_extend_remove(value: Any, is_key: bool = False) -> None:
|
||||
if extensions or removals:
|
||||
# Rebuild the original list after
|
||||
# processing all extensions and removals
|
||||
for pos, item_id, item in extensions:
|
||||
for item in extensions:
|
||||
item_id = item[CONF_ID].value
|
||||
if item_id in removals:
|
||||
continue
|
||||
old = index.get(item_id)
|
||||
if old is None:
|
||||
# Failed to find source for extension
|
||||
with cv.prepend_path(pos):
|
||||
# Find index of item to show error at correct position
|
||||
i = next(
|
||||
(
|
||||
i
|
||||
for i, d in enumerate(value)
|
||||
if d.get(CONF_ID) == item[CONF_ID]
|
||||
)
|
||||
)
|
||||
with cv.prepend_path(i):
|
||||
raise cv.Invalid(
|
||||
f"Source for extension of ID '{item_id}' was not found."
|
||||
)
|
||||
item[CONF_ID] = item_id
|
||||
index[item_id] = merge_config(old, item)
|
||||
for item_id in removals:
|
||||
index.pop(item_id, None)
|
||||
|
||||
@@ -74,6 +74,12 @@ void EntityBase::set_object_id(const char *object_id) {
|
||||
this->calc_object_id_();
|
||||
}
|
||||
|
||||
void EntityBase::set_name_and_object_id(const char *name, const char *object_id) {
|
||||
this->set_name(name);
|
||||
this->object_id_c_str_ = object_id;
|
||||
this->calc_object_id_();
|
||||
}
|
||||
|
||||
// Calculate Object ID Hash from Entity Name
|
||||
void EntityBase::calc_object_id_() {
|
||||
this->object_id_hash_ =
|
||||
|
||||
@@ -41,6 +41,9 @@ class EntityBase {
|
||||
std::string get_object_id() const;
|
||||
void set_object_id(const char *object_id);
|
||||
|
||||
// Set both name and object_id in one call (reduces generated code size)
|
||||
void set_name_and_object_id(const char *name, const char *object_id);
|
||||
|
||||
// Get the unique Object ID of this Entity
|
||||
uint32_t get_object_id_hash();
|
||||
|
||||
|
||||
@@ -84,8 +84,6 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
# Get device name for object ID calculation
|
||||
device_name = device_id_obj.id
|
||||
|
||||
add(var.set_name(config[CONF_NAME]))
|
||||
|
||||
# Calculate base object_id using the same logic as C++
|
||||
# This must match the C++ behavior in esphome/core/entity_base.cpp
|
||||
base_object_id = get_base_entity_object_id(
|
||||
@@ -97,8 +95,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
"Entity has empty name, using '%s' as object_id base", base_object_id
|
||||
)
|
||||
|
||||
# Set the object ID
|
||||
add(var.set_object_id(base_object_id))
|
||||
# Set both name and object_id in one call to reduce generated code size
|
||||
add(var.set_name_and_object_id(config[CONF_NAME], base_object_id))
|
||||
_LOGGER.debug(
|
||||
"Setting object_id '%s' for entity '%s' on platform '%s'",
|
||||
base_object_id,
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main):
|
||||
)
|
||||
|
||||
# Then
|
||||
assert 'bs_1->set_name("test bs1");' in main_cpp
|
||||
assert 'bs_1->set_name_and_object_id("test bs1", "test_bs1");' in main_cpp
|
||||
assert "bs_1->set_pin(" in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
||||
|
||||
# Then
|
||||
assert 'wol_1->set_name("wol_test_1");' in main_cpp
|
||||
assert 'wol_1->set_name_and_object_id("wol_test_1", "wol_test_1");' in main_cpp
|
||||
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
"""Tests for the web_server OTA platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.web_server.ota import _web_server_ota_final_validate
|
||||
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
|
||||
@@ -112,144 +100,3 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
|
||||
# Check web server OTA component is present
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
assert "web_server::WebServerOTAComponent" in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ota_configs", "expected_count", "warning_expected"),
|
||||
[
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web", is_manual=False),
|
||||
}
|
||||
],
|
||||
1,
|
||||
False,
|
||||
id="single_instance_no_merge",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_1", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_2", is_manual=False),
|
||||
},
|
||||
],
|
||||
1,
|
||||
True,
|
||||
id="two_instances_merged",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_1", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "esphome",
|
||||
CONF_ID: ID("ota_esphome", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_2", is_manual=False),
|
||||
},
|
||||
],
|
||||
2,
|
||||
True,
|
||||
id="mixed_platforms_web_server_merged",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_web_server_ota_instance_merging(
|
||||
ota_configs: list[dict[str, Any]],
|
||||
expected_count: int,
|
||||
warning_expected: bool,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test web_server OTA instance merging behavior."""
|
||||
full_conf = {CONF_OTA: ota_configs.copy()}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_web_server_ota_final_validate({})
|
||||
|
||||
updated_conf = fv.full_config.get()
|
||||
|
||||
# Verify total number of OTA platforms
|
||||
assert len(updated_conf[CONF_OTA]) == expected_count
|
||||
|
||||
# Verify warning
|
||||
if warning_expected:
|
||||
assert any(
|
||||
"Found and merged" in record.message
|
||||
and "web_server OTA" in record.message
|
||||
for record in caplog.records
|
||||
), "Expected merge warning not found in log"
|
||||
else:
|
||||
assert len(caplog.records) == 0, "Unexpected warnings logged"
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_web_server_ota_consistent_manual_ids(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that consistent manual IDs can be merged successfully."""
|
||||
ota_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web", is_manual=True),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web", is_manual=True),
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_OTA: ota_configs}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_web_server_ota_final_validate({})
|
||||
|
||||
updated_conf = fv.full_config.get()
|
||||
assert len(updated_conf[CONF_OTA]) == 1
|
||||
assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web"
|
||||
assert any(
|
||||
"Found and merged" in record.message and "web_server OTA" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_web_server_ota_inconsistent_manual_ids() -> None:
|
||||
"""Test that inconsistent manual IDs raise an error."""
|
||||
ota_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_1", is_manual=True),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_2", is_manual=True),
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_OTA: ota_configs}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match="Found multiple web_server OTA configurations but id is inconsistent",
|
||||
):
|
||||
_web_server_ota_final_validate({})
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Tests for SNTP component."""
|
||||
@@ -1,22 +0,0 @@
|
||||
esphome:
|
||||
name: sntp-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "testssid"
|
||||
password: "testpassword"
|
||||
|
||||
# Test multiple SNTP instances that should be merged
|
||||
time:
|
||||
- platform: sntp
|
||||
servers:
|
||||
- 192.168.1.1
|
||||
- pool.ntp.org
|
||||
- platform: sntp
|
||||
servers:
|
||||
- pool.ntp.org
|
||||
- 192.168.1.2
|
||||
@@ -1,238 +0,0 @@
|
||||
"""Tests for SNTP time configuration validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate
|
||||
from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("time_configs", "expected_count", "expected_servers", "warning_messages"),
|
||||
[
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
}
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org"],
|
||||
[],
|
||||
id="single_instance_no_merge",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.2"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
["Found and merged 2 SNTP time configurations into one instance"],
|
||||
id="two_instances_merged",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
["Found and merged 2 SNTP time configurations into one instance"],
|
||||
id="deduplication_preserves_order",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_3", is_manual=False),
|
||||
CONF_SERVERS: ["pool3.ntp.org"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
[
|
||||
"SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']",
|
||||
"Found and merged 3 SNTP time configurations into one instance",
|
||||
],
|
||||
id="three_instances_drops_excess_servers",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: [
|
||||
"192.168.1.1",
|
||||
"pool.ntp.org",
|
||||
"pool.ntp.org",
|
||||
"192.168.1.1",
|
||||
],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
["Found and merged 2 SNTP time configurations into one instance"],
|
||||
id="deduplication_multiple_duplicates",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_sntp_instance_merging(
|
||||
time_configs: list[dict[str, Any]],
|
||||
expected_count: int,
|
||||
expected_servers: list[str],
|
||||
warning_messages: list[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test SNTP instance merging behavior."""
|
||||
# Create a mock full config with time configs
|
||||
full_conf = {CONF_TIME: time_configs.copy()}
|
||||
|
||||
# Set the context var
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_sntp_final_validate({})
|
||||
|
||||
# Get the updated config
|
||||
updated_conf = fv.full_config.get()
|
||||
|
||||
# Check if merging occurred
|
||||
if len(time_configs) > 1:
|
||||
# Verify only one SNTP instance remains
|
||||
sntp_instances = [
|
||||
tc
|
||||
for tc in updated_conf[CONF_TIME]
|
||||
if tc.get(CONF_PLATFORM) == CONF_SNTP
|
||||
]
|
||||
assert len(sntp_instances) == expected_count
|
||||
|
||||
# Verify server list
|
||||
assert sntp_instances[0][CONF_SERVERS] == expected_servers
|
||||
|
||||
# Verify warnings
|
||||
for expected_msg in warning_messages:
|
||||
assert any(
|
||||
expected_msg in record.message for record in caplog.records
|
||||
), f"Expected warning message '{expected_msg}' not found in log"
|
||||
else:
|
||||
# Single instance should not trigger merging or warnings
|
||||
assert len(caplog.records) == 0
|
||||
# Config should be unchanged
|
||||
assert updated_conf[CONF_TIME] == time_configs
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_sntp_inconsistent_manual_ids() -> None:
|
||||
"""Test that inconsistent manual IDs raise an error."""
|
||||
# Create configs with manual IDs that are inconsistent
|
||||
time_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=True),
|
||||
CONF_SERVERS: ["192.168.1.1"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=True),
|
||||
CONF_SERVERS: ["192.168.1.2"],
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_TIME: time_configs}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match="Found multiple SNTP configurations but id is inconsistent",
|
||||
):
|
||||
_sntp_final_validate({})
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test that SNTP merging doesn't affect other time platforms."""
|
||||
time_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "homeassistant",
|
||||
CONF_ID: ID("homeassistant_time", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.2"],
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_TIME: time_configs.copy()}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_sntp_final_validate({})
|
||||
|
||||
updated_conf = fv.full_config.get()
|
||||
|
||||
# Should have 2 time platforms: 1 merged SNTP + 1 homeassistant
|
||||
assert len(updated_conf[CONF_TIME]) == 2
|
||||
|
||||
# Find the platforms
|
||||
platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]}
|
||||
assert platforms == {CONF_SNTP, "homeassistant"}
|
||||
|
||||
# Verify SNTP was merged
|
||||
sntp_instances = [
|
||||
tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP
|
||||
]
|
||||
assert len(sntp_instances) == 1
|
||||
assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"]
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
@@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert 'it_1->set_name("test 1 text");' in main_cpp
|
||||
assert 'it_1->set_name_and_object_id("test 1 text", "test_1_text");' in main_cpp
|
||||
|
||||
|
||||
def test_text_config_value_internal_set(generate_main):
|
||||
|
||||
@@ -25,9 +25,18 @@ def test_text_sensor_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp
|
||||
assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp
|
||||
assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp
|
||||
assert (
|
||||
'ts_1->set_name_and_object_id("Template Text Sensor 1", "template_text_sensor_1");'
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
'ts_2->set_name_and_object_id("Template Text Sensor 2", "template_text_sensor_2");'
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
'ts_3->set_name_and_object_id("Template Text Sensor 3", "template_text_sensor_3");'
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_text_sensor_config_value_internal_set(generate_main):
|
||||
|
||||
@@ -76,7 +76,7 @@ lvgl:
|
||||
line_width: 8
|
||||
line_rounded: true
|
||||
- id: date_style
|
||||
text_font: !lambda return id(roboto10);
|
||||
text_font: roboto10
|
||||
align: center
|
||||
text_color: !lambda return color_id2;
|
||||
bg_opa: cover
|
||||
@@ -267,7 +267,7 @@ lvgl:
|
||||
snprintf(buf, sizeof(buf), "Setup: %d", 42);
|
||||
return std::string(buf);
|
||||
align: top_mid
|
||||
text_font: !lambda return id(space16);
|
||||
text_font: space16
|
||||
- label:
|
||||
id: chip_info_label
|
||||
# Test complex setup lambda (real-world pattern)
|
||||
|
||||
@@ -18,7 +18,6 @@ touchscreen:
|
||||
|
||||
lvgl:
|
||||
- id: lvgl_0
|
||||
default_font: space16
|
||||
displays: sdl0
|
||||
- id: lvgl_1
|
||||
displays: sdl1
|
||||
@@ -40,8 +39,3 @@ lvgl:
|
||||
text: Click ME
|
||||
on_click:
|
||||
logger.log: Clicked
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: space16
|
||||
bpp: 4
|
||||
|
||||
@@ -27,8 +27,13 @@ from esphome.helpers import sanitize, snake_case
|
||||
|
||||
from .common import load_config_from_fixture
|
||||
|
||||
# Pre-compiled regex pattern for extracting object IDs from expressions
|
||||
# Pre-compiled regex patterns for extracting object IDs from expressions
|
||||
# Matches both old format: .set_object_id("obj_id")
|
||||
# and new format: .set_name_and_object_id("name", "obj_id")
|
||||
OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)')
|
||||
COMBINED_PATTERN = re.compile(
|
||||
r'\.set_name_and_object_id\(["\'].*?["\']\s*,\s*["\'](.*?)["\']\)'
|
||||
)
|
||||
|
||||
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers"
|
||||
|
||||
@@ -273,8 +278,10 @@ def setup_test_environment() -> Generator[list[str], None, None]:
|
||||
def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
|
||||
"""Extract the object ID that was set from the generated expressions."""
|
||||
for expr in expressions:
|
||||
# Look for set_object_id calls with regex to handle various formats
|
||||
# Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2')
|
||||
# First try new combined format: .set_name_and_object_id("name", "obj_id")
|
||||
if match := COMBINED_PATTERN.search(expr):
|
||||
return match.group(1)
|
||||
# Fall back to old format: .set_object_id("obj_id")
|
||||
if match := OBJECT_ID_PATTERN.search(expr):
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
@@ -7,27 +7,3 @@ some_component:
|
||||
value: 2
|
||||
- id: component2
|
||||
value: 5
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page1
|
||||
widgets:
|
||||
- obj:
|
||||
id: object1
|
||||
x: 3
|
||||
y: 2
|
||||
width: 4
|
||||
- obj:
|
||||
id: object3
|
||||
x: 6
|
||||
y: 12
|
||||
widgets:
|
||||
- obj:
|
||||
id: object4
|
||||
x: 14
|
||||
y: 9
|
||||
width: 15
|
||||
height: 13
|
||||
- obj:
|
||||
id: object5
|
||||
x: 10
|
||||
y: 11
|
||||
|
||||
@@ -13,30 +13,6 @@ packages:
|
||||
value: 5
|
||||
- id: component3
|
||||
value: 6
|
||||
- lvgl:
|
||||
pages:
|
||||
- id: page1
|
||||
widgets:
|
||||
- obj:
|
||||
id: object1
|
||||
x: 1
|
||||
y: 2
|
||||
- obj:
|
||||
id: object2
|
||||
x: 5
|
||||
- obj:
|
||||
id: object3
|
||||
x: 6
|
||||
y: 7
|
||||
widgets:
|
||||
- obj:
|
||||
id: object4
|
||||
x: 8
|
||||
y: 9
|
||||
- obj:
|
||||
id: object5
|
||||
x: 10
|
||||
y: 11
|
||||
|
||||
some_component:
|
||||
- id: !extend ${A}
|
||||
@@ -44,23 +20,3 @@ some_component:
|
||||
- id: component2
|
||||
value: 3
|
||||
- id: !remove ${C}
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: !extend page1
|
||||
widgets:
|
||||
- obj:
|
||||
id: !extend object1
|
||||
x: 3
|
||||
width: 4
|
||||
- obj:
|
||||
id: !remove object2
|
||||
- obj:
|
||||
id: !extend object3
|
||||
y: 12
|
||||
height: 13
|
||||
widgets:
|
||||
- obj:
|
||||
id: !extend object4
|
||||
x: 14
|
||||
width: 15
|
||||
|
||||
Reference in New Issue
Block a user