1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-01 08:18:16 +00:00

Merge branch 'dev' into component_climate_ir_samsung

This commit is contained in:
Georgi Filipov 2025-01-12 23:56:43 +02:00 committed by GitHub
commit 97352c9151
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 354 additions and 221 deletions

View File

@ -758,6 +758,14 @@ def parse_args(argv):
options_parser.add_argument( options_parser.add_argument(
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" "-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( options_parser.add_argument(
"--dashboard", help=argparse.SUPPRESS, action="store_true" "--dashboard", help=argparse.SUPPRESS, action="store_true"
) )
@ -987,11 +995,16 @@ def run_esphome(argv):
args = parse_args(argv) args = parse_args(argv)
CORE.dashboard = args.dashboard 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( setup_log(
args.verbose, log_level=args.log_level,
args.quiet,
# Show timestamp for dashboard access logs # Show timestamp for dashboard access logs
args.command == "dashboard", include_timestamp=args.command == "dashboard",
) )
if args.command in PRE_CONFIG_ACTIONS: if args.command in PRE_CONFIG_ACTIONS:

View File

@ -37,8 +37,9 @@ void ClimateIR::setup() {
this->publish_state(); this->publish_state();
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
} else } else {
this->current_temperature = NAN; this->current_temperature = NAN;
}
// restore set points // restore set points
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {

View File

@ -131,8 +131,9 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei
} else { } else {
parent->mode = climate::CLIMATE_MODE_FAN_ONLY; parent->mode = climate::CLIMATE_MODE_FAN_ONLY;
} }
} else } else {
parent->mode = climate::CLIMATE_MODE_COOL; parent->mode = climate::CLIMATE_MODE_COOL;
}
// Fan Speed // Fan Speed
if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL || if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL ||

View File

@ -118,8 +118,9 @@ std::unique_ptr<Command> CircularCommandQueue::dequeue() {
if (front_ == rear_) { if (front_ == rear_) {
front_ = -1; front_ = -1;
rear_ = -1; rear_ = -1;
} else } else {
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE; front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
}
return dequeued_cmd; return dequeued_cmd;
} }

View File

@ -157,8 +157,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
if (bit == 0) { if (bit == 0) {
bit = 7; bit = 7;
byte++; byte++;
} else } else {
bit--; bit--;
}
} }
} }
if (!report_errors && error_code != 0) if (!report_errors && error_code != 0)

View File

@ -39,6 +39,7 @@ DisplayOnPageChangeTrigger = display_ns.class_(
CONF_ON_PAGE_CHANGE = "on_page_change" CONF_ON_PAGE_CHANGE = "on_page_change"
CONF_SHOW_TEST_CARD = "show_test_card" CONF_SHOW_TEST_CARD = "show_test_card"
CONF_UNSPECIFIED = "unspecified"
DISPLAY_ROTATIONS = { DISPLAY_ROTATIONS = {
0: display_ns.DISPLAY_ROTATION_0_DEGREES, 0: display_ns.DISPLAY_ROTATION_0_DEGREES,
@ -55,16 +56,22 @@ def validate_rotation(value):
return cv.enum(DISPLAY_ROTATIONS, int=True)(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( 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")) ).extend(cv.polling_component_schema("1s"))
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{ {
cv.Optional(CONF_ROTATION): validate_rotation, cv.Optional(CONF_ROTATION): validate_rotation,
cv.Optional(CONF_PAGES): cv.All( cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list( cv.ensure_list(
{ {
cv.GenerateID(): cv.declare_id(DisplayPage), 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_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, cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
} }
) )
@ -92,8 +101,12 @@ async def setup_display_core_(var, config):
if CONF_ROTATION in config: if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if CONF_AUTO_CLEAR_ENABLED in config: if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED):
cg.add(var.set_auto_clear(config[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: if CONF_PAGES in config:
pages = [] pages = []

View File

@ -266,8 +266,9 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
if (dymax < float(-dxmax) * tan_a) { if (dymax < float(-dxmax) * tan_a) {
upd_dxmax = ceil(float(dymax) / tan_a); upd_dxmax = ceil(float(dymax) / tan_a);
hline_width = -dxmax - upd_dxmax + 1; hline_width = -dxmax - upd_dxmax + 1;
} else } else {
hline_width = 0; hline_width = 0;
}
} }
if (hline_width > 0) if (hline_width > 0)
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color); this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);

View File

@ -90,8 +90,9 @@ void Rect::info(const std::string &prefix) {
if (this->is_set()) { if (this->is_set()) {
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
this->y2()); this->y2());
} else } else {
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
}
} }
} // namespace display } // namespace display

View File

@ -112,7 +112,7 @@ void ESP32ImprovComponent::loop() {
this->set_state_(improv::STATE_AUTHORIZED); this->set_state_(improv::STATE_AUTHORIZED);
} else } else
#else #else
this->set_state_(improv::STATE_AUTHORIZED); { this->set_state_(improv::STATE_AUTHORIZED); }
#endif #endif
{ {
if (!this->check_identify_()) if (!this->check_identify_())

View File

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

View File

@ -97,8 +97,9 @@ void GCJA5Component::parse_data_() {
if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) { if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) {
ESP_LOGVV(TAG, "Discarding bad packet - failed checks."); ESP_LOGVV(TAG, "Discarding bad packet - failed checks.");
return; return;
} else } else {
ESP_LOGVV(TAG, "Good packet found."); ESP_LOGVV(TAG, "Good packet found.");
}
this->have_good_data_ = true; this->have_good_data_ = true;
uint8_t status = this->rx_message_[29]; uint8_t status = this->rx_message_[29];

View File

@ -342,8 +342,9 @@ bool HaierClimateBase::prepare_pending_action() {
this->action_request_.reset(); this->action_request_.reset();
return false; return false;
} }
} else } else {
return false; return false;
}
} }
ClimateTraits HaierClimateBase::traits() { return traits_; } ClimateTraits HaierClimateBase::traits() { return traits_; }

