1
0
mirror of https://github.com/esphome/esphome.git synced 2025-02-07 05:30:54 +00:00

Merge branch 'dev' into platform

This commit is contained in:
tomaszduda23 2025-01-12 23:27:59 +01:00 committed by GitHub
commit 9593350596
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 293 additions and 178 deletions

View File

@ -758,6 +758,14 @@ def parse_args(argv):
options_parser.add_argument(
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
)
options_parser.add_argument(
"-l",
"--log-level",
help="Set the log level.",
default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"),
action="store",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
)
options_parser.add_argument(
"--dashboard", help=argparse.SUPPRESS, action="store_true"
)
@ -987,11 +995,16 @@ def run_esphome(argv):
args = parse_args(argv)
CORE.dashboard = args.dashboard
# Override log level if verbose is set
if args.verbose:
args.log_level = "DEBUG"
elif args.quiet:
args.log_level = "CRITICAL"
setup_log(
args.verbose,
args.quiet,
log_level=args.log_level,
# Show timestamp for dashboard access logs
args.command == "dashboard",
include_timestamp=args.command == "dashboard",
)
if args.command in PRE_CONFIG_ACTIONS:

View File

@ -39,6 +39,7 @@ DisplayOnPageChangeTrigger = display_ns.class_(
CONF_ON_PAGE_CHANGE = "on_page_change"
CONF_SHOW_TEST_CARD = "show_test_card"
CONF_UNSPECIFIED = "unspecified"
DISPLAY_ROTATIONS = {
0: display_ns.DISPLAY_ROTATION_0_DEGREES,
@ -55,16 +56,22 @@ def validate_rotation(value):
return cv.enum(DISPLAY_ROTATIONS, int=True)(value)
def validate_auto_clear(value):
if value == CONF_UNSPECIFIED:
return value
return cv.boolean(value)
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Optional(CONF_PAGES): cv.All(
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
@ -82,7 +89,9 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean,
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
@ -92,8 +101,12 @@ async def setup_display_core_(var, config):
if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if CONF_AUTO_CLEAR_ENABLED in config:
cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED]))
if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED):
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
# the possible schemas are too complex to do this easily.
if auto_clear == CONF_UNSPECIFIED:
auto_clear = CONF_LAMBDA in config or CONF_PAGES in config
cg.add(var.set_auto_clear(auto_clear))
if CONF_PAGES in config:
pages = []

View File

@ -94,11 +94,11 @@ CLK_MODES = {
MANUAL_IP_SCHEMA = cv.Schema(
{
cv.Required(CONF_STATIC_IP): cv.ipv4,
cv.Required(CONF_GATEWAY): cv.ipv4,
cv.Required(CONF_SUBNET): cv.ipv4,
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
cv.Required(CONF_STATIC_IP): cv.ipv4address,
cv.Required(CONF_GATEWAY): cv.ipv4address,
cv.Required(CONF_SUBNET): cv.ipv4address,
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address,
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address,
}
)
@ -255,11 +255,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate
def manual_ip(config):
return cg.StructInitializer(
ManualIP,
("static_ip", IPAddress(*config[CONF_STATIC_IP].args)),
("gateway", IPAddress(*config[CONF_GATEWAY].args)),
("subnet", IPAddress(*config[CONF_SUBNET].args)),
("dns1", IPAddress(*config[CONF_DNS1].args)),
("dns2", IPAddress(*config[CONF_DNS2].args)),
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
("subnet", IPAddress(str(config[CONF_SUBNET]))),
("dns1", IPAddress(str(config[CONF_DNS1]))),
("dns2", IPAddress(str(config[CONF_DNS2]))),
)

View File

@ -197,11 +197,11 @@ def final_validation(configs):
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path)
if CONF_LAMBDA in display:
if CONF_LAMBDA in display or CONF_PAGES in display:
raise cv.Invalid(
"Using lambda: in display config not compatible with LVGL"
"Using lambda: or pages: in display config is not compatible with LVGL"
)
if display[CONF_AUTO_CLEAR_ENABLED]:
if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
raise cv.Invalid(
"Using auto_clear_enabled: true in display config not compatible with LVGL"
)

