1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 15:41:52 +00:00

Compare commits

...

41 Commits

Author SHA1 Message Date
Jesse Hills
7bb899bfa1 Merge pull request #8746 from esphome/bump-2025.4.2
2025.4.2
2025-05-12 10:18:35 +12:00
Jesse Hills
cae3c030d2 Bump version to 2025.4.2 2025-05-12 08:52:13 +12:00
Clyde Stubbs
d7c615ec43 [lvgl] Fix image property processing (#8691) 2025-05-12 08:52:13 +12:00
Clyde Stubbs
1294e8ccd5 [lvgl] Allow padding to be negative (#8671) 2025-05-12 08:52:13 +12:00
Clyde Stubbs
37a2cb07d1 [as3935_i2c] Remove redundant includes (#8677) 2025-05-12 08:52:13 +12:00
Clyde Stubbs
2af3994f79 [display] Fix Rect::inside (#8679) 2025-05-12 08:52:12 +12:00
Jannik
0c0fe81814 Fix HLW8012 sensor not returning values if change_mode_every is set to never (#8456) 2025-05-12 08:52:12 +12:00
Ben Winslow
82c8614315 Fix typo preventing tt21100 from autosetting the touchscreen res. (#8662) 2025-05-12 08:52:12 +12:00
Jesse Hills
a85dc65038 [media_player] Fix actions with id as value (#8654) 2025-05-12 08:52:12 +12:00
Jesse Hills
290b8bdca0 [esp32_ble] Remove explicit and now incorrect ble override for esp32-c6 (#8643) 2025-05-12 08:52:12 +12:00
bdm310
a96ed0b70a [lvgl] Fix unexpected widget update behavior (#8260) 2025-05-12 08:52:12 +12:00
Jesse Hills
5baa034d0d Merge pull request #8647 from esphome/bump-2025.4.1
2025.4.1
2025-04-29 14:20:26 +12:00
Jesse Hills
7900660bb8 Bump version to 2025.4.1 2025-04-29 11:46:20 +12:00
Steffen Banhardt
f096567ac7 Update ens160_base.cpp – fix wrong double negative (#8639) 2025-04-29 11:46:19 +12:00
Clyde Stubbs
5bfb5ccc34 [core] Fix setting of log level/verbose (#8600) 2025-04-29 11:46:19 +12:00
Jesse Hills
1c60038111 [watchdog] Fix for variants with single core (#8602) 2025-04-29 11:46:19 +12:00
Clyde Stubbs
b940db6549 [online_image] Fix printf format; comment fixes (#8607) 2025-04-29 11:46:19 +12:00
J. Nick Koston
aa6e172e14 Fix BLE connection loop caused by timeout and pending disconnect race (#8597) 2025-04-29 11:46:19 +12:00
Clyde Stubbs
86033b6612 [lvgl] Ensure pages are created on the correct display (#8596) 2025-04-29 11:46:19 +12:00
Jesse Hills
59b4a1f554 Fix psram below idf 5 (#8584) 2025-04-29 11:46:19 +12:00
Jesse Hills
71afd49e3e Merge pull request #8581 from esphome/bump-2025.4.0
2025.4.0
2025-04-17 12:55:25 +12:00
Jesse Hills
61f33d6401 Bump version to 2025.4.0 2025-04-17 09:03:08 +12:00
Jesse Hills
4a1eec567f Merge pull request #8573 from esphome/bump-2025.4.0b3
2025.4.0b3
2025-04-16 12:12:43 +12:00
Jesse Hills
5706b8476f Bump version to 2025.4.0b3 2025-04-16 08:48:38 +12:00
Marius Greuel
8981a86793 SML runtime optimizations (#8571) 2025-04-16 08:48:38 +12:00
Guillermo Ruffino
a7fd6dc382 Fix vscode validation not showing error squiggles (#8500)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-04-16 08:48:38 +12:00
dependabot[bot]
cb0a87c1f9 Bump zeroconf from 0.146.4 to 0.146.5 (#8569)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 08:48:38 +12:00
Jonathan Swoboda
b913a0b178 [am2315c] Use warning not fail during update (#8499) 2025-04-16 08:48:38 +12:00
David Woodhouse
214454ff51 Fix '--device MQTT' for devices with static IP (#8535) 2025-04-16 08:48:38 +12:00
Jesse Hills
b4cf437761 Merge pull request #8567 from esphome/bump-2025.4.0b2
2025.4.0b2
2025-04-15 12:12:31 +12:00
Jesse Hills
1d9f5f1f1e Bump version to 2025.4.0b2 2025-04-15 09:35:53 +12:00
Jonathan Swoboda
e47489708e Bump esphome-dashboard to 20250415.0 (#8565) 2025-04-15 09:35:53 +12:00
dependabot[bot]
8e1bdcd211 Bump zeroconf from 0.146.3 to 0.146.4 (#8563)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-04-15 09:35:53 +12:00
dependabot[bot]
3432d73584 Bump aioesphomeapi from 29.9.0 to 29.10.0 (#8562)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 09:35:53 +12:00
Clyde Stubbs
2bb86641f8 [lvgl] Ensure captured lambdas are in correct order (#8560) 2025-04-15 09:35:53 +12:00
Mischa Siekmann
6ca72a3a26 Speaker-Media-Player: Fix potential deadlock in audio pipeline (#8548) 2025-04-15 09:35:53 +12:00
Clyde Stubbs
c215098cb7 [lvgl] Add restore_value to select and number (#8494) 2025-04-15 09:35:53 +12:00
Clyde Stubbs
566968b6be [lvgl] Fix initial focus on roller (#8547) 2025-04-15 09:35:53 +12:00
Clyde Stubbs
fe51ee6257 [axs15231] Don't overwrite manual dimensions (#8553) 2025-04-15 09:35:53 +12:00
J. Nick Koston
2c499b326a Include MAC address in noise hello (#8551) 2025-04-15 09:35:53 +12:00
Clyde Stubbs
7c4ab7abfe [lvgl] Fix use of image without canvas (Bugfix) (#8540) 2025-04-15 09:35:52 +12:00
54 changed files with 587 additions and 376 deletions

View File

@@ -375,10 +375,12 @@ def upload_program(config, args, host):
password = ota_conf.get(CONF_PASSWORD, "")
if (
not is_ip_address(CORE.address) # pylint: disable=too-many-boolean-expressions
and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED])
and CONF_MQTT in config
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
and (not args.device or args.device in ("MQTT", "OTA"))
and (
((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
or get_port_type(host) == "MQTT"
)
):
from esphome import mqtt

View File

@@ -128,7 +128,7 @@ void AM2315C::update() {
data[2] = 0x00;
if (this->write(data, 3) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Write failed!");
this->mark_failed();
this->status_set_warning();
return;
}
@@ -138,12 +138,12 @@ void AM2315C::update() {
uint8_t status = 0;
if (this->read(&status, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read failed!");
this->mark_failed();
this->status_set_warning();
return;
}
if ((status & 0x80) == 0x80) {
ESP_LOGE(TAG, "HW still busy!");
this->mark_failed();
this->status_set_warning();
return;
}
@@ -151,7 +151,7 @@ void AM2315C::update() {
uint8_t data[7];
if (this->read(data, 7) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read failed!");
this->mark_failed();
this->status_set_warning();
return;
}

View File

@@ -311,6 +311,10 @@ APIError APINoiseFrameHelper::state_action_() {
const std::string &name = App.get_name();
const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str());
msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1);
// node mac, terminated by null byte
const std::string &mac = get_mac_address();
const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str());
msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1);
aerr = write_frame_(msg.data(), msg.size());
if (aerr != APIError::OK)

View File

@@ -1,10 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/as3935/as3935.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace as3935_i2c {

View File

@@ -30,8 +30,12 @@ void AXS15231Touchscreen::setup() {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
this->x_raw_max_ = this->display_->get_native_width();
this->y_raw_max_ = this->display_->get_native_height();
if (this->x_raw_max_ == 0) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == 0) {
this->y_raw_max_ = this->display_->get_native_height();
}
ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen setup complete");
}
@@ -44,7 +48,7 @@ void AXS15231Touchscreen::update_touches() {
err = this->read(data, sizeof(data));
ERROR_CHECK(err);
this->status_clear_warning();
if (data[0] != 0) // no touches
if (data[0] != 0 || data[1] == 0) // no touches
return;
uint16_t x = encode_uint16(data[2] & 0xF, data[3]);
uint16_t y = encode_uint16(data[4] & 0xF, data[5]);

View File

@@ -265,6 +265,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
connection->get_connection_index(), connection->address_str().c_str());
return;
} else if (connection->state() == espbt::ClientState::CONNECTING) {
if (connection->disconnect_pending()) {
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
connection->get_connection_index(), connection->address_str().c_str());
connection->cancel_pending_disconnect();
return;
}
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(),
connection->address_str().c_str());
return;

View File

@@ -69,21 +69,16 @@ bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOL
return true;
}
if (absolute) {
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
} else {
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
}
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
}
bool Rect::inside(Rect rect, bool absolute) const {
bool Rect::inside(Rect rect) const {
if (!this->is_set() || !rect.is_set()) {
return true;
}
if (absolute) {
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
} else {
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
}
return this->x2() >= rect.x && this->x <= rect.x2() && this->y2() >= rect.y && this->y <= rect.y2();
}
void Rect::info(const std::string &prefix) {

View File

@@ -26,7 +26,7 @@ class Rect {
void extend(Rect rect);
void shrink(Rect rect);
bool inside(Rect rect, bool absolute = true) const;
bool inside(Rect rect) const;
bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const;
bool equal(Rect rect) const;
void info(const std::string &prefix = "rect info:");

View File

@@ -187,7 +187,7 @@ void ENS160Component::update() {
}
return;
case INVALID_OUTPUT:
ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output");
ESP_LOGE(TAG, "ENS160 Invalid Status - No valid output");
this->status_set_warning();
return;
}

View File

@@ -2,10 +2,6 @@
#include "ble.h"
#ifdef USE_ESP32_VARIANT_ESP32C6
#include "const_esp32c6.h"
#endif // USE_ESP32_VARIANT_ESP32C6
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -127,11 +123,7 @@ bool ESP32BLE::ble_setup_() {
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
#ifdef USE_ESP32_VARIANT_ESP32C6
esp_bt_controller_config_t cfg = BT_CONTROLLER_CONFIG;
#else
esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
#endif
err = esp_bt_controller_init(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err));

View File

@@ -1,74 +0,0 @@
#pragma once
#ifdef USE_ESP32_VARIANT_ESP32C6
#include <esp_bt.h>
namespace esphome {
namespace esp32_ble {
static const esp_bt_controller_config_t BT_CONTROLLER_CONFIG = {
.config_version = CONFIG_VERSION,
.ble_ll_resolv_list_size = CONFIG_BT_LE_LL_RESOLV_LIST_SIZE,
.ble_hci_evt_hi_buf_count = DEFAULT_BT_LE_HCI_EVT_HI_BUF_COUNT,
.ble_hci_evt_lo_buf_count = DEFAULT_BT_LE_HCI_EVT_LO_BUF_COUNT,
.ble_ll_sync_list_cnt = DEFAULT_BT_LE_MAX_PERIODIC_ADVERTISER_LIST,
.ble_ll_sync_cnt = DEFAULT_BT_LE_MAX_PERIODIC_SYNCS,
.ble_ll_rsp_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT,
.ble_ll_adv_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT,
.ble_ll_tx_pwr_dbm = BLE_LL_TX_PWR_DBM_N,
.rtc_freq = RTC_FREQ_N,
.ble_ll_sca = CONFIG_BT_LE_LL_SCA,
.ble_ll_scan_phy_number = BLE_LL_SCAN_PHY_NUMBER_N,
.ble_ll_conn_def_auth_pyld_tmo = BLE_LL_CONN_DEF_AUTH_PYLD_TMO_N,
.ble_ll_jitter_usecs = BLE_LL_JITTER_USECS_N,
.ble_ll_sched_max_adv_pdu_usecs = BLE_LL_SCHED_MAX_ADV_PDU_USECS_N,
.ble_ll_sched_direct_adv_max_usecs = BLE_LL_SCHED_DIRECT_ADV_MAX_USECS_N,
.ble_ll_sched_adv_max_usecs = BLE_LL_SCHED_ADV_MAX_USECS_N,
.ble_scan_rsp_data_max_len = DEFAULT_BT_LE_SCAN_RSP_DATA_MAX_LEN_N,
.ble_ll_cfg_num_hci_cmd_pkts = BLE_LL_CFG_NUM_HCI_CMD_PKTS_N,
.ble_ll_ctrl_proc_timeout_ms = BLE_LL_CTRL_PROC_TIMEOUT_MS_N,
.nimble_max_connections = DEFAULT_BT_LE_MAX_CONNECTIONS,
.ble_whitelist_size = DEFAULT_BT_NIMBLE_WHITELIST_SIZE, // NOLINT
.ble_acl_buf_size = DEFAULT_BT_LE_ACL_BUF_SIZE,
.ble_acl_buf_count = DEFAULT_BT_LE_ACL_BUF_COUNT,
.ble_hci_evt_buf_size = DEFAULT_BT_LE_HCI_EVT_BUF_SIZE,
.ble_multi_adv_instances = DEFAULT_BT_LE_MAX_EXT_ADV_INSTANCES,
.ble_ext_adv_max_size = DEFAULT_BT_LE_EXT_ADV_MAX_SIZE,
.controller_task_stack_size = NIMBLE_LL_STACK_SIZE,
.controller_task_prio = ESP_TASK_BT_CONTROLLER_PRIO,
.controller_run_cpu = 0,
.enable_qa_test = RUN_QA_TEST,
.enable_bqb_test = RUN_BQB_TEST,
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 1)
// The following fields have been removed since ESP IDF version 5.3.1, see commit:
// https://github.com/espressif/esp-idf/commit/e761c1de8f9c0777829d597b4d5a33bb070a30a8
.enable_uart_hci = HCI_UART_EN,
.ble_hci_uart_port = DEFAULT_BT_LE_HCI_UART_PORT,
.ble_hci_uart_baud = DEFAULT_BT_LE_HCI_UART_BAUD,
.ble_hci_uart_data_bits = DEFAULT_BT_LE_HCI_UART_DATA_BITS,
.ble_hci_uart_stop_bits = DEFAULT_BT_LE_HCI_UART_STOP_BITS,
.ble_hci_uart_flow_ctrl = DEFAULT_BT_LE_HCI_UART_FLOW_CTRL,
.ble_hci_uart_uart_parity = DEFAULT_BT_LE_HCI_UART_PARITY,
#endif
.enable_tx_cca = DEFAULT_BT_LE_TX_CCA_ENABLED,
.cca_rssi_thresh = 256 - DEFAULT_BT_LE_CCA_RSSI_THRESH,
.sleep_en = NIMBLE_SLEEP_ENABLE,
.coex_phy_coded_tx_rx_time_limit = DEFAULT_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_EFF,
.dis_scan_backoff = NIMBLE_DISABLE_SCAN_BACKOFF,
.ble_scan_classify_filter_enable = 1,
.main_xtal_freq = CONFIG_XTAL_FREQ,
.version_num = (uint8_t) efuse_hal_chip_revision(),
.cpu_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
.ignore_wl_for_direct_adv = 0,
.enable_pcl = DEFAULT_BT_LE_POWER_CONTROL_ENABLED,
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 3)
.csa2_select = DEFAULT_BT_LE_50_FEATURE_SUPPORT,
#endif
.config_magic = CONFIG_MAGIC,
};
} // namespace esp32_ble
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32C6

View File

@@ -173,6 +173,8 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
virtual void connect() = 0;
virtual void disconnect() = 0;
bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
virtual void set_state(ClientState st) {
this->state_ = st;
if (st == ClientState::IDLE) {

View File

@@ -69,7 +69,7 @@ void HLW8012Component::update() {
float power = cf_hz * this->power_multiplier_;
if (this->change_mode_at_ != 0) {
if (this->change_mode_at_ != 0 || this->change_mode_every_ == 0) {
// Only read cf1 after one cycle. Apparently it's quite unstable after being changed.
if (this->current_mode_) {
float current = cf1_hz * this->current_multiplier_;

View File

@@ -4,6 +4,7 @@ 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.core import Lambda
from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_types import nullptr
@@ -64,7 +65,14 @@ async def action_to_code(
action_id,
template_arg,
args,
config=None,
):
# Ensure all required ids have been processed, so our LambdaContext doesn't get context-switched.
if config:
for lamb in config.values():
if isinstance(lamb, Lambda):
for id_ in lamb.requires_ids:
await get_variable(id_)
await wait_for_widgets()
async with LambdaContext(parameters=args, where=action_id) as context:
for widget in widgets:
@@ -84,7 +92,9 @@ async def update_to_code(config, action_id, template_arg, args):
lv.event_send(widget.obj, UPDATE_EVENT, nullptr)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(widgets, do_update, action_id, template_arg, args)
return await action_to_code(
widgets, do_update, action_id, template_arg, args, config
)
@automation.register_condition(
@@ -348,4 +358,6 @@ async def obj_update_to_code(config, action_id, template_arg, args):
await set_obj_properties(widget, config)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(widgets, do_update, action_id, template_arg, args)
return await action_to_code(
widgets, do_update, action_id, template_arg, args, config
)

View File

@@ -18,6 +18,7 @@ from .helpers import lvgl_components_required, requires_component
from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable
from .schemas import ENCODER_SCHEMA
from .types import lv_group_t, lv_indev_type_t, lv_key_t
from .widgets import get_widgets
ENCODERS_CONFIG = cv.ensure_list(
ENCODER_SCHEMA.extend(
@@ -76,5 +77,5 @@ async def encoders_to_code(var, config, default_group):
async def initial_focus_to_code(config):
for enc_conf in config[CONF_ENCODERS]:
if default_focus := enc_conf.get(CONF_INITIAL_FOCUS):
obj = await cg.get_variable(default_focus)
lv.group_focus_obj(obj)
widget = await get_widgets(default_focus)
lv.group_focus_obj(widget[0].obj)

View File

@@ -16,7 +16,7 @@ from esphome.const import (
)
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.cpp_types import ESPTime, uint32
from esphome.cpp_types import ESPTime, int32, uint32
from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@@ -263,6 +263,15 @@ def pixels_validator(value):
pixels = LValidator(pixels_validator, uint32, retmapper=literal)
def padding_validator(value):
if isinstance(value, str) and value.lower().endswith("px"):
value = value[:-2]
return cv.int_(value)
padding = LValidator(padding_validator, int32, retmapper=literal)
def zoom_validator(value):
value = cv.float_range(0.1, 10.0)(value)
return value

View File

@@ -173,7 +173,8 @@ class LambdaContext(CodeContext):
class LvContext(LambdaContext):
"""
Code generation into the LVGL initialisation code (called in `setup()`)
Code generation into the LVGL initialisation code, called before setup() and loop()
Basically just does cg.add, so now fairly redundant.
"""
added_lambda_count = 0

View File

@@ -120,6 +120,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);
lv_disp_set_default(this->disp_);
page->setup(this->pages_.size() - 1);
}
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {

View File

@@ -63,10 +63,12 @@ inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image)
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_CANVAS
inline void lv_canvas_draw_img(lv_obj_t *canvas, lv_coord_t x, lv_coord_t y, image::Image *image,
lv_draw_img_dsc_t *dsc) {
lv_canvas_draw_img(canvas, x, y, image->get_lv_img_dsc(), dsc);
}
#endif
#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,

View File

@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import CONF_RESTORE_VALUE
from esphome.cpp_generator import MockObj
from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
@@ -10,21 +11,21 @@ from ..lvcode import (
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
ReturnStatement,
lv,
lv_add,
lvgl_static,
)
from ..types import LV_EVENT, LvNumber, lvgl_ns
from ..widgets import get_widgets, wait_for_widgets
LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number)
LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number, cg.Component)
CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend(
{
cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
cv.Optional(CONF_ANIMATED, default=True): animated,
cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
)
@@ -32,32 +33,34 @@ CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend(
async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0]
var = await number.new_number(
config,
max_value=widget.get_max(),
min_value=widget.get_min(),
step=widget.get_step(),
)
await wait_for_widgets()
async with LambdaContext([], return_type=cg.float_) as value:
value.add(ReturnStatement(widget.get_value()))
async with LambdaContext([(cg.float_, "v")]) as control:
await widget.set_property(
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
)
lv.event_send(widget.obj, API_EVENT, cg.nullptr)
control.add(var.publish_state(widget.get_value()))
async with LambdaContext(EVENT_ARG) as event:
event.add(var.publish_state(widget.get_value()))
event_code = (
LV_EVENT.VALUE_CHANGED
if not config[CONF_UPDATE_ON_RELEASE]
else LV_EVENT.RELEASED
)
async with LvContext():
lv_add(var.set_control_lambda(await control.get_lambda()))
lv_add(
lvgl_static.add_event_cb(
widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code
)
var = await number.new_number(
config,
await control.get_lambda(),
await value.get_lambda(),
event_code,
config[CONF_RESTORE_VALUE],
max_value=widget.get_max(),
min_value=widget.get_min(),
step=widget.get_step(),
)
async with LambdaContext(EVENT_ARG) as event:
event.add(var.on_value())
await cg.register_component(var, config)
cg.add(
lvgl_static.add_event_cb(
widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code
)
lv_add(var.publish_state(widget.get_value()))
)

View File

@@ -3,33 +3,46 @@
#include <utility>
#include "esphome/components/number/number.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace lvgl {
class LVGLNumber : public number::Number {
class LVGLNumber : public number::Number, public Component {
public:
void set_control_lambda(std::function<void(float)> control_lambda) {
this->control_lambda_ = std::move(control_lambda);
if (this->initial_state_.has_value()) {
this->control_lambda_(this->initial_state_.value());
this->initial_state_.reset();
LVGLNumber(std::function<void(float)> control_lambda, std::function<float()> value_lambda, lv_event_code_t event,
bool restore)
: control_lambda_(std::move(control_lambda)),
value_lambda_(std::move(value_lambda)),
event_(event),
restore_(restore) {}
void setup() override {
float value = this->value_lambda_();
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
if (this->pref_.load(&value)) {
this->control_lambda_(value);
}
}
this->publish_state(value);
}
void on_value() { this->publish_state(this->value_lambda_()); }
protected:
void control(float value) override {
if (this->control_lambda_ != nullptr) {
this->control_lambda_(value);
} else {
this->initial_state_ = value;
}
this->control_lambda_(value);
this->publish_state(value);
if (this->restore_)
this->pref_.save(&value);
}
std::function<void(float)> control_lambda_{};
optional<float> initial_state_{};
std::function<void(float)> control_lambda_;
std::function<float()> value_lambda_;
lv_event_code_t event_;
bool restore_;
ESPPreferenceObject pref_{};
};
} // namespace lvgl

View File

@@ -81,7 +81,9 @@ ENCODER_SCHEMA = cv.Schema(
cv.declare_id(LVEncoderListener), requires_component("binary_sensor")
),
cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t),
cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t),
cv.Optional(df.CONF_INITIAL_FOCUS): cv.All(
LIST_ACTION_SCHEMA, cv.Length(min=1, max=1)
),
cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
}
@@ -154,13 +156,13 @@ STYLE_PROPS = {
"opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity,
"outline_pad": lvalid.pixels,
"outline_pad": lvalid.padding,
"outline_width": lvalid.pixels,
"pad_all": lvalid.pixels,
"pad_bottom": lvalid.pixels,
"pad_left": lvalid.pixels,
"pad_right": lvalid.pixels,
"pad_top": lvalid.pixels,
"pad_all": lvalid.padding,
"pad_bottom": lvalid.padding,
"pad_left": lvalid.padding,
"pad_right": lvalid.padding,
"pad_top": lvalid.padding,
"shadow_color": lvalid.lv_color,
"shadow_ofs_x": lvalid.lv_int,
"shadow_ofs_y": lvalid.lv_int,
@@ -224,8 +226,8 @@ FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend(
{
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
}
)
@@ -368,8 +370,8 @@ LAYOUT_SCHEMA = {
cv.Required(df.CONF_GRID_COLUMNS): [grid_spec],
cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
},
df.TYPE_FLEX: {
cv.Optional(
@@ -378,8 +380,8 @@ LAYOUT_SCHEMA = {
cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
},
},
lower=True,
@@ -425,8 +427,8 @@ ALL_STYLES = {
**STYLE_PROPS,
**GRID_CELL_SCHEMA,
**FLEX_OBJ_SCHEMA,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
}

View File

@@ -1,18 +1,19 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_OPTIONS
from esphome.const import CONF_ID, CONF_OPTIONS, CONF_RESTORE_VALUE
from ..defines import CONF_ANIMATED, CONF_WIDGET, literal
from ..lvcode import LvContext
from ..types import LvSelect, lvgl_ns
from ..widgets import get_widgets, wait_for_widgets
from ..widgets import get_widgets
LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select)
LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select, cg.Component)
CONFIG_SCHEMA = select.select_schema(LVGLSelect).extend(
{
cv.Required(CONF_WIDGET): cv.use_id(LvSelect),
cv.Optional(CONF_ANIMATED, default=False): cv.boolean,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
)
@@ -21,12 +22,9 @@ async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0]
options = widget.config.get(CONF_OPTIONS, [])
selector = await select.new_select(config, options=options)
await wait_for_widgets()
async with LvContext() as ctx:
ctx.add(
selector.set_widget(
widget.var,
literal("LV_ANIM_ON" if config[CONF_ANIMATED] else "LV_ANIM_OFF"),
)
)
animated = literal("LV_ANIM_ON" if config[CONF_ANIMATED] else "LV_ANIM_OFF")
selector = cg.new_Pvariable(
config[CONF_ID], widget.var, animated, config[CONF_RESTORE_VALUE]
)
await select.register_select(selector, config, options=options)
await cg.register_component(selector, config)

View File

@@ -11,12 +11,20 @@
namespace esphome {
namespace lvgl {
class LVGLSelect : public select::Select {
class LVGLSelect : public select::Select, public Component {
public:
void set_widget(LvSelectable *widget, lv_anim_enable_t anim = LV_ANIM_OFF) {
this->widget_ = widget;
this->anim_ = anim;
LVGLSelect(LvSelectable *widget, lv_anim_enable_t anim, bool restore)
: widget_(widget), anim_(anim), restore_(restore) {}
void setup() override {
this->set_options_();
if (this->restore_) {
size_t index;
this->pref_ = global_preferences->make_preference<size_t>(this->get_object_id_hash());
if (this->pref_.load(&index))
this->widget_->set_selected_index(index, LV_ANIM_OFF);
}
this->publish();
lv_obj_add_event_cb(
this->widget_->obj,
[](lv_event_t *e) {
@@ -24,11 +32,6 @@ class LVGLSelect : public select::Select {
it->set_options_();
},
LV_EVENT_REFRESH, this);
if (this->initial_state_.has_value()) {
this->control(this->initial_state_.value());
this->initial_state_.reset();
}
this->publish();
auto lamb = [](lv_event_t *e) {
auto *self = static_cast<LVGLSelect *>(e->user_data);
self->publish();
@@ -37,21 +40,25 @@ class LVGLSelect : public select::Select {
lv_obj_add_event_cb(this->widget_->obj, lamb, lv_update_event, this);
}
void publish() { this->publish_state(this->widget_->get_selected_text()); }
void publish() {
this->publish_state(this->widget_->get_selected_text());
if (this->restore_) {
auto index = this->widget_->get_selected_index();
this->pref_.save(&index);
}
}
protected:
void control(const std::string &value) override {
if (this->widget_ != nullptr) {
this->widget_->set_selected_text(value, this->anim_);
} else {
this->initial_state_ = value;
}
this->widget_->set_selected_text(value, this->anim_);
this->publish();
}
void set_options_() { this->traits.set_options(this->widget_->get_options()); }
LvSelectable *widget_{};
optional<std::string> initial_state_{};
lv_anim_enable_t anim_{LV_ANIM_OFF};
LvSelectable *widget_;
lv_anim_enable_t anim_;
bool restore_;
ESPPreferenceObject pref_{};
};
} // namespace lvgl

View File

@@ -67,12 +67,13 @@ class ArcType(NumberType):
lv.arc_set_mode(w.obj, literal(config[CONF_MODE]))
lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE])
if config.get(CONF_ADJUSTABLE) is False:
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
elif CONF_GROUP not in config:
# For some reason arc does not get automatically added to the default group
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
if CONF_ADJUSTABLE in config:
if not config[CONF_ADJUSTABLE]:
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
elif CONF_GROUP not in config:
# For some reason arc does not get automatically added to the default group
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
value = await get_start_value(config)
if value is not None:

View File

@@ -19,7 +19,7 @@ from ..defines import (
CONF_SELECTED,
)
from ..helpers import lvgl_components_required
from ..lv_validation import key_code, lv_bool, pixels
from ..lv_validation import key_code, lv_bool, padding
from ..lvcode import lv, lv_add, lv_expr
from ..schemas import automation_schema
from ..types import (
@@ -59,8 +59,8 @@ BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema(
BUTTONMATRIX_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool,
cv.Optional(CONF_PAD_ROW): pixels,
cv.Optional(CONF_PAD_COLUMN): pixels,
cv.Optional(CONF_PAD_ROW): padding,
cv.Optional(CONF_PAD_COLUMN): padding,
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
cv.Required(CONF_ROWS): cv.ensure_list(
cv.Schema(
@@ -250,7 +250,7 @@ async def button_update_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config[CONF_ID])
assert all(isinstance(w, MatrixButton) for w in widgets)
async def do_button_update(w: MatrixButton):
async def do_button_update(w):
if (width := config.get(CONF_WIDTH)) is not None:
lv.btnmatrix_set_btn_width(w.obj, w.index, width)
if config.get(CONF_SELECTED):
@@ -275,5 +275,5 @@ async def button_update_to_code(config, action_id, template_arg, args):
)
return await action_to_code(
widgets, do_button_update, action_id, template_arg, args
widgets, do_button_update, action_id, template_arg, args, config
)

View File

@@ -97,7 +97,7 @@ async def canvas_fill(config, action_id, template_arg, args):
async def do_fill(w: Widget):
lv.canvas_fill_bg(w.obj, color, opa)
return await action_to_code(widget, do_fill, action_id, template_arg, args)
return await action_to_code(widget, do_fill, action_id, template_arg, args, config)
@automation.register_action(
@@ -145,7 +145,9 @@ async def canvas_set_pixel(config, action_id, template_arg, args):
x, y = point
lv.canvas_set_px_opa(w.obj, x, y, opa_var)
return await action_to_code(widget, do_set_pixels, action_id, template_arg, args)
return await action_to_code(
widget, do_set_pixels, action_id, template_arg, args, config
)
DRAW_SCHEMA = cv.Schema(
@@ -181,7 +183,9 @@ async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg
lv_assign(getattr(dsc, mapped_prop), value)
await do_draw(w, x, y, dsc_addr)
return await action_to_code(widget, action_func, action_id, template_arg, args)
return await action_to_code(
widget, action_func, action_id, template_arg, args, config
)
RECT_PROPS = {

View File

@@ -2,7 +2,7 @@ from esphome.config_validation import Optional
from esphome.const import CONF_TEXT
from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_PAD_COLUMN
from ..lv_validation import lv_text, pixels
from ..lv_validation import lv_text, padding
from ..lvcode import lv
from ..schemas import TEXT_SCHEMA
from ..types import LvBoolean
@@ -19,7 +19,7 @@ class CheckboxType(WidgetType):
(CONF_MAIN, CONF_INDICATOR),
TEXT_SCHEMA.extend(
{
Optional(CONF_PAD_COLUMN): pixels,
Optional(CONF_PAD_COLUMN): padding,
}
),
)

View File

@@ -36,7 +36,6 @@ DROPDOWN_BASE_SCHEMA = cv.Schema(
cv.Optional(CONF_SYMBOL): lv_text,
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
}
)
@@ -44,12 +43,14 @@ DROPDOWN_BASE_SCHEMA = cv.Schema(
DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPTIONS): cv.ensure_list(option_string),
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
}
)
DROPDOWN_UPDATE_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
{
cv.Optional(CONF_OPTIONS): cv.ensure_list(option_string),
cv.Optional(CONF_DIR): DIRECTIONS.one_of,
}
)

View File

@@ -10,7 +10,7 @@ from ..defines import (
CONF_ZOOM,
LvConstant,
)
from ..lv_validation import angle, lv_bool, lv_image, size, zoom
from ..lv_validation import lv_angle, lv_bool, lv_image, size, zoom
from ..lvcode import lv
from ..types import lv_img_t
from . import Widget, WidgetType
@@ -20,9 +20,9 @@ CONF_IMAGE = "image"
BASE_IMG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PIVOT_X, default="50%"): size,
cv.Optional(CONF_PIVOT_Y, default="50%"): size,
cv.Optional(CONF_ANGLE): angle,
cv.Optional(CONF_PIVOT_X): size,
cv.Optional(CONF_PIVOT_Y): size,
cv.Optional(CONF_ANGLE): lv_angle,
cv.Optional(CONF_ZOOM): zoom,
cv.Optional(CONF_OFFSET_X): size,
cv.Optional(CONF_OFFSET_Y): size,
@@ -63,19 +63,22 @@ class ImgType(WidgetType):
async def to_code(self, w: Widget, config):
if src := config.get(CONF_SRC):
lv.img_set_src(w.obj, await lv_image.process(src))
if (pivot_x := config.get(CONF_PIVOT_X)) and (
pivot_y := config.get(CONF_PIVOT_Y)
):
lv.img_set_pivot(
w.obj, await size.process(pivot_x), await size.process(pivot_y)
)
if (cf_angle := config.get(CONF_ANGLE)) is not None:
pivot_x = config[CONF_PIVOT_X]
pivot_y = config[CONF_PIVOT_Y]
lv.img_set_pivot(w.obj, pivot_x, pivot_y)
lv.img_set_angle(w.obj, cf_angle)
lv.img_set_angle(w.obj, await lv_angle.process(cf_angle))
if (img_zoom := config.get(CONF_ZOOM)) is not None:
lv.img_set_zoom(w.obj, img_zoom)
lv.img_set_zoom(w.obj, await zoom.process(img_zoom))
if (offset := config.get(CONF_OFFSET_X)) is not None:
lv.img_set_offset_x(w.obj, offset)
lv.img_set_offset_x(w.obj, await size.process(offset))
if (offset := config.get(CONF_OFFSET_Y)) is not None:
lv.img_set_offset_y(w.obj, offset)
lv.img_set_offset_y(w.obj, await size.process(offset))
if CONF_ANTIALIAS in config:
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])
lv.img_set_antialias(w.obj, await lv_bool.process(config[CONF_ANTIALIAS]))
if mode := config.get(CONF_MODE):
await w.set_property("size_mode", mode)

View File

@@ -297,7 +297,9 @@ async def indicator_update_to_code(config, action_id, template_arg, args):
async def set_value(w: Widget):
await set_indicator_values(w.var, w.obj, config)
return await action_to_code(widget, set_value, action_id, template_arg, args)
return await action_to_code(
widget, set_value, action_id, template_arg, args, config
)
async def set_indicator_values(meter, indicator, config):

View File

@@ -134,11 +134,13 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
)
MEDIA_PLAYER_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean),
}
MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean),
}
)
)
MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id(

View File

@@ -111,7 +111,7 @@ void OnlineImage::update() {
case ImageFormat::BMP:
accept_mime_type = "image/bmp";
break;
#endif // ONLINE_IMAGE_BMP_SUPPORT
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
case ImageFormat::JPEG:
accept_mime_type = "image/jpeg";
@@ -121,7 +121,7 @@ void OnlineImage::update() {
case ImageFormat::PNG:
accept_mime_type = "image/png";
break;
#endif // ONLINE_IMAGE_PNG_SUPPORT
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
default:
accept_mime_type = "image/*";
}
@@ -159,7 +159,7 @@ void OnlineImage::update() {
ESP_LOGD(TAG, "Allocating BMP decoder");
this->decoder_ = make_unique<BmpDecoder>(this);
}
#endif // ONLINE_IMAGE_BMP_SUPPORT
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
if (this->format_ == ImageFormat::JPEG) {
ESP_LOGD(TAG, "Allocating JPEG decoder");
@@ -171,7 +171,7 @@ void OnlineImage::update() {
ESP_LOGD(TAG, "Allocating PNG decoder");
this->decoder_ = make_unique<PngDecoder>(this);
}
#endif // ONLINE_IMAGE_PNG_SUPPORT
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
if (!this->decoder_) {
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
@@ -185,7 +185,7 @@ void OnlineImage::update() {
this->download_error_callback_.call();
return;
}
ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size);
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
this->start_time_ = ::time(nullptr);
}

View File

@@ -1,7 +1,8 @@
#ifdef USE_ESP32
#include "psram.h"
#ifdef USE_ESP_IDF
#include <esp_idf_version.h>
#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5
#include <esp_psram.h>
#endif // USE_ESP_IDF
@@ -15,7 +16,7 @@ static const char *const TAG = "psram";
void PsramComponent::dump_config() {
ESP_LOGCONFIG(TAG, "PSRAM:");
#ifdef USE_ESP_IDF
#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5
bool available = esp_psram_is_initialized();
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));

View File

@@ -52,9 +52,8 @@ void Sml::loop() {
break;
// remove start/end sequence
this->sml_data_.erase(this->sml_data_.begin(), this->sml_data_.begin() + START_SEQ.size());
this->sml_data_.resize(this->sml_data_.size() - 8);
this->process_sml_file_(this->sml_data_);
this->process_sml_file_(
BytesView(this->sml_data_).subview(START_SEQ.size(), this->sml_data_.size() - START_SEQ.size() - 8));
}
break;
};
@@ -66,8 +65,8 @@ void Sml::add_on_data_callback(std::function<void(std::vector<uint8_t>, bool)> &
this->data_callbacks_.add(std::move(callback));
}
void Sml::process_sml_file_(const bytes &sml_data) {
SmlFile sml_file = SmlFile(sml_data);
void Sml::process_sml_file_(const BytesView &sml_data) {
SmlFile sml_file(sml_data);
std::vector<ObisInfo> obis_info = sml_file.get_obis_info();
this->publish_obis_info_(obis_info);
@@ -75,6 +74,7 @@ void Sml::process_sml_file_(const bytes &sml_data) {
}
void Sml::log_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
#ifdef ESPHOME_LOG_HAS_DEBUG
ESP_LOGD(TAG, "OBIS info:");
for (auto const &obis_info : obis_info_vec) {
std::string info;
@@ -83,6 +83,7 @@ void Sml::log_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
info += " [0x" + bytes_repr(obis_info.value) + "]";
ESP_LOGD(TAG, "%s", info.c_str());
}
#endif
}
void Sml::publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
@@ -92,10 +93,11 @@ void Sml::publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
}
void Sml::publish_value_(const ObisInfo &obis_info) {
const auto obis_code = obis_info.code_repr();
for (auto const &sml_listener : sml_listeners_) {
if ((!sml_listener->server_id.empty()) && (bytes_repr(obis_info.server_id) != sml_listener->server_id))
continue;
if (obis_info.code_repr() != sml_listener->obis_code)
if (obis_code != sml_listener->obis_code)
continue;
sml_listener->publish_val(obis_info);
}

View File

@@ -27,7 +27,7 @@ class Sml : public Component, public uart::UARTDevice {
void add_on_data_callback(std::function<void(std::vector<uint8_t>, bool)> &&callback);
protected:
void process_sml_file_(const bytes &sml_data);
void process_sml_file_(const BytesView &sml_data);
void log_obis_info_(const std::vector<ObisInfo> &obis_info_vec);
void publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec);
char check_start_end_bytes_(uint8_t byte);

View File

@@ -5,17 +5,17 @@
namespace esphome {
namespace sml {
SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) {
SmlFile::SmlFile(const BytesView &buffer) : buffer_(buffer) {
// extract messages
this->pos_ = 0;
while (this->pos_ < this->buffer_.size()) {
if (this->buffer_[this->pos_] == 0x00)
break; // EndOfSmlMsg
SmlNode message = SmlNode();
SmlNode message;
if (!this->setup_node(&message))
break;
this->messages.emplace_back(message);
this->messages.emplace_back(std::move(message));
}
}
@@ -62,22 +62,20 @@ bool SmlFile::setup_node(SmlNode *node) {
return false;
node->type = type;
node->nodes.clear();
node->value_bytes.clear();
if (type == SML_LIST) {
node->nodes.reserve(length);
for (size_t i = 0; i != length; i++) {
SmlNode child_node = SmlNode();
SmlNode child_node;
if (!this->setup_node(&child_node))
return false;
node->nodes.emplace_back(child_node);
node->nodes.emplace_back(std::move(child_node));
}
} else {
// Value starts at the current position
// Value ends "length" bytes later,
// (since the TL field is counted but already subtracted from length)
node->value_bytes = bytes(this->buffer_.begin() + this->pos_, this->buffer_.begin() + this->pos_ + length);
node->value_bytes = buffer_.subview(this->pos_, length);
// Increment the pointer past all consumed bytes
this->pos_ += length;
}
@@ -87,14 +85,14 @@ bool SmlFile::setup_node(SmlNode *node) {
std::vector<ObisInfo> SmlFile::get_obis_info() {
std::vector<ObisInfo> obis_info;
for (auto const &message : messages) {
SmlNode message_body = message.nodes[3];
const auto &message_body = message.nodes[3];
uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes);
if (message_type != SML_GET_LIST_RES)
continue;
SmlNode get_list_response = message_body.nodes[1];
bytes server_id = get_list_response.nodes[1].value_bytes;
SmlNode val_list = get_list_response.nodes[4];
const auto &get_list_response = message_body.nodes[1];
const auto &server_id = get_list_response.nodes[1].value_bytes;
const auto &val_list = get_list_response.nodes[4];
for (auto const &val_list_entry : val_list.nodes) {
obis_info.emplace_back(server_id, val_list_entry);
@@ -103,7 +101,7 @@ std::vector<ObisInfo> SmlFile::get_obis_info() {
return obis_info;
}
std::string bytes_repr(const bytes &buffer) {
std::string bytes_repr(const BytesView &buffer) {
std::string repr;
for (auto const value : buffer) {
repr += str_sprintf("%02x", value & 0xff);
@@ -111,7 +109,7 @@ std::string bytes_repr(const bytes &buffer) {
return repr;
}
uint64_t bytes_to_uint(const bytes &buffer) {
uint64_t bytes_to_uint(const BytesView &buffer) {
uint64_t val = 0;
for (auto const value : buffer) {
val = (val << 8) + value;
@@ -119,7 +117,7 @@ uint64_t bytes_to_uint(const bytes &buffer) {
return val;
}
int64_t bytes_to_int(const bytes &buffer) {
int64_t bytes_to_int(const BytesView &buffer) {
uint64_t tmp = bytes_to_uint(buffer);
int64_t val;
@@ -135,14 +133,14 @@ int64_t bytes_to_int(const bytes &buffer) {
return val;
}
std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); }
std::string bytes_to_string(const BytesView &buffer) { return std::string(buffer.begin(), buffer.end()); }
ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) {
ObisInfo::ObisInfo(const BytesView &server_id, const SmlNode &val_list_entry) : server_id(server_id) {
this->code = val_list_entry.nodes[0].value_bytes;
this->status = val_list_entry.nodes[1].value_bytes;
this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes);
this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes);
SmlNode value_node = val_list_entry.nodes[5];
const auto &value_node = val_list_entry.nodes[5];
this->value = value_node.value_bytes;
this->value_type = value_node.type;
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <string>
@@ -11,44 +12,73 @@ namespace sml {
using bytes = std::vector<uint8_t>;
class BytesView {
public:
BytesView() noexcept = default;
explicit BytesView(const uint8_t *first, size_t count) noexcept : data_{first}, count_{count} {}
explicit BytesView(const bytes &bytes) noexcept : data_{bytes.data()}, count_{bytes.size()} {}
size_t size() const noexcept { return count_; }
uint8_t operator[](size_t index) const noexcept {
assert(index < count_);
return data_[index];
}
BytesView subview(size_t offset, size_t count) const noexcept {
assert(offset + count <= count_);
return BytesView{data_ + offset, count};
}
const uint8_t *begin() const noexcept { return data_; }
const uint8_t *end() const noexcept { return data_ + count_; }
private:
const uint8_t *data_ = nullptr;
size_t count_ = 0;
};
class SmlNode {
public:
uint8_t type;
bytes value_bytes;
BytesView value_bytes;
std::vector<SmlNode> nodes;
};
class ObisInfo {
public:
ObisInfo(bytes server_id, SmlNode val_list_entry);
bytes server_id;
bytes code;
bytes status;
ObisInfo(const BytesView &server_id, const SmlNode &val_list_entry);
BytesView server_id;
BytesView code;
BytesView status;
char unit;
char scaler;
bytes value;
BytesView value;
uint16_t value_type;
std::string code_repr() const;
};
class SmlFile {
public:
SmlFile(bytes buffer);
SmlFile(const BytesView &buffer);
bool setup_node(SmlNode *node);
std::vector<SmlNode> messages;
std::vector<ObisInfo> get_obis_info();
protected:
const bytes buffer_;
const BytesView buffer_;
size_t pos_;
};
std::string bytes_repr(const bytes &buffer);
std::string bytes_repr(const BytesView &buffer);
uint64_t bytes_to_uint(const bytes &buffer);
uint64_t bytes_to_uint(const BytesView &buffer);
int64_t bytes_to_int(const bytes &buffer);
int64_t bytes_to_int(const BytesView &buffer);
std::string bytes_to_string(const bytes &buffer);
std::string bytes_to_string(const BytesView &buffer);
} // namespace sml
} // namespace esphome

View File

@@ -441,9 +441,10 @@ void AudioPipeline::decode_task(void *params) {
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
xEventGroupClearBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
xEventGroupClearBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
InfoErrorEvent event;
event.source = InfoErrorSource::DECODER;

View File

@@ -68,7 +68,7 @@ void TT21100Touchscreen::setup() {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
this->y_raw_max_ = this->display_->get_native_height();
}
}

View File

@@ -6,6 +6,7 @@
#include <cinttypes>
#include <cstdint>
#ifdef USE_ESP32
#include <soc/soc_caps.h>
#include "esp_idf_version.h"
#include "esp_task_wdt.h"
#endif
@@ -40,7 +41,7 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
#if ESP_IDF_VERSION_MAJOR >= 5
esp_task_wdt_config_t wdt_config = {
.timeout_ms = timeout_ms,
.idle_core_mask = 0x03,
.idle_core_mask = (1 << SOC_CPU_CORES_NUM) - 1,
.trigger_panic = true,
};
esp_task_wdt_reconfigure(&wdt_config);

View File

@@ -1499,30 +1499,9 @@ def dimensions(value):
def directory(value):
import json
value = string(value)
path = CORE.relative_config_path(value)
if CORE.vscode and (
not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
):
print(
json.dumps(
{
"type": "check_directory_exists",
"path": path,
}
)
)
data = json.loads(input())
assert data["type"] == "directory_exists_response"
if data["content"]:
return value
raise Invalid(
f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."
)
if not os.path.exists(path):
raise Invalid(
f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."
@@ -1535,30 +1514,9 @@ def directory(value):
def file_(value):
import json
value = string(value)
path = CORE.relative_config_path(value)
if CORE.vscode and (
not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
):
print(
json.dumps(
{
"type": "check_file_exists",
"path": path,
}
)
)
data = json.loads(input())
assert data["type"] == "file_exists_response"
if data["content"]:
return value
raise Invalid(
f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."
)
if not os.path.exists(path):
raise Invalid(
f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.4.0b1"
__version__ = "2025.4.2"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -475,7 +475,6 @@ class EsphomeCore:
self.dashboard = False
# True if command is run from vscode api
self.vscode = False
self.ace = False
# The name of the node
self.name: Optional[str] = None
# The friendly name of the node

View File

@@ -74,13 +74,14 @@ def setup_log(
colorama.init()
if log_level == logging.DEBUG:
CORE.verbose = True
elif log_level == logging.CRITICAL:
CORE.quiet = True
# Setup logging - will map log level from string to constant
logging.basicConfig(level=log_level)
if logging.root.level == logging.DEBUG:
CORE.verbose = True
elif logging.root.level == logging.CRITICAL:
CORE.quiet = True
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger().handlers[0].setFormatter(

View File

@@ -78,28 +78,47 @@ def _print_file_read_event(path: str) -> None:
)
def _request_and_get_stream_on_stdin(fname: str) -> StringIO:
_print_file_read_event(fname)
raw_yaml_stream = StringIO(_read_file_content_from_json_on_stdin())
return raw_yaml_stream
def _vscode_loader(fname: str) -> dict[str, Any]:
raw_yaml_stream = _request_and_get_stream_on_stdin(fname)
# it is required to set the name on StringIO so document on start_mark
# is set properly. Otherwise it is initialized with "<file>"
raw_yaml_stream.name = fname
return parse_yaml(fname, raw_yaml_stream, _vscode_loader)
def _ace_loader(fname: str) -> dict[str, Any]:
raw_yaml_stream = _request_and_get_stream_on_stdin(fname)
return parse_yaml(fname, raw_yaml_stream)
def read_config(args):
while True:
CORE.reset()
data = json.loads(input())
assert data["type"] == "validate"
assert data["type"] == "validate" or data["type"] == "exit"
if data["type"] == "exit":
return
CORE.vscode = True
CORE.ace = args.ace
f = data["file"]
if CORE.ace:
CORE.config_path = os.path.join(args.configuration, f)
if args.ace: # Running from ESPHome Compiler dashboard, not vscode
CORE.config_path = os.path.join(args.configuration, data["file"])
loader = _ace_loader
else:
CORE.config_path = data["file"]
loader = _vscode_loader
file_name = CORE.config_path
_print_file_read_event(file_name)
raw_yaml = _read_file_content_from_json_on_stdin()
command_line_substitutions: dict[str, Any] = (
dict(args.substitution) if args.substitution else {}
)
vs = VSCodeResult()
try:
config = parse_yaml(file_name, StringIO(raw_yaml))
config = loader(file_name)
res = validate_config(config, command_line_substitutions)
except Exception as err: # pylint: disable=broad-except
vs.add_yaml_error(str(err))

View File

@@ -3,12 +3,12 @@ from __future__ import annotations
import fnmatch
import functools
import inspect
from io import TextIOWrapper
from io import BytesIO, TextIOBase, TextIOWrapper
from ipaddress import _BaseAddress
import logging
import math
import os
from typing import Any
from typing import Any, Callable
import uuid
import yaml
@@ -69,7 +69,10 @@ class ESPForceValue:
pass
def make_data_base(value, from_database: ESPHomeDataBase = None):
def make_data_base(
value, from_database: ESPHomeDataBase = None
) -> ESPHomeDataBase | Any:
"""Wrap a value in a ESPHomeDataBase object."""
try:
value = add_class_to_obj(value, ESPHomeDataBase)
if from_database is not None:
@@ -102,6 +105,11 @@ def _add_data_ref(fn):
class ESPHomeLoaderMixin:
"""Loader class that keeps track of line numbers."""
def __init__(self, name: str, yaml_loader: Callable[[str], dict[str, Any]]) -> None:
"""Initialize the loader."""
self.name = name
self.yaml_loader = yaml_loader
@_add_data_ref
def construct_yaml_int(self, node):
return super().construct_yaml_int(node)
@@ -127,7 +135,7 @@ class ESPHomeLoaderMixin:
return super().construct_yaml_seq(node)
@_add_data_ref
def construct_yaml_map(self, node):
def construct_yaml_map(self, node: yaml.MappingNode) -> OrderedDict[str, Any]:
"""Traverses the given mapping node and returns a list of constructed key-value pairs."""
assert isinstance(node, yaml.MappingNode)
# A list of key-value pairs we find in the current mapping
@@ -231,7 +239,7 @@ class ESPHomeLoaderMixin:
return OrderedDict(pairs)
@_add_data_ref
def construct_env_var(self, node):
def construct_env_var(self, node: yaml.Node) -> str:
args = node.value.split()
# Check for a default value
if len(args) > 1:
@@ -243,23 +251,23 @@ class ESPHomeLoaderMixin:
)
@property
def _directory(self):
def _directory(self) -> str:
return os.path.dirname(self.name)
def _rel_path(self, *args):
def _rel_path(self, *args: str) -> str:
return os.path.join(self._directory, *args)
@_add_data_ref
def construct_secret(self, node):
def construct_secret(self, node: yaml.Node) -> str:
try:
secrets = _load_yaml_internal(self._rel_path(SECRET_YAML))
secrets = self.yaml_loader(self._rel_path(SECRET_YAML))
except EsphomeError as e:
if self.name == CORE.config_path:
raise e
try:
main_config_dir = os.path.dirname(CORE.config_path)
main_secret_yml = os.path.join(main_config_dir, SECRET_YAML)
secrets = _load_yaml_internal(main_secret_yml)
secrets = self.yaml_loader(main_secret_yml)
except EsphomeError as er:
raise EsphomeError(f"{e}\n{er}") from er
@@ -272,7 +280,9 @@ class ESPHomeLoaderMixin:
return val
@_add_data_ref
def construct_include(self, node):
def construct_include(
self, node: yaml.Node
) -> dict[str, Any] | OrderedDict[str, Any]:
from esphome.const import CONF_VARS
def extract_file_vars(node):
@@ -290,71 +300,93 @@ class ESPHomeLoaderMixin:
else:
file, vars = node.value, None
result = _load_yaml_internal(self._rel_path(file))
result = self.yaml_loader(self._rel_path(file))
if not vars:
vars = {}
result = substitute_vars(result, vars)
return result
@_add_data_ref
def construct_include_dir_list(self, node):
def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]:
files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
return [_load_yaml_internal(f) for f in files]
return [self.yaml_loader(f) for f in files]
@_add_data_ref
def construct_include_dir_merge_list(self, node):
def construct_include_dir_merge_list(self, node: yaml.Node) -> list[dict[str, Any]]:
files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
merged_list = []
for fname in files:
loaded_yaml = _load_yaml_internal(fname)
loaded_yaml = self.yaml_loader(fname)
if isinstance(loaded_yaml, list):
merged_list.extend(loaded_yaml)
return merged_list
@_add_data_ref
def construct_include_dir_named(self, node):
def construct_include_dir_named(
self, node: yaml.Node
) -> OrderedDict[str, dict[str, Any]]:
files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
mapping = OrderedDict()
for fname in files:
filename = os.path.splitext(os.path.basename(fname))[0]
mapping[filename] = _load_yaml_internal(fname)
mapping[filename] = self.yaml_loader(fname)
return mapping
@_add_data_ref
def construct_include_dir_merge_named(self, node):
def construct_include_dir_merge_named(
self, node: yaml.Node
) -> OrderedDict[str, dict[str, Any]]:
files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
mapping = OrderedDict()
for fname in files:
loaded_yaml = _load_yaml_internal(fname)
loaded_yaml = self.yaml_loader(fname)
if isinstance(loaded_yaml, dict):
mapping.update(loaded_yaml)
return mapping
@_add_data_ref
def construct_lambda(self, node):
def construct_lambda(self, node: yaml.Node) -> Lambda:
return Lambda(str(node.value))
@_add_data_ref
def construct_force(self, node):
def construct_force(self, node: yaml.Node) -> ESPForceValue:
obj = self.construct_scalar(node)
return add_class_to_obj(obj, ESPForceValue)
@_add_data_ref
def construct_extend(self, node):
def construct_extend(self, node: yaml.Node) -> Extend:
return Extend(str(node.value))
@_add_data_ref
def construct_remove(self, node):
def construct_remove(self, node: yaml.Node) -> Remove:
return Remove(str(node.value))
class ESPHomeLoader(ESPHomeLoaderMixin, FastestAvailableSafeLoader):
"""Loader class that keeps track of line numbers."""
def __init__(
self,
stream: TextIOBase | BytesIO,
name: str,
yaml_loader: Callable[[str], dict[str, Any]],
) -> None:
FastestAvailableSafeLoader.__init__(self, stream)
ESPHomeLoaderMixin.__init__(self, name, yaml_loader)
class ESPHomePurePythonLoader(ESPHomeLoaderMixin, PurePythonLoader):
"""Loader class that keeps track of line numbers."""
def __init__(
self,
stream: TextIOBase | BytesIO,
name: str,
yaml_loader: Callable[[str], dict[str, Any]],
) -> None:
PurePythonLoader.__init__(self, stream)
ESPHomeLoaderMixin.__init__(self, name, yaml_loader)
for _loader in (ESPHomeLoader, ESPHomePurePythonLoader):
_loader.add_constructor("tag:yaml.org,2002:int", _loader.construct_yaml_int)
@@ -388,17 +420,30 @@ def load_yaml(fname: str, clear_secrets: bool = True) -> Any:
return _load_yaml_internal(fname)
def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any:
def _load_yaml_internal(fname: str) -> Any:
"""Load a YAML file."""
try:
with open(fname, encoding="utf-8") as f_handle:
return parse_yaml(fname, f_handle)
except (UnicodeDecodeError, OSError) as err:
raise EsphomeError(f"Error reading file {fname}: {err}") from err
def parse_yaml(
file_name: str, file_handle: TextIOWrapper, yaml_loader=_load_yaml_internal
) -> Any:
"""Parse a YAML file."""
try:
return _load_yaml_internal_with_type(ESPHomeLoader, file_name, file_handle)
return _load_yaml_internal_with_type(
ESPHomeLoader, file_name, file_handle, yaml_loader
)
except EsphomeError:
# Loading failed, so we now load with the Python loader which has more
# readable exceptions
# Rewind the stream so we can try again
file_handle.seek(0, 0)
return _load_yaml_internal_with_type(
ESPHomePurePythonLoader, file_name, file_handle
ESPHomePurePythonLoader, file_name, file_handle, yaml_loader
)
@@ -435,23 +480,14 @@ def substitute_vars(config, vars):
return result
def _load_yaml_internal(fname: str) -> Any:
"""Load a YAML file."""
try:
with open(fname, encoding="utf-8") as f_handle:
return parse_yaml(fname, f_handle)
except (UnicodeDecodeError, OSError) as err:
raise EsphomeError(f"Error reading file {fname}: {err}") from err
def _load_yaml_internal_with_type(
loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader],
fname: str,
content: TextIOWrapper,
yaml_loader: Any,
) -> Any:
"""Load a YAML file."""
loader = loader_type(content)
loader.name = fname
loader = loader_type(content, fname, yaml_loader)
try:
return loader.get_single_data() or OrderedDict()
except yaml.YAMLError as exc:
@@ -470,7 +506,7 @@ def dump(dict_, show_secrets=False):
)
def _is_file_valid(name):
def _is_file_valid(name: str) -> bool:
"""Decide if a file is valid."""
return not name.startswith(".")

View File

@@ -12,9 +12,9 @@ pyserial==3.5
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.8.1
click==8.1.7
esphome-dashboard==20250212.0
aioesphomeapi==29.9.0
zeroconf==0.146.3
esphome-dashboard==20250415.0
aioesphomeapi==29.10.0
zeroconf==0.146.5
puremagic==1.28
ruamel.yaml==0.18.10 # dashboard_import
esphome-glyphsets==0.2.0

View File

@@ -3,4 +3,9 @@ substitutions:
sda_pin: GPIO17
irq_pin: GPIO15
<<: !include common.yaml
packages:
as3935: !include common.yaml
# Trigger issue: https://github.com/esphome/issues/issues/6990
# Compile with no binary sensor results in error
binary_sensor: !remove

View File

@@ -38,6 +38,7 @@ number:
widget: slider_id
name: LVGL Slider
update_on_release: true
restore_value: true
- platform: lvgl
widget: lv_arc
id: lvgl_arc_number

View File

@@ -134,6 +134,15 @@ lvgl:
id: style_test
bg_color: blue
bg_opa: !lambda return 0.5;
- lvgl.image.update:
id: lv_image
zoom: !lambda return 512;
angle: !lambda return 100;
pivot_x: !lambda return 20;
pivot_y: !lambda return 20;
offset_x: !lambda return 20;
offset_y: !lambda return 20;
antialias: !lambda return true;
- id: simple_msgbox
title: Simple
@@ -486,6 +495,8 @@ lvgl:
align: top_left
y: "50"
mode: real
zoom: 2.0
angle: 45
- tileview:
id: tileview_id
scrollbar_mode: active
@@ -641,6 +652,8 @@ lvgl:
knob:
radius: 1
width: "4"
pad_left: -5
pad_top: 5
height: 10%
bg_color: 0x000000
width: 100%
@@ -990,3 +1003,13 @@ color:
green_int: 123
blue_int: 64
white_int: 255
select:
- platform: lvgl
id: lv_roller_select
widget: lv_roller
restore_value: true
- platform: lvgl
id: lv_dropdown_select
widget: lv_dropdown
restore_value: false

View File

@@ -71,5 +71,6 @@ lvgl:
sensor: encoder
enter_button: pushbutton
group: general
initial_focus: lv_roller
<<: !include common.yaml

View File

@@ -0,0 +1,125 @@
import json
import os
from unittest.mock import Mock, patch
from esphome import vscode
def _run_repl_test(input_data):
"""Reusable test function for different input scenarios."""
input_data.append(_exit())
with (
patch("builtins.input", side_effect=input_data),
patch("sys.stdout") as mock_stdout,
):
args = Mock([])
args.ace = False
args.substitution = None
vscode.read_config(args)
# Capture printed output
full_output = "".join(call[0][0] for call in mock_stdout.write.call_args_list)
return full_output.strip().split("\n")
def _validate(file_path: str):
return json.dumps({"type": "validate", "file": file_path})
def _file_response(data: str):
return json.dumps({"type": "file_response", "content": data})
def _read_file(file_path: str):
return json.dumps({"type": "read_file", "path": file_path})
def _exit():
return json.dumps({"type": "exit"})
RESULT_NO_ERROR = '{"type": "result", "yaml_errors": [], "validation_errors": []}'
def test_multi_file():
source_path = os.path.join("dir_path", "x.yaml")
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
esp8266:
board: !secret my_secret_board
"""),
# read_file secrets.yaml
_file_response("""my_secret_board: esp1f"""),
]
)
expected_lines = [
_read_file(source_path),
_read_file(os.path.join("dir_path", "secrets.yaml")),
RESULT_NO_ERROR,
]
assert output_lines == expected_lines
def test_shows_correct_range_error():
source_path = os.path.join("dir_path", "x.yaml")
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
esp8266:
broad: !secret my_secret_board # typo here
"""),
# read_file secrets.yaml
_file_response("""my_secret_board: esp1f"""),
]
)
assert len(output_lines) == 3
error = json.loads(output_lines[2])
validation_error = error["validation_errors"][0]
assert validation_error["message"].startswith("[broad] is an invalid option for")
range = validation_error["range"]
assert range["document"] == source_path
assert range["start_line"] == 3
assert range["start_col"] == 2
assert range["end_line"] == 3
assert range["end_col"] == 7
def test_shows_correct_loaded_file_error():
source_path = os.path.join("dir_path", "x.yaml")
output_lines = _run_repl_test(
[
_validate(source_path),
# read_file x.yaml
_file_response("""esphome:
name: test1
packages:
board: !include .pkg.esp8266.yaml
"""),
# read_file .pkg.esp8266.yaml
_file_response("""esp8266:
broad: esp1f # typo here
"""),
]
)
assert len(output_lines) == 3
error = json.loads(output_lines[2])
validation_error = error["validation_errors"][0]
assert validation_error["message"].startswith("[broad] is an invalid option for")
range = validation_error["range"]
assert range["document"] == os.path.join("dir_path", ".pkg.esp8266.yaml")
assert range["start_line"] == 1
assert range["start_col"] == 2
assert range["end_line"] == 1
assert range["end_col"] == 7

View File

@@ -42,3 +42,23 @@ def test_loading_a_missing_file(fixture_path):
yaml_util.load_yaml(yaml_file)
except EsphomeError as err:
assert "missing.yaml" in str(err)
def test_parsing_with_custom_loader(fixture_path):
"""Test custom loader used for vscode connection
Default loader is tested in test_include_with_vars
"""
yaml_file = fixture_path / "yaml_util" / "includetest.yaml"
loader_calls = []
def custom_loader(fname):
loader_calls.append(fname)
with open(yaml_file, encoding="utf-8") as f_handle:
yaml_util.parse_yaml(yaml_file, f_handle, custom_loader)
assert len(loader_calls) == 3
assert loader_calls[0].endswith("includes/included.yaml")
assert loader_calls[1].endswith("includes/list.yaml")
assert loader_calls[2].endswith("includes/scalar.yaml")