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:
commit
9593350596
@ -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:
|
||||
|
@ -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 = []
|
||||
|
@ -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]))),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
67
esphome/components/spi_led_strip/spi_led_strip.cpp
Normal file
67
esphome/components/spi_led_strip/spi_led_strip.cpp
Normal 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
|
@ -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};
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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])
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user