Compare commits

..

41 Commits

Author SHA1 Message Date
Jesse Hills
6178ab7513 Merge pull request #9394 from esphome/bump-2025.7.0b1
2025.7.0b1
2025-07-09 19:33:49 +12:00
Jesse Hills
267574f24c Bump version to 2025.7.0b1 2025-07-09 12:06:52 +12:00
dependabot[bot]
5235c80781 Bump aioesphomeapi from 34.1.0 to 34.2.0 (#9391)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 23:54:33 +00:00
Merikei
0ccc5e340e [apds9960] Add 0x9E ID (#9392) 2025-07-08 23:52:30 +00:00
Craig Andrews
86c6e4da2a ESP_EXT1_WAKEUP_ANY_LOW is for s2/s3/c6/h2; ESP_EXT1_WAKEUP_ALL_LOW otherwise (#9387)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-09 11:30:06 +12:00
Jesse Hills
5c8b330eaa [esp32] Improve flexibility of `only_on_variant` (#9390) 2025-07-09 10:51:17 +12:00
Petr Kejval
4158a5c2a3 Add support for GL-R01 I2C - Time of Flight sensor (#8329)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-09 10:50:45 +12:00
Jesse Hills
05c5364490 [helpers] Fix `format_hex_pretty` resize without separator (#9389)
Co-authored-by: RubenKelevra <cyrond@gmail.com>
2025-07-08 22:13:21 +00:00
Jesse Hills
78eb236a4a [nfc] Update code to use `format_hex_pretty` (#9384) 2025-07-08 16:47:42 -05:00
Simonas Kazlauskas
691cc5f7dc lps22: add a component (#7540)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-07-09 09:13:58 +12:00
J. Nick Koston
b3d7f001af Fix race condition in scheduler string lifetime integration test (#9382) 2025-07-08 06:54:47 -05:00
tmpeh
3f8b691c32 Fix format string error in waveshare_epaper.cpp (#9322)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-08 04:39:07 +00:00
J. Nick Koston
a30f01d668 Fix integration test race condition by isolating PlatformIO directories (#9383) 2025-07-08 04:34:39 +00:00
Clyde Stubbs
4648804db6 [image] Add byte order option and unit tests (#9326) 2025-07-08 02:28:00 +00:00
functionpointer
51377b2625 hydreon_rgxx: remove precipitation_intensity from RG9 (#9367) 2025-07-08 14:27:33 +12:00
Jesse Hills
256f9f9943 [helpers] Improve `format_hex_pretty` (#9380) 2025-07-08 01:30:23 +00:00
J. Nick Koston
a72905191a Fix flaky test_api_conditional_memory and improve integration test patterns (#9379) 2025-07-08 11:08:21 +12:00
J. Nick Koston
7150f2806f Run integration tests only on Python 3.13 to reduce CI resource usage (#9377) 2025-07-07 22:14:34 +00:00
J. Nick Koston
ee8ee4e646 Optimize logger callback API by including message length parameter (#9368)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-07 22:00:03 +00:00
Steffen Arntz
fb357b8965 Fix brightness setting not working on SSD1305 128x32 OLEDs (#9376) 2025-07-08 09:25:11 +12:00
Edward Firmo
c4fac1a2ae [nextion] Optimize component memory usage with bitfield state management (#9373) 2025-07-08 09:21:14 +12:00
J. Nick Koston
42a1f6922f Eliminate bluetooth_proxy guard variable to save 8 bytes RAM (#9343) 2025-07-08 09:16:48 +12:00
J. Nick Koston
206659ddb8 Refactor voice assistant API methods to reduce code duplication (#9374) 2025-07-08 09:15:49 +12:00
J. Nick Koston
440de12e3f Don't compile unnecessary platform files (e.g. ESP8266 files on ESP32) (#9354) 2025-07-08 09:04:41 +12:00
J. Nick Koston
b122112d58 Refactor API entity update dispatch to reduce code duplication (#9372) 2025-07-08 08:51:17 +12:00
J. Nick Koston
fe258e1007 Refactor entity lookup methods with macros in preparation for device_id support (#9371) 2025-07-08 08:49:23 +12:00
J. Nick Koston
3976fd02ea Refactor duplicate socket read error handling in API frame helper (#9370) 2025-07-08 08:39:13 +12:00
J. Nick Koston
e58c793da2 Replace deprecated sprintf with snprintf in API protobuf code generation (#9365) 2025-07-08 08:38:41 +12:00
J. Nick Koston
90fb3680d4 Optimize logger performance by eliminating redundant strlen calls (#9369) 2025-07-08 08:36:36 +12:00
J. Nick Koston
832a787271 Fix format specifier warnings in QuantileFilter logging (#9364) 2025-07-08 08:35:27 +12:00
J. Nick Koston
29747fc730 Fix flaky test_api_conditional_memory by disabling API batch delay (#9360) 2025-07-08 08:35:11 +12:00
J. Nick Koston
e2de6ee29d Reduce core RAM usage by 40 bytes with static initialization optimizations (#9340) 2025-07-08 08:28:14 +12:00
J. Nick Koston
053feb5e3b Optimize entity icon memory usage with USE_ENTITY_ICON flag (#9337) 2025-07-08 08:22:40 +12:00
J. Nick Koston
31f36df4ba Reduce LightCall memory usage by 50 bytes per call (#9333) 2025-07-08 08:20:40 +12:00
J. Nick Koston
3ef392d433 Fix scheduler race conditions and add comprehensive test suite (#9348) 2025-07-08 07:57:55 +12:00
J. Nick Koston
138ff749f3 Optimize Bluetooth proxy batching and increase scan buffer capacity (#9328) 2025-07-08 07:34:12 +12:00
Edward Firmo
e88b8d10ec [nextion] Add optional device info storage configuration (#9366) 2025-07-07 12:04:01 -05:00
Jesse Hills
8147d117a0 [core] Move platform helper implementations into their own file (#9361) 2025-07-07 15:55:02 +00:00
Edward Firmo
c6f7e84256 [nextion] Review touch_sleep_timeout (#9345) 2025-07-07 07:30:34 -05:00
Keith Burzinski
db877e688a [ld2450] Clean-up for consistency, reduce CPU usage when idle (#9363) 2025-07-07 07:22:49 -05:00
Edward Firmo
4e25b6da7b [nextion] Optimize settings memory usage with compile-time defines (#9350)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-07 09:15:13 +00:00
92 changed files with 1695 additions and 1276 deletions

View File

@@ -214,17 +214,51 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native -n auto tests
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: |
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3
with:
token: ${{ secrets.CODECOV_TOKEN }}
integration-tests:
name: Run integration tests
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
run: |
. venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/
clang-format:
name: Check clang-format
runs-on: ubuntu-24.04
@@ -494,6 +528,7 @@ jobs:
- flake8
- pylint
- pytest
- integration-tests
- pyupgrade
- clang-tidy
- list-components

View File

@@ -170,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
@@ -254,6 +255,7 @@ esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow
esphome/components/lps22/* @nagisa
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
@@ -329,7 +331,6 @@ esphome/components/opentherm/* @olegtarasov
esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core
esphome/components/ota_base/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.7.0-dev
PROJECT_NUMBER = 2025.7.0b1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -23,7 +23,7 @@ void APDS9960::setup() {
return;
}
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
this->error_code_ = WRONG_ID;
this->mark_failed();
return;

View File

@@ -3,6 +3,7 @@ import base64
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.config_helpers import get_logger_level
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION,
@@ -23,7 +24,7 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_VARIABLES,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core import coroutine_with_priority
DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"]
@@ -320,7 +321,10 @@ def FILTER_SOURCE_FILES() -> list[str]:
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read
# all the way to the end even when ifdef'd out
if "HAS_PROTO_MESSAGE_DUMP" not in CORE.defines:
#
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
# which happens when the logger level is VERY_VERBOSE
if get_logger_level() != "VERY_VERBOSE":
return ["api_pb2_dump.cpp"]
return []

View File

@@ -45,14 +45,14 @@ static const int CAMERA_STOP_STREAM = 5000;
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object
#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if (entity_var == nullptr) \
if ((entity_var) == nullptr) \
return; \
auto call = entity_var->make_call();
auto call = (entity_var)->make_call();
// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found
#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if (entity_var == nullptr) \
if ((entity_var) == nullptr) \
return;
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
@@ -1176,66 +1176,53 @@ void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequ
#endif
#ifdef USE_VOICE_ASSISTANT
bool APIConnection::check_voice_assistant_api_connection_() const {
return voice_assistant::global_voice_assistant != nullptr &&
voice_assistant::global_voice_assistant->get_api_connection() == this;
}
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
}
}
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
if (!this->check_voice_assistant_api_connection_()) {
return;
}
if (msg.error) {
voice_assistant::global_voice_assistant->failed_to_start();
return;
}
if (msg.port == 0) {
// Use API Audio
voice_assistant::global_voice_assistant->start_streaming();
} else {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
this->helper_->getpeername((struct sockaddr *) &storage, &len);
voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
}
if (msg.error) {
voice_assistant::global_voice_assistant->failed_to_start();
return;
}
if (msg.port == 0) {
// Use API Audio
voice_assistant::global_voice_assistant->start_streaming();
} else {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
this->helper_->getpeername((struct sockaddr *) &storage, &len);
voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
}
};
void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
if (this->check_voice_assistant_api_connection_()) {
voice_assistant::global_voice_assistant->on_event(msg);
}
}
void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
if (this->check_voice_assistant_api_connection_()) {
voice_assistant::global_voice_assistant->on_audio(msg);
}
};
void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
if (this->check_voice_assistant_api_connection_()) {
voice_assistant::global_voice_assistant->on_timer_event(msg);
}
};
void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
if (this->check_voice_assistant_api_connection_()) {
voice_assistant::global_voice_assistant->on_announce(msg);
}
}
@@ -1243,35 +1230,29 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp;
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return resp;
}
auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word;
resp_wake_word.id = wake_word.id;
resp_wake_word.wake_word = wake_word.wake_word;
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
resp.available_wake_words.push_back(std::move(resp_wake_word));
}
for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words;
if (!this->check_voice_assistant_api_connection_()) {
return resp;
}
auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word;
resp_wake_word.id = wake_word.id;
resp_wake_word.wake_word = wake_word.wake_word;
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
resp.available_wake_words.push_back(std::move(resp_wake_word));
}
for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words;
return resp;
}
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
if (this->check_voice_assistant_api_connection_()) {
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
}
}

View File

@@ -301,6 +301,11 @@ class APIConnection : public APIServerConnection {
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
#ifdef USE_VOICE_ASSISTANT
// Helper to check voice assistant validity and connection ownership
inline bool check_voice_assistant_api_connection_() const;
#endif
// Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size();

View File

@@ -263,7 +263,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for entities without extra parameters
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
@@ -272,7 +272,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
@@ -358,7 +358,13 @@ void APIServer::on_event(event::Event *obj, const std::string &event_type) {
#endif
#ifdef USE_UPDATE
API_DISPATCH_UPDATE(update::UpdateEntity, update)
// Update is a special case - the method is called on_update, not on_update_update
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_update_state(obj);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL

View File

@@ -59,10 +59,12 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
// This achieves ~97% WiFi MTU utilization while staying under the limit
static constexpr size_t FLUSH_BATCH_SIZE = 16;
// Global batch buffer to avoid guard variable (saves 8 bytes)
namespace {
// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
// This is initialized at program startup before any threads
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
} // namespace
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }

View File

@@ -2,6 +2,7 @@
CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers"

View File

@@ -1,6 +1,6 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import time
from esphome.components import esp32, time
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32,
@@ -116,12 +116,20 @@ def validate_pin_number(value):
return value
def validate_config(config):
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config:
raise cv.Invalid("ESP32-C3 does not support wakeup from touch.")
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config:
raise cv.Invalid("ESP32-C3 does not support wakeup from ext1")
return config
def _validate_ex1_wakeup_mode(value):
if value == "ALL_LOW":
esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
if value == "ANY_LOW":
esp32.only_on_variant(
supported=[
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
],
msg_prefix="ANY_LOW",
)(value)
return value
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
@@ -148,6 +156,7 @@ WAKEUP_PIN_MODES = {
esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
EXT1_WAKEUP_MODES = {
"ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW,
"ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW,
"ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH,
}
@@ -187,16 +196,28 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32,
esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1"
),
cv.Schema(
{
cv.Required(CONF_PINS): cv.ensure_list(
pins.internal_gpio_input_pin_schema, validate_pin_number
),
cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True),
cv.Required(CONF_MODE): cv.All(
cv.enum(EXT1_WAKEUP_MODES, upper=True),
_validate_ex1_wakeup_mode,
),
}
),
),
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean),
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
cv.only_on_esp32,
esp32.only_on_variant(
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch"
),
cv.boolean,
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),

View File

@@ -189,7 +189,7 @@ def get_download_types(storage_json):
]
def only_on_variant(*, supported=None, unsupported=None):
def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"):
"""Config validator for features only available on some ESP32 variants."""
if supported is not None and not isinstance(supported, list):
supported = [supported]
@@ -200,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None):
variant = get_esp32_variant()
if supported is not None and variant not in supported:
raise cv.Invalid(
f"This feature is only available on {', '.join(supported)}"
f"{msg_prefix} is only available on {', '.join(supported)}"
)
if unsupported is not None and variant in unsupported:
raise cv.Invalid(
f"This feature is not available on {', '.join(unsupported)}"
f"{msg_prefix} is not available on {', '.join(unsupported)}"
)
return obj

View File

@@ -0,0 +1,68 @@
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "gl_r01_i2c.h"
namespace esphome {
namespace gl_r01_i2c {
static const char *const TAG = "gl_r01_i2c";
// Register definitions from datasheet
static const uint8_t REG_VERSION = 0x00;
static const uint8_t REG_DISTANCE = 0x02;
static const uint8_t REG_TRIGGER = 0x10;
static const uint8_t CMD_TRIGGER = 0xB0;
static const uint8_t RESTART_CMD1 = 0x5A;
static const uint8_t RESTART_CMD2 = 0xA5;
static const uint8_t READ_DELAY = 40; // minimum milliseconds from datasheet to safely read measurement result
void GLR01I2CComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C...");
// Verify sensor presence
if (!this->read_byte_16(REG_VERSION, &this->version_)) {
ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!");
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_);
}
void GLR01I2CComponent::dump_config() {
ESP_LOGCONFIG(TAG, "GL-R01 I2C:");
ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_);
LOG_I2C_DEVICE(this);
LOG_SENSOR(" ", "Distance", this);
}
void GLR01I2CComponent::update() {
// Trigger a new measurement
if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) {
ESP_LOGE(TAG, "Failed to trigger measurement!");
this->status_set_warning();
return;
}
// Schedule reading the result after the read delay
this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); });
}
void GLR01I2CComponent::read_distance_() {
uint16_t distance = 0;
if (!this->read_byte_16(REG_DISTANCE, &distance)) {
ESP_LOGE(TAG, "Failed to read distance value!");
this->status_set_warning();
return;
}
if (distance == 0xFFFF) {
ESP_LOGW(TAG, "Invalid measurement received!");
this->status_set_warning();
} else {
ESP_LOGV(TAG, "Distance: %umm", distance);
this->publish_state(distance);
this->status_clear_warning();
}
}
} // namespace gl_r01_i2c
} // namespace esphome

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace gl_r01_i2c {
class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent {
public:
void setup() override;
void dump_config() override;
void update() override;
protected:
void read_distance_();
uint16_t version_{0};
};
} // namespace gl_r01_i2c
} // namespace esphome

View File

@@ -0,0 +1,36 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIMETER,
)
CODEOWNERS = ["@pkejval"]
DEPENDENCIES = ["i2c"]
gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c")
GLR01I2CComponent = gl_r01_i2c_ns.class_(
"GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
GLR01I2CComponent,
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x74))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
await i2c.register_i2c_device(var, config)

View File

@@ -111,8 +111,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
unit_of_measurement=UNIT_INTENSITY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
state_class=STATE_CLASS_MEASUREMENT,
icon="mdi:weather-rainy",
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,

View File

@@ -10,8 +10,10 @@ from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER
import esphome.config_validation as cv
from esphome.const import (
CONF_DEFAULTS,
CONF_DITHER,
CONF_FILE,
CONF_ICON,
@@ -38,6 +40,7 @@ CONF_OPAQUE = "opaque"
CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha"
CONF_IMAGES = "images"
TRANSPARENCY_TYPES = (
CONF_OPAQUE,
@@ -188,6 +191,10 @@ class ImageRGB565(ImageEncoder):
dither,
invert_alpha,
)
self.big_endian = True
def set_big_endian(self, big_endian: bool) -> None:
self.big_endian = big_endian
def convert(self, image, path):
return image.convert("RGBA")
@@ -205,10 +212,16 @@ class ImageRGB565(ImageEncoder):
g = 1
b = 0
rgb = (r << 11) | (g << 5) | b
self.data[self.index] = rgb >> 8
self.index += 1
self.data[self.index] = rgb & 0xFF
self.index += 1
if self.big_endian:
self.data[self.index] = rgb >> 8
self.index += 1
self.data[self.index] = rgb & 0xFF
self.index += 1
else:
self.data[self.index] = rgb & 0xFF
self.index += 1
self.data[self.index] = rgb >> 8
self.index += 1
if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha:
a ^= 0xFF
@@ -364,7 +377,7 @@ def validate_file_shorthand(value):
value = cv.string_strict(value)
parts = value.strip().split(":")
if len(parts) == 2 and parts[0] in MDI_SOURCES:
match = re.match(r"[a-zA-Z0-9\-]+", parts[1])
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
if match is None:
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
return download_gh_svg(parts[1], parts[0])
@@ -434,20 +447,29 @@ def validate_type(image_types):
def validate_settings(value):
type = value[CONF_TYPE]
"""
Validate the settings for a single image configuration.
"""
conf_type = value[CONF_TYPE]
type_class = IMAGE_TYPE[conf_type]
transparency = value[CONF_TRANSPARENCY].lower()
allow_config = IMAGE_TYPE[type].allow_config
if transparency not in allow_config:
if transparency not in type_class.allow_config:
raise cv.Invalid(
f"Image format '{type}' cannot have transparency: {transparency}"
f"Image format '{conf_type}' cannot have transparency: {transparency}"
)
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
if (
invert_alpha
and transparency != CONF_ALPHA_CHANNEL
and CONF_INVERT_ALPHA not in allow_config
and CONF_INVERT_ALPHA not in type_class.allow_config
):
raise cv.Invalid("No alpha channel to invert")
if value.get(CONF_BYTE_ORDER) is not None and not callable(
getattr(type_class, "set_big_endian", None)
):
raise cv.Invalid(
f"Image format '{conf_type}' does not support byte order configuration"
)
if file := value.get(CONF_FILE):
file = Path(file)
if is_svg_file(file):
@@ -456,31 +478,82 @@ def validate_settings(value):
try:
Image.open(file)
except UnidentifiedImageError as exc:
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
raise cv.Invalid(
f"File can't be opened as image: {file.absolute()}"
) from exc
return value
IMAGE_ID_SCHEMA = {
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
OPTIONS_SCHEMA = {
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
}
OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
# image schema with no defaults, used with `CONF_IMAGES` in the config
IMAGE_SCHEMA_NO_DEFAULTS = {
**IMAGE_ID_SCHEMA,
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
}
BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
**IMAGE_ID_SCHEMA,
**OPTIONS_SCHEMA,
}
).add_extra(validate_settings)
IMAGE_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
}
)
def validate_defaults(value):
"""
Validate the options for images with defaults
"""
defaults = value[CONF_DEFAULTS]
result = []
for index, image in enumerate(value[CONF_IMAGES]):
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
if type is None:
raise cv.Invalid(
"Type is required either in the image config or in the defaults",
path=[CONF_IMAGES, index],
)
type_class = IMAGE_TYPE[type]
# A default byte order should be simply ignored if the type does not support it
available_options = [*OPTIONS]
if (
not callable(getattr(type_class, "set_big_endian", None))
and CONF_BYTE_ORDER not in image
):
available_options.remove(CONF_BYTE_ORDER)
config = {
**{key: image.get(key, defaults.get(key)) for key in available_options},
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
}
validate_settings(config)
result.append(config)
return result
def typed_image_schema(image_type):
"""
Construct a schema for a specific image type, allowing transparency options
@@ -523,10 +596,33 @@ def typed_image_schema(image_type):
# The config schema can be a (possibly empty) single list of images,
# or a dictionary of image types each with a list of images
CONFIG_SCHEMA = cv.Any(
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
cv.ensure_list(IMAGE_SCHEMA),
)
# or a dictionary with keys `defaults:` and `images:`
def _config_schema(config):
if isinstance(config, list):
return cv.Schema([IMAGE_SCHEMA])(config)
if not isinstance(config, dict):
raise cv.Invalid(
"Badly formed image configuration, expected a list or a dictionary"
)
if CONF_DEFAULTS in config or CONF_IMAGES in config:
return validate_defaults(
cv.Schema(
{
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
}
)(config)
)
if CONF_ID in config or CONF_FILE in config:
return cv.ensure_list(IMAGE_SCHEMA)([config])
return cv.Schema(
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
)(config)
CONFIG_SCHEMA = _config_schema
async def write_image(config, all_frames=False):
@@ -585,6 +681,9 @@ async def write_image(config, all_frames=False):
total_rows = height * frame_count
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
if byte_order := config.get(CONF_BYTE_ORDER):
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
for frame_index in range(frame_count):
image.seek(frame_index)
pixels = encoder.convert(image.resize((width, height)), path).getdata()

View File

@@ -90,6 +90,25 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Implementation for ESP8266 with flash string support.
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
//
// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
// The buffer is used in a special way to avoid allocating extra memory:
//
// Memory layout during execution:
// Step 1: Copy format string from flash to buffer
// tx_buffer_: [format_string][null][.....................]
// tx_buffer_at_: ------------------^
// msg_start: saved here -----------^
//
// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
// and writes formatted output starting at msg_start position
// tx_buffer_: [format_string][null][formatted_message][null]
// tx_buffer_at_: -------------------------------------^
//
// Step 3: Output the formatted message (starting at msg_start)
// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
// which points to: [formatted_message][null]
//
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT
if (level > this->level_for(tag) || global_recursion_guard_)
@@ -121,7 +140,8 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_ + msg_start);
}
size_t msg_length = this->tx_buffer_at_ - msg_start - 1; // -1 to exclude null terminator
size_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
global_recursion_guard_ = false;

View File

View File

@@ -0,0 +1,75 @@
#include "lps22.h"
namespace esphome {
namespace lps22 {
static constexpr const char *const TAG = "lps22";
static constexpr uint8_t WHO_AM_I = 0x0F;
static constexpr uint8_t LPS22HB_ID = 0xB1;
static constexpr uint8_t LPS22HH_ID = 0xB3;
static constexpr uint8_t CTRL_REG2 = 0x11;
static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
static constexpr uint8_t STATUS = 0x27;
static constexpr uint8_t STATUS_T_DA_MASK = 0b10;
static constexpr uint8_t STATUS_P_DA_MASK = 0b01;
static constexpr uint8_t TEMP_L = 0x2b;
static constexpr uint8_t PRES_OUT_XL = 0x28;
static constexpr uint8_t REF_P_XL = 0x28;
static constexpr uint8_t READ_ATTEMPTS = 10;
static constexpr uint8_t READ_INTERVAL = 5;
static constexpr float PRESSURE_SCALE = 1.0f / 4096.0f;
static constexpr float TEMPERATURE_SCALE = 0.01f;
void LPS22Component::setup() {
uint8_t value = 0x00;
this->read_register(WHO_AM_I, &value, 1);
if (value != LPS22HB_ID && value != LPS22HH_ID) {
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
this->mark_failed();
}
}
void LPS22Component::dump_config() {
ESP_LOGCONFIG(TAG, "LPS22:");
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
void LPS22Component::update() {
uint8_t value = 0x00;
this->read_register(CTRL_REG2, &value, 1);
value |= CTRL_REG2_ONE_SHOT_MASK;
this->write_register(CTRL_REG2, &value, 1);
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
}
RetryResult LPS22Component::try_read_() {
uint8_t value = 0x00;
this->read_register(STATUS, &value, 1);
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
if ((value & expected_status_mask) != expected_status_mask) {
ESP_LOGD(TAG, "STATUS not ready: %x", value);
return RetryResult::RETRY;
}
if (this->temperature_sensor_ != nullptr) {
uint8_t t_buf[2]{0};
this->read_register(TEMP_L, t_buf, 2);
int16_t encoded = static_cast<int16_t>(encode_uint16(t_buf[1], t_buf[0]));
float temp = TEMPERATURE_SCALE * static_cast<float>(encoded);
this->temperature_sensor_->publish_state(temp);
}
if (this->pressure_sensor_ != nullptr) {
uint8_t p_buf[3]{0};
this->read_register(PRES_OUT_XL, p_buf, 3);
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
}
return RetryResult::DONE;
}
} // namespace lps22
} // namespace esphome

View File

@@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace lps22 {
class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
void setup() override;
void update() override;
void dump_config() override;
protected:
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
RetryResult try_read_();
};
} // namespace lps22
} // namespace esphome

View File

@@ -0,0 +1,58 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
CONF_TEMPERATURE,
CONF_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
ICON_THERMOMETER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
)
CODEOWNERS = ["@nagisa"]
DEPENDENCIES = ["i2c"]
lps22 = cg.esphome_ns.namespace("lps22")
LPS22Component = lps22.class_("LPS22Component", cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LPS22Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x5D)) # can also be 0x5C
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))

View File

@@ -11,6 +11,7 @@ CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
CONF_COMMAND_SPACING = "command_spacing"
CONF_COMPONENT_NAME = "component_name"
CONF_DUMP_DEVICE_INFO = "dump_device_info"
CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
CONF_FONT_ID = "font_id"
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"

View File

@@ -44,7 +44,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti
return;
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->needs_to_send_update_ = false;

View File

@@ -15,6 +15,7 @@ from . import Nextion, nextion_ns, nextion_ref
from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH,
CONF_COMMAND_SPACING,
CONF_DUMP_DEVICE_INFO,
CONF_EXIT_REPARSE_ON_START,
CONF_MAX_COMMANDS_PER_LOOP,
CONF_MAX_QUEUE_SIZE,
@@ -57,6 +58,7 @@ CONFIG_SCHEMA = (
cv.positive_time_period_milliseconds,
cv.Range(max=TimePeriod(milliseconds=255)),
),
cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean,
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t,
cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int,
@@ -95,7 +97,9 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean,
cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
cv.Optional(CONF_TFT_URL): cv.url,
cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535),
cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any(
0, cv.int_range(min=3, max=65535)
),
cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t,
}
)
@@ -172,9 +176,14 @@ async def to_code(config):
cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START]))
if config[CONF_DUMP_DEVICE_INFO]:
cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO")
cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE]))
if config[CONF_EXIT_REPARSE_ON_START]:
cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START")
if config[CONF_SKIP_CONNECTION_HANDSHAKE]:
cg.add_define("USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE")
if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP):
cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP")

View File

@@ -13,14 +13,11 @@ void Nextion::setup() {
this->is_setup_ = false;
this->connection_state_.ignore_is_setup_ = true;
// Wake up the nextion
this->send_command_("bkcmd=0");
this->send_command_("sleep=0");
// Wake up the nextion and ensure clean communication state
this->send_command_("sleep=0"); // Exit sleep mode if sleeping
this->send_command_("bkcmd=0"); // Disable return data during init sequence
this->send_command_("bkcmd=0");
this->send_command_("sleep=0");
// Reboot it
// Reset device for clean state - critical for reliable communication
this->send_command_("rest");
this->connection_state_.ignore_is_setup_ = false;
@@ -51,24 +48,19 @@ bool Nextion::check_connect_() {
if (this->connection_state_.is_connected_)
return true;
// Check if the handshake should be skipped for the Nextion connection
if (this->skip_connection_handshake_) {
// Log the connection status without handshake
ESP_LOGW(TAG, "Connected (no handshake)");
// Set the connection status to true
this->connection_state_.is_connected_ = true;
// Return true indicating the connection is set
return true;
}
#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake
this->is_connected_ = true; // Set the connection status to true
return true; // Return true indicating the connection is set
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
if (this->comok_sent_ == 0) {
this->reset_(false);
this->connection_state_.ignore_is_setup_ = true;
this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating
if (this->exit_reparse_on_start_) {
this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
}
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
this->send_command_("connect");
this->comok_sent_ = App.get_loop_component_start_time();
@@ -94,7 +86,7 @@ bool Nextion::check_connect_() {
for (size_t i = 0; i < response.length(); i++) {
ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]);
}
#endif
#endif // NEXTION_PROTOCOL_LOG
ESP_LOGW(TAG, "Not connected");
comok_sent_ = 0;
@@ -118,11 +110,19 @@ bool Nextion::check_connect_() {
this->is_detected_ = (connect_info.size() == 7);
if (this->is_detected_) {
ESP_LOGN(TAG, "Connect info: %zu", connect_info.size());
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
this->device_model_ = connect_info[2];
this->firmware_version_ = connect_info[3];
this->serial_number_ = connect_info[5];
this->flash_size_ = connect_info[6];
#else // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
ESP_LOGI(TAG,
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n",
connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str());
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
} else {
ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str());
}
@@ -130,6 +130,7 @@ bool Nextion::check_connect_() {
this->connection_state_.ignore_is_setup_ = false;
this->dump_config();
return true;
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
}
void Nextion::reset_(bool reset_nextion) {
@@ -144,29 +145,33 @@ void Nextion::reset_(bool reset_nextion) {
void Nextion::dump_config() {
ESP_LOGCONFIG(TAG, "Nextion:");
if (this->skip_connection_handshake_) {
ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_));
} else {
ESP_LOGCONFIG(TAG,
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s",
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str());
}
#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
ESP_LOGCONFIG(TAG, " Skip handshake: YES");
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
ESP_LOGCONFIG(TAG,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n"
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Exit reparse: YES\n"
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Wake On Touch: %s\n"
" Exit reparse: %s",
YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_));
" Touch Timeout: %" PRIu16,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str(),
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
ESP_LOGCONFIG(TAG, " Max commands per loop: %u", this->max_commands_per_loop_);
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (this->touch_sleep_timeout_ != 0) {
ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu16, this->touch_sleep_timeout_);
}
if (this->wake_up_page_ != 255) {
ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_);
}
@@ -314,6 +319,10 @@ void Nextion::loop() {
this->set_wake_up_page(this->wake_up_page_);
}
if (this->touch_sleep_timeout_ != 0) {
this->set_touch_sleep_timeout(this->touch_sleep_timeout_);
}
this->connection_state_.ignore_is_setup_ = false;
}

View File

@@ -932,21 +932,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void set_backlight_brightness(float brightness);
/**
* Sets whether the Nextion display should skip the connection handshake process.
* @param skip_handshake True or false. When skip_connection_handshake is true,
* the connection will be established without performing the handshake.
* This can be useful when using Nextion Simulator.
*
* Example:
* ```cpp
* it.set_skip_connection_handshake(true);
* ```
*
* When set to true, the display will be marked as connected without performing a handshake.
*/
void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; }
/**
* Sets Nextion mode between sleep and awake
* @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode.
@@ -1179,18 +1164,39 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void update_components_by_prefix(const std::string &prefix);
/**
* Set the touch sleep timeout of the display.
* @param timeout Timeout in seconds.
* Set the touch sleep timeout of the display using the `thsp` command.
*
* Sets internal No-touch-then-sleep timer to specified value in seconds.
* Nextion will auto-enter sleep mode if and when this timer expires.
*
* @param touch_sleep_timeout Timeout in seconds.
* Range: 3 to 65535 seconds (minimum 3 seconds, maximum ~18 hours 12 minutes 15 seconds)
* Use 0 to disable touch sleep timeout.
*
* @note Once `thsp` is set, it will persist until reboot or reset. The Nextion device
* needs to exit sleep mode to issue `thsp=0` to disable sleep on no touch.
*
* @note The display will only wake up by a restart or by setting up `thup` (auto wake on touch).
* See set_auto_wake_on_touch() to configure wake behavior.
*
* Example:
* ```cpp
* // Set 30 second touch timeout
* it.set_touch_sleep_timeout(30);
*
* // Set maximum timeout (~18 hours)
* it.set_touch_sleep_timeout(65535);
*
* // Disable touch sleep timeout
* it.set_touch_sleep_timeout(0);
* ```
*
* After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
* `thup`.
* Related Nextion instruction: `thsp=<value>`
*
* @see set_auto_wake_on_touch() Configure automatic wake on touch
* @see sleep() Manually control sleep state
*/
void set_touch_sleep_timeout(uint16_t touch_sleep_timeout);
void set_touch_sleep_timeout(uint16_t touch_sleep_timeout = 0);
/**
* Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
@@ -1236,20 +1242,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void set_auto_wake_on_touch(bool auto_wake_on_touch);
/**
* Sets if Nextion should exit the active reparse mode before the "connect" command is sent
* @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command
* will be sent before requesting the connection from Nextion.
*
* Example:
* ```cpp
* it.set_exit_reparse_on_start(true);
* ```
*
* The display will be requested to leave active reparse mode before setup.
*/
void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; }
/**
* @brief Retrieves the number of commands pending in the Nextion command queue.
*
@@ -1292,7 +1284,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
* the Nextion display. A connection is considered established when:
*
* - The initial handshake with the display is completed successfully, or
* - The handshake is skipped via skip_connection_handshake_ flag
* - The handshake is skipped via USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE flag
*
* The connection status is particularly useful when:
* - Troubleshooting communication issues
@@ -1358,8 +1350,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
#ifdef USE_NEXTION_CONF_START_UP_PAGE
uint8_t start_up_page_ = 255;
#endif // USE_NEXTION_CONF_START_UP_PAGE
bool exit_reparse_on_start_ = false;
bool skip_connection_handshake_ = false;
bool auto_wake_on_touch_ = true;
/**
* Manually send a raw command to the display and don't wait for an acknowledgement packet.
@@ -1466,10 +1457,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
optional<nextion_writer_t> writer_;
optional<float> brightness_;
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
std::string device_model_;
std::string firmware_version_;
std::string serial_number_;
std::string flash_size_;
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
void remove_front_no_sensors_();

View File

@@ -15,14 +15,15 @@ void Nextion::set_wake_up_page(uint8_t wake_up_page) {
this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
}
void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) {
if (touch_sleep_timeout < 3) {
ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)");
return;
void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) {
// Validate range: Nextion thsp command requires min 3, max 65535 seconds (0 disables)
if (touch_sleep_timeout != 0 && touch_sleep_timeout < 3) {
this->touch_sleep_timeout_ = 3; // Auto-correct to minimum valid value
} else {
this->touch_sleep_timeout_ = touch_sleep_timeout;
}
this->touch_sleep_timeout_ = touch_sleep_timeout;
this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true);
this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true);
}
void Nextion::sleep(bool sleep) {

View File

@@ -8,8 +8,8 @@ void NextionComponent::set_background_color(Color bco) {
return; // This is a variable. no need to set color
}
this->bco_ = bco;
this->bco_needs_update_ = true;
this->bco_is_set_ = true;
this->component_flags_.bco_needs_update = true;
this->component_flags_.bco_is_set = true;
this->update_component_settings();
}
@@ -19,8 +19,8 @@ void NextionComponent::set_background_pressed_color(Color bco2) {
}
this->bco2_ = bco2;
this->bco2_needs_update_ = true;
this->bco2_is_set_ = true;
this->component_flags_.bco2_needs_update = true;
this->component_flags_.bco2_is_set = true;
this->update_component_settings();
}
@@ -29,8 +29,8 @@ void NextionComponent::set_foreground_color(Color pco) {
return; // This is a variable. no need to set color
}
this->pco_ = pco;
this->pco_needs_update_ = true;
this->pco_is_set_ = true;
this->component_flags_.pco_needs_update = true;
this->component_flags_.pco_is_set = true;
this->update_component_settings();
}
@@ -39,8 +39,8 @@ void NextionComponent::set_foreground_pressed_color(Color pco2) {
return; // This is a variable. no need to set color
}
this->pco2_ = pco2;
this->pco2_needs_update_ = true;
this->pco2_is_set_ = true;
this->component_flags_.pco2_needs_update = true;
this->component_flags_.pco2_is_set = true;
this->update_component_settings();
}
@@ -49,8 +49,8 @@ void NextionComponent::set_font_id(uint8_t font_id) {
return; // This is a variable. no need to set color
}
this->font_id_ = font_id;
this->font_id_needs_update_ = true;
this->font_id_is_set_ = true;
this->component_flags_.font_id_needs_update = true;
this->component_flags_.font_id_is_set = true;
this->update_component_settings();
}
@@ -58,20 +58,20 @@ void NextionComponent::set_visible(bool visible) {
if (this->variable_name_ == this->variable_name_to_send_) {
return; // This is a variable. no need to set color
}
this->visible_ = visible;
this->visible_needs_update_ = true;
this->visible_is_set_ = true;
this->component_flags_.visible = visible;
this->component_flags_.visible_needs_update = true;
this->component_flags_.visible_is_set = true;
this->update_component_settings();
}
void NextionComponent::update_component_settings(bool force_update) {
if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ ||
(!this->visible_needs_update_ && !this->visible_)) {
if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->component_flags_.visible_is_set ||
(!this->component_flags_.visible_needs_update && !this->component_flags_.visible)) {
this->needs_to_send_update_ = true;
return;
}
if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) {
if (this->component_flags_.visible_needs_update || (force_update && this->component_flags_.visible_is_set)) {
std::string name_to_send = this->variable_name_;
size_t pos = name_to_send.find_last_of('.');
@@ -79,9 +79,9 @@ void NextionComponent::update_component_settings(bool force_update) {
name_to_send = name_to_send.substr(pos + 1);
}
this->visible_needs_update_ = false;
this->component_flags_.visible_needs_update = false;
if (this->visible_) {
if (this->component_flags_.visible) {
this->nextion_->show_component(name_to_send.c_str());
this->send_state_to_nextion();
} else {
@@ -90,26 +90,26 @@ void NextionComponent::update_component_settings(bool force_update) {
}
}
if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) {
if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_);
this->bco_needs_update_ = false;
this->component_flags_.bco_needs_update = false;
}
if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) {
if (this->component_flags_.bco2_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_);
this->bco2_needs_update_ = false;
this->component_flags_.bco2_needs_update = false;
}
if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) {
if (this->component_flags_.pco_needs_update || (force_update && this->component_flags_.pco_is_set)) {
this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_);
this->pco_needs_update_ = false;
this->component_flags_.pco_needs_update = false;
}
if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) {
if (this->component_flags_.pco2_needs_update || (force_update && this->component_flags_.pco2_is_set)) {
this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_);
this->pco2_needs_update_ = false;
this->component_flags_.pco2_needs_update = false;
}
if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) {
if (this->component_flags_.font_id_needs_update || (force_update && this->component_flags_.font_id_is_set)) {
this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_);
this->font_id_needs_update_ = false;
this->component_flags_.font_id_needs_update = false;
}
}
} // namespace nextion

View File

@@ -21,29 +21,64 @@ class NextionComponent : public NextionComponentBase {
void set_visible(bool visible);
protected:
/**
* @brief Constructor initializes component state with visible=true (default state)
*/
NextionComponent() {
component_flags_ = {}; // Zero-initialize all state
component_flags_.visible = 1; // Set default visibility to true
}
NextionBase *nextion_;
bool bco_needs_update_ = false;
bool bco_is_set_ = false;
Color bco_;
bool bco2_needs_update_ = false;
bool bco2_is_set_ = false;
Color bco2_;
bool pco_needs_update_ = false;
bool pco_is_set_ = false;
Color pco_;
bool pco2_needs_update_ = false;
bool pco2_is_set_ = false;
Color pco2_;
// Color and styling properties
Color bco_; // Background color
Color bco2_; // Pressed background color
Color pco_; // Foreground color
Color pco2_; // Pressed foreground color
uint8_t font_id_ = 0;
bool font_id_needs_update_ = false;
bool font_id_is_set_ = false;
bool visible_ = true;
bool visible_needs_update_ = false;
bool visible_is_set_ = false;
/**
* @brief Component state management using compact bitfield structure
*
* Stores all component state flags and properties in a single 16-bit bitfield
* for efficient memory usage and improved cache locality.
*
* Each component property maintains two state flags:
* - needs_update: Indicates the property requires synchronization with the display
* - is_set: Tracks whether the property has been explicitly configured
*
* The visible field stores both the update flags and the actual visibility state.
*/
struct ComponentState {
// Background color flags
uint16_t bco_needs_update : 1;
uint16_t bco_is_set : 1;
// void send_state_to_nextion() = 0;
// Pressed background color flags
uint16_t bco2_needs_update : 1;
uint16_t bco2_is_set : 1;
// Foreground color flags
uint16_t pco_needs_update : 1;
uint16_t pco_is_set : 1;
// Pressed foreground color flags
uint16_t pco2_needs_update : 1;
uint16_t pco2_is_set : 1;
// Font ID flags
uint16_t font_id_needs_update : 1;
uint16_t font_id_is_set : 1;
// Visibility flags
uint16_t visible_needs_update : 1;
uint16_t visible_is_set : 1;
uint16_t visible : 1; // Actual visibility state
// Reserved bits for future expansion
uint16_t reserved : 3;
} component_flags_;
};
} // namespace nextion
} // namespace esphome

View File

@@ -53,7 +53,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
if (this->wave_chan_id_ == UINT8_MAX) {
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->needs_to_send_update_ = false;

View File

@@ -28,7 +28,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) {
return;
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->needs_to_send_update_ = false;

View File

@@ -26,7 +26,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s
return;
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->visible_) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->nextion_->add_no_result_to_queue_with_set(this, state);

View File

@@ -1,5 +1,6 @@
#include "nfc.h"
#include <cstdio>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -7,29 +8,9 @@ namespace nfc {
static const char *const TAG = "nfc";
std::string format_uid(std::vector<uint8_t> &uid) {
char buf[(uid.size() * 2) + uid.size() - 1];
int offset = 0;
for (size_t i = 0; i < uid.size(); i++) {
const char *format = "%02X";
if (i + 1 < uid.size())
format = "%02X-";
offset += sprintf(buf + offset, format, uid[i]);
}
return std::string(buf);
}
std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); }
std::string format_bytes(std::vector<uint8_t> &bytes) {
char buf[(bytes.size() * 2) + bytes.size() - 1];
int offset = 0;
for (size_t i = 0; i < bytes.size(); i++) {
const char *format = "%02X";
if (i + 1 < bytes.size())
format = "%02X ";
offset += sprintf(buf + offset, format, bytes[i]);
}
return std::string(buf);
}
std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); }
uint8_t guess_tag_type(uint8_t uid_length) {
if (uid_length == 4) {

View File

@@ -2,8 +2,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "ndef_record.h"
#include "ndef_message.h"
#include "ndef_record.h"
#include "nfc_tag.h"
#include <vector>
@@ -53,8 +53,8 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
std::string format_uid(std::vector<uint8_t> &uid);
std::string format_bytes(std::vector<uint8_t> &bytes);
std::string format_uid(const std::vector<uint8_t> &uid);
std::string format_bytes(const std::vector<uint8_t> &bytes);
uint8_t guess_tag_type(uint8_t uid_length);
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);

View File

@@ -1,26 +0,0 @@
"""
Runtime statistics component for ESPHome.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
DEPENDENCIES = []
CONF_ENABLED = "enabled"
CONF_LOG_INTERVAL = "log_interval"
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
cv.Optional(
CONF_LOG_INTERVAL, default=60000
): cv.positive_time_period_milliseconds,
}
)
async def to_code(config):
"""Generate code for the runtime statistics component."""
cg.add(cg.App.set_runtime_stats_enabled(config[CONF_ENABLED]))
cg.add(cg.App.set_runtime_stats_log_interval(config[CONF_LOG_INTERVAL]))

View File

@@ -224,7 +224,7 @@ bool SSD1306::is_sh1106_() const {
}
bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
bool SSD1306::is_ssd1305_() const {
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64;
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_32;
}
void SSD1306::update() {
this->do_update_();

View File

@@ -3132,7 +3132,7 @@ void HOT GDEY0583T81::display() {
} else {
// Partial out (PTOUT), makes the display exit partial mode
this->command(0x92);
ESP_LOGD(TAG, "Partial update done, next full update after %d cycles",
ESP_LOGD(TAG, "Partial update done, next full update after %" PRIu32 " cycles",
this->full_update_every_ - this->at_update_ - 1);
}

View File

@@ -40,7 +40,4 @@ async def to_code(config):
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
# Use fork with libretiny compatibility fix
cg.add_library(
"https://github.com/bdraco/ESPAsyncWebServer.git#libretiny_Fix", None
)
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")

View File

@@ -2,6 +2,8 @@ from collections.abc import Callable
from esphome.const import (
CONF_ID,
CONF_LEVEL,
CONF_LOGGER,
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
@@ -157,3 +159,20 @@ def filter_source_files_from_platform(
]
return filter_source_files
def get_logger_level() -> str:
"""Get the configured logger level.
This is used by components to determine what logging features to include
based on the configured log level.
Returns:
The configured logger level string, defaults to "DEBUG" if not configured
"""
# Check if logger config exists
if CONF_LOGGER not in CORE.config:
return "DEBUG"
logger_config = CORE.config[CONF_LOGGER]
return logger_config.get(CONF_LEVEL, "DEBUG")

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.7.0-dev"
__version__ = "2025.7.0b1"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -141,10 +141,6 @@ void Application::loop() {
this->in_loop_ = false;
this->app_state_ = new_app_state;
// Process any pending runtime stats printing after all components have run
// This ensures stats printing doesn't affect component timing measurements
runtime_stats.process_pending_stats(last_op_end_time);
// Use the last component's end time instead of calling millis() again
auto elapsed = last_op_end_time - this->last_loop_;
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {

View File

@@ -9,7 +9,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/runtime_stats.h"
#include "esphome/core/scheduler.h"
#ifdef USE_DEVICES
@@ -349,18 +348,6 @@ class Application {
uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); }
/** Enable or disable runtime statistics collection.
*
* @param enable Whether to enable runtime statistics collection.
*/
void set_runtime_stats_enabled(bool enable) { runtime_stats.set_enabled(enable); }
/** Set the interval at which runtime statistics are logged.
*
* @param interval The interval in milliseconds between logging of runtime statistics.
*/
void set_runtime_stats_log_interval(uint32_t interval) { runtime_stats.set_log_interval(interval); }
void schedule_dump_config() { this->dump_config_at_ = 0; }
void feed_wdt(uint32_t time = 0);

View File

@@ -395,9 +395,6 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis();
uint32_t blocking_time = curr_time - this->started_;
// Record component runtime stats
runtime_stats.record_component_time(this->component_, blocking_time, curr_time);
bool should_warn;
if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time);

View File

@@ -6,7 +6,6 @@
#include <string>
#include "esphome/core/optional.h"
#include "esphome/core/runtime_stats.h"
namespace esphome {

View File

@@ -258,53 +258,60 @@ std::string format_hex(const uint8_t *data, size_t length) {
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
std::string format_hex_pretty(const uint8_t *data, size_t length) {
if (length == 0)
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0)
return "";
std::string ret;
ret.resize(3 * length - 1);
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
ret.resize(multiple * length - (separator ? 1 : 0));
for (size_t i = 0; i < length; i++) {
ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
if (i != length - 1)
ret[3 * i + 2] = '.';
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
if (separator && i != length - 1)
ret[multiple * i + 2] = separator;
}
if (length > 4)
return ret + " (" + to_string(length) + ")";
if (show_length && length > 4)
return ret + " (" + std::to_string(length) + ")";
return ret;
}
std::string format_hex_pretty(const std::vector<uint8_t> &data) { return format_hex_pretty(data.data(), data.size()); }
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
return format_hex_pretty(data.data(), data.size(), separator, show_length);
}
std::string format_hex_pretty(const uint16_t *data, size_t length) {
if (length == 0)
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0)
return "";
std::string ret;
ret.resize(5 * length - 1);
uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise
ret.resize(multiple * length - (separator ? 1 : 0));
for (size_t i = 0; i < length; i++) {
ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
if (i != length - 1)
ret[5 * i + 2] = '.';
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
ret[multiple * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
ret[multiple * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
if (separator && i != length - 1)
ret[multiple * i + 4] = separator;
}
if (length > 4)
return ret + " (" + to_string(length) + ")";
if (show_length && length > 4)
return ret + " (" + std::to_string(length) + ")";
return ret;
}
std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
std::string format_hex_pretty(const std::string &data) {
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
return format_hex_pretty(data.data(), data.size(), separator, show_length);
}
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
if (data.empty())
return "";
std::string ret;
ret.resize(3 * data.length() - 1);
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
ret.resize(multiple * data.length() - (separator ? 1 : 0));
for (size_t i = 0; i < data.length(); i++) {
ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
if (i != data.length() - 1)
ret[3 * i + 2] = '.';
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
if (separator && i != data.length() - 1)
ret[multiple * i + 2] = separator;
}
if (data.length() > 4)
if (show_length && data.length() > 4)
return ret + " (" + std::to_string(data.length()) + ")";
return ret;
}

View File

@@ -344,20 +344,149 @@ template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &dat
return format_hex(data.data(), data.size());
}
/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex.
std::string format_hex_pretty(const uint8_t *data, size_t length);
/// Format the word array \p data of length \p len in pretty-printed, human-readable hex.
std::string format_hex_pretty(const uint16_t *data, size_t length);
/// Format the vector \p data in pretty-printed, human-readable hex.
std::string format_hex_pretty(const std::vector<uint8_t> &data);
/// Format the vector \p data in pretty-printed, human-readable hex.
std::string format_hex_pretty(const std::vector<uint16_t> &data);
/// Format the string \p data in pretty-printed, human-readable hex.
std::string format_hex_pretty(const std::string &data);
/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val) {
/** Format a byte array in pretty-printed, human-readable hex format.
*
* Converts binary data to a hexadecimal string representation with customizable formatting.
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
* Optionally includes the total byte count in parentheses at the end.
*
* @param data Pointer to the byte array to format.
* @param length Number of bytes in the array.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters.
*
* @note Returns empty string if data is nullptr or length is 0.
* @note The length will only be appended if show_length is true AND the length is greater than 4.
*
* Example:
* @code
* uint8_t data[] = {0xA1, 0xB2, 0xC3};
* format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts)
* uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};
* format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)"
* format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)"
* format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5"
* @endcode
*/
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
/** Format a 16-bit word array in pretty-printed, human-readable hex format.
*
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
*
* @param data Pointer to the 16-bit word array to format.
* @param length Number of 16-bit words in the array.
* @param separator Character to use between hex words (default: '.').
* @param show_length Whether to append the word count in parentheses (default: true).
* @return Formatted hex string with 4-digit hex values per word.
*
* @note The length will only be appended if show_length is true AND the length is greater than 4.
*
* Example:
* @code
* uint16_t data[] = {0xA1B2, 0xC3D4};
* format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts)
* uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6};
* format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)"
* @endcode
*/
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
/** Format a byte vector in pretty-printed, human-readable hex format.
*
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
* uppercase hex value with customizable separator.
*
* @param data Vector of bytes to format.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string representation of the vector contents.
*
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
*
* Example:
* @code
* std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF};
* format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts)
* std::vector<uint8_t> data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA};
* format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)"
* format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)"
* @endcode
*/
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
/** Format a 16-bit word vector in pretty-printed, human-readable hex format.
*
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
* as a 4-digit uppercase hex value in big-endian order.
*
* @param data Vector of 16-bit words to format.
* @param separator Character to use between hex words (default: '.').
* @param show_length Whether to append the word count in parentheses (default: true).
* @return Formatted hex string representation of the vector contents.
*
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
*
* Example:
* @code
* std::vector<uint16_t> data = {0x1234, 0x5678};
* format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts)
* std::vector<uint16_t> data2 = {0x1234, 0x5678, 0x9ABC};
* format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)"
* @endcode
*/
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
/** Format a string's bytes in pretty-printed, human-readable hex format.
*
* Treats each character in the string as a byte and formats it in hex.
* Useful for debugging binary data stored in std::string containers.
*
* @param data String whose bytes should be formatted as hex.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string representation of the string's byte contents.
*
* @note The length will only be appended if show_length is true AND the string length is greater than 4.
*
* Example:
* @code
* std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43
* format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts)
* std::string data2 = "ABCDE";
* format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)"
* @endcode
*/
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
/** Format an unsigned integer in pretty-printed, human-readable hex format.
*
* Converts the integer to big-endian byte order and formats each byte as hex.
* The most significant byte appears first in the output string.
*
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
* @param val The unsigned integer value to format.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string with most significant byte first.
*
* @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4.
*
* Example:
* @code
* uint32_t value = 0x12345678;
* format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts)
* uint64_t value2 = 0x123456789ABCDEF0;
* format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)"
* format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)"
* format_hex_pretty<uint16_t>(0x1234); // Returns "12.34"
* @endcode
*/
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) {
val = convert_big_endian(val);
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T));
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T), separator, show_length);
}
/// Format the byte array \p data of length \p len in binary.

View File

@@ -1,92 +0,0 @@
#include "esphome/core/runtime_stats.h"
#include "esphome/core/component.h"
#include <algorithm>
namespace esphome {
RuntimeStatsCollector runtime_stats;
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
if (!this->enabled_ || component == nullptr)
return;
// Check if we have cached the name for this component
auto name_it = this->component_names_cache_.find(component);
if (name_it == this->component_names_cache_.end()) {
// First time seeing this component, cache its name
const char *source = component->get_component_source();
this->component_names_cache_[component] = source;
this->component_stats_[source].record_time(duration_ms);
} else {
// Use cached name - no string operations, just map lookup
this->component_stats_[name_it->second].record_time(duration_ms);
}
// If next_log_time_ is 0, initialize it
if (this->next_log_time_ == 0) {
this->next_log_time_ = current_time + this->log_interval_;
return;
}
// Don't print stats here anymore - let process_pending_stats handle it
}
void RuntimeStatsCollector::log_stats_() {
ESP_LOGI(RUNTIME_TAG, "Component Runtime Statistics");
ESP_LOGI(RUNTIME_TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
// First collect stats we want to display
std::vector<ComponentStatPair> stats_to_display;
for (const auto &it : this->component_stats_) {
const ComponentRuntimeStats &stats = it.second;
if (stats.get_period_count() > 0) {
ComponentStatPair pair = {it.first, &stats};
stats_to_display.push_back(pair);
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
stats->get_period_time_ms());
}
// Log total stats since boot
ESP_LOGI(RUNTIME_TAG, "Total stats (since boot):");
// Re-sort by total runtime for all-time stats
std::sort(stats_to_display.begin(), stats_to_display.end(),
[](const ComponentStatPair &a, const ComponentStatPair &b) {
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
});
for (const auto &it : stats_to_display) {
const std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
}
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
if (!this->enabled_ || this->next_log_time_ == 0)
return;
if (current_time >= this->next_log_time_) {
this->log_stats_();
this->reset_stats_();
this->next_log_time_ = current_time + this->log_interval_;
}
}
} // namespace esphome

View File

@@ -1,121 +0,0 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include <cstdint>
#include <algorithm>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
static const char *const RUNTIME_TAG = "runtime";
class Component; // Forward declaration
class ComponentRuntimeStats {
public:
ComponentRuntimeStats()
: period_count_(0),
total_count_(0),
period_time_ms_(0),
total_time_ms_(0),
period_max_time_ms_(0),
total_max_time_ms_(0) {}
void record_time(uint32_t duration_ms) {
// Update period counters
this->period_count_++;
this->period_time_ms_ += duration_ms;
if (duration_ms > this->period_max_time_ms_)
this->period_max_time_ms_ = duration_ms;
// Update total counters
this->total_count_++;
this->total_time_ms_ += duration_ms;
if (duration_ms > this->total_max_time_ms_)
this->total_max_time_ms_ = duration_ms;
}
void reset_period_stats() {
this->period_count_ = 0;
this->period_time_ms_ = 0;
this->period_max_time_ms_ = 0;
}
// Period stats (reset each logging interval)
uint32_t get_period_count() const { return this->period_count_; }
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
float get_period_avg_time_ms() const {
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
}
// Total stats (persistent until reboot)
uint32_t get_total_count() const { return this->total_count_; }
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
float get_total_avg_time_ms() const {
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
}
protected:
// Period stats (reset each logging interval)
uint32_t period_count_;
uint32_t period_time_ms_;
uint32_t period_max_time_ms_;
// Total stats (persistent until reboot)
uint32_t total_count_;
uint32_t total_time_ms_;
uint32_t total_max_time_ms_;
};
// For sorting components by run time
struct ComponentStatPair {
std::string name;
const ComponentRuntimeStats *stats;
bool operator>(const ComponentStatPair &other) const {
// Sort by period time as that's what we're displaying in the logs
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
}
};
class RuntimeStatsCollector {
public:
RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0), enabled_(true) {}
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
uint32_t get_log_interval() const { return this->log_interval_; }
void set_enabled(bool enabled) { this->enabled_ = enabled; }
bool is_enabled() const { return this->enabled_; }
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
protected:
void log_stats_();
void reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
}
// Back to string keys, but we'll cache the source name per component
std::map<std::string, ComponentRuntimeStats> component_stats_;
std::map<Component *, std::string> component_names_cache_;
uint32_t log_interval_;
uint32_t next_log_time_;
bool enabled_;
};
// Global instance for runtime stats collection
extern RuntimeStatsCollector runtime_stats;
} // namespace esphome

View File

@@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.9.0
click==8.1.7
esphome-dashboard==20250514.0
aioesphomeapi==34.1.0
aioesphomeapi==34.2.0
zeroconf==0.147.0
puremagic==1.30
ruamel.yaml==0.18.14 # dashboard_import

View File

@@ -1,29 +1,71 @@
"""Fixtures for component tests."""
from __future__ import annotations
from collections.abc import Callable, Generator
from pathlib import Path
import sys
import pytest
# Add package root to python path
here = Path(__file__).parent
package_root = here.parent.parent
sys.path.insert(0, package_root.as_posix())
import pytest # noqa: E402
from esphome.__main__ import generate_cpp_contents # noqa: E402
from esphome.config import read_config # noqa: E402
from esphome.core import CORE # noqa: E402
@pytest.fixture(autouse=True)
def config_path(request: pytest.FixtureRequest) -> Generator[None]:
"""Set CORE.config_path to the component's config directory and reset it after the test."""
original_path = CORE.config_path
config_dir = Path(request.fspath).parent / "config"
# Check if config directory exists, if not use parent directory
if config_dir.exists():
# Set config_path to a dummy yaml file in the config directory
# This ensures CORE.config_dir points to the config directory
CORE.config_path = str(config_dir / "dummy.yaml")
else:
CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml")
yield
CORE.config_path = original_path
@pytest.fixture
def generate_main():
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
"""Return a function to get absolute paths relative to the component's fixtures directory."""
def _get_path(file_name: str) -> Path:
"""Get the absolute path of a file relative to the component's fixtures directory."""
return (Path(request.fspath).parent / "fixtures" / file_name).absolute()
return _get_path
@pytest.fixture
def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
"""Return a function to get absolute paths relative to the component's config directory."""
def _get_path(file_name: str) -> Path:
"""Get the absolute path of a file relative to the component's config directory."""
return (Path(request.fspath).parent / "config" / file_name).absolute()
return _get_path
@pytest.fixture
def generate_main() -> Generator[Callable[[str | Path], str]]:
"""Generates the C++ main.cpp file and returns it in string form."""
def generator(path: str) -> str:
CORE.config_path = path
def generator(path: str | Path) -> str:
CORE.config_path = str(path)
CORE.config = read_config({})
generate_cpp_contents(CORE.config)
print(CORE.cpp_main_section)
return CORE.cpp_main_section
yield generator

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

View File

@@ -0,0 +1,20 @@
esphome:
name: test
esp32:
board: esp32s3box
image:
- file: image.png
byte_order: little_endian
id: cat_img
type: rgb565
spi:
mosi_pin: 6
clk_pin: 7
display:
- platform: mipi_spi
id: lcd_display
model: s3box

View File

@@ -0,0 +1,183 @@
"""Tests for image configuration validation."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.image import CONFIG_SCHEMA
@pytest.mark.parametrize(
("config", "error_match"),
[
pytest.param(
"a string",
"Badly formed image configuration, expected a list or a dictionary",
id="invalid_string_config",
),
pytest.param(
{"id": "image_id", "type": "rgb565"},
r"required key not provided @ data\[0\]\['file'\]",
id="missing_file",
),
pytest.param(
{"file": "image.png", "type": "rgb565"},
r"required key not provided @ data\[0\]\['id'\]",
id="missing_id",
),
pytest.param(
{"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"},
"Could not parse mdi icon name",
id="invalid_mdi_icon",
),
pytest.param(
{
"id": "image_id",
"file": "image.png",
"type": "binary",
"transparency": "alpha_channel",
},
"Image format 'BINARY' cannot have transparency",
id="binary_with_transparency",
),
pytest.param(
{
"id": "image_id",
"file": "image.png",
"type": "rgb565",
"transparency": "chroma_key",
"invert_alpha": True,
},
"No alpha channel to invert",
id="invert_alpha_without_alpha_channel",
),
pytest.param(
{
"id": "image_id",
"file": "image.png",
"type": "binary",
"byte_order": "big_endian",
},
"Image format 'BINARY' does not support byte order configuration",
id="binary_with_byte_order",
),
pytest.param(
{"id": "image_id", "file": "bad.png", "type": "binary"},
"File can't be opened as image",
id="invalid_image_file",
),
pytest.param(
{"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]},
"Type is required either in the image config or in the defaults",
id="missing_type_in_defaults",
),
],
)
def test_image_configuration_errors(
config: Any,
error_match: str,
) -> None:
"""Test detection of invalid configuration."""
with pytest.raises(cv.Invalid, match=error_match):
CONFIG_SCHEMA(config)
@pytest.mark.parametrize(
"config",
[
pytest.param(
{
"id": "image_id",
"file": "image.png",
"type": "rgb565",
"transparency": "chroma_key",
"byte_order": "little_endian",
"dither": "FloydSteinberg",
"resize": "100x100",
"invert_alpha": False,
},
id="single_image_all_options",
),
pytest.param(
[
{
"id": "image_id",
"file": "image.png",
"type": "binary",
}
],
id="list_of_images",
),
pytest.param(
{
"defaults": {
"type": "rgb565",
"transparency": "chroma_key",
"byte_order": "little_endian",
"dither": "FloydSteinberg",
"resize": "100x100",
"invert_alpha": False,
},
"images": [
{
"id": "image_id",
"file": "image.png",
}
],
},
id="images_with_defaults",
),
pytest.param(
{
"rgb565": {
"alpha_channel": [
{
"id": "image_id",
"file": "image.png",
"transparency": "alpha_channel",
"byte_order": "little_endian",
"dither": "FloydSteinberg",
"resize": "100x100",
"invert_alpha": False,
}
]
},
"binary": [
{
"id": "image_id",
"file": "image.png",
"transparency": "opaque",
"dither": "FloydSteinberg",
"resize": "100x100",
"invert_alpha": False,
}
],
},
id="type_based_organization",
),
],
)
def test_image_configuration_success(
config: dict[str, Any] | list[dict[str, Any]],
) -> None:
"""Test successful configuration validation."""
CONFIG_SCHEMA(config)
def test_image_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test image generation configuration."""
main_cpp = generate_main(component_config_path("image_test.yaml"))
assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
assert (
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
in main_cpp
)

View File

@@ -0,0 +1,12 @@
i2c:
- id: i2c_gl_r01_i2c
scl: ${scl_pin}
sda: ${sda_pin}
sensor:
- platform: gl_r01_i2c
id: tof
name: "ToF sensor"
i2c_id: i2c_gl_r01_i2c
address: 0x74
update_interval: 15s

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -1,17 +0,0 @@
spi:
- id: spi_main_lcd
clk_pin: 16
mosi_pin: 17
miso_pin: 32
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 14
dc_pin: 13
reset_pin: 21
invert_colors: true
<<: !include common.yaml

View File

@@ -1,16 +0,0 @@
spi:
- id: spi_main_lcd
clk_pin: 6
mosi_pin: 7
miso_pin: 5
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 3
dc_pin: 11
reset_pin: 10
invert_colors: true
<<: !include common.yaml

View File

@@ -1,16 +0,0 @@
spi:
- id: spi_main_lcd
clk_pin: 6
mosi_pin: 7
miso_pin: 5
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 3
dc_pin: 11
reset_pin: 10
invert_colors: true
<<: !include common.yaml

View File

@@ -13,4 +13,13 @@ display:
reset_pin: 16
invert_colors: true
<<: !include common.yaml
image:
defaults:
type: rgb565
transparency: opaque
byte_order: little_endian
resize: 50x50
dither: FloydSteinberg
images:
- id: test_image
file: ../../pnglogo.png

View File

@@ -0,0 +1,8 @@
sensor:
- platform: lps22
address: 0x5d
update_interval: 10s
temperature:
name: "LPS22 Temperature"
pressure:
name: "LPS22 Pressure"

View File

@@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 16
sda: 17
<<: !include common.yaml

View File

@@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 5
sda: 4
<<: !include common.yaml

View File

@@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 5
sda: 4
<<: !include common.yaml

View File

@@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 16
sda: 17
<<: !include common.yaml

View File

@@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 5
sda: 4
<<: !include common.yaml

View File

@@ -0,0 +1,6 @@
i2c:
- id: i2c_lps22
scl: 5
sda: 4
<<: !include common.yaml

View File

@@ -78,3 +78,268 @@ pytest -s tests/integration/test_host_mode_basic.py
- Each test gets its own temporary directory and unique port
- Port allocation minimizes race conditions by holding the socket until just before ESPHome starts
- Output from ESPHome processes is displayed for debugging
## Integration Test Writing Guide
### Test Patterns and Best Practices
#### 1. Test File Naming Convention
- Use descriptive names: `test_{category}_{feature}.py`
- Common categories: `host_mode`, `api`, `scheduler`, `light`, `areas_and_devices`
- Examples:
- `test_host_mode_basic.py` - Basic host mode functionality
- `test_api_message_batching.py` - API message batching
- `test_scheduler_stress.py` - Scheduler stress testing
#### 2. Essential Imports
```python
from __future__ import annotations
import asyncio
from typing import Any
import pytest
from aioesphomeapi import EntityState, SensorState
from .types import APIClientConnectedFactory, RunCompiledFunction
```
#### 3. Common Test Patterns
##### Basic Entity Test
```python
@pytest.mark.asyncio
async def test_my_sensor(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test sensor functionality."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Get entity list
entities, services = await client.list_entities_services()
# Find specific entity
sensor = next((e for e in entities if e.object_id == "my_sensor"), None)
assert sensor is not None
```
##### State Subscription Pattern
```python
# Track state changes with futures
loop = asyncio.get_running_loop()
states: dict[int, EntityState] = {}
state_future: asyncio.Future[EntityState] = loop.create_future()
def on_state(state: EntityState) -> None:
states[state.key] = state
# Check for specific condition using isinstance
if isinstance(state, SensorState) and state.state == expected_value:
if not state_future.done():
state_future.set_result(state)
client.subscribe_states(on_state)
# Wait for state with timeout
try:
result = await asyncio.wait_for(state_future, timeout=5.0)
except asyncio.TimeoutError:
pytest.fail(f"Expected state not received. Got: {list(states.values())}")
```
##### Service Execution Pattern
```python
# Find and execute service
entities, services = await client.list_entities_services()
my_service = next((s for s in services if s.name == "my_service"), None)
assert my_service is not None
# Execute with parameters
client.execute_service(my_service, {"param1": "value1", "param2": 42})
```
##### Multiple Entity Tracking
```python
# For tests with many entities
loop = asyncio.get_running_loop()
entity_count = 50
received_states: set[int] = set()
all_states_future: asyncio.Future[bool] = loop.create_future()
def on_state(state: EntityState) -> None:
received_states.add(state.key)
if len(received_states) >= entity_count and not all_states_future.done():
all_states_future.set_result(True)
client.subscribe_states(on_state)
await asyncio.wait_for(all_states_future, timeout=10.0)
```
#### 4. YAML Fixture Guidelines
##### Naming Convention
- Match test function name: `test_my_feature``fixtures/my_feature.yaml`
- Note: Remove `test_` prefix for fixture filename
##### Basic Structure
```yaml
esphome:
name: test-name # Use kebab-case
# Optional: areas, devices, platformio_options
host: # Always use host platform for integration tests
api: # Port injected automatically
logger:
level: DEBUG # Optional: Set log level
# Component configurations
sensor:
- platform: template
name: "My Sensor"
id: my_sensor
lambda: return 42.0;
update_interval: 0.1s # Fast updates for testing
```
##### Advanced Features
```yaml
# External components for custom test code
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH # Replaced by test framework
components: [my_test_component]
# Areas and devices
esphome:
name: test-device
areas:
- id: living_room
name: "Living Room"
- id: kitchen
name: "Kitchen"
parent_id: living_room
devices:
- id: my_device
name: "Test Device"
area_id: living_room
# API services
api:
services:
- service: test_service
variables:
my_param: string
then:
- logger.log:
format: "Service called with: %s"
args: [my_param.c_str()]
```
#### 5. Testing Complex Scenarios
##### External Components
Create C++ components in `fixtures/external_components/` for:
- Stress testing
- Custom entity behaviors
- Scheduler testing
- Memory management tests
##### Log Line Monitoring
```python
log_lines: list[str] = []
def on_log_line(line: str) -> None:
log_lines.append(line)
if "expected message" in line:
# Handle specific log messages
async with run_compiled(yaml_config, line_callback=on_log_line):
# Test implementation
```
Example using futures for specific log patterns:
```python
import re
loop = asyncio.get_running_loop()
connected_future = loop.create_future()
service_future = loop.create_future()
# Patterns to match
connected_pattern = re.compile(r"Client .* connected from")
service_pattern = re.compile(r"Service called")
def check_output(line: str) -> None:
"""Check log output for expected messages."""
if not connected_future.done() and connected_pattern.search(line):
connected_future.set_result(True)
elif not service_future.done() and service_pattern.search(line):
service_future.set_result(True)
async with run_compiled(yaml_config, line_callback=check_output):
async with api_client_connected() as client:
# Wait for specific log message
await asyncio.wait_for(connected_future, timeout=5.0)
# Do test actions...
# Wait for service log
await asyncio.wait_for(service_future, timeout=5.0)
```
**Note**: Tests that monitor log messages typically have fewer race conditions compared to state-based testing, making them more reliable. However, be aware that the host platform currently does not have a thread-safe logger, so logging from threads will not work correctly.
##### Timeout Handling
```python
# Always use timeouts for async operations
try:
result = await asyncio.wait_for(some_future, timeout=5.0)
except asyncio.TimeoutError:
pytest.fail("Operation timed out - check test expectations")
```
#### 6. Common Assertions
```python
# Device info
assert device_info.name == "expected-name"
assert device_info.compilation_time is not None
# Entity properties
assert sensor.accuracy_decimals == 2
assert sensor.state_class == 1 # measurement
assert sensor.force_update is True
# Service availability
assert len(services) > 0
assert any(s.name == "expected_service" for s in services)
# State values
assert state.state == expected_value
assert state.missing_state is False
```
#### 7. Debugging Tips
- Use `pytest -s` to see ESPHome output during tests
- Add descriptive failure messages to assertions
- Use `pytest.fail()` with detailed error info for timeouts
- Check `log_lines` for compilation or runtime errors
- Enable debug logging in YAML fixtures when needed
#### 8. Performance Considerations
- Use short update intervals (0.1s) for faster tests
- Set reasonable timeouts (5-10s for most operations)
- Batch multiple assertions when possible
- Clean up resources properly using context managers
#### 9. Test Categories
- **Basic Tests**: Minimal functionality verification
- **Entity Tests**: Sensor, switch, light behavior
- **API Tests**: Message batching, services, events
- **Scheduler Tests**: Timing, defer operations, stress
- **Memory Tests**: Conditional compilation, optimization
- **Integration Tests**: Areas, devices, complex interactions

View File

@@ -165,6 +165,19 @@ async def compile_esphome(
"""Compile an ESPHome configuration and return the binary path."""
async def _compile(config_path: Path) -> Path:
# Create a unique PlatformIO directory for this test to avoid race conditions
platformio_dir = integration_test_dir / ".platformio"
platformio_dir.mkdir(parents=True, exist_ok=True)
# Create cache directory as well
platformio_cache_dir = platformio_dir / ".cache"
platformio_cache_dir.mkdir(parents=True, exist_ok=True)
# Set up environment with isolated PlatformIO directories
env = os.environ.copy()
env["PLATFORMIO_CORE_DIR"] = str(platformio_dir)
env["PLATFORMIO_CACHE_DIR"] = str(platformio_cache_dir)
# Retry compilation up to 3 times if we get a segfault
max_retries = 3
for attempt in range(max_retries):
@@ -179,6 +192,7 @@ async def compile_esphome(
stdin=asyncio.subprocess.DEVNULL,
# Start in a new process group to isolate signal handling
start_new_session=True,
env=env,
)
await proc.wait()

View File

@@ -6,9 +6,6 @@ api:
- action: test_simple_service
then:
- logger.log: "Simple service called"
- binary_sensor.template.publish:
id: service_called_sensor
state: ON
- action: test_service_with_args
variables:
arg_string: string
@@ -19,53 +16,14 @@ api:
- logger.log:
format: "Service called with: %s, %d, %d, %.2f"
args: [arg_string.c_str(), arg_int, arg_bool, arg_float]
- sensor.template.publish:
id: service_arg_sensor
state: !lambda 'return arg_float;'
on_client_connected:
- logger.log:
format: "Client %s connected from %s"
args: [client_info.c_str(), client_address.c_str()]
- binary_sensor.template.publish:
id: client_connected
state: ON
- text_sensor.template.publish:
id: last_client_info
state: !lambda 'return client_info;'
on_client_disconnected:
- logger.log:
format: "Client %s disconnected from %s"
args: [client_info.c_str(), client_address.c_str()]
- binary_sensor.template.publish:
id: client_connected
state: OFF
- binary_sensor.template.publish:
id: client_disconnected_event
state: ON
logger:
level: DEBUG
binary_sensor:
- platform: template
name: "Client Connected"
id: client_connected
device_class: connectivity
- platform: template
name: "Client Disconnected Event"
id: client_disconnected_event
- platform: template
name: "Service Called"
id: service_called_sensor
sensor:
- platform: template
name: "Service Argument Value"
id: service_arg_sensor
unit_of_measurement: ""
accuracy_decimals: 2
text_sensor:
- platform: template
name: "Last Client Info"
id: last_client_info

View File

@@ -1,109 +0,0 @@
esphome:
name: defer-fifo-simple
host:
logger:
level: DEBUG
api:
services:
- service: test_set_timeout
then:
- lambda: |-
// Test set_timeout with 0 delay (direct scheduler call)
static int set_timeout_order = 0;
static bool set_timeout_passed = true;
// Reset for this test
set_timeout_order = 0;
set_timeout_passed = true;
ESP_LOGD("defer_test", "Testing set_timeout(0) for FIFO order...");
for (int i = 0; i < 10; i++) {
int expected = i;
App.scheduler.set_timeout((Component*)nullptr, nullptr, 0, [expected]() {
ESP_LOGD("defer_test", "set_timeout(0) item %d executed, order %d", expected, set_timeout_order);
if (set_timeout_order != expected) {
ESP_LOGE("defer_test", "FIFO violation in set_timeout: expected %d but got execution order %d", expected, set_timeout_order);
set_timeout_passed = false;
}
set_timeout_order++;
if (set_timeout_order == 10) {
if (set_timeout_passed) {
ESP_LOGI("defer_test", "✓ Test PASSED - set_timeout(0) maintains FIFO order");
id(test_result)->trigger("passed");
} else {
ESP_LOGE("defer_test", "✗ Test FAILED - set_timeout(0) executed out of order");
id(test_result)->trigger("failed");
}
id(test_complete)->trigger("test_finished");
}
});
}
ESP_LOGD("defer_test", "Deferred 10 items using set_timeout(0), waiting for execution...");
- service: test_defer
then:
- lambda: |-
// Test defer() method (component method)
static int defer_order = 0;
static bool defer_passed = true;
// Reset for this test
defer_order = 0;
defer_passed = true;
ESP_LOGD("defer_test", "Testing defer() for FIFO order...");
// Create a test component class that exposes defer()
class TestComponent : public Component {
public:
void test_defer() {
for (int i = 0; i < 10; i++) {
int expected = i;
this->defer([expected]() {
ESP_LOGD("defer_test", "defer() item %d executed, order %d", expected, defer_order);
if (defer_order != expected) {
ESP_LOGE("defer_test", "FIFO violation in defer: expected %d but got execution order %d", expected, defer_order);
defer_passed = false;
}
defer_order++;
if (defer_order == 10) {
if (defer_passed) {
ESP_LOGI("defer_test", "✓ Test PASSED - defer() maintains FIFO order");
id(test_result)->trigger("passed");
} else {
ESP_LOGE("defer_test", "✗ Test FAILED - defer() executed out of order");
id(test_result)->trigger("failed");
}
id(test_complete)->trigger("test_finished");
}
});
}
}
};
// Use a static instance so it doesn't go out of scope
static TestComponent test_component;
test_component.test_defer();
ESP_LOGD("defer_test", "Deferred 10 items using defer(), waiting for execution...");
event:
- platform: template
name: "Test Complete"
id: test_complete
device_class: button
event_types:
- "test_finished"
- platform: template
name: "Test Result"
id: test_result
device_class: button
event_types:
- "passed"
- "failed"

View File

@@ -1,38 +0,0 @@
esphome:
name: defer-stress-test
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
components: [defer_stress_component]
host:
logger:
level: VERBOSE
defer_stress_component:
id: defer_stress
api:
services:
- service: run_stress_test
then:
- lambda: |-
id(defer_stress)->run_multi_thread_test();
event:
- platform: template
name: "Test Complete"
id: test_complete
device_class: button
event_types:
- "test_finished"
- platform: template
name: "Test Result"
id: test_result
device_class: button
event_types:
- "passed"
- "failed"

View File

@@ -26,7 +26,6 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
// Schedule final check
this->set_timeout("final_check", 200, [this]() {
ESP_LOGI(TAG, "String lifetime tests complete");
ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
@@ -35,6 +34,7 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
} else {
ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
}
ESP_LOGI(TAG, "String lifetime tests complete");
});
}

View File

@@ -3,15 +3,9 @@
from __future__ import annotations
import asyncio
import re
from aioesphomeapi import (
BinarySensorInfo,
EntityState,
SensorInfo,
TextSensorInfo,
UserService,
UserServiceArgType,
)
from aioesphomeapi import UserService, UserServiceArgType
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -25,50 +19,45 @@ async def test_api_conditional_memory(
) -> None:
"""Test API triggers and services work correctly with conditional compilation."""
loop = asyncio.get_running_loop()
# Keep ESPHome process running throughout the test
async with run_compiled(yaml_config):
# First connection
# Track log messages
connected_future = loop.create_future()
disconnected_future = loop.create_future()
service_simple_future = loop.create_future()
service_args_future = loop.create_future()
# Patterns to match in logs
connected_pattern = re.compile(r"Client .* connected from")
disconnected_pattern = re.compile(r"Client .* disconnected from")
service_simple_pattern = re.compile(r"Simple service called")
service_args_pattern = re.compile(
r"Service called with: test_string, 123, 1, 42\.50"
)
def check_output(line: str) -> None:
"""Check log output for expected messages."""
if not connected_future.done() and connected_pattern.search(line):
connected_future.set_result(True)
elif not disconnected_future.done() and disconnected_pattern.search(line):
disconnected_future.set_result(True)
elif not service_simple_future.done() and service_simple_pattern.search(line):
service_simple_future.set_result(True)
elif not service_args_future.done() and service_args_pattern.search(line):
service_args_future.set_result(True)
# Run with log monitoring
async with run_compiled(yaml_config, line_callback=check_output):
async with api_client_connected() as client:
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "api-conditional-memory-test"
# List entities and services
entity_info, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0
)
# Wait for connection log
await asyncio.wait_for(connected_future, timeout=5.0)
# Find our entities
client_connected: BinarySensorInfo | None = None
client_disconnected_event: BinarySensorInfo | None = None
service_called_sensor: BinarySensorInfo | None = None
service_arg_sensor: SensorInfo | None = None
last_client_info: TextSensorInfo | None = None
for entity in entity_info:
if isinstance(entity, BinarySensorInfo):
if entity.object_id == "client_connected":
client_connected = entity
elif entity.object_id == "client_disconnected_event":
client_disconnected_event = entity
elif entity.object_id == "service_called":
service_called_sensor = entity
elif isinstance(entity, SensorInfo):
if entity.object_id == "service_argument_value":
service_arg_sensor = entity
elif isinstance(entity, TextSensorInfo):
if entity.object_id == "last_client_info":
last_client_info = entity
# Verify all entities exist
assert client_connected is not None, "client_connected sensor not found"
assert client_disconnected_event is not None, (
"client_disconnected_event sensor not found"
)
assert service_called_sensor is not None, "service_called sensor not found"
assert service_arg_sensor is not None, "service_arg_sensor not found"
assert last_client_info is not None, "last_client_info sensor not found"
# List services
_, services = await client.list_entities_services()
# Verify services exist
assert len(services) == 2, f"Expected 2 services, found {len(services)}"
@@ -98,66 +87,11 @@ async def test_api_conditional_memory(
assert arg_types["arg_bool"] == UserServiceArgType.BOOL
assert arg_types["arg_float"] == UserServiceArgType.FLOAT
# Track state changes
states: dict[int, EntityState] = {}
states_future: asyncio.Future[None] = loop.create_future()
def on_state(state: EntityState) -> None:
states[state.key] = state
# Check if we have initial states for connection sensors
if (
client_connected.key in states
and last_client_info.key in states
and not states_future.done()
):
states_future.set_result(None)
client.subscribe_states(on_state)
# Wait for initial states
await asyncio.wait_for(states_future, timeout=5.0)
# Verify on_client_connected trigger fired
connected_state = states.get(client_connected.key)
assert connected_state is not None
assert connected_state.state is True, "Client should be connected"
# Verify client info was captured
client_info_state = states.get(last_client_info.key)
assert client_info_state is not None
assert isinstance(client_info_state.state, str)
assert len(client_info_state.state) > 0, "Client info should not be empty"
# Test simple service
service_future: asyncio.Future[None] = loop.create_future()
def check_service_called(state: EntityState) -> None:
if state.key == service_called_sensor.key and state.state is True:
if not service_future.done():
service_future.set_result(None)
# Update callback to check for service execution
client.subscribe_states(check_service_called)
# Call simple service
client.execute_service(simple_service, {})
# Wait for service to execute
await asyncio.wait_for(service_future, timeout=5.0)
# Test service with arguments
arg_future: asyncio.Future[None] = loop.create_future()
expected_float = 42.5
def check_arg_sensor(state: EntityState) -> None:
if (
state.key == service_arg_sensor.key
and abs(state.state - expected_float) < 0.01
):
if not arg_future.done():
arg_future.set_result(None)
client.subscribe_states(check_arg_sensor)
# Wait for service log
await asyncio.wait_for(service_simple_future, timeout=5.0)
# Call service with arguments
client.execute_service(
@@ -166,43 +100,12 @@ async def test_api_conditional_memory(
"arg_string": "test_string",
"arg_int": 123,
"arg_bool": True,
"arg_float": expected_float,
"arg_float": 42.5,
},
)
# Wait for service with args to execute
await asyncio.wait_for(arg_future, timeout=5.0)
# Wait for service with args log
await asyncio.wait_for(service_args_future, timeout=5.0)
# After disconnecting first client, reconnect and verify triggers work
async with api_client_connected() as client2:
# Subscribe to states with new client
states2: dict[int, EntityState] = {}
states_ready_future: asyncio.Future[None] = loop.create_future()
def on_state2(state: EntityState) -> None:
states2[state.key] = state
# Check if we have received both required states
if (
client_connected.key in states2
and client_disconnected_event.key in states2
and not states_ready_future.done()
):
states_ready_future.set_result(None)
client2.subscribe_states(on_state2)
# Wait for both connected and disconnected event states
await asyncio.wait_for(states_ready_future, timeout=5.0)
# Verify client is connected again (on_client_connected fired)
assert states2[client_connected.key].state is True, (
"Client should be reconnected"
)
# The client_disconnected_event should be ON from when we disconnected
# (it was set ON by on_client_disconnected trigger)
disconnected_state = states2.get(client_disconnected_event.key)
assert disconnected_state is not None
assert disconnected_state.state is True, (
"Disconnect event should be ON from previous disconnect"
)
# Client disconnected here, wait for disconnect log
await asyncio.wait_for(disconnected_future, timeout=5.0)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
from typing import Any
from aioesphomeapi import LogLevel
from aioesphomeapi import LogLevel, SensorInfo
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -63,7 +63,7 @@ async def test_api_vv_logging(
entity_info, _ = await client.list_entities_services()
# Count sensors
sensor_count = sum(1 for e in entity_info if hasattr(e, "unit_of_measurement"))
sensor_count = sum(1 for e in entity_info if isinstance(e, SensorInfo))
assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}"
# Wait for sensor updates to flow with VV logging active

View File

@@ -76,8 +76,8 @@ async def test_areas_and_devices(
# Get entity list to verify device_id mapping
entities = await client.list_entities_services()
# Collect sensor entities
sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")]
# Collect sensor entities (all entities have device_id)
sensor_entities = entities[0]
assert len(sensor_entities) >= 4, (
f"Expected at least 4 sensor entities, got {len(sensor_entities)}"
)

View File

@@ -1,117 +0,0 @@
"""Simple test that defer() maintains FIFO order."""
import asyncio
from aioesphomeapi import EntityState, Event, EventInfo, UserService
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_defer_fifo_simple(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that defer() maintains FIFO order with a simple test."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Verify we can connect
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "defer-fifo-simple"
# List entities and services
entity_info, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0
)
# Find our test entities
test_complete_entity: EventInfo | None = None
test_result_entity: EventInfo | None = None
for entity in entity_info:
if isinstance(entity, EventInfo):
if entity.object_id == "test_complete":
test_complete_entity = entity
elif entity.object_id == "test_result":
test_result_entity = entity
assert test_complete_entity is not None, "test_complete event not found"
assert test_result_entity is not None, "test_result event not found"
# Find our test services
test_set_timeout_service: UserService | None = None
test_defer_service: UserService | None = None
for service in services:
if service.name == "test_set_timeout":
test_set_timeout_service = service
elif service.name == "test_defer":
test_defer_service = service
assert test_set_timeout_service is not None, (
"test_set_timeout service not found"
)
assert test_defer_service is not None, "test_defer service not found"
# Get the event loop
loop = asyncio.get_running_loop()
# Subscribe to states
# (events are delivered as EventStates through subscribe_states)
test_complete_future: asyncio.Future[bool] = loop.create_future()
test_result_future: asyncio.Future[bool] = loop.create_future()
def on_state(state: EntityState) -> None:
if not isinstance(state, Event):
return
if (
state.key == test_complete_entity.key
and state.event_type == "test_finished"
and not test_complete_future.done()
):
test_complete_future.set_result(True)
return
if state.key == test_result_entity.key and not test_result_future.done():
if state.event_type == "passed":
test_result_future.set_result(True)
elif state.event_type == "failed":
test_result_future.set_result(False)
client.subscribe_states(on_state)
# Test 1: Test set_timeout(0)
client.execute_service(test_set_timeout_service, {})
# Wait for first test completion
try:
await asyncio.wait_for(test_complete_future, timeout=5.0)
test1_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
except asyncio.TimeoutError:
pytest.fail("Test set_timeout(0) did not complete within 5 seconds")
assert test1_passed is True, (
"set_timeout(0) FIFO test failed - items executed out of order"
)
# Reset futures for second test
test_complete_future = loop.create_future()
test_result_future = loop.create_future()
# Test 2: Test defer()
client.execute_service(test_defer_service, {})
# Wait for second test completion
try:
await asyncio.wait_for(test_complete_future, timeout=5.0)
test2_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
except asyncio.TimeoutError:
pytest.fail("Test defer() did not complete within 5 seconds")
# Verify the test passed
assert test2_passed is True, (
"defer() FIFO test failed - items executed out of order"
)

View File

@@ -1,137 +0,0 @@
"""Stress test for defer() thread safety with multiple threads."""
import asyncio
from pathlib import Path
import re
from aioesphomeapi import UserService
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_defer_stress(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that defer() doesn't crash when called rapidly from multiple threads."""
# Get the absolute path to the external components directory
external_components_path = str(
Path(__file__).parent / "fixtures" / "external_components"
)
# Replace the placeholder in the YAML config with the actual path
yaml_config = yaml_config.replace(
"EXTERNAL_COMPONENT_PATH", external_components_path
)
# Create a future to signal test completion
loop = asyncio.get_event_loop()
test_complete_future: asyncio.Future[None] = loop.create_future()
# Track executed defers and their order
executed_defers: set[int] = set()
thread_executions: dict[
int, list[int]
] = {} # thread_id -> list of indices in execution order
fifo_violations: list[str] = []
def on_log_line(line: str) -> None:
# Track all executed defers with thread and index info
match = re.search(r"Executed defer (\d+) \(thread (\d+), index (\d+)\)", line)
if not match:
return
defer_id = int(match.group(1))
thread_id = int(match.group(2))
index = int(match.group(3))
executed_defers.add(defer_id)
# Track execution order per thread
if thread_id not in thread_executions:
thread_executions[thread_id] = []
# Check FIFO ordering within thread
if thread_executions[thread_id] and thread_executions[thread_id][-1] >= index:
fifo_violations.append(
f"Thread {thread_id}: index {index} executed after "
f"{thread_executions[thread_id][-1]}"
)
thread_executions[thread_id].append(index)
# Check if we've executed all 1000 defers (0-999)
if len(executed_defers) == 1000 and not test_complete_future.done():
test_complete_future.set_result(None)
async with (
run_compiled(yaml_config, line_callback=on_log_line),
api_client_connected() as client,
):
# Verify we can connect
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "defer-stress-test"
# List entities and services
entity_info, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0
)
# Find our test service
run_stress_test_service: UserService | None = None
for service in services:
if service.name == "run_stress_test":
run_stress_test_service = service
break
assert run_stress_test_service is not None, "run_stress_test service not found"
# Call the run_stress_test service to start the test
client.execute_service(run_stress_test_service, {})
# Wait for all defers to execute (should be quick)
try:
await asyncio.wait_for(test_complete_future, timeout=5.0)
except asyncio.TimeoutError:
# Report how many we got
pytest.fail(
f"Stress test timed out. Only {len(executed_defers)} of "
f"1000 defers executed. Missing IDs: "
f"{sorted(set(range(1000)) - executed_defers)[:10]}..."
)
# Verify all defers executed
assert len(executed_defers) == 1000, (
f"Expected 1000 defers, got {len(executed_defers)}"
)
# Verify we have all IDs from 0-999
expected_ids = set(range(1000))
missing_ids = expected_ids - executed_defers
assert not missing_ids, f"Missing defer IDs: {sorted(missing_ids)}"
# Verify FIFO ordering was maintained within each thread
assert not fifo_violations, "FIFO ordering violations detected:\n" + "\n".join(
fifo_violations[:10]
)
# Verify each thread executed all its defers in order
for thread_id, indices in thread_executions.items():
assert len(indices) == 100, (
f"Thread {thread_id} executed {len(indices)} defers, expected 100"
)
# Indices should be 0-99 in ascending order
assert indices == list(range(100)), (
f"Thread {thread_id} executed indices out of order: {indices[:10]}..."
)
# If we got here without crashing and with proper ordering, the test passed
assert True, (
"Test completed successfully - all 1000 defers executed with "
"FIFO ordering preserved"
)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState
from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -40,28 +40,22 @@ async def test_device_id_in_state(
entity_device_mapping: dict[int, int] = {}
for entity in all_entities:
if hasattr(entity, "name") and hasattr(entity, "key"):
if entity.name == "Temperature":
entity_device_mapping[entity.key] = device_ids[
"Temperature Monitor"
]
elif entity.name == "Humidity":
entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
elif entity.name == "Motion Detected":
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
elif entity.name == "Temperature Monitor Power":
entity_device_mapping[entity.key] = device_ids[
"Temperature Monitor"
]
elif entity.name == "Temperature Status":
entity_device_mapping[entity.key] = device_ids[
"Temperature Monitor"
]
elif entity.name == "Motion Light":
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
elif entity.name == "No Device Sensor":
# Entity without device_id should have device_id 0
entity_device_mapping[entity.key] = 0
# All entities have name and key attributes
if entity.name == "Temperature":
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
elif entity.name == "Humidity":
entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
elif entity.name == "Motion Detected":
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
elif entity.name == "Temperature Monitor Power":
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
elif entity.name == "Temperature Status":
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
elif entity.name == "Motion Light":
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
elif entity.name == "No Device Sensor":
# Entity without device_id should have device_id 0
entity_device_mapping[entity.key] = 0
assert len(entity_device_mapping) >= 6, (
f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}"
@@ -111,7 +105,7 @@ async def test_device_id_in_state(
(
s
for s in states.values()
if hasattr(s, "state")
if isinstance(s, SensorState)
and isinstance(s.state, float)
and s.device_id != 0
),
@@ -122,11 +116,7 @@ async def test_device_id_in_state(
# Find a binary sensor state
binary_sensor_state = next(
(
s
for s in states.values()
if hasattr(s, "state") and isinstance(s.state, bool)
),
(s for s in states.values() if isinstance(s, BinarySensorState)),
None,
)
assert binary_sensor_state is not None, "No binary sensor state found"
@@ -136,11 +126,7 @@ async def test_device_id_in_state(
# Find a text sensor state
text_sensor_state = next(
(
s
for s in states.values()
if hasattr(s, "state") and isinstance(s.state, str)
),
(s for s in states.values() if isinstance(s, TextSensorState)),
None,
)
assert text_sensor_state is not None, "No text sensor state found"

View File

@@ -51,9 +51,6 @@ async def test_entity_icon(
entity = entity_map[entity_name]
# Check icon field
assert hasattr(entity, "icon"), (
f"{entity_name}: Entity should have icon attribute"
)
assert entity.icon == expected_icon, (
f"{entity_name}: icon mismatch - "
f"expected '{expected_icon}', got '{entity.icon}'"
@@ -67,9 +64,6 @@ async def test_entity_icon(
entity = entity_map[entity_name]
# Check icon field is empty
assert hasattr(entity, "icon"), (
f"{entity_name}: Entity should have icon attribute"
)
assert entity.icon == "", (
f"{entity_name}: icon should be empty string for entities without icons, "
f"got '{entity.icon}'"

View File

@@ -25,8 +25,8 @@ async def test_host_mode_entity_fields(
# Create a map of entity names to entity info
entity_map = {}
for entity in entities[0]:
if hasattr(entity, "name"):
entity_map[entity.name] = entity
# All entities should have a name attribute
entity_map[entity.name] = entity
# Test entities that should be visible via API (non-internal)
visible_test_cases = [

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState
from aioesphomeapi import EntityState, SensorState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -30,7 +30,7 @@ async def test_host_mode_many_entities(
sensor_states = [
s
for s in states.values()
if hasattr(s, "state") and isinstance(s.state, float)
if isinstance(s, SensorState) and isinstance(s.state, float)
]
# When we have received states from at least 50 sensors, resolve the future
if len(sensor_states) >= 50 and not sensor_count_future.done():
@@ -45,7 +45,7 @@ async def test_host_mode_many_entities(
sensor_states = [
s
for s in states.values()
if hasattr(s, "state") and isinstance(s.state, float)
if isinstance(s, SensorState) and isinstance(s.state, float)
]
pytest.fail(
f"Did not receive states from at least 50 sensors within 10 seconds. "
@@ -61,7 +61,7 @@ async def test_host_mode_many_entities(
sensor_states = [
s
for s in states.values()
if hasattr(s, "state") and isinstance(s.state, float)
if isinstance(s, SensorState) and isinstance(s.state, float)
]
assert sensor_count >= 50, (

View File

@@ -19,16 +19,17 @@ async def test_host_mode_with_sensor(
) -> None:
"""Test Host mode with a sensor component."""
# Write, compile and run the ESPHome device, then connect to API
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
# Subscribe to state changes
states: dict[int, EntityState] = {}
sensor_future: asyncio.Future[EntityState] = asyncio.Future()
sensor_future: asyncio.Future[EntityState] = loop.create_future()
def on_state(state: EntityState) -> None:
states[state.key] = state
# If this is our sensor with value 42.0, resolve the future
if (
hasattr(state, "state")
isinstance(state, aioesphomeapi.SensorState)
and state.state == 42.0
and not sensor_future.done()
):

View File

@@ -7,6 +7,7 @@ including RGB, color temperature, effects, transitions, and flash.
import asyncio
from typing import Any
from aioesphomeapi import LightState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -76,7 +77,7 @@ async def test_light_calls(
client.light_command(key=rgbcw_light.key, white=0.6)
state = await wait_for_state_change(rgbcw_light.key)
# White might need more tolerance or might not be directly settable
if hasattr(state, "white"):
if isinstance(state, LightState) and state.white is not None:
assert state.white == pytest.approx(0.6, abs=0.1)
# Test 8: color_temperature only

View File

@@ -3,8 +3,10 @@
from collections.abc import Callable
from unittest.mock import patch
from esphome.config_helpers import filter_source_files_from_platform
from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
from esphome.const import (
CONF_LEVEL,
CONF_LOGGER,
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
@@ -108,3 +110,26 @@ def test_filter_source_files_from_platform_handles_missing_data() -> None:
excluded: list[str] = filter_func()
# Should return empty list when platform/framework not set
assert excluded == []
def test_get_logger_level() -> None:
"""Test get_logger_level helper function."""
# Test no logger config - should return default DEBUG
mock_config = {}
with patch("esphome.config_helpers.CORE.config", mock_config):
assert get_logger_level() == "DEBUG"
# Test with logger set to INFO
mock_config = {CONF_LOGGER: {CONF_LEVEL: "INFO"}}
with patch("esphome.config_helpers.CORE.config", mock_config):
assert get_logger_level() == "INFO"
# Test with VERY_VERBOSE
mock_config = {CONF_LOGGER: {CONF_LEVEL: "VERY_VERBOSE"}}
with patch("esphome.config_helpers.CORE.config", mock_config):
assert get_logger_level() == "VERY_VERBOSE"
# Test with logger missing level (uses default DEBUG)
mock_config = {CONF_LOGGER: {}}
with patch("esphome.config_helpers.CORE.config", mock_config):
assert get_logger_level() == "DEBUG"