View File

@ -4,24 +4,26 @@ from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
from esphome.cpp_generator import get_variable
from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_types import nullptr
from .defines import (
CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE,
CONF_DISP_BG_OPA,
CONF_EDITING,
CONF_FREEZE,
CONF_LVGL_ID,
CONF_SHOW_SNOW,
literal,
)
from .lv_validation import lv_bool, lv_color, lv_image
from .lv_validation import lv_bool, lv_color, lv_image, opacity
from .lvcode import (
LVGL_COMP_ARG,
UPDATE_EVENT,
LambdaContext,
LocalVariable,
LvglComponent,
ReturnStatement,
add_line_marks,
lv,
@ -92,7 +94,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID]
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_paused()))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
var = cg.new_Pvariable(
condition_id,
TemplateArguments(LvglComponent, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, lvgl)
return var
@ -113,19 +119,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
var = cg.new_Pvariable(
condition_id,
TemplateArguments(LvglComponent, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, lvgl)
return var
async def disp_update(disp, config: dict):
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
if (
CONF_DISP_BG_COLOR not in config
and CONF_DISP_BG_IMAGE not in config
and CONF_DISP_BG_OPA not in config
):
return
with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp:
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE):
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
if bg_image == "none":
lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr"))
else:
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None:
lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa))
@automation.register_action(

View File

@ -215,7 +215,7 @@ LV_LONG_MODES = LvConstant(
)
STATES = (
"default",
# default state not included here
"checked",
"focused",
"focus_key",
@ -403,6 +403,7 @@ CONF_COLUMN = "column"
CONF_DIGITS = "digits"
CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_DISP_BG_OPA = "disp_bg_opa"
CONF_BODY = "body"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"

View File

@ -119,6 +119,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev
}
void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page);
page->set_parent(this);
page->setup(this->pages_.size() - 1);
}
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
@ -143,6 +144,8 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area);

View File

