mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 15:41:52 +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