mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			41 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7bb899bfa1 | ||
| 
						 | 
					cae3c030d2 | ||
| 
						 | 
					d7c615ec43 | ||
| 
						 | 
					1294e8ccd5 | ||
| 
						 | 
					37a2cb07d1 | ||
| 
						 | 
					2af3994f79 | ||
| 
						 | 
					0c0fe81814 | ||
| 
						 | 
					82c8614315 | ||
| 
						 | 
					a85dc65038 | ||
| 
						 | 
					290b8bdca0 | ||
| 
						 | 
					a96ed0b70a | ||
| 
						 | 
					5baa034d0d | ||
| 
						 | 
					7900660bb8 | ||
| 
						 | 
					f096567ac7 | ||
| 
						 | 
					5bfb5ccc34 | ||
| 
						 | 
					1c60038111 | ||
| 
						 | 
					b940db6549 | ||
| 
						 | 
					aa6e172e14 | ||
| 
						 | 
					86033b6612 | ||
| 
						 | 
					59b4a1f554 | ||
| 
						 | 
					71afd49e3e | ||
| 
						 | 
					61f33d6401 | ||
| 
						 | 
					4a1eec567f | ||
| 
						 | 
					5706b8476f | ||
| 
						 | 
					8981a86793 | ||
| 
						 | 
					a7fd6dc382 | ||
| 
						 | 
					cb0a87c1f9 | ||
| 
						 | 
					b913a0b178 | ||
| 
						 | 
					214454ff51 | ||
| 
						 | 
					b4cf437761 | ||
| 
						 | 
					1d9f5f1f1e | ||
| 
						 | 
					e47489708e | ||
| 
						 | 
					8e1bdcd211 | ||
| 
						 | 
					3432d73584 | ||
| 
						 | 
					2bb86641f8 | ||
| 
						 | 
					6ca72a3a26 | ||
| 
						 | 
					c215098cb7 | ||
| 
						 | 
					566968b6be | ||
| 
						 | 
					fe51ee6257 | ||
| 
						 | 
					2c499b326a | ||
| 
						 | 
					7c4ab7abfe | 
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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]);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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:");
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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_;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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()))
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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)})."
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
"""Constants used by esphome."""
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.4.0b1"
 | 
			
		||||
__version__ = "2025.4.2"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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(".")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -71,5 +71,6 @@ lvgl:
 | 
			
		||||
    sensor: encoder
 | 
			
		||||
    enter_button: pushbutton
 | 
			
		||||
    group: general
 | 
			
		||||
    initial_focus: lv_roller
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										125
									
								
								tests/unit_tests/test_vscode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								tests/unit_tests/test_vscode.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user