@ -59,6 +59,16 @@ inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) {
lv_disp_set_bg_image(disp, image->get_lv_img_dsc());
}
inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) {
lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector);
}
#ifdef USE_LVGL_METER
inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src,
lv_coord_t pivot_x, lv_coord_t pivot_y) {
return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y);
}
#endif // USE_LVGL_METER
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
@ -84,7 +94,9 @@ class LvCompound {
lv_obj_t *obj{};
};
class LvPageType {
class LvglComponent;
class LvPageType : public Parented<LvglComponent> {
public:
LvPageType(bool skip) : skip(skip) {}
@ -92,6 +104,9 @@ class LvPageType {
this->index = index;
this->obj = lv_obj_create(nullptr);
}
bool is_showing() const;
lv_obj_t *obj{};
size_t index{};
bool skip;
@ -178,6 +193,7 @@ class LvglComponent : public PollingComponent {
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
size_t get_current_page() const;
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
void restore_focus_mark(lv_group_t *group) {
auto *mark = this->focus_marks_[group];
@ -241,14 +257,13 @@ template<typename... Ts> class LvglAction : public Action<Ts...>, public Parente
std::function<void(LvglComponent *)> action_{};
};
template<typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<LvglComponent> {
template<typename Tc, typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<Tc> {
public:
LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda)
: condition_lambda_(std::move(condition_lambda)) {}
LvglCondition(std::function<bool(Tc *)> &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {}
bool check(Ts... x) override { return this->condition_lambda_(this->parent_); }
protected:
std::function<bool(LvglComponent *)> condition_lambda_{};
std::function<bool(Tc *)> condition_lambda_{};
};
#ifdef USE_LVGL_TOUCHSCREEN

View File

@ -19,7 +19,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
from .lvcode import LvglComponent, lv_event_t_ptr
from .types import (
LVEncoderListener,
@ -344,8 +344,11 @@ FLEX_OBJ_SCHEMA = {
DISP_BG_SCHEMA = cv.Schema(
{
cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
cv.one_of("none", lower=True), lv_image
),
cv.Optional(df.CONF_DISP_BG_COLOR): lv_color,
cv.Optional(df.CONF_DISP_BG_OPA): opacity,
}
)

View File

@ -27,7 +27,7 @@ from ..defines import (
CONF_START_VALUE,
CONF_TICKS,
)
from ..helpers import add_lv_use
from ..helpers import add_lv_use, lvgl_components_required
from ..lv_validation import (
angle,
get_end_value,
@ -182,6 +182,7 @@ class MeterType(WidgetType):
async def to_code(self, w: Widget, config):
"""For a meter object, create and set parameters"""
lvgl_components_required.add(CONF_METER)
var = w.obj
for scale_conf in config.get(CONF_SCALES, ()):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2

View File

@ -2,6 +2,7 @@ from esphome import automation, codegen as cg
from esphome.automation import Trigger
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID
from esphome.cpp_generator import MockObj, TemplateArguments
from ..defines import (
CONF_ANIMATION,
@ -17,18 +18,28 @@ from ..lvcode import (
EVENT_ARG,
LVGL_COMP_ARG,
LambdaContext,
ReturnStatement,
add_line_marks,
lv_add,
lvgl_comp,
lvgl_static,
)
from ..schemas import LVGL_SCHEMA
from ..types import LvglAction, lv_page_t
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
from ..types import LvglAction, LvglCondition, lv_page_t
from . import (
Widget,
WidgetType,
add_widgets,
get_widgets,
set_obj_properties,
wait_for_widgets,
)
CONF_ON_LOAD = "on_load"
CONF_ON_UNLOAD = "on_unload"
PAGE_ARG = "_page"
PAGE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SKIP, default=False): lv_bool,
@ -86,6 +97,30 @@ async def page_next_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
"lvgl.page.is_showing",
LvglCondition,
cv.maybe_simple_value(
cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_page_t)}),
key=CONF_ID,
),
)
async def page_is_showing_to_code(config, condition_id, template_arg, args):
await wait_for_widgets()
page = await cg.get_variable(config[CONF_ID])
async with LambdaContext(
[(lv_page_t.operator("ptr"), PAGE_ARG)], return_type=cg.bool_
) as context:
lv_add(ReturnStatement(MockObj(PAGE_ARG, "->").is_showing()))
var = cg.new_Pvariable(
condition_id,
TemplateArguments(lv_page_t, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, page)
return var
@automation.register_action(
"lvgl.page.previous",
LvglAction,

View File

@ -1,8 +1,7 @@
import esphome.codegen as cg
from esphome.components import light, spi
import esphome.config_validation as cv
from esphome.components import light
from esphome.components import spi
from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS
from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID
spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip")
SpiLedStrip = spi_led_strip_ns.class_(
@ -18,8 +17,7 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS])
await light.register_light(var, config)
await spi.register_spi_device(var, config)
await cg.register_component(var, config)

View File

@ -0,0 +1,67 @@
#include "spi_led_strip.h"
namespace esphome {
namespace spi_led_strip {
SpiLedStrip::SpiLedStrip(uint16_t num_leds) {
this->num_leds_ = num_leds;
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buffer_size_ = num_leds * 4 + 8;
this->buf_ = allocator.allocate(this->buffer_size_);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", this->buffer_size_);
return;
}
this->effect_data_ = allocator.allocate(num_leds);
if (this->effect_data_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", num_leds);
return;
}
memset(this->buf_, 0xFF, this->buffer_size_);
memset(this->buf_, 0, 4);
}
void SpiLedStrip::setup() {
if (this->effect_data_ == nullptr || this->buf_ == nullptr) {
this->mark_failed();
return;
}
this->spi_setup();
}
light::LightTraits SpiLedStrip::get_traits() {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
}
void SpiLedStrip::dump_config() {
esph_log_config(TAG, "SPI LED Strip:");
esph_log_config(TAG, " LEDs: %d", this->num_leds_);
if (this->data_rate_ >= spi::DATA_RATE_1MHZ) {
esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
} else {
esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000));
}
}
void SpiLedStrip::write_state(light::LightState *state) {
if (this->is_failed())
return;
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
char strbuf[49];
size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3);
memset(strbuf, 0, sizeof(strbuf));
for (size_t i = 0; i != len; i++) {
sprintf(strbuf + i * 3, "%02X ", this->buf_[i]);
}
esph_log_v(TAG, "write_state: buf = %s", strbuf);
}
this->enable();
this->write_array(this->buf_, this->buffer_size_);
this->disable();
}
light::ESPColorView SpiLedStrip::get_view_internal(int32_t index) const {
size_t pos = index * 4 + 5;
return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr,
this->effect_data_ + index, &this->correction_};
}
} // namespace spi_led_strip
} // namespace esphome