View File

@ -710,8 +710,9 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
alarm_code++; alarm_code++;
} }
active_alarms_[i] = packet[2 + i]; active_alarms_[i] = packet[2 + i];
} else } else {
alarm_code += 8; alarm_code += 8;
}
} }
} else { } else {
float alarm_count = 0.0f; float alarm_count = 0.0f;

View File

@ -87,8 +87,9 @@ void HeatpumpIRClimate::setup() {
this->publish_state(); this->publish_state();
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
} else } else {
this->current_temperature = NAN; this->current_temperature = NAN;
}
} }
void HeatpumpIRClimate::transmit_state() { void HeatpumpIRClimate::transmit_state() {

View File

@ -25,11 +25,13 @@ void I2SAudioMicrophone::setup() {
} }
} else } else
#endif #endif
if (this->pdm_) { {
if (this->parent_->get_port() != I2S_NUM_0) { if (this->pdm_) {
ESP_LOGE(TAG, "PDM only works on I2S0!"); if (this->parent_->get_port() != I2S_NUM_0) {
this->mark_failed(); ESP_LOGE(TAG, "PDM only works on I2S0!");
return; this->mark_failed();
return;
}
} }
} }
} }

View File

@ -197,11 +197,11 @@ def final_validation(configs):
for display_id in config[df.CONF_DISPLAYS]: for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1] path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path) 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( 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( raise cv.Invalid(
"Using auto_clear_enabled: true in display config not compatible with LVGL" "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.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT 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 esphome.cpp_types import nullptr
from .defines import ( from .defines import (
CONF_DISP_BG_COLOR, CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE, CONF_DISP_BG_IMAGE,
CONF_DISP_BG_OPA,
CONF_EDITING, CONF_EDITING,
CONF_FREEZE, CONF_FREEZE,
CONF_LVGL_ID, CONF_LVGL_ID,
CONF_SHOW_SNOW, CONF_SHOW_SNOW,
literal, 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 ( from .lvcode import (
LVGL_COMP_ARG, LVGL_COMP_ARG,
UPDATE_EVENT, UPDATE_EVENT,
LambdaContext, LambdaContext,
LocalVariable, LocalVariable,
LvglComponent,
ReturnStatement, ReturnStatement,
add_line_marks, add_line_marks,
lv, lv,
@ -92,7 +94,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID] lvgl = config[CONF_LVGL_ID]
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_paused())) 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) await cg.register_parented(var, lvgl)
return var 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) timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) 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) await cg.register_parented(var, lvgl)
return var return var
async def disp_update(disp, config: dict): 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 return
with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp: with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp:
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: 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)) lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE): 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( @automation.register_action(

View File

@ -215,7 +215,7 @@ LV_LONG_MODES = LvConstant(
) )
STATES = ( STATES = (
"default", # default state not included here
"checked", "checked",
"focused", "focused",
"focus_key", "focus_key",
@ -403,6 +403,7 @@ CONF_COLUMN = "column"
CONF_DIGITS = "digits" CONF_DIGITS = "digits"
CONF_DISP_BG_COLOR = "disp_bg_color" CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image" CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_DISP_BG_OPA = "disp_bg_opa"
CONF_BODY = "body" CONF_BODY = "body"
CONF_BUTTONS = "buttons" CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order" 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) { void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page); this->pages_.push_back(page);
page->set_parent(this);
page->setup(this->pages_.size() - 1); page->setup(this->pages_.size() - 1);
} }
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { 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() } while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time); 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) { void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area); auto width = lv_area_get_width(area);
auto height = lv_area_get_height(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) { 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()); 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 #endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG #ifdef USE_LVGL_ANIMIMG
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) { 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{}; lv_obj_t *obj{};
}; };
class LvPageType { class LvglComponent;
class LvPageType : public Parented<LvglComponent> {
public: public:
LvPageType(bool skip) : skip(skip) {} LvPageType(bool skip) : skip(skip) {}
@ -92,6 +104,9 @@ class LvPageType {
this->index = index; this->index = index;
this->obj = lv_obj_create(nullptr); this->obj = lv_obj_create(nullptr);
} }
bool is_showing() const;
lv_obj_t *obj{}; lv_obj_t *obj{};
size_t index{}; size_t index{};
bool skip; 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_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 show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } 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 set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
void restore_focus_mark(lv_group_t *group) { void restore_focus_mark(lv_group_t *group) {
auto *mark = this->focus_marks_[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_{}; 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: public:
LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda) LvglCondition(std::function<bool(Tc *)> &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {}
: condition_lambda_(std::move(condition_lambda)) {}
bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } bool check(Ts... x) override { return this->condition_lambda_(this->parent_); }
protected: protected:
std::function<bool(LvglComponent *)> condition_lambda_{}; std::function<bool(Tc *)> condition_lambda_{};
}; };
#ifdef USE_LVGL_TOUCHSCREEN #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 . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .helpers import add_lv_use, requires_component, validate_printf 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 .lvcode import LvglComponent, lv_event_t_ptr
from .types import ( from .types import (
LVEncoderListener, LVEncoderListener,
@ -344,8 +344,11 @@ FLEX_OBJ_SCHEMA = {
DISP_BG_SCHEMA = cv.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_COLOR): lv_color,
cv.Optional(df.CONF_DISP_BG_OPA): opacity,
} }
) )

