mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
Merge upstream/dev into integration
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] Developer breaking change (an API change that could break external components)
|
||||||
- [ ] Code quality improvements to existing code or addition of tests
|
- [ ] Code quality improvements to existing code or addition of tests
|
||||||
- [ ] Other
|
- [ ] Other
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Set up Python ${{ inputs.python-version }}
|
- name: Set up Python ${{ inputs.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ jobs:
|
|||||||
'bugfix',
|
'bugfix',
|
||||||
'new-feature',
|
'new-feature',
|
||||||
'breaking-change',
|
'breaking-change',
|
||||||
|
'developer-breaking-change',
|
||||||
'code-quality'
|
'code-quality'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -367,6 +368,7 @@ jobs:
|
|||||||
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
||||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||||
|
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
@@ -240,7 +240,7 @@ jobs:
|
|||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
path: lib/home-assistant
|
path: lib/home-assistant
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.13
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
|
|||||||
if (this->response_offset_ >= this->response_length_) {
|
if (this->response_offset_ >= this->response_length_) {
|
||||||
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str());
|
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str());
|
||||||
if (length < GENI_RESPONSE_HEADER_LENGTH) {
|
if (length < GENI_RESPONSE_HEADER_LENGTH) {
|
||||||
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str());
|
ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
|
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
|
||||||
|
|||||||
@@ -854,6 +854,10 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"board_upload.maximum_size",
|
||||||
|
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||||
|
)
|
||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
cg.add_build_flag("-DUSE_ESP32")
|
cg.add_build_flag("-DUSE_ESP32")
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16];
|
extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16];
|
||||||
extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16];
|
extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16];
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::esp8266 {} // namespace esphome::esp8266
|
||||||
namespace esp8266 {} // namespace esp8266
|
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ESP8266
|
#endif // USE_ESP8266
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
#include "gpio.h"
|
#include "gpio.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::esp8266 {
|
||||||
namespace esp8266 {
|
|
||||||
|
|
||||||
static const char *const TAG = "esp8266";
|
static const char *const TAG = "esp8266";
|
||||||
|
|
||||||
@@ -110,9 +109,11 @@ void ESP8266GPIOPin::digital_write(bool value) {
|
|||||||
}
|
}
|
||||||
void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); }
|
void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); }
|
||||||
|
|
||||||
} // namespace esp8266
|
} // namespace esphome::esp8266
|
||||||
|
|
||||||
using namespace esp8266;
|
namespace esphome {
|
||||||
|
|
||||||
|
using esp8266::ISRPinArg;
|
||||||
|
|
||||||
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::esp8266 {
|
||||||
namespace esp8266 {
|
|
||||||
|
|
||||||
class ESP8266GPIOPin : public InternalGPIOPin {
|
class ESP8266GPIOPin : public InternalGPIOPin {
|
||||||
public:
|
public:
|
||||||
@@ -33,7 +32,6 @@ class ESP8266GPIOPin : public InternalGPIOPin {
|
|||||||
gpio::Flags flags_{};
|
gpio::Flags flags_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp8266
|
} // namespace esphome::esp8266
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ESP8266
|
#endif // USE_ESP8266
|
||||||
|
|||||||
@@ -15,24 +15,24 @@ extern "C" {
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::esp8266 {
|
||||||
namespace esp8266 {
|
|
||||||
|
|
||||||
static const char *const TAG = "esp8266.preferences";
|
static const char *const TAG = "esp8266.preferences";
|
||||||
|
|
||||||
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
||||||
|
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
||||||
|
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
|
||||||
|
|
||||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||||
static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
|
||||||
static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
|
|
||||||
|
|
||||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||||
static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||||
#else
|
#else
|
||||||
static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
|
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
|
static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
|
||||||
@@ -284,10 +284,10 @@ void setup_preferences() {
|
|||||||
}
|
}
|
||||||
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }
|
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }
|
||||||
|
|
||||||
} // namespace esp8266
|
} // namespace esphome::esp8266
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ESP8266
|
#endif // USE_ESP8266
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::esp8266 {
|
||||||
namespace esp8266 {
|
|
||||||
|
|
||||||
void setup_preferences();
|
void setup_preferences();
|
||||||
void preferences_prevent_write(bool prevent);
|
void preferences_prevent_write(bool prevent);
|
||||||
|
|
||||||
} // namespace esp8266
|
} // namespace esphome::esp8266
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ESP8266
|
#endif // USE_ESP8266
|
||||||
|
|||||||
@@ -279,6 +279,8 @@ KEYBOARD_MODES = LvConstant(
|
|||||||
)
|
)
|
||||||
ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE")
|
ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE")
|
||||||
TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL")
|
TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL")
|
||||||
|
SCROLL_DIRECTIONS = TILE_DIRECTIONS.extend("NONE")
|
||||||
|
SNAP_DIRECTIONS = LvConstant("LV_SCROLL_SNAP_", "NONE", "START", "END", "CENTER")
|
||||||
CHILD_ALIGNMENTS = LvConstant(
|
CHILD_ALIGNMENTS = LvConstant(
|
||||||
"LV_ALIGN_",
|
"LV_ALIGN_",
|
||||||
"TOP_LEFT",
|
"TOP_LEFT",
|
||||||
@@ -511,6 +513,9 @@ CONF_ROLLOVER = "rollover"
|
|||||||
CONF_ROOT_BACK_BTN = "root_back_btn"
|
CONF_ROOT_BACK_BTN = "root_back_btn"
|
||||||
CONF_SCALE_LINES = "scale_lines"
|
CONF_SCALE_LINES = "scale_lines"
|
||||||
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
||||||
|
CONF_SCROLL_DIR = "scroll_dir"
|
||||||
|
CONF_SCROLL_SNAP_X = "scroll_snap_x"
|
||||||
|
CONF_SCROLL_SNAP_Y = "scroll_snap_y"
|
||||||
CONF_SELECTED_INDEX = "selected_index"
|
CONF_SELECTED_INDEX = "selected_index"
|
||||||
CONF_SELECTED_TEXT = "selected_text"
|
CONF_SELECTED_TEXT = "selected_text"
|
||||||
CONF_SHOW_SNOW = "show_snow"
|
CONF_SHOW_SNOW = "show_snow"
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ from .defines import (
|
|||||||
)
|
)
|
||||||
from .lv_validation import padding, size
|
from .lv_validation import padding, size
|
||||||
|
|
||||||
|
CONF_MULTIPLE_WIDGETS_PER_CELL = "multiple_widgets_per_cell"
|
||||||
|
|
||||||
cell_alignments = LV_CELL_ALIGNMENTS.one_of
|
cell_alignments = LV_CELL_ALIGNMENTS.one_of
|
||||||
grid_alignments = LV_GRID_ALIGNMENTS.one_of
|
grid_alignments = LV_GRID_ALIGNMENTS.one_of
|
||||||
flex_alignments = LV_FLEX_ALIGNMENTS.one_of
|
flex_alignments = LV_FLEX_ALIGNMENTS.one_of
|
||||||
@@ -170,10 +172,14 @@ class DirectionalLayout(FlexLayout):
|
|||||||
|
|
||||||
def validate(self, config):
|
def validate(self, config):
|
||||||
assert config[CONF_LAYOUT].lower() == self.direction
|
assert config[CONF_LAYOUT].lower() == self.direction
|
||||||
config[CONF_LAYOUT] = {
|
layout = {
|
||||||
**FLEX_HV_STYLE,
|
**FLEX_HV_STYLE,
|
||||||
CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(),
|
CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(),
|
||||||
}
|
}
|
||||||
|
if pad_all := config.get("pad_all"):
|
||||||
|
layout[CONF_PAD_ROW] = pad_all
|
||||||
|
layout[CONF_PAD_COLUMN] = pad_all
|
||||||
|
config[CONF_LAYOUT] = layout
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
@@ -220,6 +226,7 @@ class GridLayout(Layout):
|
|||||||
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
|
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||||
cv.Optional(CONF_PAD_ROW): padding,
|
cv.Optional(CONF_PAD_ROW): padding,
|
||||||
cv.Optional(CONF_PAD_COLUMN): padding,
|
cv.Optional(CONF_PAD_COLUMN): padding,
|
||||||
|
cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||||
@@ -263,6 +270,7 @@ class GridLayout(Layout):
|
|||||||
# should be guaranteed to be a dict at this point
|
# should be guaranteed to be a dict at this point
|
||||||
assert isinstance(layout, dict)
|
assert isinstance(layout, dict)
|
||||||
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
|
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
|
||||||
|
allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False)
|
||||||
rows = len(layout[CONF_GRID_ROWS])
|
rows = len(layout[CONF_GRID_ROWS])
|
||||||
columns = len(layout[CONF_GRID_COLUMNS])
|
columns = len(layout[CONF_GRID_COLUMNS])
|
||||||
used_cells = [[None] * columns for _ in range(rows)]
|
used_cells = [[None] * columns for _ in range(rows)]
|
||||||
@@ -299,7 +307,10 @@ class GridLayout(Layout):
|
|||||||
f"exceeds grid size {rows}x{columns}",
|
f"exceeds grid size {rows}x{columns}",
|
||||||
[CONF_WIDGETS, index],
|
[CONF_WIDGETS, index],
|
||||||
)
|
)
|
||||||
if used_cells[row + i][column + j] is not None:
|
if (
|
||||||
|
not allow_multiple
|
||||||
|
and used_cells[row + i][column + j] is not None
|
||||||
|
):
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}",
|
f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}",
|
||||||
[CONF_WIDGETS, index],
|
[CONF_WIDGETS, index],
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from .helpers import (
|
|||||||
lv_fonts_used,
|
lv_fonts_used,
|
||||||
requires_component,
|
requires_component,
|
||||||
)
|
)
|
||||||
from .types import lv_font_t, lv_gradient_t
|
from .types import lv_gradient_t
|
||||||
|
|
||||||
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
||||||
|
|
||||||
@@ -498,7 +498,9 @@ class LvFont(LValidator):
|
|||||||
esphome_fonts_used.add(fontval)
|
esphome_fonts_used.add(fontval)
|
||||||
return requires_component("font")(fontval)
|
return requires_component("font")(fontval)
|
||||||
|
|
||||||
super().__init__(validator, lv_font_t)
|
# Use font::Font* as return type for lambdas returning ESPHome fonts
|
||||||
|
# The inline overloads in lvgl_esphome.h handle conversion to lv_font_t*
|
||||||
|
super().__init__(validator, Font.operator("ptr"))
|
||||||
|
|
||||||
async def process(self, value, args=()):
|
async def process(self, value, args=()):
|
||||||
if is_lv_font(value):
|
if is_lv_font(value):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from esphome import config_validation as cv
|
from esphome import config_validation as cv
|
||||||
from esphome.automation import Trigger, validate_automation
|
from esphome.automation import Trigger, validate_automation
|
||||||
from esphome.components.time import RealTimeClock
|
from esphome.components.time import RealTimeClock
|
||||||
|
from esphome.config_validation import prepend_path
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ARGS,
|
CONF_ARGS,
|
||||||
CONF_FORMAT,
|
CONF_FORMAT,
|
||||||
@@ -19,7 +20,14 @@ from esphome.core import TimePeriod
|
|||||||
from esphome.core.config import StartupTrigger
|
from esphome.core.config import StartupTrigger
|
||||||
|
|
||||||
from . import defines as df, lv_validation as lvalid
|
from . import defines as df, lv_validation as lvalid
|
||||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
from .defines import (
|
||||||
|
CONF_SCROLL_DIR,
|
||||||
|
CONF_SCROLL_SNAP_X,
|
||||||
|
CONF_SCROLL_SNAP_Y,
|
||||||
|
CONF_SCROLLBAR_MODE,
|
||||||
|
CONF_TIME_FORMAT,
|
||||||
|
LV_GRAD_DIR,
|
||||||
|
)
|
||||||
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
||||||
from .layout import (
|
from .layout import (
|
||||||
FLEX_OBJ_SCHEMA,
|
FLEX_OBJ_SCHEMA,
|
||||||
@@ -233,9 +241,19 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex
|
|||||||
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
|
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
|
||||||
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
|
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
|
||||||
).one_of,
|
).one_of,
|
||||||
|
cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of,
|
||||||
|
cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of,
|
||||||
|
cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OBJ_PROPERTIES = {
|
||||||
|
CONF_SCROLL_SNAP_X,
|
||||||
|
CONF_SCROLL_SNAP_Y,
|
||||||
|
CONF_SCROLL_DIR,
|
||||||
|
CONF_SCROLLBAR_MODE,
|
||||||
|
}
|
||||||
|
|
||||||
# Also allow widget specific properties for use in style definitions
|
# Also allow widget specific properties for use in style definitions
|
||||||
FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend(
|
FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
@@ -422,7 +440,10 @@ def any_widget_schema(extras=None):
|
|||||||
def validator(value):
|
def validator(value):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
# Convert to list
|
# Convert to list
|
||||||
|
is_dict = True
|
||||||
value = [{k: v} for k, v in value.items()]
|
value = [{k: v} for k, v in value.items()]
|
||||||
|
else:
|
||||||
|
is_dict = False
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
raise cv.Invalid("Expected a list of widgets")
|
raise cv.Invalid("Expected a list of widgets")
|
||||||
result = []
|
result = []
|
||||||
@@ -443,7 +464,9 @@ def any_widget_schema(extras=None):
|
|||||||
)
|
)
|
||||||
# Apply custom validation
|
# Apply custom validation
|
||||||
value = widget_type.validate(value or {})
|
value = widget_type.validate(value or {})
|
||||||
result.append({key: container_validator(value)})
|
path = [key] if is_dict else [index, key]
|
||||||
|
with prepend_path(path):
|
||||||
|
result.append({key: container_validator(value)})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return validator
|
return validator
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from ..defines import (
|
|||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
CONF_PAD_COLUMN,
|
CONF_PAD_COLUMN,
|
||||||
CONF_PAD_ROW,
|
CONF_PAD_ROW,
|
||||||
CONF_SCROLLBAR_MODE,
|
|
||||||
CONF_STYLES,
|
CONF_STYLES,
|
||||||
CONF_WIDGETS,
|
CONF_WIDGETS,
|
||||||
OBJ_FLAGS,
|
OBJ_FLAGS,
|
||||||
@@ -45,7 +44,7 @@ from ..lvcode import (
|
|||||||
lv_obj,
|
lv_obj,
|
||||||
lv_Pvariable,
|
lv_Pvariable,
|
||||||
)
|
)
|
||||||
from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
|
from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES
|
||||||
from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr
|
from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr
|
||||||
|
|
||||||
EVENT_LAMB = "event_lamb__"
|
EVENT_LAMB = "event_lamb__"
|
||||||
@@ -414,7 +413,8 @@ async def set_obj_properties(w: Widget, config):
|
|||||||
w.add_state(state)
|
w.add_state(state)
|
||||||
cond.else_()
|
cond.else_()
|
||||||
w.clear_state(state)
|
w.clear_state(state)
|
||||||
await w.set_property(CONF_SCROLLBAR_MODE, config, lv_name="obj")
|
for property in OBJ_PROPERTIES:
|
||||||
|
await w.set_property(property, config, lv_name="obj")
|
||||||
|
|
||||||
|
|
||||||
async def add_widgets(parent: Widget, config: dict):
|
async def add_widgets(parent: Widget, config: dict):
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ from ..defines import (
|
|||||||
CONF_START_ANGLE,
|
CONF_START_ANGLE,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from ..lv_validation import get_start_value, lv_angle_degrees, lv_float, lv_int
|
from ..lv_validation import (
|
||||||
|
get_start_value,
|
||||||
|
lv_angle_degrees,
|
||||||
|
lv_float,
|
||||||
|
lv_int,
|
||||||
|
lv_positive_int,
|
||||||
|
)
|
||||||
from ..lvcode import lv, lv_expr, lv_obj
|
from ..lvcode import lv, lv_expr, lv_obj
|
||||||
from ..types import LvNumber, NumberType
|
from ..types import LvNumber, NumberType
|
||||||
from . import Widget
|
from . import Widget
|
||||||
@@ -36,13 +42,20 @@ ARC_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees,
|
cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees,
|
||||||
cv.Optional(CONF_ADJUSTABLE, default=False): bool,
|
cv.Optional(CONF_ADJUSTABLE, default=False): bool,
|
||||||
cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of,
|
cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of,
|
||||||
cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t,
|
cv.Optional(CONF_CHANGE_RATE, default=720): lv_positive_int,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ARC_MODIFY_SCHEMA = cv.Schema(
|
ARC_MODIFY_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Optional(CONF_VALUE): lv_float,
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
|
cv.Optional(CONF_MIN_VALUE): lv_int,
|
||||||
|
cv.Optional(CONF_MAX_VALUE): lv_int,
|
||||||
|
cv.Optional(CONF_START_ANGLE): lv_angle_degrees,
|
||||||
|
cv.Optional(CONF_END_ANGLE): lv_angle_degrees,
|
||||||
|
cv.Optional(CONF_ROTATION): lv_angle_degrees,
|
||||||
|
cv.Optional(CONF_MODE): ARC_MODES.one_of,
|
||||||
|
cv.Optional(CONF_CHANGE_RATE): lv_positive_int,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,17 +71,34 @@ class ArcType(NumberType):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
if CONF_MIN_VALUE in config:
|
if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config:
|
||||||
max_value = await lv_int.process(config[CONF_MAX_VALUE])
|
max_value = await lv_int.process(config[CONF_MAX_VALUE])
|
||||||
min_value = await lv_int.process(config[CONF_MIN_VALUE])
|
min_value = await lv_int.process(config[CONF_MIN_VALUE])
|
||||||
lv.arc_set_range(w.obj, min_value, max_value)
|
lv.arc_set_range(w.obj, min_value, max_value)
|
||||||
start = await lv_angle_degrees.process(config[CONF_START_ANGLE])
|
elif CONF_MIN_VALUE in config:
|
||||||
end = await lv_angle_degrees.process(config[CONF_END_ANGLE])
|
max_value = w.get_property(CONF_MAX_VALUE)
|
||||||
rotation = await lv_angle_degrees.process(config[CONF_ROTATION])
|
min_value = await lv_int.process(config[CONF_MIN_VALUE])
|
||||||
lv.arc_set_bg_angles(w.obj, start, end)
|
lv.arc_set_range(w.obj, min_value, max_value)
|
||||||
lv.arc_set_rotation(w.obj, rotation)
|
elif CONF_MAX_VALUE in config:
|
||||||
lv.arc_set_mode(w.obj, literal(config[CONF_MODE]))
|
max_value = await lv_int.process(config[CONF_MAX_VALUE])
|
||||||
lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE])
|
min_value = w.get_property(CONF_MIN_VALUE)
|
||||||
|
lv.arc_set_range(w.obj, min_value, max_value)
|
||||||
|
|
||||||
|
await w.set_property(
|
||||||
|
CONF_START_ANGLE,
|
||||||
|
await lv_angle_degrees.process(config.get(CONF_START_ANGLE)),
|
||||||
|
)
|
||||||
|
await w.set_property(
|
||||||
|
CONF_END_ANGLE, await lv_angle_degrees.process(config.get(CONF_END_ANGLE))
|
||||||
|
)
|
||||||
|
await w.set_property(
|
||||||
|
CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION))
|
||||||
|
)
|
||||||
|
await w.set_property(CONF_MODE, config)
|
||||||
|
await w.set_property(
|
||||||
|
CONF_CHANGE_RATE,
|
||||||
|
await lv_positive_int.process(config.get(CONF_CHANGE_RATE)),
|
||||||
|
)
|
||||||
|
|
||||||
if CONF_ADJUSTABLE in config:
|
if CONF_ADJUSTABLE in config:
|
||||||
if not config[CONF_ADJUSTABLE]:
|
if not config[CONF_ADJUSTABLE]:
|
||||||
@@ -78,9 +108,7 @@ class ArcType(NumberType):
|
|||||||
# For some reason arc does not get automatically added to the default group
|
# For some reason arc does not get automatically added to the default group
|
||||||
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
|
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
|
||||||
|
|
||||||
value = await get_start_value(config)
|
await w.set_property(CONF_VALUE, await get_start_value(config))
|
||||||
if value is not None:
|
|
||||||
lv.arc_set_value(w.obj, value)
|
|
||||||
|
|
||||||
|
|
||||||
arc_spec = ArcType()
|
arc_spec = ArcType()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from esphome.core import Lambda
|
|||||||
from ..defines import CONF_MAIN, call_lambda
|
from ..defines import CONF_MAIN, call_lambda
|
||||||
from ..lvcode import lv_add
|
from ..lvcode import lv_add
|
||||||
from ..schemas import point_schema
|
from ..schemas import point_schema
|
||||||
from ..types import LvCompound, LvType
|
from ..types import LvCompound, LvType, lv_coord_t
|
||||||
from . import Widget, WidgetType
|
from . import Widget, WidgetType
|
||||||
|
|
||||||
CONF_LINE = "line"
|
CONF_LINE = "line"
|
||||||
@@ -23,9 +23,7 @@ LINE_SCHEMA = {
|
|||||||
|
|
||||||
async def process_coord(coord):
|
async def process_coord(coord):
|
||||||
if isinstance(coord, Lambda):
|
if isinstance(coord, Lambda):
|
||||||
coord = call_lambda(
|
coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t))
|
||||||
await cg.process_lambda(coord, [], return_type="lv_coord_t")
|
|
||||||
)
|
|
||||||
if not coord.endswith("()"):
|
if not coord.endswith("()"):
|
||||||
coord = f"static_cast<lv_coord_t>({coord})"
|
coord = f"static_cast<lv_coord_t>({coord})"
|
||||||
return cg.RawExpression(coord)
|
return cg.RawExpression(coord)
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
|
|||||||
|
|
||||||
// Check if baud rate is supported
|
// Check if baud rate is supported
|
||||||
this->original_baud_rate_ = this->parent_->get_baud_rate();
|
this->original_baud_rate_ = this->parent_->get_baud_rate();
|
||||||
|
if (baud_rate <= 0) {
|
||||||
|
baud_rate = this->original_baud_rate_;
|
||||||
|
}
|
||||||
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
|
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
|
||||||
|
|
||||||
// Define the configuration for the HTTP client
|
// Define the configuration for the HTTP client
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
|
|||||||
|
|
||||||
// Check if baud rate is supported
|
// Check if baud rate is supported
|
||||||
this->original_baud_rate_ = this->parent_->get_baud_rate();
|
this->original_baud_rate_ = this->parent_->get_baud_rate();
|
||||||
|
if (baud_rate <= 0) {
|
||||||
|
baud_rate = this->original_baud_rate_;
|
||||||
|
}
|
||||||
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
|
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
|
||||||
|
|
||||||
// Define the configuration for the HTTP client
|
// Define the configuration for the HTTP client
|
||||||
|
|||||||
@@ -278,7 +278,12 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
|||||||
|
|
||||||
void setup() override {
|
void setup() override {
|
||||||
// Start with loop disabled - only enable when there's work to do
|
// Start with loop disabled - only enable when there's work to do
|
||||||
this->disable_loop();
|
// IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
|
||||||
|
// called before our setup() (e.g., from on_boot trigger at same priority level)
|
||||||
|
// and we must not undo its enable_loop() call
|
||||||
|
if (this->num_running_ == 0) {
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void play_complex(const Ts &...x) override {
|
void play_complex(const Ts &...x) override {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace sht4x {
|
|||||||
static const char *const TAG = "sht4x";
|
static const char *const TAG = "sht4x";
|
||||||
|
|
||||||
static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0};
|
static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0};
|
||||||
|
static const uint8_t SERIAL_NUMBER_COMMAND = 0x89;
|
||||||
|
|
||||||
void SHT4XComponent::start_heater_() {
|
void SHT4XComponent::start_heater_() {
|
||||||
uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]};
|
uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]};
|
||||||
@@ -17,6 +18,17 @@ void SHT4XComponent::start_heater_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SHT4XComponent::read_serial_number_() {
|
||||||
|
uint16_t buffer[2];
|
||||||
|
if (!this->get_8bit_register(SERIAL_NUMBER_COMMAND, buffer, 2, 1)) {
|
||||||
|
ESP_LOGE(TAG, "Get serial number failed");
|
||||||
|
this->serial_number_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->serial_number_ = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1]));
|
||||||
|
ESP_LOGD(TAG, "Serial number: %08" PRIx32, this->serial_number_);
|
||||||
|
}
|
||||||
|
|
||||||
void SHT4XComponent::setup() {
|
void SHT4XComponent::setup() {
|
||||||
auto err = this->write(nullptr, 0);
|
auto err = this->write(nullptr, 0);
|
||||||
if (err != i2c::ERROR_OK) {
|
if (err != i2c::ERROR_OK) {
|
||||||
@@ -24,6 +36,8 @@ void SHT4XComponent::setup() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->read_serial_number_();
|
||||||
|
|
||||||
if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) {
|
if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) {
|
||||||
uint32_t heater_interval = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_);
|
uint32_t heater_interval = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_);
|
||||||
ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval);
|
ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval);
|
||||||
@@ -54,11 +68,18 @@ void SHT4XComponent::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SHT4XComponent::dump_config() {
|
void SHT4XComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "SHT4x:");
|
ESP_LOGCONFIG(TAG,
|
||||||
|
"SHT4x:\n"
|
||||||
|
" Serial number: %08" PRIx32,
|
||||||
|
this->serial_number_);
|
||||||
|
|
||||||
LOG_I2C_DEVICE(this);
|
LOG_I2C_DEVICE(this);
|
||||||
if (this->is_failed()) {
|
if (this->is_failed()) {
|
||||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
}
|
}
|
||||||
|
if (this->serial_number_ == 0) {
|
||||||
|
ESP_LOGW(TAG, "Get serial number failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SHT4XComponent::update() {
|
void SHT4XComponent::update() {
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri
|
|||||||
float duty_cycle_;
|
float duty_cycle_;
|
||||||
|
|
||||||
void start_heater_();
|
void start_heater_();
|
||||||
|
void read_serial_number_();
|
||||||
uint8_t heater_command_;
|
uint8_t heater_command_;
|
||||||
|
uint32_t serial_number_;
|
||||||
|
|
||||||
sensor::Sensor *temp_sensor_{nullptr};
|
sensor::Sensor *temp_sensor_{nullptr};
|
||||||
sensor::Sensor *humidity_sensor_{nullptr};
|
sensor::Sensor *humidity_sensor_{nullptr};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ static constexpr size_t MAX_STATE_LENGTH = 255;
|
|||||||
|
|
||||||
void IPAddressWiFiInfo::setup() {
|
void IPAddressWiFiInfo::setup() {
|
||||||
wifi::global_wifi_component->add_on_ip_state_callback(
|
wifi::global_wifi_component->add_on_ip_state_callback(
|
||||||
[this](network::IPAddresses ips, network::IPAddress dns1_ip, network::IPAddress dns2_ip) {
|
[this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) {
|
||||||
this->state_callback_(ips);
|
this->state_callback_(ips);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this);
|
|||||||
void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) {
|
void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) {
|
||||||
this->publish_state(ips[0].str());
|
this->publish_state(ips[0].str());
|
||||||
uint8_t sensor = 0;
|
uint8_t sensor = 0;
|
||||||
for (auto &ip : ips) {
|
for (const auto &ip : ips) {
|
||||||
if (ip.is_set()) {
|
if (ip.is_set()) {
|
||||||
if (this->ip_sensors_[sensor] != nullptr) {
|
if (this->ip_sensors_[sensor] != nullptr) {
|
||||||
this->ip_sensors_[sensor]->publish_state(ip.str());
|
this->ip_sensors_[sensor]->publish_state(ip.str());
|
||||||
@@ -40,14 +40,14 @@ void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) {
|
|||||||
|
|
||||||
void DNSAddressWifiInfo::setup() {
|
void DNSAddressWifiInfo::setup() {
|
||||||
wifi::global_wifi_component->add_on_ip_state_callback(
|
wifi::global_wifi_component->add_on_ip_state_callback(
|
||||||
[this](network::IPAddresses ips, network::IPAddress dns1_ip, network::IPAddress dns2_ip) {
|
[this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) {
|
||||||
this->state_callback_(dns1_ip, dns2_ip);
|
this->state_callback_(dns1_ip, dns2_ip);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); }
|
void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); }
|
||||||
|
|
||||||
void DNSAddressWifiInfo::state_callback_(network::IPAddress dns1_ip, network::IPAddress dns2_ip) {
|
void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) {
|
||||||
std::string dns_results = dns1_ip.str() + " " + dns2_ip.str();
|
std::string dns_results = dns1_ip.str() + " " + dns2_ip.str();
|
||||||
this->publish_state(dns_results);
|
this->publish_state(dns_results);
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t<wifi::W
|
|||||||
|
|
||||||
void SSIDWiFiInfo::setup() {
|
void SSIDWiFiInfo::setup() {
|
||||||
wifi::global_wifi_component->add_on_wifi_connect_state_callback(
|
wifi::global_wifi_component->add_on_wifi_connect_state_callback(
|
||||||
[this](const std::string &ssid, wifi::bssid_t bssid) { this->state_callback_(ssid); });
|
[this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(ssid); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); }
|
void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); }
|
||||||
@@ -100,12 +100,12 @@ void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_stat
|
|||||||
|
|
||||||
void BSSIDWiFiInfo::setup() {
|
void BSSIDWiFiInfo::setup() {
|
||||||
wifi::global_wifi_component->add_on_wifi_connect_state_callback(
|
wifi::global_wifi_component->add_on_wifi_connect_state_callback(
|
||||||
[this](const std::string &ssid, wifi::bssid_t bssid) { this->state_callback_(bssid); });
|
[this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(bssid); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); }
|
void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); }
|
||||||
|
|
||||||
void BSSIDWiFiInfo::state_callback_(wifi::bssid_t bssid) {
|
void BSSIDWiFiInfo::state_callback_(const wifi::bssid_t &bssid) {
|
||||||
char buf[18] = "unknown";
|
char buf[18] = "unknown";
|
||||||
if (mac_address_is_valid(bssid.data())) {
|
if (mac_address_is_valid(bssid.data())) {
|
||||||
format_mac_addr_upper(bssid.data(), buf);
|
format_mac_addr_upper(bssid.data(), buf);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor {
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void state_callback_(network::IPAddress dns1_ip, network::IPAddress dns2_ip);
|
void state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip);
|
||||||
};
|
};
|
||||||
|
|
||||||
class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor {
|
class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor {
|
||||||
@@ -54,7 +54,7 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor {
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void state_callback_(wifi::bssid_t bssid);
|
void state_callback_(const wifi::bssid_t &bssid);
|
||||||
};
|
};
|
||||||
|
|
||||||
class MacAddressWifiInfo : public Component, public text_sensor::TextSensor {
|
class MacAddressWifiInfo : public Component, public text_sensor::TextSensor {
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
|||||||
esptool==5.1.0
|
esptool==5.1.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20251013.0
|
esphome-dashboard==20251013.0
|
||||||
aioesphomeapi==42.7.0
|
aioesphomeapi==42.8.0
|
||||||
zeroconf==0.148.0
|
zeroconf==0.148.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.18.16 # dashboard_import
|
ruamel.yaml==0.18.16 # dashboard_import
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Then
|
# Then
|
||||||
assert 'bs_1->set_name("test bs1");' in main_cpp
|
assert 'bs_1->set_name_and_object_id("test bs1", "test_bs1");' in main_cpp
|
||||||
assert "bs_1->set_pin(" in main_cpp
|
assert "bs_1->set_pin(" in main_cpp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main):
|
|||||||
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
||||||
|
|
||||||
# Then
|
# Then
|
||||||
assert 'wol_1->set_name("wol_test_1");' in main_cpp
|
assert 'wol_1->set_name_and_object_id("wol_test_1", "wol_test_1");' in main_cpp
|
||||||
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
|
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def test_text_sets_mandatory_fields(generate_main):
|
|||||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||||
|
|
||||||
# Then
|
# Then
|
||||||
assert 'it_1->set_name("test 1 text");' in main_cpp
|
assert 'it_1->set_name_and_object_id("test 1 text", "test_1_text");' in main_cpp
|
||||||
|
|
||||||
|
|
||||||
def test_text_config_value_internal_set(generate_main):
|
def test_text_config_value_internal_set(generate_main):
|
||||||
|
|||||||
@@ -25,9 +25,18 @@ def test_text_sensor_sets_mandatory_fields(generate_main):
|
|||||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||||
|
|
||||||
# Then
|
# Then
|
||||||
assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp
|
assert (
|
||||||
assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp
|
'ts_1->set_name_and_object_id("Template Text Sensor 1", "template_text_sensor_1");'
|
||||||
assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp
|
in main_cpp
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
'ts_2->set_name_and_object_id("Template Text Sensor 2", "template_text_sensor_2");'
|
||||||
|
in main_cpp
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
'ts_3->set_name_and_object_id("Template Text Sensor 3", "template_text_sensor_3");'
|
||||||
|
in main_cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_text_sensor_config_value_internal_set(generate_main):
|
def test_text_sensor_config_value_internal_set(generate_main):
|
||||||
|
|||||||
@@ -537,6 +537,9 @@ lvgl:
|
|||||||
- tileview:
|
- tileview:
|
||||||
id: tileview_id
|
id: tileview_id
|
||||||
scrollbar_mode: active
|
scrollbar_mode: active
|
||||||
|
scroll_dir: all
|
||||||
|
scroll_elastic: true
|
||||||
|
scroll_momentum: true
|
||||||
on_value:
|
on_value:
|
||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
@@ -546,7 +549,10 @@ lvgl:
|
|||||||
- logger.log: "tile 1 is now showing"
|
- logger.log: "tile 1 is now showing"
|
||||||
tiles:
|
tiles:
|
||||||
- id: tile_1
|
- id: tile_1
|
||||||
|
scroll_snap_y: center
|
||||||
|
scroll_snap_x: start
|
||||||
layout: vertical
|
layout: vertical
|
||||||
|
pad_all: 6px
|
||||||
row: 0
|
row: 0
|
||||||
column: 0
|
column: 0
|
||||||
dir: ALL
|
dir: ALL
|
||||||
@@ -781,6 +787,18 @@ lvgl:
|
|||||||
arc_color: 0xFFFF00
|
arc_color: 0xFFFF00
|
||||||
focused:
|
focused:
|
||||||
arc_color: 0x808080
|
arc_color: 0x808080
|
||||||
|
on_click:
|
||||||
|
then:
|
||||||
|
- lvgl.arc.update:
|
||||||
|
id: lv_arc_1
|
||||||
|
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||||
|
min_value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||||
|
max_value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||||
|
start_angle: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||||
|
end_angle: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||||
|
rotation: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||||
|
change_rate: !lambda return (uint)((float)rand() / RAND_MAX * 100);
|
||||||
|
mode: NORMAL
|
||||||
- bar:
|
- bar:
|
||||||
id: bar_id
|
id: bar_id
|
||||||
align: top_mid
|
align: top_mid
|
||||||
@@ -881,6 +899,7 @@ lvgl:
|
|||||||
grid_columns: [40, fr(1), fr(1)]
|
grid_columns: [40, fr(1), fr(1)]
|
||||||
pad_row: 6px
|
pad_row: 6px
|
||||||
pad_column: 0
|
pad_column: 0
|
||||||
|
multiple_widgets_per_cell: true
|
||||||
widgets:
|
widgets:
|
||||||
- image:
|
- image:
|
||||||
grid_cell_row_pos: 0
|
grid_cell_row_pos: 0
|
||||||
@@ -905,6 +924,10 @@ lvgl:
|
|||||||
grid_cell_row_pos: 1
|
grid_cell_row_pos: 1
|
||||||
grid_cell_column_pos: 0
|
grid_cell_column_pos: 0
|
||||||
text: "Grid cell 1/0"
|
text: "Grid cell 1/0"
|
||||||
|
- label:
|
||||||
|
grid_cell_row_pos: 1
|
||||||
|
grid_cell_column_pos: 0
|
||||||
|
text: "Duplicate for 1/0"
|
||||||
- label:
|
- label:
|
||||||
styles: bdr_style
|
styles: bdr_style
|
||||||
grid_cell_row_pos: 1
|
grid_cell_row_pos: 1
|
||||||
@@ -1027,6 +1050,7 @@ lvgl:
|
|||||||
opa: 0%
|
opa: 0%
|
||||||
- id: page3
|
- id: page3
|
||||||
layout: Horizontal
|
layout: Horizontal
|
||||||
|
pad_all: 6px
|
||||||
widgets:
|
widgets:
|
||||||
- keyboard:
|
- keyboard:
|
||||||
id: lv_keyboard
|
id: lv_keyboard
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
esphome:
|
||||||
|
name: test-script-wait-on-boot
|
||||||
|
on_boot:
|
||||||
|
# Use default priority (600.0) which is same as ScriptWaitAction's setup priority
|
||||||
|
# This tests the race condition where on_boot runs before ScriptWaitAction::setup()
|
||||||
|
then:
|
||||||
|
- logger.log: "=== on_boot: Starting boot sequence ==="
|
||||||
|
- script.execute: show_start_page
|
||||||
|
- script.wait: show_start_page
|
||||||
|
- logger.log: "=== on_boot: First script completed, starting second ==="
|
||||||
|
- script.execute: flip_thru_pages
|
||||||
|
- script.wait: flip_thru_pages
|
||||||
|
- logger.log: "=== on_boot: All boot scripts completed successfully ==="
|
||||||
|
|
||||||
|
host:
|
||||||
|
|
||||||
|
api:
|
||||||
|
actions:
|
||||||
|
# Manual trigger for additional testing
|
||||||
|
- action: test_script_wait
|
||||||
|
then:
|
||||||
|
- logger.log: "=== Manual test: Starting ==="
|
||||||
|
- script.execute: show_start_page
|
||||||
|
- script.wait: show_start_page
|
||||||
|
- logger.log: "=== Manual test: First script completed ==="
|
||||||
|
- script.execute: flip_thru_pages
|
||||||
|
- script.wait: flip_thru_pages
|
||||||
|
- logger.log: "=== Manual test: All completed ==="
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
script:
|
||||||
|
# First script - simulates display initialization
|
||||||
|
- id: show_start_page
|
||||||
|
mode: single
|
||||||
|
then:
|
||||||
|
- logger.log: "show_start_page: Starting"
|
||||||
|
- delay: 100ms
|
||||||
|
- logger.log: "show_start_page: After delay 1"
|
||||||
|
- delay: 100ms
|
||||||
|
- logger.log: "show_start_page: Completed"
|
||||||
|
|
||||||
|
# Second script - simulates page flip sequence
|
||||||
|
- id: flip_thru_pages
|
||||||
|
mode: single
|
||||||
|
then:
|
||||||
|
- logger.log: "flip_thru_pages: Starting"
|
||||||
|
- delay: 50ms
|
||||||
|
- logger.log: "flip_thru_pages: Page 1"
|
||||||
|
- delay: 50ms
|
||||||
|
- logger.log: "flip_thru_pages: Page 2"
|
||||||
|
- delay: 50ms
|
||||||
|
- logger.log: "flip_thru_pages: Completed"
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""Integration test for script.wait during on_boot (issue #12043).
|
||||||
|
|
||||||
|
This test verifies that script.wait works correctly when triggered from on_boot.
|
||||||
|
The issue was that ScriptWaitAction::setup() unconditionally disabled the loop,
|
||||||
|
even if play_complex() had already been called (from an on_boot trigger at the
|
||||||
|
same priority level) and enabled it.
|
||||||
|
|
||||||
|
The race condition occurs because:
|
||||||
|
1. on_boot's default priority is 600.0 (setup_priority::DATA)
|
||||||
|
2. ScriptWaitAction's default setup priority is also DATA (600.0)
|
||||||
|
3. When they have the same priority, if on_boot runs first and triggers a script,
|
||||||
|
ScriptWaitAction::play_complex() enables the loop
|
||||||
|
4. Then ScriptWaitAction::setup() runs and unconditionally disables the loop
|
||||||
|
5. The wait never completes because the loop is disabled
|
||||||
|
|
||||||
|
The fix adds a conditional check (like WaitUntilAction has) to only disable the
|
||||||
|
loop in setup() if num_running_ is 0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_script_wait_on_boot(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that script.wait works correctly when triggered from on_boot.
|
||||||
|
|
||||||
|
This reproduces issue #12043 where script.wait would hang forever when
|
||||||
|
triggered from on_boot due to a race condition in ScriptWaitAction::setup().
|
||||||
|
"""
|
||||||
|
test_complete = asyncio.Event()
|
||||||
|
|
||||||
|
# Track progress through the boot sequence
|
||||||
|
boot_started = False
|
||||||
|
first_script_started = False
|
||||||
|
first_script_completed = False
|
||||||
|
first_wait_returned = False
|
||||||
|
second_script_started = False
|
||||||
|
second_script_completed = False
|
||||||
|
all_completed = False
|
||||||
|
|
||||||
|
# Patterns for boot sequence logs
|
||||||
|
boot_start_pattern = re.compile(r"on_boot: Starting boot sequence")
|
||||||
|
show_start_pattern = re.compile(r"show_start_page: Starting")
|
||||||
|
show_complete_pattern = re.compile(r"show_start_page: Completed")
|
||||||
|
first_wait_pattern = re.compile(r"on_boot: First script completed")
|
||||||
|
flip_start_pattern = re.compile(r"flip_thru_pages: Starting")
|
||||||
|
flip_complete_pattern = re.compile(r"flip_thru_pages: Completed")
|
||||||
|
all_complete_pattern = re.compile(r"on_boot: All boot scripts completed")
|
||||||
|
|
||||||
|
def check_output(line: str) -> None:
|
||||||
|
"""Check log output for boot sequence progress."""
|
||||||
|
nonlocal boot_started, first_script_started, first_script_completed
|
||||||
|
nonlocal first_wait_returned, second_script_started, second_script_completed
|
||||||
|
nonlocal all_completed
|
||||||
|
|
||||||
|
if boot_start_pattern.search(line):
|
||||||
|
boot_started = True
|
||||||
|
elif show_start_pattern.search(line):
|
||||||
|
first_script_started = True
|
||||||
|
elif show_complete_pattern.search(line):
|
||||||
|
first_script_completed = True
|
||||||
|
elif first_wait_pattern.search(line):
|
||||||
|
first_wait_returned = True
|
||||||
|
elif flip_start_pattern.search(line):
|
||||||
|
second_script_started = True
|
||||||
|
elif flip_complete_pattern.search(line):
|
||||||
|
second_script_completed = True
|
||||||
|
elif all_complete_pattern.search(line):
|
||||||
|
all_completed = True
|
||||||
|
test_complete.set()
|
||||||
|
|
||||||
|
async with (
|
||||||
|
run_compiled(yaml_config, line_callback=check_output),
|
||||||
|
api_client_connected() as client,
|
||||||
|
):
|
||||||
|
# Verify device info
|
||||||
|
device_info = await client.device_info()
|
||||||
|
assert device_info is not None
|
||||||
|
assert device_info.name == "test-script-wait-on-boot"
|
||||||
|
|
||||||
|
# Wait for on_boot sequence to complete
|
||||||
|
# The boot sequence should complete automatically
|
||||||
|
# Timeout is generous to allow for delays in the scripts
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
|
||||||
|
except TimeoutError:
|
||||||
|
# Build a detailed error message showing where the boot sequence got stuck
|
||||||
|
progress = []
|
||||||
|
if boot_started:
|
||||||
|
progress.append("boot started")
|
||||||
|
if first_script_started:
|
||||||
|
progress.append("show_start_page started")
|
||||||
|
if first_script_completed:
|
||||||
|
progress.append("show_start_page completed")
|
||||||
|
if first_wait_returned:
|
||||||
|
progress.append("first script.wait returned")
|
||||||
|
if second_script_started:
|
||||||
|
progress.append("flip_thru_pages started")
|
||||||
|
if second_script_completed:
|
||||||
|
progress.append("flip_thru_pages completed")
|
||||||
|
|
||||||
|
if not first_wait_returned and first_script_completed:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test timed out - script.wait hung after show_start_page completed! "
|
||||||
|
f"This is the issue #12043 bug. Progress: {', '.join(progress)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test timed out. Progress: {', '.join(progress) if progress else 'none'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the complete boot sequence executed in order
|
||||||
|
assert boot_started, "on_boot did not start"
|
||||||
|
assert first_script_started, "show_start_page did not start"
|
||||||
|
assert first_script_completed, "show_start_page did not complete"
|
||||||
|
assert first_wait_returned, "First script.wait did not return"
|
||||||
|
assert second_script_started, "flip_thru_pages did not start"
|
||||||
|
assert second_script_completed, "flip_thru_pages did not complete"
|
||||||
|
assert all_completed, "Boot sequence did not complete"
|
||||||
@@ -27,8 +27,13 @@ from esphome.helpers import sanitize, snake_case
|
|||||||
|
|
||||||
from .common import load_config_from_fixture
|
from .common import load_config_from_fixture
|
||||||
|
|
||||||
# Pre-compiled regex pattern for extracting object IDs from expressions
|
# Pre-compiled regex patterns for extracting object IDs from expressions
|
||||||
|
# Matches both old format: .set_object_id("obj_id")
|
||||||
|
# and new format: .set_name_and_object_id("name", "obj_id")
|
||||||
OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)')
|
OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)')
|
||||||
|
COMBINED_PATTERN = re.compile(
|
||||||
|
r'\.set_name_and_object_id\(["\'].*?["\']\s*,\s*["\'](.*?)["\']\)'
|
||||||
|
)
|
||||||
|
|
||||||
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers"
|
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers"
|
||||||
|
|
||||||
@@ -273,8 +278,10 @@ def setup_test_environment() -> Generator[list[str], None, None]:
|
|||||||
def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
|
def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
|
||||||
"""Extract the object ID that was set from the generated expressions."""
|
"""Extract the object ID that was set from the generated expressions."""
|
||||||
for expr in expressions:
|
for expr in expressions:
|
||||||
# Look for set_object_id calls with regex to handle various formats
|
# First try new combined format: .set_name_and_object_id("name", "obj_id")
|
||||||
# Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2')
|
if match := COMBINED_PATTERN.search(expr):
|
||||||
|
return match.group(1)
|
||||||
|
# Fall back to old format: .set_object_id("obj_id")
|
||||||
if match := OBJECT_ID_PATTERN.search(expr):
|
if match := OBJECT_ID_PATTERN.search(expr):
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user