View File

@ -13,74 +13,22 @@ class SpiLedStrip : public light::AddressableLight,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_1MHZ> {
public:
void setup() override { this->spi_setup(); }
SpiLedStrip(uint16_t num_leds);
void setup() override;
float get_setup_priority() const override { return setup_priority::IO; }
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
}
void set_num_leds(uint16_t num_leds) {
this->num_leds_ = num_leds;
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buffer_size_ = num_leds * 4 + 8;
this->buf_ = allocator.allocate(this->buffer_size_);
if (this->buf_ == nullptr) {
esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_);
this->mark_failed();
return;
}
light::LightTraits get_traits() override;
this->effect_data_ = allocator.allocate(num_leds);
if (this->effect_data_ == nullptr) {
esph_log_e(TAG, "Failed to allocate effect data of size %u", num_leds);
this->mark_failed();
return;
}
memset(this->buf_, 0xFF, this->buffer_size_);
memset(this->buf_, 0, 4);
}
void dump_config() override;
void dump_config() override {
esph_log_config(TAG, "SPI LED Strip:");
esph_log_config(TAG, " LEDs: %d", this->num_leds_);
if (this->data_rate_ >= spi::DATA_RATE_1MHZ) {
esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
} else {
esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000));
}
}
void write_state(light::LightState *state) override;
void write_state(light::LightState *state) override {
if (this->is_failed())
return;
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
char strbuf[49];
size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3);
memset(strbuf, 0, sizeof(strbuf));
for (size_t i = 0; i != len; i++) {
sprintf(strbuf + i * 3, "%02X ", this->buf_[i]);
}
esph_log_v(TAG, "write_state: buf = %s", strbuf);
}
this->enable();
this->write_array(this->buf_, this->buffer_size_);
this->disable();
}
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
}
void clear_effect_data() override { memset(this->effect_data_, 0, this->num_leds_ * sizeof(this->effect_data_[0])); }
protected:
light::ESPColorView get_view_internal(int32_t index) const override {
size_t pos = index * 4 + 5;
return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr,
this->effect_data_ + index, &this->correction_};
}
light::ESPColorView get_view_internal(int32_t index) const override;
size_t buffer_size_{};
uint8_t *effect_data_{nullptr};

View File

@ -85,7 +85,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(UDPComponent),
cv.Optional(CONF_PORT, default=18511): cv.port,
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list(
cv.ipv4
cv.ipv4address,
),
cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean,

View File

@ -93,16 +93,16 @@ def validate_channel(value):
AP_MANUAL_IP_SCHEMA = cv.Schema(
{
cv.Required(CONF_STATIC_IP): cv.ipv4,
cv.Required(CONF_GATEWAY): cv.ipv4,
cv.Required(CONF_SUBNET): cv.ipv4,
cv.Required(CONF_STATIC_IP): cv.ipv4address,
cv.Required(CONF_GATEWAY): cv.ipv4address,
cv.Required(CONF_SUBNET): cv.ipv4address,
}
)
STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend(
{
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address,
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address,
}
)
@ -364,7 +364,7 @@ def eap_auth(config):
def safe_ip(ip):
if ip is None:
return IPAddress(0, 0, 0, 0)
return IPAddress(*ip.args)
return IPAddress(str(ip))
def manual_ip(config):

View File