View File

@ -27,7 +27,7 @@ from ..defines import (
CONF_START_VALUE, CONF_START_VALUE,
CONF_TICKS, CONF_TICKS,
) )
from ..helpers import add_lv_use from ..helpers import add_lv_use, lvgl_components_required
from ..lv_validation import ( from ..lv_validation import (
angle, angle,
get_end_value, get_end_value,
@ -182,6 +182,7 @@ class MeterType(WidgetType):
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
"""For a meter object, create and set parameters""" """For a meter object, create and set parameters"""
lvgl_components_required.add(CONF_METER)
var = w.obj var = w.obj
for scale_conf in config.get(CONF_SCALES, ()): for scale_conf in config.get(CONF_SCALES, ()):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 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 from esphome.automation import Trigger
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID
from esphome.cpp_generator import MockObj, TemplateArguments
from ..defines import ( from ..defines import (
CONF_ANIMATION, CONF_ANIMATION,
@ -17,18 +18,28 @@ from ..lvcode import (
EVENT_ARG, EVENT_ARG,
LVGL_COMP_ARG, LVGL_COMP_ARG,
LambdaContext, LambdaContext,
ReturnStatement,
add_line_marks, add_line_marks,
lv_add, lv_add,
lvgl_comp, lvgl_comp,
lvgl_static, lvgl_static,
) )
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LvglAction, lv_page_t from ..types import LvglAction, LvglCondition, lv_page_t
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from . import (
Widget,
WidgetType,
add_widgets,
get_widgets,
set_obj_properties,
wait_for_widgets,
)
CONF_ON_LOAD = "on_load" CONF_ON_LOAD = "on_load"
CONF_ON_UNLOAD = "on_unload" CONF_ON_UNLOAD = "on_unload"
PAGE_ARG = "_page"
PAGE_SCHEMA = cv.Schema( PAGE_SCHEMA = cv.Schema(
{ {
cv.Optional(CONF_SKIP, default=False): lv_bool, 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 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( @automation.register_action(
"lvgl.page.previous", "lvgl.page.previous",
LvglAction, LvglAction,

View File

@ -11,15 +11,17 @@ void MicroNovaSwitch::write_state(bool state) {
if (this->micronova_->get_current_stove_state() == 0) { if (this->micronova_->get_current_stove_state() == 0) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_); this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
this->publish_state(true); this->publish_state(true);
} else } else {
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state()); ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state());
}
} else { } else {
// don't send power-off when status is Off or Final cleaning // don't send power-off when status is Off or Final cleaning
if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) { if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_); this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
this->publish_state(false); this->publish_state(false);
} else } else {
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state()); ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state());
}
} }
this->micronova_->update(); this->micronova_->update();
break; break;

