Compare commits

..

102 Commits

Author SHA1 Message Date
J. Nick Koston
e44eb59e3c Merge branch 'integration' into memory_api 2025-08-22 08:52:52 -05:00
J. Nick Koston
6e9acfdebd Merge branch 'ble_retries_no_give_up_before_timeout' into integration 2025-08-22 08:52:45 -05:00
J. Nick Koston
29b25194ba [esp32_ble] Increase GATT connection retry count to use full timeout window 2025-08-22 08:48:27 -05:00
J. Nick Koston
a38b994f2b [esp32_ble] Increase GATT connection retry count to use full timeout window 2025-08-22 08:45:35 -05:00
J. Nick Koston
101d98014d Merge branch 'integration' into memory_api 2025-08-21 21:47:24 -05:00
J. Nick Koston
9fd9c1b9d3 Merge branch 'connection_params' into integration 2025-08-21 21:47:16 -05:00
J. Nick Koston
7d7dbefb60 tweak 2025-08-21 21:46:36 -05:00
J. Nick Koston
caf4fcce5f Merge branch 'dev' into connection_params 2025-08-21 21:45:18 -05:00
J. Nick Koston
65eb57ca1b tweak 2025-08-21 21:44:59 -05:00
J. Nick Koston
082f795adc Merge branch 'integration' into memory_api 2025-08-21 20:27:21 -05:00
J. Nick Koston
f83f05b704 Merge branch 'connection_params' into integration 2025-08-21 20:27:15 -05:00
J. Nick Koston
9d16eeeb77 tweak 2025-08-21 20:27:04 -05:00
dependabot[bot]
83fe4b4ff3 Bump ruff from 0.12.9 to 0.12.10 (#10362)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-21 15:36:06 -05:00
J. Nick Koston
3ac5285515 Merge branch 'integration' into memory_api 2025-08-21 09:00:18 -05:00
J. Nick Koston
8f44ec0b51 Merge branch 'connection_params' into integration 2025-08-21 09:00:12 -05:00
J. Nick Koston
f12bcc621c dry, review 2025-08-21 09:00:01 -05:00
J. Nick Koston
2993940e1d Merge branch 'integration' into memory_api 2025-08-21 08:25:28 -05:00
J. Nick Koston
f4077fdd52 Merge branch 'connection_params' into integration 2025-08-21 08:25:23 -05:00
J. Nick Koston
1ba37ca7c9 preen 2025-08-21 08:25:13 -05:00
J. Nick Koston
db96cfaf14 Merge branch 'integration' into memory_api 2025-08-21 08:12:51 -05:00
J. Nick Koston
6054c3eac8 Merge branch 'connection_params' into integration 2025-08-21 08:12:43 -05:00
J. Nick Koston
2f101c0a20 [esp32_ble_client] Adjust connection parameters to improve device compatibility 2025-08-21 08:11:17 -05:00
Jesse Hills
94accd5abe [ld2420] Rename c++ files for predictable doxygen generation (#10315) 2025-08-21 18:49:26 +12:00
Jesse Hills
3ca0015284 [opentherm] Rename c++ files for predictable doxygen generation (#10314) 2025-08-21 18:48:48 +12:00
J. Nick Koston
bad7b0598b Merge branch 'integration' into memory_api 2025-08-20 20:26:20 -05:00
J. Nick Koston
840a814f75 Merge remote-tracking branch 'origin/event_39' into integration 2025-08-20 20:26:14 -05:00
J. Nick Koston
4dd01ea9ac [esp32_ble_client] Add missing ESP_GATTC_UNREG_FOR_NOTIFY_EVT logging 2025-08-20 20:25:50 -05:00
J. Nick Koston
b4f0d4b0c3 Merge branch 'integration' into memory_api 2025-08-20 20:24:31 -05:00
J. Nick Koston
7577153baa Merge remote-tracking branch 'origin/event_39' into integration 2025-08-20 20:24:22 -05:00
J. Nick Koston
a6850786e2 [esp32_ble_client] Add missing ESP_GATTC_UNREG_FOR_NOTIFY_EVT logging 2025-08-20 20:23:37 -05:00
J. Nick Koston
df7c2473c1 Merge branch 'integration' into memory_api 2025-08-20 20:07:21 -05:00
J. Nick Koston
18ad9804d3 Merge branch 'open_error_fix' into integration 2025-08-20 20:07:13 -05:00
J. Nick Koston
8ee46435a3 cleanup 2025-08-20 20:04:13 -05:00
dependabot[bot]
33eddb6035 Bump codecov/codecov-action from 5.4.3 to 5.5.0 (#10336)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-20 16:46:04 -05:00
J. Nick Koston
dde1cec3ce Merge branch 'integration' into memory_api 2025-08-20 14:54:39 -05:00
J. Nick Koston
fbd3cb0965 Merge branch 'open_error_fix' into integration 2025-08-20 14:54:31 -05:00
J. Nick Koston
977ff9b481 [esp32_ble_client] Fix race condition causing "ESP_GATTC_OPEN_EVT in IDLE state" error spam 2025-08-20 14:53:42 -05:00
J. Nick Koston
3c6a54bb70 Merge branch 'integration' into memory_api 2025-08-20 13:26:15 -05:00
J. Nick Koston
ec7919e294 Merge branch 'pvvx_mithermometer_defer_write' into integration 2025-08-20 13:26:09 -05:00
J. Nick Koston
5253b77cfe Merge branch 'improve_entity_error_reporting_sanitized_name' into integration 2025-08-20 13:26:04 -05:00
J. Nick Koston
77aba57078 Merge branch 'prefs_device_id' into integration 2025-08-20 13:26:00 -05:00
J. Nick Koston
6c01e7196c preen 2025-08-20 13:09:15 -05:00
J. Nick Koston
2c44198cb5 preen 2025-08-20 13:06:18 -05:00
J. Nick Koston
757ad2ff96 [core] Fix preference storage to account for device_id 2025-08-20 13:02:14 -05:00
J. Nick Koston
86c3812174 preen 2025-08-20 12:15:54 -05:00
J. Nick Koston
d182ce8bf6 preen 2025-08-20 12:12:39 -05:00
J. Nick Koston
ea5da950c0 [core] Improve error reporting for entity name conflicts with non-ASCII characters 2025-08-20 12:09:26 -05:00
J. Nick Koston
c88f2eb4d1 reduce 2025-08-20 11:38:07 -05:00
J. Nick Koston
998a9264a1 fix race 2025-08-20 10:05:46 -05:00
Jesse Hills
72c58ae36d [core] Add idf-tidy env for esp32-c6 (#10270) 2025-08-20 10:13:50 -04:00
J. Nick Koston
35411d199f [homeassistant] Add compilation test for homeassistant.tag_scanned action (#10319) 2025-08-20 10:10:20 -04:00
J. Nick Koston
6fd46966e2 Merge branch 'integration' into memory_api 2025-08-20 08:07:50 -05:00
J. Nick Koston
916c661544 Merge branch 'duplicate_stop_path' into integration 2025-08-20 08:07:45 -05:00
J. Nick Koston
4d4ab5b804 preen 2025-08-20 08:07:16 -05:00
J. Nick Koston
558cdda8a4 Merge branch 'integration' into memory_api 2025-08-20 08:06:47 -05:00
J. Nick Koston
3e1be2b987 Merge branch 'duplicate_stop_path' into integration 2025-08-20 08:06:39 -05:00
J. Nick Koston
d555766306 fix 2025-08-20 08:06:27 -05:00
J. Nick Koston
856ffc2e86 Merge branch 'integration' into memory_api 2025-08-20 07:46:07 -05:00
J. Nick Koston
5a740fb390 Merge branch 'remove_searching' into integration 2025-08-20 07:46:01 -05:00
J. Nick Koston
cdf85b419b Merge branch 'duplicate_stop_path' into integration 2025-08-20 07:45:56 -05:00
J. Nick Koston
d03eec5a58 [esp32_ble_tracker] Remove duplicate client promotion logic 2025-08-20 07:44:35 -05:00
J. Nick Koston
9f4e31b07c preen 2025-08-20 07:01:13 -05:00
J. Nick Koston
df0ed57667 preen 2025-08-20 06:58:43 -05:00
J. Nick Koston
963b0333ba preen 2025-08-20 06:58:27 -05:00
J. Nick Koston
fbc9b751c5 preen 2025-08-20 06:58:20 -05:00
Jesse Hills
d45944a9e2 [api] Add `USE_API_HOMEASSISTANT_SERVICES if using tag_scanned` action (#10316) 2025-08-20 06:47:20 -05:00
J. Nick Koston
d8c85bfc44 [bluetooth_proxy] Remove unused ClientState::SEARCHING state 2025-08-20 06:44:25 -05:00
Jesse Hills
86f306ba9e [CI] Also require tests for `new-features` (#10311) 2025-08-20 22:02:14 +12:00
Jesse Hills
1b3b2f6e6f Merge branch 'release' into dev 2025-08-20 19:58:48 +12:00
Jesse Hills
2adb993242 Merge pull request #10309 from esphome/bump-2025.8.0
2025.8.0
2025-08-20 19:58:01 +12:00
J. Nick Koston
518443606c Merge branch 'integration' into memory_api 2025-08-19 21:53:35 -05:00
J. Nick Koston
c5b794e41c merge 2025-08-19 21:53:28 -05:00
J. Nick Koston
72dd419c49 Merge branch 'integration' into memory_api 2025-08-19 21:52:31 -05:00
J. Nick Koston
951842e30f Merge remote-tracking branch 'upstream/dev' into integration 2025-08-19 21:52:22 -05:00
J. Nick Koston
3ff5b4773b [bluetooth_proxy] Mark BluetoothConnection and BluetoothProxy as final for compiler optimizations (#10280) 2025-08-20 14:48:40 +12:00
J. Nick Koston
2cbf4f30f9 [libretiny] Optimize preferences is_changed() by replacing temporary vector with unique_ptr (#10272) 2025-08-20 14:48:04 +12:00
J. Nick Koston
56b6dd31f1 [core] Eliminate heap allocation in teardown_components by using StaticVector (#10256)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-20 14:45:13 +12:00
J. Nick Koston
997223c93e Merge branch 'integration' into memory_api 2025-08-19 21:43:20 -05:00
J. Nick Koston
f0da3d6c09 Merge branch 'dev' into integration 2025-08-19 21:43:09 -05:00
dependabot[bot]
fc1b49e87d Bump ruamel-yaml from 0.18.14 to 0.18.15 (#10310)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-20 14:42:33 +12:00
J. Nick Koston
0089619518 [web_server] Reduce flash usage by consolidating defer calls in switch and lock handlers (#10297) 2025-08-19 21:41:34 -05:00
Jesse Hills
5a6db28f1d [CI] Base `too-big` label on new additions only (#10307) 2025-08-20 14:39:29 +12:00
J. Nick Koston
6819bbd8f8 [esp32_ble_client] Add log helper functions to reduce flash usage by 120 bytes (#10243) 2025-08-20 14:38:32 +12:00
Edward Firmo
634f687c3e [light] Add support for querying effects by index (#10195)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-08-20 14:38:13 +12:00
J. Nick Koston
e2a9b85924 [number] Convert LOG_NUMBER macro to function to reduce flash usage (#10293) 2025-08-20 14:36:05 +12:00
J. Nick Koston
4ccc6aee09 [button] Convert LOG_BUTTON macro to function to reduce flash usage (#10295)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-20 14:35:53 +12:00
J. Nick Koston
0eab908b0e [sensor] Convert LOG_SENSOR macro to function to reduce flash usage (#10290) 2025-08-20 14:35:45 +12:00
J. Nick Koston
3964f9794b [binary_sensor] Convert LOG_BINARY_SENSOR macro to function to reduce flash usage (#10294) 2025-08-20 14:35:09 +12:00
Jesse Hills
a45137434b [quality] Convert remaining `to_code to async` (#10271) 2025-08-20 14:34:45 +12:00
J. Nick Koston
9b1ebdb6da [mdns] Reduce flash usage and prevent RAM over-allocation in service compilation (#10287) 2025-08-20 14:34:34 +12:00
J. Nick Koston
5a1533bea9 [api] Avoid object_id string allocations for all entity info messages (#10260) 2025-08-20 14:28:13 +12:00
Jesse Hills
0b50ef227b [helper] Make crc8 function more flexible to avoid reimplementation in individual components (#10201) 2025-08-20 14:27:08 +12:00
J. Nick Koston
0e31bc1a67 [api] Add zero-copy StringRef methods for compilation_time and effect_name (#10257) 2025-08-20 14:26:53 +12:00
Jesse Hills
8e67df8059 Bump version to 2025.8.0 2025-08-20 10:45:57 +12:00
Jesse Hills
e1a0949ddb Merge branch 'beta' into dev 2025-08-20 10:31:10 +12:00
Jesse Hills
c5b2c8d971 Merge pull request #10308 from esphome/bump-2025.8.0b4
2025.8.0b4
2025-08-20 10:30:37 +12:00
J. Nick Koston
a8775ba60b [safe_mode] Reduce flash usage by 184 bytes through code optimization (#10284) 2025-08-19 16:57:24 -05:00
Jesse Hills
104906ca11 Bump version to 2025.8.0b4 2025-08-20 09:40:19 +12:00
J. Nick Koston
ad5f6f0cfe [bluetooth_proxy] Fix connection slot race by deferring slot release until GATT close (#10303) 2025-08-20 09:40:19 +12:00
Patrick
8356f7fcd3 [pipsolar] fix faults_present, fix update interval (#10289) 2025-08-20 09:40:19 +12:00
Ben Winslow
225de226b0 [atm90e32] Only read 1 register per SPI transaction per datasheet. (#10258) 2025-08-20 09:40:19 +12:00
J. Nick Koston
2aaf951357 [bluetooth_proxy] Fix connection slot race by deferring slot release until GATT close (#10303) 2025-08-20 07:27:22 +12:00
102 changed files with 1171 additions and 480 deletions

View File

@@ -1 +1 @@
6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273
0440e35cf89a49e8a35fd3690ed453a72b7b6f61b9d346ced6140e1c0d39dff6

View File

@@ -105,7 +105,9 @@ jobs:
// Calculate data from PR files
const changedFiles = prFiles.map(file => file.filename);
const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
const totalChanges = totalAdditions + totalDeletions;
console.log('Current labels:', currentLabels.join(', '));
console.log('Changed files:', changedFiles.length);
@@ -231,16 +233,21 @@ jobs:
// Strategy: PR size detection
async function detectPRSize() {
const labels = new Set();
const testChanges = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const nonTestChanges = totalChanges - testChanges;
if (totalChanges <= SMALL_PR_THRESHOLD) {
labels.add('small-pr');
return labels;
}
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
// Don't add too-big if mega-pr label is already present
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
labels.add('too-big');
@@ -375,7 +382,7 @@ jobs:
const labels = new Set();
// Check for missing tests
if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) {
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
labels.add('needs-tests');
}
@@ -412,10 +419,13 @@ jobs:
// Too big message
if (finalLabels.includes('too-big')) {
const testChanges = prFiles
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const nonTestChanges = totalChanges - testChanges;
.reduce((sum, file) => sum + (file.additions || 0), 0);
const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = finalLabels.length > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;

View File

@@ -156,7 +156,7 @@ jobs:
. venv/bin/activate
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
uses: codecov/codecov-action@v5.5.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.9
rev: v0.12.10
hooks:
# Run the linter.
- id: ruff

View File

@@ -89,7 +89,7 @@ void AGS10Component::dump_config() {
bool AGS10Component::new_i2c_address(uint8_t newaddress) {
uint8_t rev_newaddress = ~newaddress;
std::array<uint8_t, 5> data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0};
data[4] = calc_crc8_(data, 4);
data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
if (!this->write_bytes(REG_ADDRESS, data)) {
this->error_code_ = COMMUNICATION_FAILED;
this->status_set_warning();
@@ -109,7 +109,7 @@ bool AGS10Component::set_zero_point_with_current_resistance() { return this->set
bool AGS10Component::set_zero_point_with(uint16_t value) {
std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0};
data[4] = calc_crc8_(data, 4);
data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
if (!this->write_bytes(REG_CALIBRATION, data)) {
this->error_code_ = COMMUNICATION_FAILED;
this->status_set_warning();
@@ -184,7 +184,7 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che
auto res = *data;
auto crc_byte = res[len];
if (crc_byte != calc_crc8_(res, len)) {
if (crc_byte != crc8(res.data(), len, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED;
ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!");
return optional<std::array<uint8_t, N>>();
@@ -192,20 +192,5 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che
return data;
}
template<size_t N> uint8_t AGS10Component::calc_crc8_(std::array<uint8_t, N> dat, uint8_t num) {
uint8_t i, byte1, crc = 0xFF;
for (byte1 = 0; byte1 < num; byte1++) {
crc ^= (dat[byte1]);
for (i = 0; i < 8; i++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc = (crc << 1);
}
}
}
return crc;
}
} // namespace ags10
} // namespace esphome

View File

@@ -1,9 +1,9 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace ags10 {
@@ -99,16 +99,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice {
* Read, checks and returns data from the sensor.
*/
template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register);
/**
* Calculates CRC8 value.
*
* CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1)
*
* @param[in] dat the data buffer
* @param num number of bytes in the buffer
*/
template<size_t N> uint8_t calc_crc8_(std::array<uint8_t, N> dat, uint8_t num);
};
template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>, public Parented<AGS10Component> {

View File

@@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema(
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
def to_code(config):
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield esp32_ble_tracker.register_ble_device(var, config)
await esp32_ble_tracker.register_ble_device(var, config)

View File

@@ -29,22 +29,6 @@ namespace am2315c {
static const char *const TAG = "am2315c";
uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) {
uint8_t crc = 0xFF;
while (len--) {
crc ^= *data++;
for (uint8_t i = 0; i < 8; i++) {
if (crc & 0x80) {
crc <<= 1;
crc ^= 0x31;
} else {
crc <<= 1;
}
}
}
return crc;
}
bool AM2315C::reset_register_(uint8_t reg) {
// code based on demo code sent by www.aosong.com
// no further documentation.
@@ -86,7 +70,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
humidity = raw * 9.5367431640625e-5;
raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
temperature = raw * 1.9073486328125e-4 - 50;
return this->crc8_(data, 6) == data[6];
return crc8(data, 6, 0xFF, 0x31, true) == data[6];
}
void AM2315C::setup() {

View File

@@ -21,9 +21,9 @@
// SOFTWARE.
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace am2315c {
@@ -39,7 +39,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice {
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
protected:
uint8_t crc8_(uint8_t *data, uint8_t len);
bool convert_(uint8_t *data, float &humidity, float &temperature);
bool reset_register_(uint8_t reg);

View File

@@ -321,6 +321,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
)
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))

View File

@@ -11,7 +11,6 @@
#include <utility>
#include <functional>
#include <limits>
#include "api_timing.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
@@ -141,9 +140,7 @@ void APIConnection::loop() {
return;
}
API_TIMING_START(helper);
APIError err = this->helper_->loop();
API_TIMING_END(helper, HELPER_LOOP);
if (err != APIError::OK) {
on_fatal_error();
this->log_socket_operation_failed_(err);
@@ -152,16 +149,11 @@ void APIConnection::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Check if socket has data ready before attempting to read
API_TIMING_START(socket_check);
bool socket_ready = this->helper_->is_socket_ready();
API_TIMING_END(socket_check, SOCKET_CHECK);
if (socket_ready) {
if (this->helper_->is_socket_ready()) {
// Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
ReadPacketBuffer buffer;
API_TIMING_START(read);
err = this->helper_->read_packet(&buffer);
API_TIMING_END(read, READ_PACKET);
if (err == APIError::WOULD_BLOCK) {
// No more data available
break;
@@ -172,13 +164,11 @@ void APIConnection::loop() {
} else {
this->last_traffic_ = now;
// read a packet
API_TIMING_START(process);
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
API_TIMING_END(process, PROCESS_MESSAGE);
if (this->flags_.remove)
return;
}
@@ -187,27 +177,19 @@ void APIConnection::loop() {
// Process deferred batch if scheduled and timer has expired
if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
API_TIMING_START(deferred);
this->process_batch_();
API_TIMING_END(deferred, DEFERRED_BATCH);
}
if (!this->list_entities_iterator_.completed()) {
API_TIMING_START(iterator1);
this->process_iterator_batch_(this->list_entities_iterator_);
API_TIMING_END(iterator1, ITERATOR_BATCH);
} else if (!this->initial_state_iterator_.completed()) {
API_TIMING_START(iterator2);
this->process_iterator_batch_(this->initial_state_iterator_);
API_TIMING_END(iterator2, ITERATOR_BATCH);
// If we've completed initial states, process any remaining and clear the flag
if (this->initial_state_iterator_.completed()) {
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
API_TIMING_START(deferred2);
this->process_batch_();
API_TIMING_END(deferred2, DEFERRED_BATCH);
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
@@ -221,7 +203,6 @@ void APIConnection::loop() {
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
}
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
API_TIMING_GUARD(KEEPALIVE);
// Only send ping if we're not disconnecting
ESP_LOGVV(TAG, "Sending keepalive PING");
PingRequest req;
@@ -238,7 +219,6 @@ void APIConnection::loop() {
#ifdef USE_CAMERA
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
API_TIMING_GUARD(CAMERA_IMAGE);
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
@@ -293,11 +273,9 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
#endif
// Calculate size
API_TIMING_START(msg_size_calc);
ProtoSize size_calc;
msg.calculate_size(size_calc);
uint32_t calculated_size = size_calc.get_size();
API_TIMING_END(msg_size_calc, ENCODE_MESSAGE);
// Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding();
@@ -330,9 +308,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
// Encode directly into buffer
size_t size_before_encode = shared_buf.size();
API_TIMING_START(msg_encode);
msg.encode({&shared_buf});
API_TIMING_END(msg_encode, ENCODE_MESSAGE);
// Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode;
@@ -1584,9 +1560,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false;
}
API_TIMING_START(write);
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
API_TIMING_END(write, WRITE_PACKET);
if (err == APIError::WOULD_BLOCK)
return false;
if (err != APIError::OK) {
@@ -1662,14 +1636,11 @@ void APIConnection::process_batch_() {
return;
}
API_TIMING_START(buffer_clear);
// Try to clear buffer first
if (!this->try_to_clear_buffer(true)) {
// Can't write now, we'll try again later
API_TIMING_END(buffer_clear, BUFFER_MANAGEMENT);
return;
}
API_TIMING_END(buffer_clear, BUFFER_MANAGEMENT);
// Get shared buffer reference once to avoid multiple calls
auto &shared_buf = this->parent_->get_shared_buffer_ref();
@@ -1680,10 +1651,8 @@ void APIConnection::process_batch_() {
const auto &item = this->deferred_batch_[0];
// Let the creator calculate size and encode if it fits
API_TIMING_START(single_encode);
uint16_t payload_size =
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
API_TIMING_END(single_encode, BATCH_ENCODE);
if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&shared_buf}, item.message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1736,14 +1705,11 @@ void APIConnection::process_batch_() {
uint32_t current_offset = 0;
// Process items and encode directly to buffer (up to our limit)
API_TIMING_START(batch_encode_loop);
for (size_t i = 0; i < packets_to_process; i++) {
const auto &item = this->deferred_batch_[i];
// Try to encode message
// The creator will calculate overhead to determine if the message fits
API_TIMING_START(item_encode);
uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type);
API_TIMING_END(item_encode, BATCH_ENCODE);
if (payload_size == 0) {
// Message won't fit, stop processing
@@ -1770,7 +1736,6 @@ void APIConnection::process_batch_() {
// Current buffer size + footer space for this message
current_offset = shared_buf.size() + footer_size;
}
API_TIMING_END(batch_encode_loop, BATCH_ENCODE);
if (items_processed == 0) {
this->deferred_batch_.clear();
@@ -1783,10 +1748,8 @@ void APIConnection::process_batch_() {
}
// Send all collected packets
API_TIMING_START(batch_write);
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
std::span<const PacketInfo>(packet_info, packet_count));
API_TIMING_END(batch_write, PROTO_WRITE);
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error();
this->log_warning_("Batch write failed", err);

View File

@@ -6,7 +6,6 @@
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
#include "api_timing.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
@@ -336,11 +335,9 @@ class APIConnection final : public APIServerConnection {
// 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();
API_TIMING_START(iter_advance);
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
iterator.advance();
}
API_TIMING_END(iter_advance, ITERATOR_ENCODE);
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
@@ -684,7 +681,6 @@ class APIConnection final : public APIServerConnection {
// Helper method to send a message either immediately or via batching
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) {
API_TIMING_GUARD(SEND_STATE);
// Try to send immediately if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)

View File

@@ -1,7 +1,6 @@
// This file was automatically generated with a tool.
// See script/api_protobuf/api_protobuf.py
#include "api_pb2_service.h"
#include "api_timing.h"
#include "esphome/core/log.h"
namespace esphome::api {
@@ -15,18 +14,14 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
#endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
API_TIMING_START(msg_decode);
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg;
msg.decode(msg_data, msg_size);
API_TIMING_END(msg_decode, PROTO_READ);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str());
#endif
API_TIMING_START(msg_handle);
this->on_hello_request(msg);
API_TIMING_END(msg_handle, MESSAGE_DISPATCH);
break;
}
case ConnectRequest::MESSAGE_TYPE: {

View File

@@ -2,7 +2,6 @@
#ifdef USE_API
#include <cerrno>
#include "api_connection.h"
#include "api_timing.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
@@ -136,7 +135,6 @@ void APIServer::schedule_reboot_timeout_() {
void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) {
API_TIMING_GUARD(ACCEPT_CLIENT);
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
@@ -163,7 +161,6 @@ void APIServer::loop() {
// Process clients and remove disconnected ones in a single pass
// Check network connectivity once for all clients
API_TIMING_START(network_check);
if (!network::is_connected()) {
// Network is down - disconnect all clients
for (auto &client : this->clients_) {
@@ -172,7 +169,6 @@ void APIServer::loop() {
}
// Continue to process and clean up the clients below
}
API_TIMING_END(network_check, NETWORK_CHECK);
size_t client_index = 0;
while (client_index < this->clients_.size()) {
@@ -180,15 +176,12 @@ void APIServer::loop() {
if (!client->flags_.remove) {
// Common case: process active client
API_TIMING_START(client_loop);
client->loop();
API_TIMING_END(client_loop, CLIENT_LOOP);
client_index++;
continue;
}
// Rare case: handle disconnection
API_TIMING_GUARD(CLIENT_REMOVAL);
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
#endif

View File

@@ -10,11 +10,9 @@ namespace esphome {
namespace binary_sensor {
// Forward declaration
class BinarySensor;
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
// Macro that calls the function - kept for backward compatibility
#define LOG_BINARY_SENSOR(prefix, type, obj) log_binary_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj)
#define SUB_BINARY_SENSOR(name) \

View File

@@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
)
def to_code(config):
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add(
@@ -63,6 +63,6 @@ def to_code(config):
)
cg.add(var.set_char_uuid128(uuid128))
cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE]))
yield output.register_output(var, config)
yield ble_client.register_ble_node(var, config)
yield cg.register_component(var, config)
await output.register_output(var, config)
await ble_client.register_ble_node(var, config)
await cg.register_component(var, config)

View File

@@ -183,6 +183,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
this->send_device_connection(msg.address, false);
return;
}
if (!msg.has_address_type) {
ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(),
connection->address_str().c_str());
this->send_device_connection(msg.address, false);
return;
}
if (connection->state() == espbt::ClientState::CONNECTED ||
connection->state() == espbt::ClientState::ESTABLISHED) {
this->log_connection_request_ignored_(connection, connection->state());
@@ -209,13 +215,9 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
this->log_connection_info_(connection, "v3 without cache");
}
if (msg.has_address_type) {
uint64_to_bd_addr(msg.address, connection->remote_bda_);
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
connection->set_state(espbt::ClientState::DISCOVERED);
} else {
connection->set_state(espbt::ClientState::SEARCHING);
}
uint64_to_bd_addr(msg.address, connection->remote_bda_);
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
connection->set_state(espbt::ClientState::DISCOVERED);
this->send_connections_free();
break;
}

View File

@@ -7,11 +7,9 @@
namespace esphome {
namespace button {
// Forward declaration
class Button;
void log_button(const char *tag, const char *prefix, const char *type, Button *obj);
// Macro that calls the function - kept for backward compatibility
#define LOG_BUTTON(prefix, type, obj) log_button(TAG, prefix, LOG_STR_LITERAL(type), obj)
#define SUB_BUTTON(name) \

View File

@@ -327,7 +327,7 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_object_id_hash() ^
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^
RESTORE_STATE_VERSION);
ClimateDeviceRestoreState recovered{};
if (!this->rtc_.load(&recovered))

View File

@@ -228,9 +228,9 @@ async def cover_stop_to_code(config, action_id, template_arg, args):
@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA)
def cover_toggle_to_code(config, action_id, template_arg, args):
paren = yield cg.get_variable(config[CONF_ID])
yield cg.new_Pvariable(action_id, template_arg, paren)
async def cover_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
COVER_CONTROL_ACTION_SCHEMA = cv.Schema(

View File

@@ -194,7 +194,7 @@ void Cover::publish_state(bool save) {
}
}
optional<CoverRestoreState> Cover::restore_state_() {
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash());
CoverRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -41,7 +41,7 @@ void DutyTimeSensor::setup() {
uint32_t seconds = 0;
if (this->restore_) {
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_preference_hash());
this->pref_.load(&seconds);
}

View File

@@ -280,6 +280,10 @@ async def to_code(config):
add_idf_sdkconfig_option(
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
)
# Increase GATT client connection retry count for problematic devices
# Default in ESP-IDF is 3, we increase to 10 for better reliability with
# low-power/timing-sensitive devices
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
# Set the maximum number of notification registrations
# This controls how many BLE characteristics can have notifications enabled

View File

@@ -7,6 +7,7 @@
#include <esp_gap_ble_api.h>
#include <esp_gatt_defs.h>
#include <esp_gattc_api.h>
namespace esphome::esp32_ble_client {
@@ -92,7 +93,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false;
if (this->address_ == 0 || device.address_uint64() != this->address_)
return false;
if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING)
if (this->state_ != espbt::ClientState::IDLE)
return false;
this->log_event_("Found device");
@@ -111,43 +112,19 @@ void BLEClientBase::connect() {
this->remote_addr_type_);
this->paired_ = false;
// Set preferred connection parameters before connecting
// Use FAST for all V3 connections (better latency and reliability)
// Use MEDIUM for V1/legacy connections (balanced performance)
uint16_t min_interval, max_interval, timeout;
const char *param_type;
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
min_interval = FAST_MIN_CONN_INTERVAL;
max_interval = FAST_MAX_CONN_INTERVAL;
timeout = FAST_CONN_TIMEOUT;
param_type = "fast";
} else {
min_interval = MEDIUM_MIN_CONN_INTERVAL;
max_interval = MEDIUM_MAX_CONN_INTERVAL;
timeout = MEDIUM_CONN_TIMEOUT;
param_type = "medium";
// Determine connection parameters based on connection type
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// V3 without cache needs fast params for service discovery
this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast");
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// V3 with cache can use medium params
this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
}
// For V1/Legacy, don't set params - use ESP-IDF defaults
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
0, // latency: 0
timeout);
if (param_ret != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
this->address_str_.c_str(), param_ret);
} else {
this->log_connection_params_(param_type);
}
// Now open the connection
// Open the connection
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
}
this->handle_connection_result_(ret);
}
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
@@ -159,7 +136,8 @@ void BLEClientBase::disconnect() {
return;
}
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
this->log_warning_("Disconnect before connected, disconnect scheduled.");
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
this->address_str_.c_str());
this->want_disconnect_ = true;
return;
}
@@ -190,8 +168,7 @@ void BLEClientBase::unconditional_disconnect() {
this->log_gattc_warning_("esp_ble_gattc_close", err);
}
if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT ||
this->state_ == espbt::ClientState::DISCOVERED) {
if (this->state_ == espbt::ClientState::READY_TO_CONNECT || this->state_ == espbt::ClientState::DISCOVERED) {
this->set_address(0);
this->set_state(espbt::ClientState::IDLE);
} else {
@@ -231,6 +208,15 @@ void BLEClientBase::log_connection_params_(const char *param_type) {
ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
}
void BLEClientBase::handle_connection_result_(esp_err_t ret) {
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
}
}
void BLEClientBase::log_error_(const char *message) {
ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
}
@@ -243,17 +229,30 @@ void BLEClientBase::log_warning_(const char *message) {
ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
}
void BLEClientBase::restore_medium_conn_params_() {
// Restore to medium connection parameters after initial connection phase
// This balances performance with bandwidth usage for normal operation
void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
conn_params.latency = 0;
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
this->log_connection_params_("medium");
esp_ble_gap_update_conn_params(&conn_params);
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
conn_params.latency = latency;
conn_params.timeout = timeout;
this->log_connection_params_(param_type);
esp_err_t err = esp_ble_gap_update_conn_params(&conn_params);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gap_update_conn_params", err);
}
}
void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type) {
// Set preferred connection parameters before connecting
// These will be used when establishing the connection
this->log_connection_params_(param_type);
esp_err_t err = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, latency, timeout);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gap_set_prefer_conn_params", err);
}
}
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
@@ -285,11 +284,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->log_gattc_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0;
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
// error, if the error occurred at the BTA/GATT layer. This can result in the event
// arriving after we've already transitioned to IDLE state.
if (this->state_ == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_.c_str(), param->open.status);
break;
}
if (this->state_ != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
this->log_error_("ESP_GATTC_OPEN_EVT wrong state status", param->open.status);
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
@@ -308,12 +318,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->set_state(espbt::ClientState::CONNECTED);
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// Restore to medium connection parameters for cached connections too
this->restore_medium_conn_params_();
// Cached connections already connected with medium parameters, no update needed
// only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED;
break;
}
// For V3_WITHOUT_CACHE, we already set fast params before connecting
// No need to update them again here
this->log_event_("Searching for services");
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
break;
@@ -395,12 +406,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
this->log_gattc_event_("SEARCH_CMPL");
// For V3 connections, restore to medium connection parameters after service discovery
// For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
this->restore_medium_conn_params_();
} else {
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
#ifdef USE_ESP32_BLE_DEVICE
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
@@ -484,6 +494,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
this->log_gattc_event_("UNREG_FOR_NOTIFY");
break;
}
default:
// ideally would check all other events for matching conn_id
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event);

View File

@@ -133,10 +133,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void log_event_(const char *name);
void log_gattc_event_(const char *name);
void restore_medium_conn_params_();
void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void log_gattc_warning_(const char *operation, esp_gatt_status_t status);
void log_gattc_warning_(const char *operation, esp_err_t err);
void log_connection_params_(const char *param_type);
void handle_connection_result_(esp_err_t ret);
// Compact error logging helpers to reduce flash usage
void log_error_(const char *message);
void log_error_(const char *message, int code);

View File

@@ -49,8 +49,6 @@ const char *client_state_to_string(ClientState state) {
return "DISCONNECTING";
case ClientState::IDLE:
return "IDLE";
case ClientState::SEARCHING:
return "SEARCHING";
case ClientState::DISCOVERED:
return "DISCOVERED";
case ClientState::READY_TO_CONNECT:
@@ -136,9 +134,8 @@ void ESP32BLETracker::loop() {
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
}
if (this->scanner_state_ == ScannerState::FAILED ||
@@ -158,10 +155,8 @@ void ESP32BLETracker::loop() {
https://github.com/espressif/esp-idf/issues/6688
*/
bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting;
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting &&
!promote_to_connecting) {
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
#endif
@@ -170,12 +165,11 @@ void ESP32BLETracker::loop() {
}
}
// If there is a discovered client and no connecting
// clients and no clients using the scanner to search for
// devices, then promote the discovered client to ready to connect.
// clients, then promote the discovered client to ready to connect.
// We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
if (promote_to_connecting &&
if (counts.discovered && !counts.connecting &&
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
this->try_promote_discovered_clients_();
}
@@ -307,14 +301,7 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
// Process the scan result immediately
bool found_discovered_client = this->process_scan_result_(scan_result);
// If we found a discovered client that needs promotion, stop scanning
// This replaces the promote_to_connecting logic from loop()
if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGD(TAG, "Found discovered client, stopping scan for connection");
this->stop_scan_();
}
this->process_scan_result_(scan_result);
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) {
@@ -640,9 +627,8 @@ void ESP32BLETracker::dump_config() {
this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_));
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
}
@@ -720,20 +706,9 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
}
bool ESP32BLETracker::has_connecting_clients_() const {
for (auto *client : this->clients_) {
auto state = client->state();
if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) {
return true;
}
}
return false;
}
#endif // USE_ESP32_BLE_DEVICE
bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
bool found_discovered_client = false;
void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
// Process raw advertisements
if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
@@ -759,14 +734,6 @@ bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
// Check if this client is discovered and needs promotion
if (client->state() == ClientState::DISCOVERED) {
// Only check for connecting clients if we found a discovered client
// This matches the original logic: !connecting && client->state() == DISCOVERED
if (!this->has_connecting_clients_()) {
found_discovered_client = true;
}
}
}
}
@@ -775,8 +742,6 @@ bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
}
#endif // USE_ESP32_BLE_DEVICE
}
return found_discovered_client;
}
void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {

View File

@@ -141,12 +141,10 @@ class ESPBTDeviceListener {
struct ClientStateCounts {
uint8_t connecting = 0;
uint8_t discovered = 0;
uint8_t searching = 0;
uint8_t disconnecting = 0;
bool operator==(const ClientStateCounts &other) const {
return connecting == other.connecting && discovered == other.discovered && searching == other.searching &&
disconnecting == other.disconnecting;
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting;
}
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
@@ -159,8 +157,6 @@ enum class ClientState : uint8_t {
DISCONNECTING,
// Connection is idle, no device detected.
IDLE,
// Searching for device.
SEARCHING,
// Device advertisement found.
DISCOVERED,
// Device is discovered and the scanner is stopped
@@ -292,12 +288,7 @@ class ESP32BLETracker : public Component,
/// Common cleanup logic when transitioning scanner to IDLE state
void cleanup_scan_state_(bool is_stop_complete);
/// Process a single scan result immediately
/// Returns true if a discovered client needs promotion to READY_TO_CONNECT
bool process_scan_result_(const BLEScanResult &scan_result);
#ifdef USE_ESP32_BLE_DEVICE
/// Check if any clients are in connecting or ready to connect state
bool has_connecting_clients_() const;
#endif
void process_scan_result_(const BLEScanResult &scan_result);
/// Handle scanner failure states
void handle_scanner_failure_();
/// Try to promote discovered clients to ready to connect
@@ -321,9 +312,6 @@ class ESP32BLETracker : public Component,
case ClientState::DISCOVERED:
counts.discovered++;
break;
case ClientState::SEARCHING:
counts.searching++;
break;
case ClientState::CONNECTING:
case ClientState::READY_TO_CONNECT:
counts.connecting++;

View File

@@ -148,7 +148,8 @@ void Fan::publish_state() {
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
optional<FanRestoreState> Fan::restore_state_() {
FanRestoreState recovered{};
this->rtc_ = global_preferences->make_preference<FanRestoreState>(this->get_object_id_hash() ^ RESTORE_STATE_VERSION);
this->rtc_ =
global_preferences->make_preference<FanRestoreState>(this->get_preference_hash() ^ RESTORE_STATE_VERSION);
bool restored = this->rtc_.load(&recovered);
switch (this->restore_mode_) {

View File

@@ -351,7 +351,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::initialization() {
constexpr uint32_t restore_settings_version = 0xA77D21EF;
this->base_rtc_ =
global_preferences->make_preference<HaierBaseSettings>(this->get_object_id_hash() ^ restore_settings_version);
global_preferences->make_preference<HaierBaseSettings>(this->get_preference_hash() ^ restore_settings_version);
HaierBaseSettings recovered;
if (!this->base_rtc_.load(&recovered)) {
recovered = {false, true};

View File

@@ -516,7 +516,7 @@ void HonClimate::initialization() {
HaierClimateBase::initialization();
constexpr uint32_t restore_settings_version = 0x57EB59DDUL;
this->hon_rtc_ =
global_preferences->make_preference<HonSettings>(this->get_object_id_hash() ^ restore_settings_version);
global_preferences->make_preference<HonSettings>(this->get_preference_hash() ^ restore_settings_version);
HonSettings recovered;
if (this->hon_rtc_.load(&recovered)) {
this->settings_ = recovered;

View File

@@ -12,7 +12,7 @@ void HTE501Component::setup() {
this->write(address, 2, false);
uint8_t identification[9];
this->read(identification, 9);
if (identification[8] != calc_crc8_(identification, 0, 7)) {
if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED;
this->mark_failed();
return;
@@ -46,7 +46,8 @@ void HTE501Component::update() {
this->set_timeout(50, [this]() {
uint8_t i2c_response[6];
this->read(i2c_response, 6);
if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1) && i2c_response[5] != calc_crc8_(i2c_response, 3, 4)) {
if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) &&
i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED;
this->status_set_warning();
return;
@@ -67,24 +68,5 @@ void HTE501Component::update() {
this->status_clear_warning();
});
}
unsigned char HTE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) {
unsigned char crc_val = 0xFF;
unsigned char i = 0;
unsigned char j = 0;
for (i = from; i <= to; i++) {
int cur_val = buf[i];
for (j = 0; j < 8; j++) {
if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal
{
crc_val = ((crc_val << 1) ^ 0x31);
} else {
crc_val = (crc_val << 1);
}
cur_val = cur_val << 1;
}
}
return crc_val;
}
} // namespace hte501
} // namespace esphome

View File

@@ -1,8 +1,8 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace hte501 {
@@ -19,7 +19,6 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice {
void update() override;
protected:
unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to);
sensor::Sensor *temperature_sensor_;
sensor::Sensor *humidity_sensor_;

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "integration";
void IntegrationSensor::setup() {
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
float preference_value = 0;
this->pref_.load(&preference_value);
this->result_ = preference_value;

View File

@@ -1,5 +1,6 @@
#include "esphome/core/log.h"
#include "lc709203f.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace lc709203f {
@@ -189,7 +190,7 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va
// Error on the i2c bus
this->status_set_warning(
str_sprintf("Error code %d when reading from register 0x%02X", return_code, register_to_read).c_str());
} else if (this->crc8_(read_buffer, 5) != read_buffer[5]) {
} else if (crc8(read_buffer, 5, 0x00, 0x07, true) != read_buffer[5]) {
// I2C indicated OK, but the CRC of the data does not matcth.
this->status_set_warning(str_sprintf("CRC error reading from register 0x%02X", register_to_read).c_str());
} else {
@@ -220,7 +221,7 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set)
write_buffer[1] = register_to_set;
write_buffer[2] = value_to_set & 0xFF; // Low byte
write_buffer[3] = (value_to_set >> 8) & 0xFF; // High byte
write_buffer[4] = this->crc8_(write_buffer, 4);
write_buffer[4] = crc8(write_buffer, 4, 0x00, 0x07, true);
for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) {
// Note: we don't write the first byte of the write buffer to the device.
@@ -239,20 +240,6 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set)
return return_code;
}
uint8_t Lc709203f::crc8_(uint8_t *byte_buffer, uint8_t length_of_crc) {
uint8_t crc = 0x00;
const uint8_t polynomial(0x07);
for (uint8_t j = length_of_crc; j; --j) {
crc ^= *byte_buffer++;
for (uint8_t i = 8; i; --i) {
crc = (crc & 0x80) ? (crc << 1) ^ polynomial : (crc << 1);
}
}
return crc;
}
void Lc709203f::set_pack_size(uint16_t pack_size) {
static const uint16_t PACK_SIZE_ARRAY[6] = {100, 200, 500, 1000, 2000, 3000};
static const uint16_t APA_ARRAY[6] = {0x08, 0x0B, 0x10, 0x19, 0x2D, 0x36};

View File

@@ -1,8 +1,8 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace lc709203f {
@@ -38,7 +38,6 @@ class Lc709203f : public sensor::Sensor, public PollingComponent, public i2c::I2
private:
uint8_t get_register_(uint8_t register_to_read, uint16_t *register_value);
uint8_t set_register_(uint8_t register_to_set, uint16_t value_to_set);
uint8_t crc8_(uint8_t *byte_buffer, uint8_t length_of_crc);
protected:
sensor::Sensor *voltage_sensor_{nullptr};

View File

@@ -1,4 +1,4 @@
#include "text_sensor.h"
#include "ld2420_text_sensor.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

View File

@@ -184,7 +184,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
void LD2450Component::setup() {
#ifdef USE_NUMBER
if (this->presence_timeout_number_ != nullptr) {
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_preference_hash());
this->set_presence_timeout();
}
#endif

View File

@@ -44,6 +44,13 @@ class AddressableLightEffect : public LightEffect {
this->apply(*this->get_addressable_(), current_color);
}
/// Get effect index specifically for addressable effects.
/// Can be used by effects to modify behavior based on their position in the list.
uint32_t get_effect_index() const { return this->get_index(); }
/// Check if this is the currently running addressable effect.
bool is_current_effect() const { return this->is_active() && this->get_addressable_()->is_effect_active(); }
protected:
AddressableLight *get_addressable_() const { return (AddressableLight *) this->state_->get_output(); }
};

View File

@@ -125,6 +125,10 @@ class LambdaLightEffect : public LightEffect {
}
}
/// Get the current effect index for use in lambda functions.
/// This can be useful for lambda effects that need to know their own index.
uint32_t get_current_index() const { return this->get_index(); }
protected:
std::function<void(bool initial_run)> f_;
uint32_t update_interval_;
@@ -143,6 +147,10 @@ class AutomationLightEffect : public LightEffect {
}
Trigger<> *get_trig() const { return trig_; }
/// Get the current effect index for use in automations.
/// Useful for automations that need to know which effect is running.
uint32_t get_current_index() const { return this->get_index(); }
protected:
Trigger<> *trig_{new Trigger<>};
};

View File

@@ -0,0 +1,36 @@
#include "light_effect.h"
#include "light_state.h"
namespace esphome {
namespace light {
uint32_t LightEffect::get_index() const {
if (this->state_ == nullptr) {
return 0;
}
return this->get_index_in_parent_();
}
bool LightEffect::is_active() const {
if (this->state_ == nullptr) {
return false;
}
return this->get_index() != 0 && this->state_->get_current_effect_index() == this->get_index();
}
uint32_t LightEffect::get_index_in_parent_() const {
if (this->state_ == nullptr) {
return 0;
}
const auto &effects = this->state_->get_effects();
for (size_t i = 0; i < effects.size(); i++) {
if (effects[i] == this) {
return i + 1; // Effects are 1-indexed in the API
}
}
return 0; // Not found
}
} // namespace light
} // namespace esphome

View File

@@ -34,9 +34,23 @@ class LightEffect {
this->init();
}
/// Get the index of this effect in the parent light's effect list.
/// Returns 0 if not found or not initialized.
uint32_t get_index() const;
/// Check if this effect is currently active.
bool is_active() const;
/// Get a reference to the parent light state.
/// Returns nullptr if not initialized.
LightState *get_light_state() const { return this->state_; }
protected:
LightState *state_{nullptr};
std::string name_;
/// Internal method to find this effect's index in the parent light's effect list.
uint32_t get_index_in_parent_() const;
};
} // namespace light

View File

@@ -36,8 +36,11 @@ static constexpr const char *get_color_mode_json_str(ColorMode mode) {
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (state.supports_effects())
if (state.supports_effects()) {
root["effect"] = state.get_effect_name();
root["effect_index"] = state.get_current_effect_index();
root["effect_count"] = state.get_effect_count();
}
auto values = state.remote_values;
auto traits = state.get_output()->get_traits();
@@ -160,6 +163,11 @@ void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject
const char *effect = root["effect"];
call.set_effect(effect);
}
if (root["effect_index"].is<uint32_t>()) {
uint32_t effect_index = root["effect_index"];
call.set_effect(effect_index);
}
}
} // namespace light

View File

@@ -41,7 +41,7 @@ void LightState::setup() {
case LIGHT_RESTORE_DEFAULT_ON:
case LIGHT_RESTORE_INVERTED_DEFAULT_OFF:
case LIGHT_RESTORE_INVERTED_DEFAULT_ON:
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
// Attempt to load from preferences, else fall back to default values
if (!this->rtc_.load(&recovered)) {
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON ||
@@ -54,7 +54,7 @@ void LightState::setup() {
break;
case LIGHT_RESTORE_AND_OFF:
case LIGHT_RESTORE_AND_ON:
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
this->rtc_.load(&recovered);
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
break;

View File

@@ -163,6 +163,44 @@ class LightState : public EntityBase, public Component {
/// Add effects for this light state.
void add_effects(const std::vector<LightEffect *> &effects);
/// Get the total number of effects available for this light.
size_t get_effect_count() const { return this->effects_.size(); }
/// Get the currently active effect index (0 = no effect, 1+ = effect index).
uint32_t get_current_effect_index() const { return this->active_effect_index_; }
/// Get effect index by name. Returns 0 if effect not found.
uint32_t get_effect_index(const std::string &effect_name) const {
if (strcasecmp(effect_name.c_str(), "none") == 0) {
return 0;
}
for (size_t i = 0; i < this->effects_.size(); i++) {
if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) {
return i + 1; // Effects are 1-indexed in active_effect_index_
}
}
return 0; // Effect not found
}
/// Get effect by index. Returns nullptr if index is invalid.
LightEffect *get_effect_by_index(uint32_t index) const {
if (index == 0 || index > this->effects_.size()) {
return nullptr;
}
return this->effects_[index - 1]; // Effects are 1-indexed in active_effect_index_
}
/// Get effect name by index. Returns "None" for index 0, empty string for invalid index.
std::string get_effect_name_by_index(uint32_t index) const {
if (index == 0) {
return "None";
}
if (index > this->effects_.size()) {
return ""; // Invalid index
}
return this->effects_[index - 1]->get_name();
}
/// The result of all the current_values_as_* methods have gamma correction applied.
void current_values_as_binary(bool *binary);

View File

@@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component {
void setup() override {
float value = this->value_lambda_();
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (this->pref_.load(&value)) {
this->control_lambda_(value);
}

View File

@@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component {
this->set_options_();
if (this->restore_) {
size_t index;
this->pref_ = global_preferences->make_preference<size_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
if (this->pref_.load(&index))
this->widget_->set_selected_index(index, LV_ANIM_OFF);
}

View File

@@ -50,28 +50,13 @@ bool MLX90614Component::write_emissivity_() {
return true;
}
uint8_t MLX90614Component::crc8_pec_(const uint8_t *data, uint8_t len) {
uint8_t crc = 0;
for (uint8_t i = 0; i < len; i++) {
uint8_t in = data[i];
for (uint8_t j = 0; j < 8; j++) {
bool carry = (crc ^ in) & 0x80;
crc <<= 1;
if (carry)
crc ^= 0x07;
in <<= 1;
}
}
return crc;
}
bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) {
uint8_t buf[5];
buf[0] = this->address_ << 1;
buf[1] = reg;
buf[2] = data & 0xFF;
buf[3] = data >> 8;
buf[4] = this->crc8_pec_(buf, 4);
buf[4] = crc8(buf, 4, 0x00, 0x07, true);
return this->write_bytes(reg, buf + 2, 3);
}

View File

@@ -22,7 +22,6 @@ class MLX90614Component : public PollingComponent, public i2c::I2CDevice {
protected:
bool write_emissivity_();
uint8_t crc8_pec_(const uint8_t *data, uint8_t len);
bool write_bytes_(uint8_t reg, uint16_t data);
sensor::Sensor *ambient_sensor_{nullptr};

View File

@@ -15,7 +15,7 @@ void ValueRangeTrigger::setup() {
float local_min = this->min_.value(0.0);
float local_max = this->max_.value(0.0);
convert hash = {.from = (local_max - local_min)};
uint32_t myhash = hash.to ^ this->parent_->get_object_id_hash();
uint32_t myhash = hash.to ^ this->parent_->get_preference_hash();
this->rtc_ = global_preferences->make_preference<bool>(myhash);
bool initial_state;
if (this->rtc_.load(&initial_state)) {

View File

@@ -9,11 +9,9 @@
namespace esphome {
namespace number {
// Forward declaration
class Number;
void log_number(const char *tag, const char *prefix, const char *type, Number *obj);
// Macro that calls the function - kept for backward compatibility
#define LOG_NUMBER(prefix, type, obj) log_number(TAG, prefix, LOG_STR_LITERAL(type), obj)
#define SUB_NUMBER(name) \

View File

@@ -1,10 +1,10 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include <vector>
#include "opentherm.h"
@@ -17,21 +17,21 @@
#endif
#ifdef OPENTHERM_USE_SWITCH
#include "esphome/components/opentherm/switch/switch.h"
#include "esphome/components/opentherm/switch/opentherm_switch.h"
#endif
#ifdef OPENTHERM_USE_OUTPUT
#include "esphome/components/opentherm/output/output.h"
#include "esphome/components/opentherm/output/opentherm_output.h"
#endif
#ifdef OPENTHERM_USE_NUMBER
#include "esphome/components/opentherm/number/number.h"
#include "esphome/components/opentherm/number/opentherm_number.h"
#endif
#include <functional>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include "opentherm_macros.h"

View File

@@ -1,4 +1,4 @@
#include "number.h"
#include "opentherm_number.h"
namespace esphome {
namespace opentherm {
@@ -17,7 +17,7 @@ void OpenthermNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -1,5 +1,5 @@
#include "esphome/core/helpers.h" // for clamp() and lerp()
#include "output.h"
#include "opentherm_output.h"
namespace esphome {
namespace opentherm {

View File

@@ -1,4 +1,4 @@
#include "switch.h"
#include "opentherm_switch.h"
namespace esphome {
namespace opentherm {

View File

@@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.All(
)
def to_code(config):
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield uart.register_uart_device(var, config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

View File

@@ -99,9 +99,9 @@ async def to_code(config):
}
),
)
def output_pipsolar_set_level_to_code(config, action_id, template_arg, args):
paren = yield cg.get_variable(config[CONF_ID])
async def output_pipsolar_set_level_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = yield cg.templatable(config[CONF_VALUE], args, float)
template_ = await cg.templatable(config[CONF_VALUE], args, float)
cg.add(var.set_level(template_))
yield var
return var

View File

@@ -46,10 +46,32 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
}
this->connection_established_ = true;
this->char_handle_ = chr->handle;
#ifdef USE_TIME
this->sync_time_();
#endif
this->display();
// Attempt to write immediately
// For devices without security, this will work
// For devices with security that are already paired, this will work
// For devices that need pairing, the write will be retried after auth completes
this->sync_time_and_display_();
break;
}
default:
break;
}
}
void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
if (!this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
return;
if (param->ble_security.auth_cmpl.success) {
ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str());
// Now that pairing is complete, perform the pending writes
this->sync_time_and_display_();
} else {
ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str());
}
break;
}
default:
@@ -127,6 +149,13 @@ void PVVXDisplay::delayed_disconnect_() {
this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
}
void PVVXDisplay::sync_time_and_display_() {
#ifdef USE_TIME
this->sync_time_();
#endif
this->display();
}
#ifdef USE_TIME
void PVVXDisplay::sync_time_() {
if (this->time_ == nullptr)

View File

@@ -43,6 +43,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
/// Set validity period of the display information in seconds (1..65535)
void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; }
@@ -112,6 +113,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
void setcfgbit_(uint8_t bit, bool value);
void send_to_setup_char_(uint8_t *blk, size_t size);
void delayed_disconnect_();
void sync_time_and_display_();
#ifdef USE_TIME
void sync_time_();
time::RealTimeClock *time_{nullptr};

View File

@@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema(
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
def to_code(config):
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield esp32_ble_tracker.register_ble_device(var, config)
await esp32_ble_tracker.register_ble_device(var, config)

View File

@@ -1782,14 +1782,12 @@ def nexa_dumper(var, config):
@register_action("nexa", NexaAction, NEXA_SCHEMA)
def nexa_action(var, config, args):
cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint32))))
cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8))))
cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, cg.uint8))))
cg.add(
var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8)))
)
cg.add(var.set_level((yield cg.templatable(config[CONF_LEVEL], args, cg.uint8))))
async def nexa_action(var, config, args):
cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint32)))
cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.uint8)))
cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.uint8)))
cg.add(var.set_channel(await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)))
cg.add(var.set_level(await cg.templatable(config[CONF_LEVEL], args, cg.uint8)))
# Midea

View File

@@ -132,7 +132,7 @@ void RotaryEncoderSensor::setup() {
int32_t initial_value = 0;
switch (this->restore_mode_) {
case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO:
this->rtc_ = global_preferences->make_preference<int32_t>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<int32_t>(this->get_preference_hash());
if (!this->rtc_.load(&initial_value)) {
initial_value = 0;
}

View File

@@ -4,10 +4,6 @@
#include "esphome/core/component.h"
#include <algorithm>
#include <cinttypes>
#ifdef USE_API
#include "esphome/components/api/api_timing.h"
#endif
namespace esphome {
@@ -66,29 +62,6 @@ void RuntimeStatsCollector::log_stats_() {
stats->get_period_time_ms());
}
#ifdef USE_API
// Log detailed API timing breakdown if available
bool has_api_timing = false;
for (int i = 0; i < static_cast<int>(api::APITimingCategory::MAX_CATEGORIES); i++) {
if (api::api_timing_stats[i].count > 0) {
has_api_timing = true;
break;
}
}
if (has_api_timing) {
ESP_LOGI(TAG, "API Component Breakdown (microseconds):");
for (int i = 0; i < static_cast<int>(api::APITimingCategory::MAX_CATEGORIES); i++) {
const api::APITimingStats &stats = api::api_timing_stats[i];
if (stats.count > 0) {
const char *category_name = api::api_timing_category_to_str(static_cast<api::APITimingCategory>(i));
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.1fus, max=%" PRIu32 "us, total=%" PRIu64 "us", category_name,
stats.count, stats.get_avg_us(), stats.max_us, stats.total_us);
}
}
}
#endif
// Log total stats since boot
ESP_LOGI(TAG, "Total stats (since boot):");
@@ -108,16 +81,6 @@ void RuntimeStatsCollector::log_stats_() {
}
}
void RuntimeStatsCollector::reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
#ifdef USE_API
// Reset API timing stats for next period
api::reset_api_timing_stats();
#endif
}
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
if (this->next_log_time_ == 0)
return;

View File

@@ -103,7 +103,11 @@ class RuntimeStatsCollector {
protected:
void log_stats_();
void reset_stats_();
void reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
}
// Use const char* keys for efficiency
// Custom comparator for const char* keys in map

View File

@@ -40,7 +40,7 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
template<typename V> void set_max(V max) { this->max_ = max; }
void setup() override {
this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_preference_hash());
bool initial_state;
if (this->rtc_.load(&initial_state)) {
this->previous_in_range_ = initial_state;

View File

@@ -183,7 +183,7 @@ CONFIG_SCHEMA = (
)
def to_code(config):
async def to_code(config):
fw_hex = get_firmware(config[CONF_FIRMWARE])
fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION])
@@ -193,17 +193,17 @@ def to_code(config):
cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor)
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
yield cg.register_component(var, config)
await cg.register_component(var, config)
config.pop(
CONF_UPDATE_INTERVAL
) # drop UPDATE_INTERVAL as it does not apply to the light component
yield light.register_light(var, config)
yield uart.register_uart_device(var, config)
await light.register_light(var, config)
await uart.register_uart_device(var, config)
nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN])
nrst_pin = await cg.gpio_pin_expression(config[CONF_NRST_PIN])
cg.add(var.set_nrst_pin(nrst_pin))
boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN])
boot0_pin = await cg.gpio_pin_expression(config[CONF_BOOT0_PIN])
cg.add(var.set_boot0_pin(boot0_pin))
cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE]))
@@ -217,5 +217,5 @@ def to_code(config):
continue
conf = config[key]
sens = yield sensor.new_sensor(conf)
sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{key}_sensor")(sens))

View File

@@ -55,7 +55,7 @@ void SpeakerMediaPlayer::setup() {
this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand));
this->pref_ = global_preferences->make_preference<VolumeRestoreState>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<VolumeRestoreState>(this->get_preference_hash());
VolumeRestoreState volume_restore_state;
if (this->pref_.load(&volume_restore_state)) {

View File

@@ -81,7 +81,7 @@ void SprinklerControllerNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -32,7 +32,7 @@ optional<bool> Switch::get_initial_state() {
if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK))
return {};
this->rtc_ = global_preferences->make_preference<bool>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<bool>(this->get_preference_hash());
bool initial_state;
if (!this->rtc_.load(&initial_state))
return {};

View File

@@ -12,7 +12,7 @@ void TEE501Component::setup() {
this->write(address, 2, false);
uint8_t identification[9];
this->read(identification, 9);
if (identification[8] != calc_crc8_(identification, 0, 7)) {
if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED;
this->mark_failed();
return;
@@ -45,7 +45,7 @@ void TEE501Component::update() {
this->set_timeout(50, [this]() {
uint8_t i2c_response[3];
this->read(i2c_response, 3);
if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1)) {
if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED;
this->status_set_warning();
return;
@@ -62,24 +62,5 @@ void TEE501Component::update() {
});
}
unsigned char TEE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) {
unsigned char crc_val = 0xFF;
unsigned char i = 0;
unsigned char j = 0;
for (i = from; i <= to; i++) {
int cur_val = buf[i];
for (j = 0; j < 8; j++) {
if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal
{
crc_val = ((crc_val << 1) ^ 0x31);
} else {
crc_val = (crc_val << 1);
}
cur_val = cur_val << 1;
}
}
return crc_val;
}
} // namespace tee501
} // namespace esphome