@ -67,8 +67,8 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(Wireguard),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Required(CONF_ADDRESS): cv.ipv4,
cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4,
cv.Required(CONF_ADDRESS): cv.ipv4address,
cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address,
cv.Required(CONF_PRIVATE_KEY): _wireguard_key,
cv.Required(CONF_PEER_ENDPOINT): cv.string,
cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key,

View File

@ -18,6 +18,7 @@ from esphome.const import (
CONF_ESPHOME,
CONF_EXTERNAL_COMPONENTS,
CONF_ID,
CONF_MIN_VERSION,
CONF_PACKAGES,
CONF_PLATFORM,
CONF_SUBSTITUTIONS,
@ -838,6 +839,10 @@ def validate_config(
# Remove temporary esphome config path again, it will be reloaded later
result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME)
# Check version number now to avoid loading components that are not supported
if min_version := config[CONF_ESPHOME].get(CONF_MIN_VERSION):
cv.All(cv.version_number, cv.validate_esphome_version)(min_version)
# First run platform validation steps
result.add_validation_step(
LoadValidationStep(target_platform, config[target_platform])

View File

@ -3,6 +3,7 @@
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime
from ipaddress import AddressValueError, IPv4Address, ip_address
import logging
import os
import re
@ -67,7 +68,6 @@ from esphome.const import (
from esphome.core import (
CORE,
HexInt,
IPAddress,
Lambda,
TimePeriod,
TimePeriodMicroseconds,
@ -1130,7 +1130,7 @@ def domain(value):
if re.match(vol.DOMAIN_REGEX, value) is not None:
return value
try:
return str(ipv4(value))
return str(ipaddress(value))
except Invalid as err:
raise Invalid(f"Invalid domain: {value}") from err
@ -1160,21 +1160,20 @@ def ssid(value):
return value
def ipv4(value):
if isinstance(value, list):
parts = value
elif isinstance(value, str):
parts = value.split(".")
elif isinstance(value, IPAddress):
return value
else:
raise Invalid("IPv4 address must consist of either string or integer list")
if len(parts) != 4:
raise Invalid("IPv4 address must consist of four point-separated integers")
parts_ = list(map(int, parts))
if not all(0 <= x < 256 for x in parts_):
raise Invalid("IPv4 address parts must be in range from 0 to 255")
return IPAddress(*parts_)
def ipv4address(value):
try:
address = IPv4Address(value)
except AddressValueError as exc:
raise Invalid(f"{value} is not a valid IPv4 address") from exc
return address
def ipaddress(value):
try:
address = ip_address(value)
except ValueError as exc:
raise Invalid(f"{value} is not a valid IP address") from exc
return address
def _valid_topic(value):

View File

@ -54,16 +54,6 @@ class HexInt(int):
return f"{sign}0x{value:X}"
class IPAddress:
def __init__(self, *args):
if len(args) != 4:
raise ValueError("IPAddress must consist of 4 items")
self.args = args
def __str__(self):
return ".".join(str(x) for x in self.args)
class MACAddress:
def __init__(self, *parts):
if len(parts) != 6:

View File

@ -49,6 +49,7 @@
#define USE_LVGL_IMAGE
#define USE_LVGL_KEY_LISTENER
#define USE_LVGL_KEYBOARD
#define USE_LVGL_METER
#define USE_LVGL_ROLLER
#define USE_LVGL_ROTARY_ENCODER
#define USE_LVGL_TOUCHSCREEN

View File

@ -67,20 +67,18 @@ class ESPHomeLogFormatter(logging.Formatter):
def setup_log(
debug: bool = False, quiet: bool = False, include_timestamp: bool = False
log_level=logging.INFO,
include_timestamp: bool = False,
) -> None:
import colorama
colorama.init()
if debug:
log_level = logging.DEBUG
if log_level == logging.DEBUG:
CORE.verbose = True
elif quiet:
log_level = logging.CRITICAL
elif log_level == logging.CRITICAL:
CORE.quiet = True
else:
log_level = logging.INFO
logging.basicConfig(level=log_level)
logging.getLogger("urllib3").setLevel(logging.WARNING)

View File

@ -4,6 +4,7 @@ import fnmatch
import functools
import inspect
from io import TextIOWrapper
from ipaddress import _BaseAddress
import logging
import math
import os
@ -25,7 +26,6 @@ from esphome.core import (
CORE,
DocumentRange,
EsphomeError,
IPAddress,
Lambda,
MACAddress,
TimePeriod,
@ -576,7 +576,7 @@ ESPHomeDumper.add_multi_representer(bool, ESPHomeDumper.represent_bool)
ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int)
ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float)
ESPHomeDumper.add_multi_representer(IPAddress, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda)

View File

@ -27,6 +27,7 @@ lvgl:
bg_color: light_blue
disp_bg_color: color_id
disp_bg_image: cat_image
disp_bg_opa: cover
theme:
obj:
border_width: 1
@ -132,10 +133,16 @@ lvgl:
pages:
- id: page1
bg_image_src: cat_image
on_load:
- logger.log: page loaded
- lvgl.widget.focus:
action: restore
- if:
condition:
lvgl.page.is_showing: page1
then:
logger.log: "Yes, page1 showing"
on_unload:
- logger.log: page unloaded
- lvgl.widget.focus: mark
@ -206,7 +213,7 @@ lvgl:
- lvgl.animimg.stop: anim_img
- lvgl.update:
disp_bg_color: 0xffff00
disp_bg_image: cat_image
disp_bg_image: none
- lvgl.widget.show: message_box
- label:
text: "Hello shiny day"

View File

@ -7,7 +7,6 @@ display:
height: 320
- platform: sdl
id: sdl1
auto_clear_enabled: false
dimensions:
width: 480
height: 480
@ -40,4 +39,3 @@ lvgl:
text: Click ME
on_click:
logger.log: Clicked

View File

@ -1,12 +1,12 @@
import pytest
import string
from hypothesis import given, example
from hypothesis.strategies import one_of, text, integers, builds
from hypothesis import example, given
from hypothesis.strategies import builds, integers, ip_addresses, one_of, text
import pytest
from esphome import config_validation
from esphome.config_validation import Invalid
from esphome.core import CORE, Lambda, HexInt
from esphome.core import CORE, HexInt, Lambda
def test_check_not_templatable__invalid():
@ -145,6 +145,28 @@ def test_boolean__invalid(value):
config_validation.boolean(value)
@given(value=ip_addresses(v=4).map(str))
def test_ipv4__valid(value):
config_validation.ipv4address(value)
@pytest.mark.parametrize("value", ("127.0.0", "localhost", ""))
def test_ipv4__invalid(value):
with pytest.raises(Invalid, match="is not a valid IPv4 address"):
config_validation.ipv4address(value)
@given(value=ip_addresses(v=6).map(str))
def test_ipv6__valid(value):
config_validation.ipaddress(value)
@pytest.mark.parametrize("value", ("127.0.0", "localhost", "", "2001:db8::2::3"))
def test_ipv6__invalid(value):
with pytest.raises(Invalid, match="is not a valid IP address"):
config_validation.ipaddress(value)
# TODO: ensure_list
@given(integers())
def hex_int__valid(value):

View File

@ -1,10 +1,8 @@
import pytest
from hypothesis import given
from hypothesis.strategies import ip_addresses
import pytest
from strategies import mac_addr_strings
from esphome import core, const
from esphome import const, core
class TestHexInt:
@ -26,25 +24,6 @@ class TestHexInt:
assert actual == expected
class TestIPAddress:
@given(value=ip_addresses(v=4).map(str))
def test_init__valid(self, value):
core.IPAddress(*value.split("."))
@pytest.mark.parametrize("value", ("127.0.0", "localhost", ""))
def test_init__invalid(self, value):
with pytest.raises(ValueError, match="IPAddress must consist of 4 items"):
core.IPAddress(*value.split("."))
@given(value=ip_addresses(v=4).map(str))
def test_str(self, value):
target = core.IPAddress(*value.split("."))
actual = str(target)
assert actual == value
class TestMACAddress:
@given(value=mac_addr_strings())
def test_init__valid(self, value):