View File

@ -1,8 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import light, spi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import light from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID
from esphome.components import spi
from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS
spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip") spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip")
SpiLedStrip = spi_led_strip_ns.class_( SpiLedStrip = spi_led_strip_ns.class_(
@ -18,8 +17,7 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS])
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
await light.register_light(var, config) await light.register_light(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
await cg.register_component(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, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_1MHZ> { spi::DATA_RATE_1MHZ> {
public: 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_; } int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override { 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;
}
this->effect_data_ = allocator.allocate(num_leds); void dump_config() override;
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 write_state(light::LightState *state) 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 clear_effect_data() override { memset(this->effect_data_, 0, this->num_leds_ * sizeof(this->effect_data_[0])); }
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;
}
protected: protected:
light::ESPColorView get_view_internal(int32_t index) const override { 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_};
}
size_t buffer_size_{}; size_t buffer_size_{};
uint8_t *effect_data_{nullptr}; uint8_t *effect_data_{nullptr};

View File

@ -184,11 +184,13 @@ void SprinklerValveOperator::set_controller(Sprinkler *controller) {
void SprinklerValveOperator::set_valve(SprinklerValve *valve) { void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
if (valve != nullptr) { if (valve != nullptr) {
if (this->state_ != IDLE) { // Only kill if not already idle
this->kill_(); // ensure everything is off before we let go!
}
this->state_ = IDLE; // reset state this->state_ = IDLE; // reset state
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
this->start_millis_ = 0; // reset because (new) valve has not been started yet this->start_millis_ = 0; // reset because (new) valve has not been started yet
this->stop_millis_ = 0; // reset because (new) valve has not been started yet this->stop_millis_ = 0; // reset because (new) valve has not been started yet
this->kill_(); // ensure everything is off before we let go!
this->valve_ = valve; // finally, set the pointer to the new valve this->valve_ = valve; // finally, set the pointer to the new valve
} }
} }

View File

@ -106,8 +106,9 @@ void ToshibaClimate::setup() {
this->publish_state(); this->publish_state();
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
} else } else {
this->current_temperature = NAN; this->current_temperature = NAN;
}
// restore set points // restore set points
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {

View File

@ -120,8 +120,9 @@ light::LightTraits TuyaLight::get_traits() {
traits.set_supported_color_modes( traits.set_supported_color_modes(
{light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); {light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE});
} }
} else } else {
traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE});
}
traits.set_min_mireds(this->cold_white_temperature_); traits.set_min_mireds(this->cold_white_temperature_);
traits.set_max_mireds(this->warm_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_);
} else if (this->color_id_.has_value()) { } else if (this->color_id_.has_value()) {
@ -131,8 +132,9 @@ light::LightTraits TuyaLight::get_traits() {
} else { } else {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); traits.set_supported_color_modes({light::ColorMode::RGB_WHITE});
} }
} else } else {
traits.set_supported_color_modes({light::ColorMode::RGB}); traits.set_supported_color_modes({light::ColorMode::RGB});
}
} else if (this->dimmer_id_.has_value()) { } else if (this->dimmer_id_.has_value()) {
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
} else { } else {

View File

@ -85,7 +85,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(UDPComponent), cv.GenerateID(): cv.declare_id(UDPComponent),
cv.Optional(CONF_PORT, default=18511): cv.port, cv.Optional(CONF_PORT, default=18511): cv.port,
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( 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_ROLLING_CODE_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean, cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean,

View File

@ -245,13 +245,9 @@ void UDPComponent::setup() {
} }
struct sockaddr_in server {}; struct sockaddr_in server {};
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); server.sin_family = AF_INET;
if (sl == 0) { server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno); server.sin_port = htons(this->port_);
this->mark_failed();
this->status_set_error("Unable to set sockaddr");
return;
}
err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) { if (err != 0) {

View File

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

View File

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

View File

@ -104,8 +104,9 @@ void YashimaClimate::setup() {
this->publish_state(); this->publish_state();
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
} else } else {
this->current_temperature = NAN; this->current_temperature = NAN;
}
// restore set points // restore set points
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {

View File

@ -18,6 +18,7 @@ from esphome.const import (
CONF_ESPHOME, CONF_ESPHOME,
CONF_EXTERNAL_COMPONENTS, CONF_EXTERNAL_COMPONENTS,
CONF_ID, CONF_ID,
CONF_MIN_VERSION,
CONF_PACKAGES, CONF_PACKAGES,
CONF_PLATFORM, CONF_PLATFORM,
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
@ -839,6 +840,10 @@ def validate_config(
# Remove temporary esphome config path again, it will be reloaded later # Remove temporary esphome config path again, it will be reloaded later
result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME) 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 # First run platform validation steps
for key in TARGET_PLATFORMS: for key in TARGET_PLATFORMS:
if key in config: if key in config:

View File

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

View File

@ -54,16 +54,6 @@ class HexInt(int):
return f"{sign}0x{value:X}" 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: class MACAddress:
def __init__(self, *parts): def __init__(self, *parts):
if len(parts) != 6: if len(parts) != 6:

View File

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

View File

@ -126,19 +126,21 @@ uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse
} }
} else } else
#endif #endif
if (reverse_poly == 0xa001) { {
while (len--) { if (reverse_poly == 0xa001) {
uint8_t combo = crc ^ (uint8_t) *data++; while (len--) {
crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4]; uint8_t combo = crc ^ (uint8_t) *data++;
} crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4];
} else { }
while (len--) { } else {
crc ^= *data++; while (len--) {
for (uint8_t i = 0; i < 8; i++) { crc ^= *data++;
if (crc & 0x0001) { for (uint8_t i = 0; i < 8; i++) {
crc = (crc >> 1) ^ reverse_poly; if (crc & 0x0001) {
} else { crc = (crc >> 1) ^ reverse_poly;
crc >>= 1; } else {
crc >>= 1;
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import pytest
import string import string
from hypothesis import given, example from hypothesis import example, given
from hypothesis.strategies import one_of, text, integers, builds from hypothesis.strategies import builds, integers, ip_addresses, one_of, text
import pytest
from esphome import config_validation from esphome import config_validation
from esphome.config_validation import Invalid 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(): def test_check_not_templatable__invalid():
@ -145,6 +145,28 @@ def test_boolean__invalid(value):
config_validation.boolean(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 # TODO: ensure_list
@given(integers()) @given(integers())
def hex_int__valid(value): def hex_int__valid(value):

View File

@ -1,10 +1,8 @@
import pytest
from hypothesis import given from hypothesis import given
from hypothesis.strategies import ip_addresses import pytest
from strategies import mac_addr_strings from strategies import mac_addr_strings
from esphome import core, const from esphome import const, core
class TestHexInt: class TestHexInt:
@ -26,25 +24,6 @@ class TestHexInt:
assert actual == expected 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: class TestMACAddress:
@given(value=mac_addr_strings()) @given(value=mac_addr_strings())
def test_init__valid(self, value): def test_init__valid(self, value):