View File

@@ -1,8 +1,8 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace tee501 {
@@ -16,8 +16,6 @@ class TEE501Component : public sensor::Sensor, public PollingComponent, public i
void update() override;
protected:
unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to);
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
};

View File

@@ -86,7 +86,7 @@ void TemplateAlarmControlPanel::setup() {
break;
case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: {
uint8_t value;
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_preference_hash());
if (this->pref_.load(&value)) {
this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
} else {

View File

@@ -20,7 +20,7 @@ void TemplateDate::setup() {
} else {
datetime::DateEntityRestoreState temp;
this->pref_ =
global_preferences->make_preference<datetime::DateEntityRestoreState>(194434030U ^ this->get_object_id_hash());
global_preferences->make_preference<datetime::DateEntityRestoreState>(194434030U ^ this->get_preference_hash());
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -19,8 +19,8 @@ void TemplateDateTime::setup() {
state = this->initial_value_;
} else {
datetime::DateTimeEntityRestoreState temp;
this->pref_ = global_preferences->make_preference<datetime::DateTimeEntityRestoreState>(194434090U ^
this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<datetime::DateTimeEntityRestoreState>(
194434090U ^ this->get_preference_hash());
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -20,7 +20,7 @@ void TemplateTime::setup() {
} else {
datetime::TimeEntityRestoreState temp;
this->pref_ =
global_preferences->make_preference<datetime::TimeEntityRestoreState>(194434060U ^ this->get_object_id_hash());
global_preferences->make_preference<datetime::TimeEntityRestoreState>(194434060U ^ this->get_preference_hash());
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -14,7 +14,7 @@ void TemplateNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -16,7 +16,7 @@ void TemplateSelect::setup() {
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
} else {
size_t index;
this->pref_ = global_preferences->make_preference<size_t>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
if (!this->pref_.load(&index)) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str());

View File

@@ -15,7 +15,7 @@ void TemplateText::setup() {
if (!this->pref_) {
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
} else {
uint32_t key = this->get_object_id_hash();
uint32_t key = this->get_preference_hash();
key += this->traits.get_min_length() << 2;
key += this->traits.get_max_length() << 4;
key += fnv1_hash(this->traits.get_pattern()) << 6;

View File

@@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() {
float initial_value = 0;
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_.load(&initial_value);
}
this->publish_state_and_save(initial_value);

View File

@@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number";
void TuyaNumber::setup() {
if (this->restore_value_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
}
this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) {

View File

@@ -202,9 +202,9 @@ async def valve_stop_to_code(config, action_id, template_arg, args):
@automation.register_action("valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA)
def valve_toggle_to_code(config, action_id, template_arg, args):
paren = yield cg.get_variable(config[CONF_ID])
yield cg.new_Pvariable(action_id, template_arg, paren)
async def valve_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
VALVE_CONTROL_ACTION_SCHEMA = cv.Schema(

View File

@@ -155,7 +155,7 @@ void Valve::publish_state(bool save) {
}
}
optional<ValveRestoreState> Valve::restore_state_() {
this->rtc_ = global_preferences->make_preference<ValveRestoreState>(this->get_object_id_hash());
this->rtc_ = global_preferences->make_preference<ValveRestoreState>(this->get_preference_hash());
ValveRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -272,26 +272,42 @@ void Application::teardown_components(uint32_t timeout_ms) {
uint32_t now = start_time;
size_t pending_count = num_components;
// Compaction algorithm for teardown
// ==================================
// We repeatedly call teardown() on each component until it returns true.
// Components that are done are removed using array compaction:
// Teardown Algorithm
// ==================
// We iterate through pending components, calling teardown() on each.
// Components that return false (need more time) are copied forward
// in the array. Components that return true (finished) are skipped.
//
// Initial state (all components pending):
// pending_components: [A, B, C, D, E, F]
// pending_count: 6 ^
// The compaction happens in-place during iteration:
// - still_pending tracks the write position (where to put next pending component)
// - i tracks the read position (which component we're testing)
// - When teardown() returns false, we copy component[i] to component[still_pending]
// - When teardown() returns true, we just skip it (don't increment still_pending)
//
// After first iteration (B and D finish teardown):
// pending_components: [A, C, E, F | B, D] (B, D are still in memory but ignored)
// pending_count: 4 ^
// Example with 4 components where B can teardown immediately:
//
// After second iteration (A finishes):
// pending_components: [C, E, F | A, B, D]
// pending_count: 3 ^
// Start:
// pending_components: [A, B, C, D]
// pending_count: 4 ^----------^
//
// The algorithm compacts remaining components to the front of the array,
// tracking only the count of pending components. This avoids expensive
// erase operations while maintaining O(n) complexity per iteration.
// Iteration 1:
// i=0: A needs more time → keep at pos 0 (no copy needed)
// i=1: B finished → skip
// i=2: C needs more time → copy to pos 1
// i=3: D needs more time → copy to pos 2
//
// After iteration 1:
// pending_components: [A, C, D | D]
// pending_count: 3 ^--------^
//
// Iteration 2:
// i=0: A finished → skip
// i=1: C needs more time → copy to pos 0
// i=2: D finished → skip
//
// After iteration 2:
// pending_components: [C | C, D, D] (positions 1-3 have old values)
// pending_count: 1 ^--^
while (pending_count > 0 && (now - start_time) < timeout_ms) {
// Feed watchdog during teardown to prevent triggering
@@ -301,7 +317,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
size_t still_pending = 0;
for (size_t i = 0; i < pending_count; ++i) {
if (!pending_components[i]->teardown()) {
// Component still needs time, keep it in the list
// Component still needs time, copy it forward
if (still_pending != i) {
pending_components[still_pending] = pending_components[i];
}

View File

@@ -85,6 +85,19 @@ class EntityBase {
// Set has_state - for components that need to manually set this
void set_has_state(bool state) { this->flags_.has_state = state; }
// Get a unique hash for preferences that includes device_id
uint32_t get_preference_hash() {
#ifdef USE_DEVICES
// Combine object_id_hash with device_id to ensure uniqueness across devices
// Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash
// This ensures backward compatibility for existing single-device configurations
return this->get_object_id_hash() ^ this->get_device_id();
#else
// Without devices, just use object_id_hash as before
return this->get_object_id_hash();
#endif
}
protected:
friend class api::APIConnection;

View File

@@ -236,10 +236,21 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
if existing_component != "unknown":
conflict_msg += f" from component '{existing_component}'"
# Show both original names and their ASCII-only versions if they differ
sanitized_msg = ""
if entity_name != existing_name:
sanitized_msg = (
f"\n Original names: '{entity_name}' and '{existing_name}'"
f"\n Both convert to ASCII ID: '{name_key}'"
"\n To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B')"
"\n to distinguish them"
)
raise cv.Invalid(
f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
f"{conflict_msg}. "
f"Each entity on a device must have a unique name within its platform."
"Each entity on a device must have a unique name within its platform."
f"{sanitized_msg}"
)
# Store metadata about this entity

View File

@@ -41,17 +41,28 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0
// Mathematics
uint8_t crc8(const uint8_t *data, uint8_t len) {
uint8_t crc = 0;
uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc, uint8_t poly, bool msb_first) {
while ((len--) != 0u) {
uint8_t inbyte = *data++;
for (uint8_t i = 8; i != 0u; i--) {
bool mix = (crc ^ inbyte) & 0x01;
crc >>= 1;
if (mix)
crc ^= 0x8C;
inbyte >>= 1;
if (msb_first) {
// MSB first processing (for polynomials like 0x31, 0x07)
crc ^= inbyte;
for (uint8_t i = 8; i != 0u; i--) {
if (crc & 0x80) {
crc = (crc << 1) ^ poly;
} else {
crc <<= 1;
}
}
} else {
// LSB first processing (default for Dallas/Maxim 0x8C)
for (uint8_t i = 8; i != 0u; i--) {
bool mix = (crc ^ inbyte) & 0x01;
crc >>= 1;
if (mix)
crc ^= poly;
inbyte >>= 1;
}
}
}
return crc;

View File

@@ -145,8 +145,8 @@ template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max
return (value - min) * (max_out - min_out) / (max - min) + min_out;
}
/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial.
uint8_t crc8(const uint8_t *data, uint8_t len);
/// Calculate a CRC-8 checksum of \p data with size \p len.
uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc = 0x00, uint8_t poly = 0x8C, bool msb_first = false);
/// Calculate a CRC-16 checksum of \p data with size \p len.
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc = 0xffff, uint16_t reverse_poly = 0xa001,

View File

@@ -357,6 +357,19 @@ build_flags =
${common:esp32-idf.build_flags}
${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32C6
build_unflags =
${common.build_unflags}
[env:esp32c6-idf-tidy]
extends = common:esp32-idf
board = esp32-c6-devkitc-1
board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c6-idf-tidy
build_flags =
${common:esp32-idf.build_flags}
${flags:clangtidy.build_flags}
-DUSE_ESP32_VARIANT_ESP32C6
build_unflags =
${common.build_unflags}
;;;;;;;; ESP32-S2 ;;;;;;;;

View File

@@ -15,7 +15,7 @@ esphome-dashboard==20250814.0
aioesphomeapi==39.0.0
zeroconf==0.147.0
puremagic==1.30
ruamel.yaml==0.18.14 # dashboard_import
ruamel.yaml==0.18.15 # dashboard_import
esphome-glyphsets==0.2.0
pillow==10.4.0
cairosvg==2.8.2

View File

@@ -1,6 +1,6 @@
pylint==3.3.8
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.12.9 # also change in .pre-commit-config.yaml when updating
ruff==0.12.10 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -0,0 +1,14 @@
wifi:
ssid: MySSID
password: password1
api:
esphome:
on_boot:
then:
- homeassistant.tag_scanned: 'test_tag_123'
- homeassistant.tag_scanned:
tag: 'another_tag'
- homeassistant.tag_scanned:
tag: !lambda 'return "dynamic_tag";'

View File

@@ -0,0 +1,17 @@
esphome:
name: crc8-helper-test
host:
api:
logger:
level: INFO
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
components: [crc8_test_component]
crc8_test_component:

View File

@@ -0,0 +1,17 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
crc8_test_component_ns = cg.esphome_ns.namespace("crc8_test_component")
CRC8TestComponent = crc8_test_component_ns.class_("CRC8TestComponent", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CRC8TestComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,170 @@
#include "crc8_test_component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace crc8_test_component {
static const char *const TAG = "crc8_test";
void CRC8TestComponent::setup() {
ESP_LOGI(TAG, "CRC8 Helper Function Integration Test Starting");
// Run all test suites
test_crc8_dallas_maxim();
test_crc8_sensirion_style();
test_crc8_pec_style();
test_crc8_parameter_equivalence();
test_crc8_edge_cases();
test_component_compatibility();
ESP_LOGI(TAG, "CRC8 Integration Test Complete");
}
void CRC8TestComponent::test_crc8_dallas_maxim() {
ESP_LOGI(TAG, "Testing Dallas/Maxim CRC8 (default parameters)");
// Test vectors for Dallas/Maxim CRC8 (polynomial 0x8C, LSB-first, init 0x00)
const uint8_t test1[] = {0x01};
const uint8_t test2[] = {0xFF};
const uint8_t test3[] = {0x12, 0x34};
const uint8_t test4[] = {0xAA, 0xBB, 0xCC};
const uint8_t test5[] = {0x01, 0x02, 0x03, 0x04, 0x05};
bool all_passed = true;
all_passed &= verify_crc8("Dallas [0x01]", test1, sizeof(test1), 0x5E);
all_passed &= verify_crc8("Dallas [0xFF]", test2, sizeof(test2), 0x35);
all_passed &= verify_crc8("Dallas [0x12, 0x34]", test3, sizeof(test3), 0xA2);
all_passed &= verify_crc8("Dallas [0xAA, 0xBB, 0xCC]", test4, sizeof(test4), 0xD4);
all_passed &= verify_crc8("Dallas [0x01...0x05]", test5, sizeof(test5), 0x2A);
log_test_result("Dallas/Maxim CRC8", all_passed);
}
void CRC8TestComponent::test_crc8_sensirion_style() {
ESP_LOGI(TAG, "Testing Sensirion CRC8 (0x31 poly, MSB-first, init 0xFF)");
const uint8_t test1[] = {0x00};
const uint8_t test2[] = {0x01};
const uint8_t test3[] = {0xFF};
const uint8_t test4[] = {0x12, 0x34};
const uint8_t test5[] = {0xBE, 0xEF};
bool all_passed = true;
all_passed &= verify_crc8("Sensirion [0x00]", test1, sizeof(test1), 0xAC, 0xFF, 0x31, true);
all_passed &= verify_crc8("Sensirion [0x01]", test2, sizeof(test2), 0x9D, 0xFF, 0x31, true);
all_passed &= verify_crc8("Sensirion [0xFF]", test3, sizeof(test3), 0x00, 0xFF, 0x31, true);
all_passed &= verify_crc8("Sensirion [0x12, 0x34]", test4, sizeof(test4), 0x37, 0xFF, 0x31, true);
all_passed &= verify_crc8("Sensirion [0xBE, 0xEF]", test5, sizeof(test5), 0x92, 0xFF, 0x31, true);
log_test_result("Sensirion CRC8", all_passed);
}
void CRC8TestComponent::test_crc8_pec_style() {
ESP_LOGI(TAG, "Testing PEC CRC8 (0x07 poly, MSB-first, init 0x00)");
const uint8_t test1[] = {0x00};
const uint8_t test2[] = {0x01};
const uint8_t test3[] = {0xFF};
const uint8_t test4[] = {0x12, 0x34};
const uint8_t test5[] = {0xAA, 0xBB};
bool all_passed = true;
all_passed &= verify_crc8("PEC [0x00]", test1, sizeof(test1), 0x00, 0x00, 0x07, true);
all_passed &= verify_crc8("PEC [0x01]", test2, sizeof(test2), 0x07, 0x00, 0x07, true);
all_passed &= verify_crc8("PEC [0xFF]", test3, sizeof(test3), 0xF3, 0x00, 0x07, true);
all_passed &= verify_crc8("PEC [0x12, 0x34]", test4, sizeof(test4), 0xF1, 0x00, 0x07, true);
all_passed &= verify_crc8("PEC [0xAA, 0xBB]", test5, sizeof(test5), 0xB2, 0x00, 0x07, true);
log_test_result("PEC CRC8", all_passed);
}
void CRC8TestComponent::test_crc8_parameter_equivalence() {
ESP_LOGI(TAG, "Testing parameter equivalence");
const uint8_t test_data[] = {0x12, 0x34, 0x56, 0x78};
// Test that default parameters work as expected
uint8_t default_result = crc8(test_data, sizeof(test_data));
uint8_t explicit_result = crc8(test_data, sizeof(test_data), 0x00, 0x8C, false);
bool passed = (default_result == explicit_result);
if (!passed) {
ESP_LOGE(TAG, "Parameter equivalence FAILED: default=0x%02X, explicit=0x%02X", default_result, explicit_result);
}
log_test_result("Parameter equivalence", passed);
}
void CRC8TestComponent::test_crc8_edge_cases() {
ESP_LOGI(TAG, "Testing edge cases");
bool all_passed = true;
// Empty array test
const uint8_t empty[] = {};
uint8_t empty_result = crc8(empty, 0);
bool empty_passed = (empty_result == 0x00); // Should return init value
if (!empty_passed) {
ESP_LOGE(TAG, "Empty array test FAILED: expected 0x00, got 0x%02X", empty_result);
}
all_passed &= empty_passed;
// Single byte tests
const uint8_t single_zero[] = {0x00};
const uint8_t single_ff[] = {0xFF};
all_passed &= verify_crc8("Single [0x00]", single_zero, 1, 0x00);
all_passed &= verify_crc8("Single [0xFF]", single_ff, 1, 0x35);
log_test_result("Edge cases", all_passed);
}
void CRC8TestComponent::test_component_compatibility() {
ESP_LOGI(TAG, "Testing component compatibility");
// Test specific component use cases
bool all_passed = true;
// AGS10-style data (Sensirion CRC8)
const uint8_t ags10_data[] = {0x12, 0x34, 0x56};
uint8_t ags10_result = crc8(ags10_data, sizeof(ags10_data), 0xFF, 0x31, true);
ESP_LOGI(TAG, "AGS10-style CRC8: 0x%02X", ags10_result);
// LC709203F-style data (PEC CRC8)
const uint8_t lc_data[] = {0xAA, 0xBB};
uint8_t lc_result = crc8(lc_data, sizeof(lc_data), 0x00, 0x07, true);
ESP_LOGI(TAG, "LC709203F-style CRC8: 0x%02X", lc_result);
// DallasTemperature-style data (Dallas CRC8)
const uint8_t dallas_data[] = {0x28, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC};
uint8_t dallas_result = crc8(dallas_data, sizeof(dallas_data));
ESP_LOGI(TAG, "Dallas-style CRC8: 0x%02X", dallas_result);
all_passed = true; // These are just demonstration tests
log_test_result("Component compatibility", all_passed);
}
bool CRC8TestComponent::verify_crc8(const char *test_name, const uint8_t *data, uint8_t len, uint8_t expected,
uint8_t crc, uint8_t poly, bool msb_first) {
uint8_t result = esphome::crc8(data, len, crc, poly, msb_first);
bool passed = (result == expected);
if (passed) {
ESP_LOGI(TAG, "%s: PASS (0x%02X)", test_name, result);
} else {
ESP_LOGE(TAG, "%s: FAIL - expected 0x%02X, got 0x%02X", test_name, expected, result);
}
return passed;
}
void CRC8TestComponent::log_test_result(const char *test_name, bool passed) {
if (passed) {
ESP_LOGI(TAG, "%s: ALL TESTS PASSED", test_name);
} else {
ESP_LOGE(TAG, "%s: SOME TESTS FAILED", test_name);
}
}
} // namespace crc8_test_component
} // namespace esphome

View File

@@ -0,0 +1,29 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace crc8_test_component {
class CRC8TestComponent : public Component {
public:
void setup() override;
private:
void test_crc8_dallas_maxim();
void test_crc8_sensirion_style();
void test_crc8_pec_style();
void test_crc8_parameter_equivalence();
void test_crc8_edge_cases();
void test_component_compatibility();
void test_old_vs_new_implementations();
void log_test_result(const char *test_name, bool passed);
bool verify_crc8(const char *test_name, const uint8_t *data, uint8_t len, uint8_t expected, uint8_t crc = 0x00,
uint8_t poly = 0x8C, bool msb_first = false);
};
} // namespace crc8_test_component
} // namespace esphome

View File

@@ -0,0 +1,165 @@
esphome:
name: multi-device-preferences-test
# Define multiple devices for testing preference storage
devices:
- id: device_a
name: Device A
- id: device_b
name: Device B
host:
api: # Port will be automatically injected
logger:
level: DEBUG
# Test entities with restore modes to verify preference storage
# Switches with same name on different devices - test restore mode
switch:
- platform: template
name: Light
id: light_device_a
device_id: device_a
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- lambda: |-
ESP_LOGI("test", "Device A Light turned ON");
turn_off_action:
- lambda: |-
ESP_LOGI("test", "Device A Light turned OFF");
- platform: template
name: Light
id: light_device_b
device_id: device_b
restore_mode: RESTORE_DEFAULT_ON # Different default to test uniqueness
turn_on_action:
- lambda: |-
ESP_LOGI("test", "Device B Light turned ON");
turn_off_action:
- lambda: |-
ESP_LOGI("test", "Device B Light turned OFF");
- platform: template
name: Light
id: light_main
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- lambda: |-
ESP_LOGI("test", "Main Light turned ON");
turn_off_action:
- lambda: |-
ESP_LOGI("test", "Main Light turned OFF");
# Numbers with restore to test preference storage
number:
- platform: template
name: Setpoint
id: setpoint_device_a
device_id: device_a
min_value: 10.0
max_value: 30.0
step: 0.5
restore_value: true
initial_value: 20.0
set_action:
- lambda: |-
ESP_LOGI("test", "Device A Setpoint set to %.1f", x);
id(setpoint_device_a).state = x;
- platform: template
name: Setpoint
id: setpoint_device_b
device_id: device_b
min_value: 10.0
max_value: 30.0
step: 0.5
restore_value: true
initial_value: 25.0 # Different initial to test uniqueness
set_action:
- lambda: |-
ESP_LOGI("test", "Device B Setpoint set to %.1f", x);
id(setpoint_device_b).state = x;
- platform: template
name: Setpoint
id: setpoint_main
min_value: 10.0
max_value: 30.0
step: 0.5
restore_value: true
initial_value: 22.0
set_action:
- lambda: |-
ESP_LOGI("test", "Main Setpoint set to %.1f", x);
id(setpoint_main).state = x;
# Selects with restore to test preference storage
select:
- platform: template
name: Mode
id: mode_device_a
device_id: device_a
options:
- "Auto"
- "Manual"
- "Off"
restore_value: true
initial_option: "Auto"
set_action:
- lambda: |-
ESP_LOGI("test", "Device A Mode set to %s", x.c_str());
id(mode_device_a).state = x;
- platform: template
name: Mode
id: mode_device_b
device_id: device_b
options:
- "Auto"
- "Manual"
- "Off"
restore_value: true
initial_option: "Manual" # Different initial to test uniqueness
set_action:
- lambda: |-
ESP_LOGI("test", "Device B Mode set to %s", x.c_str());
id(mode_device_b).state = x;
- platform: template
name: Mode
id: mode_main
options:
- "Auto"
- "Manual"
- "Off"
restore_value: true
initial_option: "Off"
set_action:
- lambda: |-
ESP_LOGI("test", "Main Mode set to %s", x.c_str());
id(mode_main).state = x;
# Button to trigger preference logging test
button:
- platform: template
name: Test Preferences
on_press:
- lambda: |-
ESP_LOGI("test", "Testing preference storage uniqueness:");
ESP_LOGI("test", "Device A Light state: %s", id(light_device_a).state ? "ON" : "OFF");
ESP_LOGI("test", "Device B Light state: %s", id(light_device_b).state ? "ON" : "OFF");
ESP_LOGI("test", "Main Light state: %s", id(light_main).state ? "ON" : "OFF");
ESP_LOGI("test", "Device A Setpoint: %.1f", id(setpoint_device_a).state);
ESP_LOGI("test", "Device B Setpoint: %.1f", id(setpoint_device_b).state);
ESP_LOGI("test", "Main Setpoint: %.1f", id(setpoint_main).state);
ESP_LOGI("test", "Device A Mode: %s", id(mode_device_a).state.c_str());
ESP_LOGI("test", "Device B Mode: %s", id(mode_device_b).state.c_str());
ESP_LOGI("test", "Main Mode: %s", id(mode_main).state.c_str());
// Log preference hashes for entities that actually store preferences
ESP_LOGI("test", "Device A Switch Pref Hash: %u", id(light_device_a).get_preference_hash());
ESP_LOGI("test", "Device B Switch Pref Hash: %u", id(light_device_b).get_preference_hash());
ESP_LOGI("test", "Main Switch Pref Hash: %u", id(light_main).get_preference_hash());
ESP_LOGI("test", "Device A Number Pref Hash: %u", id(setpoint_device_a).get_preference_hash());
ESP_LOGI("test", "Device B Number Pref Hash: %u", id(setpoint_device_b).get_preference_hash());
ESP_LOGI("test", "Main Number Pref Hash: %u", id(setpoint_main).get_preference_hash());

View File

@@ -0,0 +1,100 @@
"""Integration test for CRC8 helper function."""
from __future__ import annotations
import asyncio
from pathlib import Path
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_crc8_helper(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test the CRC8 helper function through integration testing."""
# Get the 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
)
# Track test completion with asyncio.Event
test_complete = asyncio.Event()
# Track test results
test_results = {
"dallas_maxim": False,
"sensirion": False,
"pec": False,
"parameter_equivalence": False,
"edge_cases": False,
"component_compatibility": False,
"setup_started": False,
}
def on_log_line(line):
"""Process log lines to track test progress and results."""
# Track test start
if "CRC8 Helper Function Integration Test Starting" in line:
test_results["setup_started"] = True
# Track test completion
elif "CRC8 Integration Test Complete" in line:
test_complete.set()
# Track individual test results
elif "ALL TESTS PASSED" in line:
if "Dallas/Maxim CRC8" in line:
test_results["dallas_maxim"] = True
elif "Sensirion CRC8" in line:
test_results["sensirion"] = True
elif "PEC CRC8" in line:
test_results["pec"] = True
elif "Parameter equivalence" in line:
test_results["parameter_equivalence"] = True
elif "Edge cases" in line:
test_results["edge_cases"] = True
elif "Component compatibility" in line:
test_results["component_compatibility"] = True
# Log failures for debugging
elif "TEST FAILED:" in line or "SUBTEST FAILED:" in line:
print(f"CRC8 Test Failure: {line}")
# Compile and run the test
async with (
run_compiled(yaml_config, line_callback=on_log_line),
api_client_connected() as client,
):
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "crc8-helper-test"
# Wait for tests to complete with timeout
try:
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
except TimeoutError:
pytest.fail("CRC8 integration test timed out after 5 seconds")
# Verify all tests passed
assert test_results["setup_started"], "CRC8 test setup never started"
assert test_results["dallas_maxim"], "Dallas/Maxim CRC8 test failed"
assert test_results["sensirion"], "Sensirion CRC8 test failed"
assert test_results["pec"], "PEC CRC8 test failed"
assert test_results["parameter_equivalence"], (
"Parameter equivalence test failed"
)
assert test_results["edge_cases"], "Edge cases test failed"
assert test_results["component_compatibility"], (
"Component compatibility test failed"
)

Some files were not shown because too many files have changed in this diff Show More