mirror of
https://github.com/esphome/esphome.git
synced 2026-07-04 14:13:41 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c37ce25fb7 | |||
| 8921a9dd60 | |||
| 14b804f3e1 | |||
| d15a9597d7 |
+1
-1
@@ -1 +1 @@
|
||||
dc8ad5472d9fb44ce1ca29a0601afd65705642799a2819704dfc8459fbaf9815
|
||||
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
@@ -20,14 +20,14 @@ env:
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
+15
-15
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -387,14 +387,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -466,14 +466,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -555,14 +555,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -817,7 +817,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -841,7 +841,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -883,7 +883,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -930,7 +930,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
+1
-8
@@ -750,15 +750,8 @@ def upload_using_esptool(
|
||||
platformio_api.FlashImage(
|
||||
path=idedata.firmware_bin_path, offset=firmware_offset
|
||||
),
|
||||
*idedata.extra_flash_images,
|
||||
]
|
||||
for image in idedata.extra_flash_images:
|
||||
if not image.path.is_file():
|
||||
_LOGGER.warning(
|
||||
"Skipping missing flash image declared by platform: %s",
|
||||
image.path,
|
||||
)
|
||||
continue
|
||||
flash_images.append(image)
|
||||
|
||||
mcu = "esp8266"
|
||||
if CORE.is_esp32:
|
||||
|
||||
@@ -2,11 +2,7 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
require_adc_oneshot_iram,
|
||||
)
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -28,7 +24,6 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
@@ -70,13 +65,6 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def _require_adc_iram(config: ConfigType) -> ConfigType:
|
||||
"""Register ADC oneshot IRAM requirement during config validation."""
|
||||
if CORE.is_esp32:
|
||||
require_adc_oneshot_iram()
|
||||
return config
|
||||
|
||||
|
||||
ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
@@ -107,7 +95,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
validate_config,
|
||||
_require_adc_iram,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
@@ -671,7 +671,6 @@ message SensorStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SENSOR";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float state = 2;
|
||||
@@ -778,10 +777,9 @@ message SubscribeLogsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (log) = false;
|
||||
option (no_delay) = false;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
LogLevel level = 1 [(force) = true];
|
||||
bytes message = 3 [(force) = true];
|
||||
LogLevel level = 1;
|
||||
bytes message = 3;
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
@@ -1640,7 +1638,6 @@ message BluetoothLERawAdvertisementsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ extend google.protobuf.MessageOptions {
|
||||
optional bool no_delay = 1040 [default=false];
|
||||
optional string base_class = 1041;
|
||||
optional bool inline_encode = 1042 [default=false];
|
||||
optional bool speed_optimized = 1043 [default=false];
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
|
||||
@@ -745,9 +745,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
|
||||
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
|
||||
@@ -757,9 +755,7 @@ SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) c
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SensorStateResponse::calculate_size() const {
|
||||
uint32_t SensorStateResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 5;
|
||||
size += ProtoSize::calc_float(1, this->state);
|
||||
@@ -916,22 +912,16 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
|
||||
}
|
||||
return true;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
|
||||
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 2;
|
||||
size += ProtoSize::calc_length_force(1, this->message_len_);
|
||||
size += this->level ? 2 : 0;
|
||||
size += ProtoSize::calc_length(1, this->message_len_);
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -2338,9 +2328,7 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
@@ -2362,9 +2350,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
|
||||
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
|
||||
#endif
|
||||
float get_reference_voltage(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
#else
|
||||
return 120.0; // Default voltage
|
||||
#endif
|
||||
}
|
||||
float get_reference_current(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
#else
|
||||
return 5.0f; // Default current
|
||||
#endif
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
from esphome.core import CORE
|
||||
@@ -31,7 +27,6 @@ class AudioData:
|
||||
flac_support: bool = False
|
||||
mp3_support: bool = False
|
||||
opus_support: bool = False
|
||||
micro_decoder_support: bool = False
|
||||
|
||||
|
||||
def _get_data() -> AudioData:
|
||||
@@ -55,11 +50,6 @@ def request_opus_support() -> None:
|
||||
_get_data().opus_support = True
|
||||
|
||||
|
||||
def request_micro_decoder_support() -> None:
|
||||
"""Request micro-decoder library support for audio decoding."""
|
||||
_get_data().micro_decoder_support = True
|
||||
|
||||
|
||||
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
|
||||
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
|
||||
CONF_MIN_CHANNELS = "min_channels"
|
||||
@@ -218,19 +208,6 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
|
||||
if data.micro_decoder_support:
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
|
||||
|
||||
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
|
||||
if not data.flac_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False)
|
||||
if not data.mp3_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
|
||||
if not data.opus_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
|
||||
|
||||
# Legacy audio_decoder.cpp support defines and components
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
|
||||
|
||||
@@ -17,7 +17,6 @@ CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
DOMAIN = "bme68x_bsec2"
|
||||
|
||||
BSEC2_LIBRARY_VERSION = "1.10.2610"
|
||||
BME68x_LIBRARY_VERSION = "v1.3.40408"
|
||||
|
||||
CONF_ALGORITHM_OUTPUT = "algorithm_output"
|
||||
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
|
||||
@@ -185,31 +184,16 @@ async def to_code_base(config):
|
||||
if core.CORE.using_arduino:
|
||||
cg.add_library("Wire", None)
|
||||
cg.add_library("SPI", None)
|
||||
|
||||
if core.CORE.is_esp32:
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(
|
||||
name="boschsensortec/Bosch-BME68x-Library",
|
||||
repo="https://github.com/esphome-libs/Bosch-BME68x-Library",
|
||||
ref=BME68x_LIBRARY_VERSION,
|
||||
)
|
||||
add_idf_component(
|
||||
name="boschsensortec/Bosch-BSEC2-Library",
|
||||
repo="https://github.com/esphome-libs/Bosch-BSEC2-Library",
|
||||
ref=BSEC2_LIBRARY_VERSION,
|
||||
)
|
||||
else:
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BME68x-Library#{BME68x_LIBRARY_VERSION}",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
|
||||
)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
None,
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
|
||||
)
|
||||
|
||||
cg.add_define("USE_BSEC2")
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
||||
await cg.register_parented(var, config[CONF_CANBUS_ID])
|
||||
|
||||
if (can_id := config.get(CONF_CAN_ID)) is not None:
|
||||
can_id = await cg.templatable(can_id, args, cg.uint32)
|
||||
cg.add(var.set_can_id(can_id))
|
||||
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
|
||||
|
||||
|
||||
@@ -106,30 +106,6 @@ void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_lo
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->configure();
|
||||
if (this->is_failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::configure() {
|
||||
// Manual reset sequence per CC1101 datasheet section 19.1.2
|
||||
this->cs_->digital_write(true);
|
||||
delayMicroseconds(1);
|
||||
this->cs_->digital_write(false);
|
||||
@@ -152,6 +128,11 @@ void CC1101Component::configure() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup GDO0 pin if configured
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
|
||||
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
|
||||
@@ -161,11 +142,21 @@ void CC1101Component::configure() {
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
|
||||
if (!this->enter_rx_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
|
||||
@@ -282,7 +273,7 @@ void CC1101Component::begin_rx() {
|
||||
|
||||
void CC1101Component::reset() {
|
||||
this->strobe_(Command::RES);
|
||||
this->configure();
|
||||
this->setup();
|
||||
}
|
||||
|
||||
void CC1101Component::set_idle() {
|
||||
|
||||
@@ -25,7 +25,6 @@ class CC1101Component : public Component,
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void configure();
|
||||
|
||||
// Actions
|
||||
void begin_tx();
|
||||
|
||||
@@ -43,11 +43,3 @@ wave_4_26.extend(
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
ssd1677.extend(
|
||||
"waveshare-3.97in",
|
||||
width=800,
|
||||
height=480,
|
||||
mirror_x=True,
|
||||
)
|
||||
|
||||
@@ -671,12 +671,11 @@ def _is_framework_url(source: str) -> bool:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 8),
|
||||
"latest": cv.Version(3, 3, 8),
|
||||
"dev": cv.Version(3, 3, 8),
|
||||
"recommended": cv.Version(3, 3, 7),
|
||||
"latest": cv.Version(3, 3, 7),
|
||||
"dev": cv.Version(3, 3, 7),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
@@ -696,7 +695,6 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
|
||||
cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"),
|
||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||
@@ -716,15 +714,17 @@ ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(5, 5, 4),
|
||||
"latest": cv.Version(5, 5, 4),
|
||||
"recommended": cv.Version(5, 5, 3, "1"),
|
||||
"latest": cv.Version(5, 5, 3, "1"),
|
||||
"dev": cv.Version(5, 5, 4),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
6, 0, 0
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(
|
||||
5, 5, 4
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
@@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 38, "1"),
|
||||
"latest": cv.Version(55, 3, 38, "1"),
|
||||
"recommended": cv.Version(55, 3, 37),
|
||||
"latest": cv.Version(55, 3, 37),
|
||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,6 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
||||
|
||||
# VFS requirement tracking
|
||||
# Components that need VFS features can call require_vfs_*() functions
|
||||
@@ -1072,7 +1071,6 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
@@ -1170,17 +1168,6 @@ def require_fatfs() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_adc_oneshot_iram() -> None:
|
||||
"""Mark that ADC oneshot IRAM safety is required by a component.
|
||||
|
||||
Call this from components that use the ADC oneshot driver. When flash cache is
|
||||
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
|
||||
the ADC oneshot read function must be in IRAM to avoid crashes.
|
||||
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
|
||||
|
||||
|
||||
def _parse_idf_component(value: str) -> ConfigType:
|
||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||
# Match operator followed by version-like string (digit or *)
|
||||
@@ -1281,7 +1268,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
@@ -2082,16 +2068,6 @@ async def to_code(config):
|
||||
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
|
||||
|
||||
# Place ADC oneshot control functions in IRAM for cache safety
|
||||
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
|
||||
# power management, etc.), ADC reads will crash if these functions are in flash.
|
||||
# Components using ADC call require_adc_oneshot_iram() to force this.
|
||||
if (
|
||||
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
|
||||
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
|
||||
|
||||
# Disable FATFS support
|
||||
# Components that need FATFS (SD card, etc.) can call require_fatfs()
|
||||
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
|
||||
|
||||
@@ -1960,10 +1960,6 @@ BOARDS = {
|
||||
"name": "Hornbill ESP32 Minima",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"huidu_hd_wf1": {
|
||||
"name": "Huidu HD-WF1",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"huidu_hd_wf2": {
|
||||
"name": "Huidu HD-WF2",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2032,10 +2028,6 @@ BOARDS = {
|
||||
"name": "LilyGo T-Display-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lilygo-t-energy-s3": {
|
||||
"name": "LilyGo T-Energy-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lilygo-t3-s3": {
|
||||
"name": "LilyGo T3-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2297,18 +2289,10 @@ BOARDS = {
|
||||
"name": "S.ODI Ultra v1",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"seeed_xiao_esp32_s3_plus": {
|
||||
"name": "Seeed Studio XIAO ESP32S3 Plus",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"seeed_xiao_esp32c3": {
|
||||
"name": "Seeed Studio XIAO ESP32C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"seeed_xiao_esp32c5": {
|
||||
"name": "Seeed Studio XIAO ESP32C5",
|
||||
"variant": VARIANT_ESP32C5,
|
||||
},
|
||||
"seeed_xiao_esp32c6": {
|
||||
"name": "Seeed Studio XIAO ESP32C6",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
|
||||
@@ -61,9 +61,6 @@ uint32_t arch_get_cpu_freq_hz() {
|
||||
}
|
||||
|
||||
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static StackType_t
|
||||
loop_task_stack[ESPHOME_LOOP_TASK_STACK_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void loop_task(void *pv_params) {
|
||||
setup();
|
||||
@@ -76,11 +73,9 @@ extern "C" void app_main() {
|
||||
initArduino();
|
||||
esp32::setup_preferences();
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
loop_task_handle = xTaskCreateStatic(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack,
|
||||
&loop_task_tcb);
|
||||
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
|
||||
#else
|
||||
loop_task_handle = xTaskCreateStaticPinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1,
|
||||
loop_task_stack, &loop_task_tcb, 1);
|
||||
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -11,6 +12,9 @@ namespace esphome::esp32 {
|
||||
|
||||
static const char *const TAG = "preferences";
|
||||
|
||||
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
|
||||
static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
@@ -47,8 +51,8 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, this->key);
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
@@ -104,8 +108,8 @@ bool ESP32Preferences::sync() {
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, save.key);
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
|
||||
@@ -108,13 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
# Use the global's value_type alias as the lambda return type so
|
||||
# TemplatableFn stores a direct function pointer instead of going through
|
||||
# the deprecated converting trampoline when the value expression deduces
|
||||
# to a different type (e.g. int literal assigned to a float global).
|
||||
value_type = cg.RawExpression(f"{full_id.type}::value_type")
|
||||
templ = await cg.templatable(
|
||||
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
|
||||
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
|
||||
)
|
||||
cg.add(var.set_value(templ))
|
||||
return var
|
||||
|
||||
@@ -127,6 +127,6 @@ async def to_code(config):
|
||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||
cg.add_build_flag("-Wno-error=overloaded-virtual")
|
||||
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.41")
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.40")
|
||||
if CORE.is_libretiny or CORE.is_esp32:
|
||||
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])
|
||||
|
||||
@@ -360,8 +360,8 @@ void LD2410Component::handle_periodic_data_() {
|
||||
*/
|
||||
#ifdef USE_SENSOR
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]);
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
|
||||
@@ -375,26 +375,26 @@ void LD2410Component::handle_periodic_data_() {
|
||||
Moving energy: 20~28th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]);
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
|
||||
}
|
||||
/*
|
||||
Still energy: 29~37th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]);
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
|
||||
}
|
||||
/*
|
||||
Light sensor: 38th bytes
|
||||
*/
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]);
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
|
||||
} else {
|
||||
for (auto &gate_move_sensor : this->gate_move_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
|
||||
}
|
||||
for (auto &gate_still_sensor : this->gate_still_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor)
|
||||
}
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_)
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -786,12 +786,13 @@ void LD2410Component::set_light_out_control() {
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
|
||||
void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_move_sensors_[gate].set_sensor(s);
|
||||
this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
}
|
||||
|
||||
void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_still_sensors_[gate].set_sensor(s);
|
||||
this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -129,8 +129,8 @@ class LD2410Component : public Component, public uart::UARTDevice {
|
||||
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -397,12 +397,12 @@ void LD2412Component::handle_periodic_data_() {
|
||||
*/
|
||||
#ifdef USE_SENSOR
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]);
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
|
||||
if (this->detection_distance_sensor_.has_sensor()) {
|
||||
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]))
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY])
|
||||
if (this->detection_distance_sensor_ != nullptr) {
|
||||
int new_detect_distance = 0;
|
||||
if (target_state != 0x00 && (target_state & MOVE_BITMASK)) {
|
||||
new_detect_distance =
|
||||
@@ -410,7 +410,7 @@ void LD2412Component::handle_periodic_data_() {
|
||||
} else if (target_state != 0x00) {
|
||||
new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]);
|
||||
}
|
||||
this->detection_distance_sensor_.publish_state_if_not_dup(new_detect_distance);
|
||||
this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance);
|
||||
}
|
||||
if (engineering_mode) {
|
||||
// Engineering mode needs at least LIGHT_SENSOR + 1 bytes
|
||||
@@ -423,27 +423,27 @@ void LD2412Component::handle_periodic_data_() {
|
||||
Moving energy: 20~28th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]);
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
|
||||
}
|
||||
/*
|
||||
Still energy: 29~37th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]);
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
|
||||
}
|
||||
/*
|
||||
Light sensor value
|
||||
*/
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]);
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
|
||||
}
|
||||
} else {
|
||||
for (auto &gate_move_sensor : this->gate_move_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
|
||||
}
|
||||
for (auto &gate_still_sensor : this->gate_still_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor)
|
||||
}
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_)
|
||||
}
|
||||
#endif
|
||||
// the radar module won't tell us when it's done, so we just have to keep polling...
|
||||
@@ -846,11 +846,12 @@ void LD2412Component::set_light_out_control() {
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
|
||||
void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_move_sensors_[gate].set_sensor(s);
|
||||
this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
}
|
||||
void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_still_sensors_[gate].set_sensor(s);
|
||||
this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -133,8 +133,8 @@ class LD2412Component : public Component, public uart::UARTDevice {
|
||||
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -565,7 +565,6 @@ void LD2450Component::handle_periodic_data_() {
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count);
|
||||
// Moving Target Count
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count);
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -873,32 +872,33 @@ void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QU
|
||||
void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
|
||||
void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_x_sensors_[target].set_sensor(s);
|
||||
this->move_x_sensors_[target] = new SensorWithDedup<int16_t>(s);
|
||||
}
|
||||
void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_y_sensors_[target].set_sensor(s);
|
||||
this->move_y_sensors_[target] = new SensorWithDedup<int16_t>(s);
|
||||
}
|
||||
void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_speed_sensors_[target].set_sensor(s);
|
||||
this->move_speed_sensors_[target] = new SensorWithDedup<int16_t>(s);
|
||||
}
|
||||
void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_angle_sensors_[target].set_sensor(s);
|
||||
this->move_angle_sensors_[target] = new SensorWithDedup<float>(s);
|
||||
}
|
||||
void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_distance_sensors_[target].set_sensor(s);
|
||||
this->move_distance_sensors_[target] = new SensorWithDedup<uint16_t>(s);
|
||||
}
|
||||
void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_resolution_sensors_[target].set_sensor(s);
|
||||
this->move_resolution_sensors_[target] = new SensorWithDedup<uint16_t>(s);
|
||||
}
|
||||
void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
|
||||
this->zone_target_count_sensors_[zone].set_sensor(s);
|
||||
this->zone_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
|
||||
}
|
||||
void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
|
||||
this->zone_still_target_count_sensors_[zone].set_sensor(s);
|
||||
this->zone_still_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
|
||||
}
|
||||
void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
|
||||
this->zone_moving_target_count_sensors_[zone].set_sensor(s);
|
||||
this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
@@ -182,15 +182,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
ZoneOfNumbers zone_numbers_[MAX_ZONES];
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_x_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_y_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_speed_sensors_{};
|
||||
std::array<SensorWithDedup<float>, MAX_TARGETS> move_angle_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_distance_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_resolution_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_still_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_moving_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_x_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_y_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_speed_sensors_{};
|
||||
std::array<SensorWithDedup<float> *, MAX_TARGETS> move_angle_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_distance_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_resolution_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_still_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_moving_target_count_sensors_{};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
|
||||
|
||||
@@ -11,20 +11,28 @@
|
||||
|
||||
#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \
|
||||
protected: \
|
||||
ld24xx::SensorWithDedup<dedup_type> name##_sensor_{}; \
|
||||
ld24xx::SensorWithDedup<dedup_type> *name##_sensor_{nullptr}; \
|
||||
\
|
||||
public: \
|
||||
void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_.set_sensor(sensor); }
|
||||
void set_##name##_sensor(sensor::Sensor *sensor) { \
|
||||
this->name##_sensor_ = new ld24xx::SensorWithDedup<dedup_type>(sensor); \
|
||||
}
|
||||
#endif
|
||||
|
||||
#define LOG_SENSOR_WITH_DEDUP_SAFE(tag, name, sensor) \
|
||||
if ((sensor).has_sensor()) { \
|
||||
LOG_SENSOR(tag, name, (sensor).get_sensor()); \
|
||||
if ((sensor) != nullptr) { \
|
||||
LOG_SENSOR(tag, name, (sensor)->sens); \
|
||||
}
|
||||
|
||||
#define SAFE_PUBLISH_SENSOR(sensor, value) (sensor).publish_state_if_not_dup(value)
|
||||
#define SAFE_PUBLISH_SENSOR(sensor, value) \
|
||||
if ((sensor) != nullptr) { \
|
||||
(sensor)->publish_state_if_not_dup(value); \
|
||||
}
|
||||
|
||||
#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) (sensor).publish_state_unknown()
|
||||
#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) \
|
||||
if ((sensor) != nullptr) { \
|
||||
(sensor)->publish_state_unknown(); \
|
||||
}
|
||||
|
||||
#define highbyte(val) (uint8_t)((val) >> 8)
|
||||
#define lowbyte(val) (uint8_t)((val) &0xff)
|
||||
@@ -62,33 +70,25 @@ inline void format_version_str(const uint8_t *version, std::span<char, 20> buffe
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
/// Sensor with deduplication — sensor may be null, null check is internal.
|
||||
/// Stored inline, no heap allocation. Does nothing when no sensor is set.
|
||||
// Helper class to store a sensor with a deduplicator & publish state only when the value changes
|
||||
template<typename T> class SensorWithDedup {
|
||||
public:
|
||||
void set_sensor(sensor::Sensor *sens) {
|
||||
this->sens_ = sens;
|
||||
this->dedup_ = {};
|
||||
}
|
||||
SensorWithDedup(sensor::Sensor *sens) : sens(sens) {}
|
||||
|
||||
void publish_state_if_not_dup(T state) {
|
||||
if (this->sens_ != nullptr && this->dedup_.next(state)) {
|
||||
this->sens_->publish_state(static_cast<float>(state));
|
||||
if (this->publish_dedup.next(state)) {
|
||||
this->sens->publish_state(static_cast<float>(state));
|
||||
}
|
||||
}
|
||||
|
||||
void publish_state_unknown() {
|
||||
if (this->sens_ != nullptr && this->dedup_.next_unknown()) {
|
||||
this->sens_->publish_state(NAN);
|
||||
if (this->publish_dedup.next_unknown()) {
|
||||
this->sens->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
|
||||
bool has_sensor() const { return this->sens_ != nullptr; }
|
||||
sensor::Sensor *get_sensor() const { return this->sens_; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *sens_{nullptr};
|
||||
Deduplicator<T> dedup_;
|
||||
sensor::Sensor *sens;
|
||||
Deduplicator<T> publish_dedup;
|
||||
};
|
||||
#endif
|
||||
} // namespace esphome::ld24xx
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "preferences.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -10,6 +11,9 @@ namespace esphome::libretiny {
|
||||
|
||||
static const char *const TAG = "preferences";
|
||||
|
||||
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
|
||||
static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
@@ -46,8 +50,8 @@ bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, this->key);
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
fdb_blob_make(this->blob, data, len);
|
||||
size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob);
|
||||
if (actual_len != len) {
|
||||
@@ -88,8 +92,8 @@ bool LibreTinyPreferences::sync() {
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, save.key);
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
|
||||
@@ -22,20 +22,4 @@ uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
|
||||
return (target - a <= b - target) ? lo : lo + 1;
|
||||
}
|
||||
|
||||
Color ESPColorCorrection::color_uncorrect(Color color) const {
|
||||
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
|
||||
return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green),
|
||||
this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white));
|
||||
}
|
||||
|
||||
uint8_t ESPColorCorrection::color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const {
|
||||
if (max_brightness == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
// Use 32-bit intermediates: when max_brightness and local_brightness_ are small but non-zero,
|
||||
// (uncorrected / max_brightness) * 255 can exceed 65535 before the std::min(255) clamp runs.
|
||||
uint32_t uncorrected = this->gamma_uncorrect_(value) * 255UL;
|
||||
uint32_t res = ((uncorrected / max_brightness) * 255UL) / this->local_brightness_;
|
||||
return static_cast<uint8_t>(std::min(res, uint32_t(255)));
|
||||
}
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -46,18 +46,38 @@ class ESPColorCorrection {
|
||||
uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_);
|
||||
return this->gamma_correct_(res);
|
||||
}
|
||||
Color color_uncorrect(Color color) const;
|
||||
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
|
||||
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
|
||||
return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green),
|
||||
this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white));
|
||||
}
|
||||
inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
|
||||
return this->color_uncorrect_channel_(red, this->max_brightness_.red);
|
||||
if (this->max_brightness_.red == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
}
|
||||
inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
|
||||
return this->color_uncorrect_channel_(green, this->max_brightness_.green);
|
||||
if (this->max_brightness_.green == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
}
|
||||
inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
|
||||
return this->color_uncorrect_channel_(blue, this->max_brightness_.blue);
|
||||
if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
}
|
||||
inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
|
||||
return this->color_uncorrect_channel_(white, this->max_brightness_.white);
|
||||
if (this->max_brightness_.white == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -65,9 +85,6 @@ class ESPColorCorrection {
|
||||
uint8_t gamma_correct_(uint8_t value) const;
|
||||
/// Reverse gamma: binary search the forward PROGMEM table
|
||||
uint8_t gamma_uncorrect_(uint8_t value) const;
|
||||
/// Shared body of color_uncorrect_{red,green,blue,white}. Kept out-of-line
|
||||
/// to avoid duplicating two 16-bit divides at every call site.
|
||||
uint8_t color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const;
|
||||
|
||||
const uint16_t *gamma_table_{nullptr};
|
||||
Color max_brightness_{255, 255, 255, 255};
|
||||
|
||||
@@ -341,7 +341,7 @@ async def to_code(configs):
|
||||
df.LOGGER.info("LVGL will use hardware rotation via display driver")
|
||||
else:
|
||||
rotation_type = RotationType.ROTATION_SOFTWARE
|
||||
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32P4:
|
||||
if get_esp32_variant() == VARIANT_ESP32P4:
|
||||
df.LOGGER.info("LVGL will use software rotation (PPA accelerated)")
|
||||
else:
|
||||
df.LOGGER.info("LVGL will use software rotation")
|
||||
|
||||
@@ -170,7 +170,7 @@ async def to_code(config):
|
||||
cg.add_library("LEAmDNS", None)
|
||||
|
||||
if CORE.is_esp32:
|
||||
add_idf_component(name="espressif/mdns", ref="1.11.0")
|
||||
add_idf_component(name="espressif/mdns", ref="1.10.0")
|
||||
|
||||
cg.add_define("USE_MDNS")
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ async def to_code(config):
|
||||
|
||||
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
|
||||
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1")
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
|
||||
@@ -28,8 +28,7 @@ void AirConditioner::on_status_change() {
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
|
||||
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
|
||||
// Read existing presets (set by codegen), append frost protection, write back
|
||||
auto traits = this->get_traits();
|
||||
const auto &existing = traits.get_supported_custom_presets();
|
||||
const auto &existing = this->get_traits().get_supported_custom_presets();
|
||||
bool found = false;
|
||||
for (const char *p : existing) {
|
||||
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
|
||||
|
||||
@@ -234,9 +234,9 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
|
||||
void dump_config() override {
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
|
||||
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
|
||||
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
|
||||
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
HAS_HARDWARE_ROTATION);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ class MipiSpi : public display::Display,
|
||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||
|
||||
// calculate new madctl value from base value adjusted for rotation
|
||||
uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only
|
||||
uint8_t madctl = MADCTL; // lower 8 bits only
|
||||
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
|
||||
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
|
||||
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
|
||||
|
||||
@@ -45,18 +45,6 @@ def is_remote_package(package_config: dict) -> bool:
|
||||
return CONF_URL in package_config
|
||||
|
||||
|
||||
def is_package_definition(value: object) -> bool:
|
||||
"""Returns True if the value looks like a package definition rather than a config fragment.
|
||||
|
||||
Package definitions are IncludeFile objects, git URL shorthand strings, or
|
||||
remote package dicts (containing a ``url:`` key). Config fragments are
|
||||
plain dicts that represent component configuration.
|
||||
"""
|
||||
return isinstance(value, (yaml_util.IncludeFile, str)) or (
|
||||
isinstance(value, dict) and is_remote_package(value)
|
||||
)
|
||||
|
||||
|
||||
def valid_package_contents(package_config: dict) -> dict:
|
||||
"""Validate that a package looks like a plausible ESPHome config fragment.
|
||||
|
||||
@@ -321,23 +309,20 @@ def _walk_packages(
|
||||
return config
|
||||
packages = config[CONF_PACKAGES]
|
||||
|
||||
with cv.prepend_path(CONF_PACKAGES):
|
||||
if isinstance(packages, yaml_util.IncludeFile):
|
||||
# If the packages key is an IncludeFile, resolve it first before processing.
|
||||
packages, _ = resolve_include(packages, [], context, strict_undefined=False)
|
||||
if not isinstance(packages, (dict, list)):
|
||||
raise cv.Invalid(
|
||||
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
|
||||
)
|
||||
if not isinstance(packages, (dict, list)):
|
||||
raise cv.Invalid(
|
||||
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
|
||||
)
|
||||
|
||||
with cv.prepend_path(CONF_PACKAGES):
|
||||
if not isinstance(packages, dict):
|
||||
_walk_package_list(packages, callback, context)
|
||||
elif (result := _walk_package_dict(packages, callback, context)) is not None:
|
||||
if not validate_deprecated or any(
|
||||
is_package_definition(v) for v in packages.values()
|
||||
):
|
||||
if not validate_deprecated:
|
||||
raise result
|
||||
# Fallback: treat the dict as a single deprecated package.
|
||||
# Note: this catches *any* cv.Invalid from the callback, which may
|
||||
# mask real validation errors in named package dicts.
|
||||
# This block can be removed once the single-package
|
||||
# deprecation period (2026.7.0) is over.
|
||||
config[CONF_PACKAGES] = [packages]
|
||||
@@ -476,9 +461,6 @@ class _PackageProcessor:
|
||||
self, package_config: dict | str, context_vars: ContextVars | None
|
||||
) -> dict:
|
||||
"""Resolve a single package and recurse into any nested packages."""
|
||||
from_remote = isinstance(package_config, dict) and is_remote_package(
|
||||
package_config
|
||||
)
|
||||
package_config = self.resolve_package(package_config, context_vars)
|
||||
self.collect_substitutions(package_config)
|
||||
|
||||
@@ -488,18 +470,7 @@ class _PackageProcessor:
|
||||
# Push context from !include vars on the package root and on the packages key
|
||||
context_vars = push_context(package_config, context_vars)
|
||||
context_vars = push_context(package_config[CONF_PACKAGES], context_vars)
|
||||
# Disable the deprecated single-package fallback for remote
|
||||
# packages. _process_remote_package returns dicts with
|
||||
# already-resolved values that is_package_definition cannot
|
||||
# distinguish from config fragments, so the fallback would
|
||||
# always fire and mask real errors with wrong paths
|
||||
# (packages->0 instead of packages-><name>).
|
||||
return _walk_packages(
|
||||
package_config,
|
||||
self.process_package,
|
||||
context_vars,
|
||||
validate_deprecated=not from_remote,
|
||||
)
|
||||
return _walk_packages(package_config, self.process_package, context_vars)
|
||||
|
||||
|
||||
def do_packages_pass(
|
||||
|
||||
@@ -315,7 +315,7 @@ void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration
|
||||
my_integration_time_regval = integration_time;
|
||||
this->integration_time_auto_ = false;
|
||||
}
|
||||
this->integration_time_ = (256.f - (float) my_integration_time_regval) * 2.4f;
|
||||
this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
|
||||
ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
|
||||
}
|
||||
void TCS34725Component::set_gain(TCS34725Gain gain) {
|
||||
|
||||
@@ -114,25 +114,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
uint8_t *data, size_t len, bool final) {
|
||||
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
|
||||
|
||||
// First byte of a new upload: index==0 with actual data. (web_server_idf
|
||||
// fires a separate start-marker call with data==nullptr/len==0 before the
|
||||
// first real chunk; gate on len>0 so we only trigger once per upload.)
|
||||
if (index == 0 && len > 0) {
|
||||
// If a previous upload was interrupted (e.g. client closed the tab, TCP
|
||||
// reset) the backend from that session may still be open. Tear it down
|
||||
// so flash state doesn't get concatenated with the new image (which can
|
||||
// produce a technically-valid-sized but corrupted firmware that bricks
|
||||
// the device once it reboots).
|
||||
if (this->ota_backend_) {
|
||||
ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
|
||||
this->ota_backend_->abort();
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Notify listeners that the previous session was aborted before the new one starts.
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
|
||||
#endif
|
||||
this->ota_backend_.reset();
|
||||
}
|
||||
|
||||
if (index == 0 && !this->ota_backend_) {
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ int main() {
|
||||
setup();
|
||||
while (true) {
|
||||
loop();
|
||||
esphome::yield();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+109
-48
@@ -85,12 +85,8 @@ void Application::setup() {
|
||||
if (component->can_proceed())
|
||||
continue;
|
||||
|
||||
// Force the status LED to blink WARNING while we wait for a slow
|
||||
// component to come up. Cleared after setup() finishes if no real
|
||||
// component has warning set.
|
||||
this->app_state_ |= STATUS_LED_WARNING;
|
||||
|
||||
do {
|
||||
uint8_t new_app_state = STATUS_LED_WARNING;
|
||||
uint32_t now = millis();
|
||||
|
||||
// Process pending loop enables to handle GPIO interrupts during setup
|
||||
@@ -100,26 +96,17 @@ void Application::setup() {
|
||||
// Update loop_component_start_time_ right before calling each component
|
||||
this->loop_component_start_time_ = millis();
|
||||
this->components_[j]->call();
|
||||
new_app_state |= this->components_[j]->get_component_state();
|
||||
this->app_state_ |= new_app_state;
|
||||
this->feed_wdt();
|
||||
}
|
||||
|
||||
this->after_loop_tasks_();
|
||||
this->app_state_ = new_app_state;
|
||||
yield();
|
||||
} while (!component->can_proceed() && !component->is_failed());
|
||||
}
|
||||
|
||||
// Setup is complete. Reconcile STATUS_LED_WARNING: the slow-setup path
|
||||
// above may have forced it on, and any status_clear_warning() calls
|
||||
// from components during setup were intentional no-ops (gated by
|
||||
// APP_STATE_SETUP_COMPLETE). Walk components once here to pick up the
|
||||
// real state. STATUS_LED_ERROR is never artificially forced, so its
|
||||
// clear path always works and needs no reconciliation. Finally, set
|
||||
// APP_STATE_SETUP_COMPLETE so subsequent warning clears go through
|
||||
// the normal walk-and-clear path.
|
||||
if (!this->any_component_has_status_flag_(STATUS_LED_WARNING))
|
||||
this->app_state_ &= ~STATUS_LED_WARNING;
|
||||
this->app_state_ |= APP_STATE_SETUP_COMPLETE;
|
||||
|
||||
ESP_LOGI(TAG, "setup() finished successfully!");
|
||||
|
||||
#ifdef USE_SETUP_PRIORITY_OVERRIDE
|
||||
@@ -209,40 +196,21 @@ void Application::process_dump_config_() {
|
||||
this->dump_config_at_++;
|
||||
}
|
||||
|
||||
void Application::feed_wdt() {
|
||||
// Cold entry: callers without a millis() timestamp in hand. Fetches the
|
||||
// time and takes the same rate-limit path as feed_wdt_with_time().
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
|
||||
this->feed_wdt_slow_(now);
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
// Callers (both feed_wdt() and feed_wdt_with_time()) have already
|
||||
// confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded.
|
||||
arch_feed_wdt();
|
||||
this->last_wdt_feed_ = time;
|
||||
void HOT Application::feed_wdt(uint32_t time) {
|
||||
static uint32_t last_feed = 0;
|
||||
// Use provided time if available, otherwise get current time
|
||||
uint32_t now = time ? time : millis();
|
||||
// Compare in milliseconds (3ms threshold)
|
||||
if (now - last_feed > 3) {
|
||||
arch_feed_wdt();
|
||||
last_feed = now;
|
||||
#ifdef USE_STATUS_LED
|
||||
if (status_led::global_status_led != nullptr) {
|
||||
status_led::global_status_led->call();
|
||||
}
|
||||
if (status_led::global_status_led != nullptr) {
|
||||
status_led::global_status_led->call();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Application::any_component_has_status_flag_(uint8_t flag) const {
|
||||
// Walk all components (not just looping ones) so non-looping components'
|
||||
// status bits are respected. Only called from the slow-path clear helpers
|
||||
// (status_clear_warning_slow_path_ / status_clear_error_slow_path_) on an
|
||||
// actual set→clear transition, so walking O(N) here is paid once per
|
||||
// transition — not once per loop iteration.
|
||||
for (auto *component : this->components_) {
|
||||
if ((component->get_component_state() & flag) != 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Application::reboot() {
|
||||
ESP_LOGI(TAG, "Forcing a reboot");
|
||||
for (auto &component : std::ranges::reverse_view(this->components_)) {
|
||||
@@ -331,7 +299,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
|
||||
|
||||
while (pending_count > 0 && (now - start_time) < timeout_ms) {
|
||||
// Feed watchdog during teardown to prevent triggering
|
||||
this->feed_wdt_with_time(now);
|
||||
this->feed_wdt(now);
|
||||
|
||||
// Process components and compact the array, keeping only those still pending
|
||||
size_t still_pending = 0;
|
||||
@@ -482,6 +450,99 @@ void Application::enable_pending_loops_() {
|
||||
}
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::atomic<uint32_t> Application::fast_select_scan_total_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_found_data_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_race_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_micro_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_stall_{0};
|
||||
|
||||
void Application::log_fast_select_scan_stats_() {
|
||||
uint32_t total = fast_select_scan_total_.load(std::memory_order_relaxed);
|
||||
uint32_t found = fast_select_scan_found_data_.load(std::memory_order_relaxed);
|
||||
uint32_t load_bearing = fast_select_scan_load_bearing_.load(std::memory_order_relaxed);
|
||||
uint32_t lb_race = fast_select_scan_load_bearing_race_.load(std::memory_order_relaxed);
|
||||
uint32_t lb_micro = fast_select_scan_load_bearing_micro_.load(std::memory_order_relaxed);
|
||||
uint32_t lb_stall = fast_select_scan_load_bearing_stall_.load(std::memory_order_relaxed);
|
||||
ESP_LOGD(TAG,
|
||||
"fast_select scan: total=%" PRIu32 " found_data=%" PRIu32 " load_bearing=%" PRIu32 " (race<10us=%" PRIu32
|
||||
" micro<100us=%" PRIu32 " stall>100us=%" PRIu32 ")",
|
||||
total, found, load_bearing, lb_race, lb_micro, lb_stall);
|
||||
}
|
||||
|
||||
void Application::note_fast_select_load_bearing_(struct lwip_sock *sock, uint32_t delay_ms) {
|
||||
uint32_t load_bearing = fast_select_scan_load_bearing_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
|
||||
// Spin-poll the task notification value for a short bounded window to measure how long
|
||||
// the counterfactual ulTaskNotifyTake would actually have blocked. This distinguishes
|
||||
// three cases:
|
||||
// race (<10µs) — notification arrived within ~10µs of scan start: callback-ordering
|
||||
// race between the lwip event_callback writing rcvevent and calling
|
||||
// xTaskNotifyGive a few instructions later. Scan is noise.
|
||||
// micro (<100µs) — notification arrived within 100µs: still noise at loop_interval scale.
|
||||
// stall (≥100µs) — notification did not arrive within our polling window. This is the
|
||||
// only case where the scan could be rescuing a real latency spike.
|
||||
// Cap the spin at 100µs so that if we're wrong and this IS a real stall, we only add
|
||||
// 100µs of extra work to that one unlucky loop iteration.
|
||||
uint32_t t_start = micros();
|
||||
uint32_t gap_us = UINT32_MAX;
|
||||
while (true) {
|
||||
if (ulTaskNotifyValueClear(nullptr, 0) != 0) {
|
||||
gap_us = micros() - t_start;
|
||||
break;
|
||||
}
|
||||
uint32_t elapsed = micros() - t_start;
|
||||
if (elapsed >= 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char *bucket;
|
||||
if (gap_us == UINT32_MAX) {
|
||||
fast_select_scan_load_bearing_stall_.fetch_add(1, std::memory_order_relaxed);
|
||||
bucket = "STALL";
|
||||
} else if (gap_us < 10) {
|
||||
fast_select_scan_load_bearing_race_.fetch_add(1, std::memory_order_relaxed);
|
||||
bucket = "race";
|
||||
} else {
|
||||
fast_select_scan_load_bearing_micro_.fetch_add(1, std::memory_order_relaxed);
|
||||
bucket = "micro";
|
||||
}
|
||||
|
||||
// Find the socket's index in monitored_sockets_ for easier correlation with registration order.
|
||||
int index = -1;
|
||||
for (size_t i = 0; i < this->monitored_sockets_.size(); i++) {
|
||||
if (this->monitored_sockets_[i] == sock) {
|
||||
index = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Read the rcvevent value directly. This is the same offset-based read used by
|
||||
// esphome_lwip_socket_has_data(); value > 0 means unread data is queued.
|
||||
int16_t rcvevent =
|
||||
*reinterpret_cast<volatile int16_t *>(reinterpret_cast<char *>(sock) + ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET);
|
||||
// Count how many other sockets also had data at this scan (could reveal whether it's always
|
||||
// the same socket or a burst across multiple).
|
||||
size_t sockets_with_data = 0;
|
||||
for (struct lwip_sock *s : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(s))
|
||||
sockets_with_data++;
|
||||
}
|
||||
if (gap_us == UINT32_MAX) {
|
||||
ESP_LOGW(TAG,
|
||||
"fast_select LOAD-BEARING #%" PRIu32 " [%s]: sock=%p idx=%d/%u rcvevent=%d delay_ms=%" PRIu32
|
||||
" sockets_with_data=%u gap_us=>100",
|
||||
load_bearing, bucket, sock, index, static_cast<unsigned>(this->monitored_sockets_.size()), rcvevent,
|
||||
delay_ms, static_cast<unsigned>(sockets_with_data));
|
||||
} else {
|
||||
ESP_LOGW(TAG,
|
||||
"fast_select LOAD-BEARING #%" PRIu32 " [%s]: sock=%p idx=%d/%u rcvevent=%d delay_ms=%" PRIu32
|
||||
" sockets_with_data=%u gap_us=%" PRIu32,
|
||||
load_bearing, bucket, sock, index, static_cast<unsigned>(this->monitored_sockets_.size()), rcvevent,
|
||||
delay_ms, static_cast<unsigned>(sockets_with_data), gap_us);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::register_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
if (sock == nullptr)
|
||||
|
||||
+63
-47
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <ctime>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
@@ -385,24 +386,7 @@ class Application {
|
||||
|
||||
void schedule_dump_config() { this->dump_config_at_ = 0; }
|
||||
|
||||
/// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the
|
||||
/// rate of HAL pokes low while still being small enough that any plausible
|
||||
/// watchdog timeout (seconds) has orders of magnitude of safety margin.
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3;
|
||||
|
||||
/// Feed the task watchdog. Cold entry — callers without a millis()
|
||||
/// timestamp in hand. Out of line to keep call sites tiny.
|
||||
void feed_wdt();
|
||||
|
||||
/// Feed the task watchdog, hot entry. Callers that already have a
|
||||
/// millis() timestamp pay only a load + sub + branch on the common
|
||||
/// (no-op) path. The actual arch feed + status LED update live in
|
||||
/// feed_wdt_slow_.
|
||||
void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) {
|
||||
if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] {
|
||||
this->feed_wdt_slow_(time);
|
||||
}
|
||||
}
|
||||
void feed_wdt(uint32_t time = 0);
|
||||
|
||||
void reboot();
|
||||
|
||||
@@ -418,18 +402,7 @@ class Application {
|
||||
*/
|
||||
void teardown_components(uint32_t timeout_ms);
|
||||
|
||||
/// Return the public app state status bits (STATUS_LED_* only).
|
||||
/// Internal bookkeeping bits like APP_STATE_SETUP_COMPLETE are masked
|
||||
/// out so external readers (status_led components, etc.) never see them.
|
||||
uint8_t get_app_state() const { return this->app_state_ & ~APP_STATE_SETUP_COMPLETE; }
|
||||
|
||||
/// True once Application::setup() has finished walking all components
|
||||
/// and finalized the initial status flags. Before this point, the
|
||||
/// slow-setup busy-wait may be forcing STATUS_LED_WARNING on, and
|
||||
/// status_clear_* intentionally skips its walk-and-clear step so the
|
||||
/// forced bit doesn't get wiped. Stored as a free bit on app_state_
|
||||
/// (bit 6) to avoid costing additional RAM.
|
||||
bool is_setup_complete() const { return (this->app_state_ & APP_STATE_SETUP_COMPLETE) != 0; }
|
||||
uint8_t get_app_state() const { return this->app_state_; }
|
||||
|
||||
// Helper macro for entity getter method declarations
|
||||
#ifdef USE_DEVICES
|
||||
@@ -605,12 +578,6 @@ class Application {
|
||||
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
|
||||
#endif
|
||||
|
||||
/// Walk all registered components looking for any whose component_state_
|
||||
/// has the given flag set. Used by Component::status_clear_*_slow_path_()
|
||||
/// (which is a friend) to decide whether to clear the corresponding bit on
|
||||
/// this->app_state_ (the app-wide "any component has this status" indicator).
|
||||
bool any_component_has_status_flag_(uint8_t flag) const;
|
||||
|
||||
/// Register a component, detecting loop() override at compile time.
|
||||
/// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance.
|
||||
template<typename T> void register_component_(T *comp) {
|
||||
@@ -649,10 +616,7 @@ class Application {
|
||||
/// Caller must ensure dump_config_at_ < components_.size().
|
||||
void __attribute__((noinline)) process_dump_config_();
|
||||
|
||||
/// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates
|
||||
/// last_wdt_feed_, and re-dispatches the status LED. Out of line so the
|
||||
/// inline wrapper stays tiny.
|
||||
void feed_wdt_slow_(uint32_t time);
|
||||
void feed_wdt_arch_();
|
||||
|
||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||
#ifdef USE_HOST
|
||||
@@ -692,6 +656,25 @@ class Application {
|
||||
FixedVector<Component *> looping_components_{};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
|
||||
// Stats to verify whether the pre-sleep socket scan in yield_with_select_() is ever load-bearing.
|
||||
// If fast_select_scan_load_bearing_ stays 0 under real workloads, the scan can be removed.
|
||||
// These are static because yield_with_select_() is inlined at every call site.
|
||||
static std::atomic<uint32_t> fast_select_scan_total_;
|
||||
static std::atomic<uint32_t> fast_select_scan_found_data_;
|
||||
// Umbrella counter: pre-scan notify peek was 0 and scan found data.
|
||||
// Broken down into three buckets based on the post-scan spin-poll result:
|
||||
// _race_ — notify arrived in < 10µs (callback-ordering race, scan is noise)
|
||||
// _micro_ — notify arrived in 10..100µs (still noise at loop_interval scale)
|
||||
// _stall_ — notify did not arrive within 100µs (the only case that could be a real stall)
|
||||
// If _stall_ stays 0, the scan is provably irrelevant under this workload.
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_;
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_race_;
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_micro_;
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_stall_;
|
||||
uint32_t fast_select_scan_stats_last_log_{0};
|
||||
void log_fast_select_scan_stats_();
|
||||
// Non-inline, called only on the rare load-bearing event so the hot path stays unchanged.
|
||||
void note_fast_select_load_bearing_(struct lwip_sock *sock, uint32_t delay_ms);
|
||||
#elif defined(USE_HOST)
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
#endif
|
||||
@@ -706,7 +689,6 @@ class Application {
|
||||
// 4-byte members
|
||||
uint32_t last_loop_{0};
|
||||
uint32_t loop_component_start_time_{0};
|
||||
uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path
|
||||
|
||||
#ifdef USE_HOST
|
||||
int max_fd_{-1}; // Highest file descriptor number for select()
|
||||
@@ -851,13 +833,12 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_
|
||||
this->drain_wake_notifications_();
|
||||
#endif
|
||||
|
||||
// Process scheduled tasks. Scheduler::call now feeds the watchdog itself
|
||||
// after each scheduled item that actually runs, so we no longer need an
|
||||
// unconditional feed here — when Scheduler::call has no work to do, the
|
||||
// only elapsed time is a sleep wake + a few instructions, and when it does
|
||||
// have work, it fed the wdt as it went.
|
||||
// Process scheduled tasks
|
||||
this->scheduler.call(loop_start_time);
|
||||
|
||||
// Feed the watchdog timer
|
||||
this->feed_wdt(loop_start_time);
|
||||
|
||||
// Process any pending enable_loop requests from ISRs
|
||||
// This must be done before marking in_loop_ = true to avoid race conditions
|
||||
if (this->has_pending_enable_loop_requests_) {
|
||||
@@ -877,6 +858,8 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_
|
||||
}
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
uint8_t new_app_state = 0;
|
||||
|
||||
// Get the initial loop time at the start
|
||||
uint32_t last_op_end_time = millis();
|
||||
|
||||
@@ -896,10 +879,13 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
// Use the finish method to get the current time as the end time
|
||||
last_op_end_time = guard.finish();
|
||||
}
|
||||
this->feed_wdt_with_time(last_op_end_time);
|
||||
new_app_state |= component->get_component_state();
|
||||
this->app_state_ |= new_app_state;
|
||||
this->feed_wdt(last_op_end_time);
|
||||
}
|
||||
|
||||
this->after_loop_tasks_();
|
||||
this->app_state_ = new_app_state;
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Process any pending runtime stats printing after all components have run
|
||||
@@ -923,6 +909,14 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
this->yield_with_select_(delay_time);
|
||||
this->last_loop_ = last_op_end_time;
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Periodic fast-select scan stats (debug). Remove once the scan is proven unneeded.
|
||||
if (last_op_end_time - this->fast_select_scan_stats_last_log_ >= 30000) {
|
||||
this->fast_select_scan_stats_last_log_ = last_op_end_time;
|
||||
this->log_fast_select_scan_stats_();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->dump_config_at_ < this->components_.size()) {
|
||||
this->process_dump_config_();
|
||||
}
|
||||
@@ -943,8 +937,30 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay
|
||||
// If a socket still has unread data (rcvevent > 0) but the task notification was already
|
||||
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
|
||||
// This scan preserves select() semantics: return immediately when any fd is ready.
|
||||
//
|
||||
// Debug stats: peek the task notification value BEFORE scanning. This answers the
|
||||
// counterfactual "if the scan did not exist and we called ulTaskNotifyTake right now,
|
||||
// would it stall?". ulTaskNotifyValueClear(nullptr, 0) is a pure read — it returns the
|
||||
// current value and clears zero bits, leaving the notification state untouched. Reading
|
||||
// before the loop (rather than after finding data) makes the answer TOCTOU-free: the
|
||||
// value we compare against is the value at the moment Take would have been called.
|
||||
// LibreTiny's FreeRTOS port predates ulTaskNotifyValueClear (added in FreeRTOS 10.4.0),
|
||||
// so we fall back to a pessimistic 0, which makes load_bearing an upper bound == found_data
|
||||
// on that platform. Zero there is still a valid proof that the scan is unused.
|
||||
#ifdef USE_ESP32
|
||||
uint32_t fast_select_notify_value_before_scan = ulTaskNotifyValueClear(nullptr, 0);
|
||||
#else
|
||||
uint32_t fast_select_notify_value_before_scan = 0;
|
||||
#endif
|
||||
fast_select_scan_total_.fetch_add(1, std::memory_order_relaxed);
|
||||
for (struct lwip_sock *sock : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(sock)) {
|
||||
fast_select_scan_found_data_.fetch_add(1, std::memory_order_relaxed);
|
||||
if (fast_select_notify_value_before_scan == 0) {
|
||||
// Scan was load-bearing: no notification pending, so Take would have stalled.
|
||||
// Delegate to a non-inline helper so the hot path stays the same size.
|
||||
this->note_fast_select_load_bearing_(sock, delay_ms);
|
||||
}
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,23 +411,10 @@ void Component::status_set_error(const LogString *message) {
|
||||
}
|
||||
void Component::status_clear_warning_slow_path_() {
|
||||
this->component_state_ &= ~STATUS_LED_WARNING;
|
||||
// Clear the app-wide STATUS_LED_WARNING bit only if setup has finished
|
||||
// AND no other component still has it set. During setup the forced
|
||||
// STATUS_LED_WARNING (from the slow-setup busy-wait) must not be wiped
|
||||
// by a transient component clear — Application::setup() reconciles
|
||||
// the warning bit once at the end before setting APP_STATE_SETUP_COMPLETE.
|
||||
// The set path is unchanged (set_status_flag_ still writes directly).
|
||||
if (App.is_setup_complete() && !App.any_component_has_status_flag_(STATUS_LED_WARNING))
|
||||
App.app_state_ &= ~STATUS_LED_WARNING;
|
||||
ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str()));
|
||||
}
|
||||
void Component::status_clear_error_slow_path_() {
|
||||
this->component_state_ &= ~STATUS_LED_ERROR;
|
||||
// STATUS_LED_ERROR is never artificially forced — it only ever lands
|
||||
// in app_state_ via a real set_status_flag_ call. So the walk-and-clear
|
||||
// path is always safe, including during setup.
|
||||
if (!App.any_component_has_status_flag_(STATUS_LED_ERROR))
|
||||
App.app_state_ &= ~STATUS_LED_ERROR;
|
||||
ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str()));
|
||||
}
|
||||
void Component::status_momentary_warning(const char *name, uint32_t length) {
|
||||
|
||||
@@ -89,11 +89,6 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08;
|
||||
inline constexpr uint8_t STATUS_LED_ERROR = 0x10;
|
||||
// Component loop override flag uses bit 5 (set at registration time)
|
||||
inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
|
||||
// Bit 6 on Application::app_state_ (ONLY) — set at the end of
|
||||
// Application::setup(). Component::status_clear_*_slow_path_() uses this to
|
||||
// decide whether to propagate clears to App.app_state_. Never set on a
|
||||
// Component's component_state_.
|
||||
inline constexpr uint8_t APP_STATE_SETUP_COMPLETE = 0x40;
|
||||
// Remove before 2026.8.0
|
||||
enum class RetryResult { DONE, RETRY };
|
||||
|
||||
|
||||
@@ -347,18 +347,17 @@ std::string format_mac_address_pretty(const uint8_t *mac) {
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase.
|
||||
// When separator is set, it is written unconditionally after each byte and the last
|
||||
// one is overwritten with '\0', eliminating the per-byte `i < length - 1` check.
|
||||
// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase
|
||||
static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator,
|
||||
char base) {
|
||||
if (length == 0 || buffer_size == 0) {
|
||||
if (buffer_size > 0)
|
||||
buffer[0] = '\0';
|
||||
if (length == 0) {
|
||||
buffer[0] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
// With separator: total length is 3*length (2*length hex chars, (length-1) separators, 1 null terminator)
|
||||
// Without separator: total length is 2*length + 1 (2*length hex chars, 1 null terminator)
|
||||
uint8_t stride = separator ? 3 : 2;
|
||||
size_t max_bytes = separator ? (buffer_size / 3) : ((buffer_size - 1) / 2);
|
||||
size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride);
|
||||
if (max_bytes == 0) {
|
||||
buffer[0] = '\0';
|
||||
return buffer;
|
||||
@@ -370,30 +369,14 @@ static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t
|
||||
size_t pos = i * stride;
|
||||
buffer[pos] = format_hex_char(data[i] >> 4, base);
|
||||
buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base);
|
||||
if (separator) {
|
||||
if (separator && i < length - 1) {
|
||||
buffer[pos + 2] = separator;
|
||||
}
|
||||
}
|
||||
// With separator: overwrite last separator with '\0'
|
||||
// Without: write '\0' after last hex char
|
||||
buffer[length * stride - (separator ? 1 : 0)] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
char *uint32_to_str_unchecked(char *buf, uint32_t val) {
|
||||
if (val == 0) {
|
||||
*buf++ = '0';
|
||||
return buf;
|
||||
}
|
||||
char *start = buf;
|
||||
while (val > 0) {
|
||||
*buf++ = '0' + (val % 10);
|
||||
val /= 10;
|
||||
}
|
||||
std::reverse(start, buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
|
||||
return format_hex_internal(buffer, buffer_size, data, length, 0, 'a');
|
||||
}
|
||||
|
||||
+3
-18
@@ -1263,13 +1263,13 @@ constexpr uint8_t parse_hex_char(char c) {
|
||||
}
|
||||
|
||||
/// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase)
|
||||
ESPHOME_ALWAYS_INLINE inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; }
|
||||
inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; }
|
||||
|
||||
/// Convert a nibble (0-15) to lowercase hex char
|
||||
ESPHOME_ALWAYS_INLINE inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); }
|
||||
inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); }
|
||||
|
||||
/// Convert a nibble (0-15) to uppercase hex char (used for pretty printing)
|
||||
ESPHOME_ALWAYS_INLINE inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); }
|
||||
inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); }
|
||||
|
||||
/// Write int8 value to buffer without modulo operations.
|
||||
/// Buffer must have at least 4 bytes free. Returns pointer past last char written.
|
||||
@@ -1295,21 +1295,6 @@ inline char *int8_to_str(char *buf, int8_t val) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Minimum buffer size for uint32_to_str: 10 digits + null terminator.
|
||||
static constexpr size_t UINT32_MAX_STR_SIZE = 11;
|
||||
|
||||
/// Write unsigned 32-bit integer to buffer (internal, no size check).
|
||||
/// Buffer must have at least 10 bytes free. Returns pointer past last char written.
|
||||
char *uint32_to_str_unchecked(char *buf, uint32_t val);
|
||||
|
||||
/// Write unsigned 32-bit integer to buffer with compile-time size check.
|
||||
/// Null-terminates the output. Returns number of chars written (excluding null).
|
||||
inline size_t uint32_to_str(std::span<char, UINT32_MAX_STR_SIZE> buf, uint32_t val) {
|
||||
char *end = uint32_to_str_unchecked(buf.data(), val);
|
||||
*end = '\0';
|
||||
return static_cast<size_t>(end - buf.data());
|
||||
}
|
||||
|
||||
/// Format byte array as lowercase hex to buffer (base implementation).
|
||||
char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length);
|
||||
|
||||
|
||||
@@ -739,13 +739,7 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
|
||||
App.set_current_component(item->component);
|
||||
WarnIfComponentBlockingGuard guard{item->component, now};
|
||||
item->callback();
|
||||
uint32_t end = guard.finish();
|
||||
// Feed the watchdog after each scheduled item (both main heap and defer
|
||||
// queue paths go through here). A run of back-to-back callbacks cannot
|
||||
// starve the wdt. The inline fast path is a load + sub + branch — nearly
|
||||
// free when the 3 ms rate limit hasn't elapsed.
|
||||
App.feed_wdt_with_time(end);
|
||||
return end;
|
||||
return guard.finish();
|
||||
}
|
||||
|
||||
// Common implementation for cancel operations - handles locking
|
||||
|
||||
@@ -138,7 +138,7 @@ class Scheduler {
|
||||
// (single-threaded). This is safe because the main loop is the only thread
|
||||
// that reads to_add_ without holding lock_; other threads may read it only
|
||||
// while holding the mutex (e.g. cancel_item_locked_).
|
||||
inline void ESPHOME_ALWAYS_INLINE HOT process_to_add() {
|
||||
inline void HOT process_to_add() {
|
||||
if (this->to_add_empty_())
|
||||
return;
|
||||
this->process_to_add_slow_path_();
|
||||
@@ -302,7 +302,7 @@ class Scheduler {
|
||||
// loop thread structurally modifies items_ (push/pop/erase). Other threads may
|
||||
// iterate items_ and mark items removed under lock_, but never change the
|
||||
// vector's size or data pointer.
|
||||
inline bool ESPHOME_ALWAYS_INLINE HOT cleanup_() {
|
||||
inline bool HOT cleanup_() {
|
||||
if (this->to_remove_empty_())
|
||||
return !this->items_.empty();
|
||||
return this->cleanup_slow_path_();
|
||||
@@ -407,7 +407,7 @@ class Scheduler {
|
||||
// Process defer queue for FIFO execution of deferred items.
|
||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||
// Inlined: the fast path (nothing deferred) is just an atomic load check.
|
||||
inline void ESPHOME_ALWAYS_INLINE HOT process_defer_queue_(uint32_t &now) {
|
||||
inline void HOT process_defer_queue_(uint32_t &now) {
|
||||
// Fast path: nothing to process, avoid lock entirely.
|
||||
// Worst case is a one-loop-iteration delay before newly deferred items are processed.
|
||||
if (this->defer_empty_())
|
||||
|
||||
@@ -113,8 +113,7 @@ def _generate_source_table_code(
|
||||
entries = ", ".join(var_names)
|
||||
lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};")
|
||||
lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{")
|
||||
cond = "index == 0" if count >= 255 else f"index == 0 || index > {count}"
|
||||
lines.append(f' if ({cond}) return LOG_STR("<unknown>");')
|
||||
lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");')
|
||||
lines.append(" return reinterpret_cast<const LogString *>(")
|
||||
lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));")
|
||||
lines.append("}")
|
||||
|
||||
@@ -3,8 +3,6 @@ dependencies:
|
||||
version: "7.4.2"
|
||||
esphome/esp-audio-libs:
|
||||
version: 2.0.4
|
||||
esphome/micro-decoder:
|
||||
version: 0.1.1
|
||||
esphome/micro-flac:
|
||||
version: 0.1.1
|
||||
esphome/micro-opus:
|
||||
@@ -16,7 +14,7 @@ dependencies:
|
||||
espressif/esp32-camera:
|
||||
version: 2.1.6
|
||||
espressif/mdns:
|
||||
version: 1.11.0
|
||||
version: 1.10.0
|
||||
espressif/esp_wifi_remote:
|
||||
version: 1.4.0
|
||||
rules:
|
||||
|
||||
+167
-4
@@ -5,15 +5,157 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.util import run_external_process
|
||||
from esphome.util import run_external_command, run_external_process
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def patch_structhash():
|
||||
# Patch platformio's structhash to not recompile the entire project when files are
|
||||
# removed/added. This might have unintended consequences, but this improves compile
|
||||
# times greatly when adding/removing components and a simple clean build solves
|
||||
# all issues
|
||||
from platformio.run import cli, helpers
|
||||
|
||||
def patched_clean_build_dir(build_dir, *args):
|
||||
from platformio import fs
|
||||
from platformio.project.helpers import get_project_dir
|
||||
|
||||
platformio_ini = Path(get_project_dir()) / "platformio.ini"
|
||||
|
||||
build_dir = Path(build_dir)
|
||||
|
||||
# if project's config is modified
|
||||
if (
|
||||
build_dir.is_dir()
|
||||
and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime
|
||||
):
|
||||
fs.rmtree(build_dir)
|
||||
|
||||
if not build_dir.is_dir():
|
||||
build_dir.mkdir(parents=True)
|
||||
|
||||
helpers.clean_build_dir = patched_clean_build_dir
|
||||
cli.clean_build_dir = patched_clean_build_dir
|
||||
|
||||
|
||||
def patch_file_downloader():
|
||||
"""Patch PlatformIO's FileDownloader to retry on PackageException errors.
|
||||
|
||||
PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry
|
||||
for 502/503 errors. We add retries with exponential backoff and close the
|
||||
session between attempts to force a fresh TCP connection, which may route
|
||||
to a different CDN edge node.
|
||||
"""
|
||||
from platformio.package.download import FileDownloader
|
||||
from platformio.package.exception import PackageException
|
||||
|
||||
if getattr(FileDownloader.__init__, "_esphome_patched", False):
|
||||
return
|
||||
|
||||
original_init = FileDownloader.__init__
|
||||
|
||||
def patched_init(self, *args: Any, **kwargs: Any) -> None:
|
||||
max_retries = 5
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
original_init(self, *args, **kwargs)
|
||||
return
|
||||
except PackageException as e:
|
||||
if attempt < max_retries - 1:
|
||||
# Exponential backoff: 2, 4, 8, 16 seconds
|
||||
delay = 2 ** (attempt + 1)
|
||||
_LOGGER.warning(
|
||||
"Package download failed: %s. "
|
||||
"Retrying in %d seconds... (attempt %d/%d)",
|
||||
str(e),
|
||||
delay,
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
)
|
||||
# Close the response and session to free resources
|
||||
# and force a new TCP connection on retry, which may
|
||||
# route to a different CDN edge node
|
||||
# pylint: disable=protected-access,broad-except
|
||||
try:
|
||||
if (
|
||||
hasattr(self, "_http_response")
|
||||
and self._http_response is not None
|
||||
):
|
||||
self._http_response.close()
|
||||
if hasattr(self, "_http_session"):
|
||||
self._http_session.close()
|
||||
except Exception:
|
||||
pass
|
||||
# pylint: enable=protected-access,broad-except
|
||||
time.sleep(delay)
|
||||
else:
|
||||
# Final attempt - re-raise
|
||||
raise
|
||||
|
||||
patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access
|
||||
FileDownloader.__init__ = patched_init
|
||||
|
||||
|
||||
IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})"
|
||||
FILTER_PLATFORMIO_LINES = [
|
||||
r"Verbose mode can be enabled via `-v, --verbose` option.*",
|
||||
r"CONFIGURATION: https://docs.platformio.org/.*",
|
||||
r"DEBUG: Current.*",
|
||||
r"LDF Modes:.*",
|
||||
r"LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf.*",
|
||||
f"Looking for {IGNORE_LIB_WARNINGS} library in registry",
|
||||
f"Warning! Library `.*'{IGNORE_LIB_WARNINGS}.*` has not been found in PlatformIO Registry.",
|
||||
f"You can ignore this message, if `.*{IGNORE_LIB_WARNINGS}.*` is a built-in library.*",
|
||||
r"Scanning dependencies...",
|
||||
r"Found \d+ compatible libraries",
|
||||
r"Memory Usage -> https://bit.ly/pio-memory-usage",
|
||||
r"Found: https://platformio.org/lib/show/.*",
|
||||
r"Using cache: .*",
|
||||
r"Installing dependencies",
|
||||
r"Library Manager: Already installed, built-in library",
|
||||
r"Building in .* mode",
|
||||
r"Advanced Memory Usage is available via .*",
|
||||
r"Merged .* ELF section",
|
||||
r"esptool.py v.*",
|
||||
r"esptool v.*",
|
||||
r"Checking size .*",
|
||||
r"Retrieving maximum program size .*",
|
||||
r"PLATFORM: .*",
|
||||
r"PACKAGES:.*",
|
||||
r" - framework-arduinoespressif.* \(.*\)",
|
||||
r" - tool-esptool.* \(.*\)",
|
||||
r" - toolchain-.* \(.*\)",
|
||||
r"Creating BIN file .*",
|
||||
r"Warning! Could not find file \".*.crt\"",
|
||||
r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.",
|
||||
r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.",
|
||||
r"Warning: esp-idf-size exited with code 2",
|
||||
r"esp_idf_size: error: unrecognized arguments: --ng",
|
||||
r"Package configuration completed successfully",
|
||||
]
|
||||
|
||||
|
||||
class PlatformioLogFilter(logging.Filter):
|
||||
"""Filter to suppress noisy platformio log messages."""
|
||||
|
||||
_PATTERN = re.compile(
|
||||
r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES)
|
||||
)
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
# Only filter messages from platformio-related loggers
|
||||
if "platformio" not in record.name.lower():
|
||||
return True
|
||||
return self._PATTERN.match(record.getMessage()) is None
|
||||
|
||||
|
||||
def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
|
||||
os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute())
|
||||
@@ -24,9 +166,30 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
|
||||
# Increase uv retry count to handle transient network errors (default is 3)
|
||||
os.environ.setdefault("UV_HTTP_RETRIES", "10")
|
||||
cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args)
|
||||
cmd = ["platformio"] + list(args)
|
||||
|
||||
return run_external_process(*cmd, **kwargs)
|
||||
if not CORE.verbose:
|
||||
kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES
|
||||
|
||||
if os.environ.get("ESPHOME_USE_SUBPROCESS") is not None:
|
||||
return run_external_process(*cmd, **kwargs)
|
||||
|
||||
import platformio.__main__
|
||||
|
||||
patch_structhash()
|
||||
patch_file_downloader()
|
||||
|
||||
# Add log filter to suppress noisy platformio messages
|
||||
log_filter = PlatformioLogFilter() if not CORE.verbose else None
|
||||
if log_filter:
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.addFilter(log_filter)
|
||||
try:
|
||||
return run_external_command(platformio.__main__.main, *cmd, **kwargs)
|
||||
finally:
|
||||
if log_filter:
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.removeFilter(log_filter)
|
||||
|
||||
|
||||
def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int:
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
"""Subprocess entry point that applies ESPHome's PlatformIO patches.
|
||||
|
||||
Invoked via ``python -m esphome.platformio_runner`` instead of
|
||||
``python -m platformio`` so that the patches (incremental rebuild
|
||||
preservation, download retries) apply inside the subprocess. Running
|
||||
PlatformIO in a subprocess keeps its ``sys.path`` mutations and other
|
||||
global state from leaking into the ESPHome process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def patch_structhash() -> None:
|
||||
"""Avoid full rebuilds when files are added or removed.
|
||||
|
||||
PlatformIO clears the build dir whenever its structure hash changes.
|
||||
We replace that with an mtime check against ``platformio.ini`` so
|
||||
incremental builds are preserved unless the project config changed.
|
||||
"""
|
||||
from platformio.run import cli, helpers
|
||||
|
||||
def patched_clean_build_dir(build_dir, *_args):
|
||||
from platformio import fs
|
||||
from platformio.project.helpers import get_project_dir
|
||||
|
||||
platformio_ini = Path(get_project_dir()) / "platformio.ini"
|
||||
build_dir = Path(build_dir)
|
||||
|
||||
if (
|
||||
build_dir.is_dir()
|
||||
and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime
|
||||
):
|
||||
fs.rmtree(build_dir)
|
||||
|
||||
if not build_dir.is_dir():
|
||||
build_dir.mkdir(parents=True)
|
||||
|
||||
helpers.clean_build_dir = patched_clean_build_dir
|
||||
cli.clean_build_dir = patched_clean_build_dir
|
||||
|
||||
|
||||
def patch_file_downloader() -> None:
|
||||
"""Retry PlatformIO package downloads with exponential backoff.
|
||||
|
||||
PlatformIO's ``FileDownloader`` uses an ``HTTPSession`` without built-in
|
||||
retry for 502/503 errors. We wrap ``__init__`` to retry on
|
||||
``PackageException`` and close the session between attempts so a new
|
||||
TCP connection can route to a different CDN edge node.
|
||||
"""
|
||||
from platformio.package.download import FileDownloader
|
||||
from platformio.package.exception import PackageException
|
||||
|
||||
if getattr(FileDownloader.__init__, "_esphome_patched", False):
|
||||
return
|
||||
|
||||
original_init = FileDownloader.__init__
|
||||
|
||||
def patched_init(self, *args: Any, **kwargs: Any) -> None:
|
||||
max_retries = 5
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
original_init(self, *args, **kwargs)
|
||||
return
|
||||
except PackageException as e:
|
||||
if attempt < max_retries - 1:
|
||||
delay = 2 ** (attempt + 1)
|
||||
_LOGGER.warning(
|
||||
"Package download failed: %s. "
|
||||
"Retrying in %d seconds... (attempt %d/%d)",
|
||||
str(e),
|
||||
delay,
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
)
|
||||
# pylint: disable=protected-access,broad-except
|
||||
try:
|
||||
if (
|
||||
hasattr(self, "_http_response")
|
||||
and self._http_response is not None
|
||||
):
|
||||
self._http_response.close()
|
||||
if hasattr(self, "_http_session"):
|
||||
self._http_session.close()
|
||||
except Exception:
|
||||
pass
|
||||
# pylint: enable=protected-access,broad-except
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access
|
||||
FileDownloader.__init__ = patched_init
|
||||
|
||||
|
||||
_IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})"
|
||||
# Regex patterns matched against each line of PlatformIO output. Lines that
|
||||
# match are dropped by RedirectText before they reach the parent process.
|
||||
# Patterns are anchored at the start of the line (RedirectText uses
|
||||
# ``re.match``). Disabled when the user passes ``-v`` / ``--verbose`` to
|
||||
# ``esphome compile``.
|
||||
FILTER_PLATFORMIO_LINES = [
|
||||
r"Verbose mode can be enabled via `-v, --verbose` option.*",
|
||||
r"CONFIGURATION: https://docs.platformio.org/.*",
|
||||
r"DEBUG: Current.*",
|
||||
r"LDF Modes:.*",
|
||||
r"LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf.*",
|
||||
f"Looking for {_IGNORE_LIB_WARNINGS} library in registry",
|
||||
f"Warning! Library `.*'{_IGNORE_LIB_WARNINGS}.*` has not been found in PlatformIO Registry.",
|
||||
f"You can ignore this message, if `.*{_IGNORE_LIB_WARNINGS}.*` is a built-in library.*",
|
||||
r"Scanning dependencies...",
|
||||
r"Found \d+ compatible libraries",
|
||||
r"Memory Usage -> https://bit.ly/pio-memory-usage",
|
||||
r"Found: https://platformio.org/lib/show/.*",
|
||||
r"Using cache: .*",
|
||||
r"Installing dependencies",
|
||||
r"Library Manager: Already installed, built-in library",
|
||||
r"Building in .* mode",
|
||||
r"Advanced Memory Usage is available via .*",
|
||||
r"Merged .* ELF section",
|
||||
r"esptool.py v.*",
|
||||
r"esptool v.*",
|
||||
r"Checking size .*",
|
||||
r"Retrieving maximum program size .*",
|
||||
r"PLATFORM: .*",
|
||||
r"PACKAGES:.*",
|
||||
r" - framework-arduinoespressif.* \(.*\)",
|
||||
r" - tool-esptool.* \(.*\)",
|
||||
r" - toolchain-.* \(.*\)",
|
||||
r"Creating BIN file .*",
|
||||
r"Warning! Could not find file \".*.crt\"",
|
||||
r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.",
|
||||
r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.",
|
||||
r"Warning: esp-idf-size exited with code 2",
|
||||
r"esp_idf_size: error: unrecognized arguments: --ng",
|
||||
r"Package configuration completed successfully",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
patch_structhash()
|
||||
patch_file_downloader()
|
||||
|
||||
# Wrap stdout/stderr with RedirectText before PlatformIO runs:
|
||||
#
|
||||
# 1. RedirectText.isatty() unconditionally returns True. Click, tqdm, and
|
||||
# PlatformIO's own progress-bar code check ``stream.isatty()`` to
|
||||
# decide whether to emit TTY-format output (``\r`` cursor moves, ANSI
|
||||
# colors, fancy progress bars). With the wrapper in place they always
|
||||
# emit TTY format, even when our real stdout is a pipe to the parent
|
||||
# process. Downstream consumers (local terminals and the Home
|
||||
# Assistant dashboard log viewer) render the TTY control sequences
|
||||
# correctly, so the user sees real progress bars.
|
||||
#
|
||||
# 2. FILTER_PLATFORMIO_LINES is applied inside RedirectText.write() in
|
||||
# this subprocess, so noisy PlatformIO output is dropped before it
|
||||
# ever leaves the runner. This replaces the parent-side filtering
|
||||
# that was lost when we switched from in-process to subprocess — the
|
||||
# parent's ``subprocess.run`` uses ``.fileno()`` on RedirectText and
|
||||
# bypasses its ``write()`` path entirely.
|
||||
#
|
||||
# Filtering is disabled when the user passed -v / --verbose to
|
||||
# ``esphome compile``, preserving the previous in-process behavior where
|
||||
# verbose mode let all PlatformIO output through unfiltered.
|
||||
from esphome.util import RedirectText
|
||||
|
||||
is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[1:])
|
||||
filter_lines = None if is_verbose else FILTER_PLATFORMIO_LINES
|
||||
|
||||
sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
|
||||
sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
|
||||
|
||||
import platformio.__main__
|
||||
|
||||
return platformio.__main__.main() or 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+7
-7
@@ -83,7 +83,7 @@ lib_deps =
|
||||
fastled/FastLED@3.9.16 ; fastled_base
|
||||
freekode/TM1651@1.0.1 ; tm1651
|
||||
dudanov/MideaUART@1.1.9 ; midea
|
||||
tonia/HeatpumpIR@1.0.41 ; heatpumpir
|
||||
tonia/HeatpumpIR@1.0.40 ; heatpumpir
|
||||
build_flags =
|
||||
${common.build_flags}
|
||||
-DUSE_ARDUINO
|
||||
@@ -133,10 +133,10 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||
[common:esp32-arduino]
|
||||
extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz
|
||||
|
||||
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||
lib_deps =
|
||||
@@ -169,16 +169,16 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
[common:esp32-idf]
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz
|
||||
|
||||
framework = espidf
|
||||
lib_deps =
|
||||
${common:idf.lib_deps}
|
||||
droscy/esp_wireguard@0.4.4 ; wireguard
|
||||
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
|
||||
tonia/HeatpumpIR@1.0.41 ; heatpumpir
|
||||
tonia/HeatpumpIR@1.0.40 ; heatpumpir
|
||||
build_flags =
|
||||
${common:idf.build_flags}
|
||||
-Wno-nonnull-compare
|
||||
|
||||
@@ -20,6 +20,7 @@ classifiers = [
|
||||
"Topic :: Home Automation",
|
||||
]
|
||||
|
||||
# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76
|
||||
requires-python = ">=3.11.0,<3.15"
|
||||
|
||||
dynamic = ["dependencies", "optional-dependencies", "version"]
|
||||
|
||||
+2
-2
@@ -12,14 +12,14 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.2
|
||||
esphome-dashboard==20260408.1
|
||||
aioesphomeapi==44.15.0
|
||||
aioesphomeapi==44.13.2
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==12.2.0
|
||||
resvg-py==0.3.1
|
||||
resvg-py==0.3.0
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
|
||||
@@ -1028,8 +1028,7 @@ class BytesType(TypeInfo):
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
calc_fn = "calc_length_force" if force else "calc_length"
|
||||
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
|
||||
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
|
||||
|
||||
def get_estimated_size(self) -> int:
|
||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
||||
@@ -1110,8 +1109,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
calc_fn = "calc_length_force" if force else "calc_length"
|
||||
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len);"
|
||||
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);"
|
||||
|
||||
|
||||
class PointerToStringBufferType(PointerToBufferTypeBase):
|
||||
@@ -2681,16 +2679,6 @@ def build_message_type(
|
||||
and get_opt(desc, inline_opt, False)
|
||||
)
|
||||
|
||||
# Check if this message wants speed-optimized encode/calculate_size.
|
||||
# When set, __attribute__((optimize("O2"))) is added to the definitions
|
||||
# so GCC inlines the small ProtoEncode helpers even under -Os.
|
||||
is_speed_optimized = get_opt(desc, pb.speed_optimized, False)
|
||||
speed_attr = (
|
||||
'__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n'
|
||||
if is_speed_optimized
|
||||
else ""
|
||||
)
|
||||
|
||||
# Only generate encode method if this message needs encoding and has fields
|
||||
if needs_encode and encode and not is_inline_only:
|
||||
# Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls
|
||||
@@ -2700,7 +2688,7 @@ def build_message_type(
|
||||
)
|
||||
for line in encode
|
||||
]
|
||||
o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
|
||||
o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
|
||||
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
|
||||
o += indent("\n".join(encode_debug)) + "\n"
|
||||
o += " return pos;\n"
|
||||
@@ -2714,7 +2702,7 @@ def build_message_type(
|
||||
|
||||
# Add calculate_size method only if this message needs encoding and has fields
|
||||
if needs_encode and size_calc and not is_inline_only:
|
||||
o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n"
|
||||
o = f"uint32_t {desc.name}::calculate_size() const {{\n"
|
||||
o += " uint32_t size = 0;\n"
|
||||
o += indent("\n".join(size_calc)) + "\n"
|
||||
o += " return size;\n"
|
||||
|
||||
@@ -26,11 +26,12 @@ CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core"
|
||||
STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs"
|
||||
|
||||
PLATFORMIO_OPTIONS = {
|
||||
"build_unflags": [
|
||||
"-Os", # remove default size-opt
|
||||
],
|
||||
"build_flags": [
|
||||
"-Os", # match firmware optimization level (detects inlining regressions)
|
||||
"-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo)
|
||||
"-g", # debug symbols for profiling
|
||||
"-ffunction-sections", # required for dead-code stripping with -Os
|
||||
"-fdata-sections", # required for dead-code stripping with -Os
|
||||
"-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish()
|
||||
f"-I{STUBS_DIR}", # stub headers for ESP32-only components
|
||||
],
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Typical log line: "[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy"
|
||||
static constexpr const char *kTypicalLogLine =
|
||||
"[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy";
|
||||
|
||||
// Short log line: "[12:34:56][I][app:029]: Running..."
|
||||
static constexpr const char *kShortLogLine = "[12:34:56][I][app:029]: Running...";
|
||||
|
||||
// --- Encode ---
|
||||
|
||||
static void Encode_LogResponse_Typical(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
SubscribeLogsResponse msg;
|
||||
msg.level = enums::LOG_LEVEL_DEBUG;
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_LogResponse_Typical);
|
||||
|
||||
static void Encode_LogResponse_Short(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
SubscribeLogsResponse msg;
|
||||
msg.level = enums::LOG_LEVEL_INFO;
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(kShortLogLine), strlen(kShortLogLine));
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_LogResponse_Short);
|
||||
|
||||
// --- Calculate Size ---
|
||||
|
||||
static void CalculateSize_LogResponse_Typical(benchmark::State &state) {
|
||||
SubscribeLogsResponse msg;
|
||||
msg.level = enums::LOG_LEVEL_DEBUG;
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_LogResponse_Typical);
|
||||
|
||||
// --- Calc + Encode (steady state) ---
|
||||
|
||||
static void CalcAndEncode_LogResponse_Typical(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
SubscribeLogsResponse msg;
|
||||
msg.level = enums::LOG_LEVEL_DEBUG;
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_LogResponse_Typical);
|
||||
|
||||
// --- Calc + Encode (fresh allocation each time) ---
|
||||
|
||||
static void CalcAndEncode_LogResponse_Typical_Fresh(benchmark::State &state) {
|
||||
SubscribeLogsResponse msg;
|
||||
msg.level = enums::LOG_LEVEL_DEBUG;
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_LogResponse_Typical_Fresh);
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
@@ -1,6 +1,4 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -309,58 +307,4 @@ static void Base64Decode_32Bytes(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(Base64Decode_32Bytes);
|
||||
|
||||
// --- uint32_to_str() vs snprintf ---
|
||||
|
||||
static void Uint32ToStr_Small(benchmark::State &state) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_to_str(buf, 12345);
|
||||
benchmark::DoNotOptimize(buf);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Uint32ToStr_Small);
|
||||
|
||||
static void Snprintf_Uint32_Small(benchmark::State &state) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(12345));
|
||||
benchmark::DoNotOptimize(buf);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Snprintf_Uint32_Small);
|
||||
|
||||
static void Uint32ToStr_Large(benchmark::State &state) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_to_str(buf, 4294967295u);
|
||||
benchmark::DoNotOptimize(buf);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Uint32ToStr_Large);
|
||||
|
||||
static void Snprintf_Uint32_Large(benchmark::State &state) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(4294967295u));
|
||||
benchmark::DoNotOptimize(buf);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Snprintf_Uint32_Large);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
"""Tests for the packages component."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.packages import (
|
||||
CONFIG_SCHEMA,
|
||||
_walk_packages,
|
||||
do_packages_pass,
|
||||
is_package_definition,
|
||||
merge_packages,
|
||||
)
|
||||
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages
|
||||
from esphome.components.substitutions import do_substitution_pass
|
||||
import esphome.config as config_module
|
||||
from esphome.config import resolve_extend_remove
|
||||
@@ -44,7 +37,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.util import OrderedDict
|
||||
from esphome.yaml_util import IncludeFile, add_context
|
||||
from esphome.yaml_util import add_context
|
||||
|
||||
# Test strings
|
||||
TEST_DEVICE_NAME = "test_device_name"
|
||||
@@ -86,44 +79,6 @@ def packages_pass(config):
|
||||
return config
|
||||
|
||||
|
||||
_INCLUDE_FILE = "INCLUDE_FILE"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
# IncludeFile objects are package definitions
|
||||
(_INCLUDE_FILE, True),
|
||||
# Git URL shorthand strings are package definitions
|
||||
("github://esphome/firmware/base.yaml@main", True),
|
||||
# Remote package dicts (with url key) are package definitions
|
||||
({"url": "https://github.com/esphome/firmware", "file": "base.yaml"}, True),
|
||||
# Plain config dicts are NOT package definitions (they are config fragments)
|
||||
({"wifi": {"ssid": "test"}}, False),
|
||||
# None is not a package definition
|
||||
(None, False),
|
||||
# Lists are not package definitions
|
||||
([{"wifi": {"ssid": "test"}}], False),
|
||||
# Empty dicts are not package definitions
|
||||
({}, False),
|
||||
],
|
||||
ids=[
|
||||
"include_file",
|
||||
"git_shorthand",
|
||||
"remote_package",
|
||||
"config_fragment",
|
||||
"none",
|
||||
"list",
|
||||
"empty_dict",
|
||||
],
|
||||
)
|
||||
def test_is_package_definition(value: object, expected: bool) -> None:
|
||||
"""Test that is_package_definition correctly identifies package definitions."""
|
||||
if value is _INCLUDE_FILE:
|
||||
value = MagicMock(spec=IncludeFile)
|
||||
assert is_package_definition(value) is expected
|
||||
|
||||
|
||||
def test_package_unused(basic_esphome, basic_wifi) -> None:
|
||||
"""
|
||||
Ensures do_package_pass does not change a config if packages aren't used.
|
||||
@@ -1106,51 +1061,6 @@ def test_packages_invalid_type_raises() -> None:
|
||||
do_packages_pass(config)
|
||||
|
||||
|
||||
@patch("esphome.components.packages.resolve_include")
|
||||
def test_packages_include_file_resolves_to_list(mock_resolve_include) -> None:
|
||||
"""When packages: is an IncludeFile that resolves to a list, it is processed correctly."""
|
||||
include_file = MagicMock(spec=IncludeFile)
|
||||
package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}}
|
||||
mock_resolve_include.return_value = ([package_content], None)
|
||||
|
||||
config = {CONF_PACKAGES: include_file}
|
||||
result = do_packages_pass(config)
|
||||
result = merge_packages(result)
|
||||
|
||||
assert result == {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}}
|
||||
|
||||
|
||||
@patch("esphome.components.packages.resolve_include")
|
||||
def test_packages_include_file_resolves_to_dict(mock_resolve_include) -> None:
|
||||
"""When packages: is an IncludeFile that resolves to a dict, it is processed correctly."""
|
||||
include_file = MagicMock(spec=IncludeFile)
|
||||
package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}}
|
||||
mock_resolve_include.return_value = ({"network": package_content}, None)
|
||||
|
||||
config = {CONF_PACKAGES: include_file}
|
||||
result = do_packages_pass(config)
|
||||
result = merge_packages(result)
|
||||
|
||||
assert result == {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}}
|
||||
|
||||
|
||||
@patch("esphome.components.packages.resolve_include")
|
||||
def test_packages_include_file_resolves_to_invalid_type_raises(
|
||||
mock_resolve_include,
|
||||
) -> None:
|
||||
"""When packages: is an IncludeFile that resolves to an invalid type, cv.Invalid is raised."""
|
||||
include_file = MagicMock(spec=IncludeFile)
|
||||
mock_resolve_include.return_value = ("not_a_dict_or_list", None)
|
||||
|
||||
config = {CONF_PACKAGES: include_file}
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="Packages must be a key to value mapping or list"
|
||||
) as exc_info:
|
||||
do_packages_pass(config)
|
||||
|
||||
assert exc_info.value.path == [CONF_PACKAGES]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_package",
|
||||
[
|
||||
@@ -1197,134 +1107,6 @@ def test_invalid_package_contents_masked_by_deprecation(
|
||||
do_packages_pass(config)
|
||||
|
||||
|
||||
def test_named_dict_with_include_files_no_false_deprecation_warning(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Package errors in named dicts must not trigger the deprecated fallback."""
|
||||
good_include = MagicMock(spec=IncludeFile)
|
||||
bad_include = MagicMock(spec=IncludeFile)
|
||||
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"good_pkg": good_include,
|
||||
"bad_pkg": bad_include,
|
||||
},
|
||||
}
|
||||
|
||||
call_count = 0
|
||||
|
||||
def failing_callback(package_config: dict, context: object) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
# First package processes fine
|
||||
return {CONF_WIFI: {CONF_SSID: "test"}}
|
||||
# Second package has an error (e.g. jinja syntax error)
|
||||
raise cv.Invalid("simulated jinja error in bad_pkg")
|
||||
|
||||
with (
|
||||
caplog.at_level(logging.WARNING),
|
||||
pytest.raises(cv.Invalid, match="simulated jinja error"),
|
||||
):
|
||||
_walk_packages(config, failing_callback)
|
||||
|
||||
# Must NOT emit the deprecated single-package warning
|
||||
assert "deprecated" not in caplog.text.lower()
|
||||
|
||||
|
||||
def test_validate_deprecated_false_raises_directly(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""With validate_deprecated=False, errors raise directly without fallback.
|
||||
|
||||
This is the codepath used for remote packages where _process_remote_package
|
||||
returns already-resolved dicts that is_package_definition cannot detect.
|
||||
"""
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"pkg_a": {CONF_WIFI: {CONF_SSID: "test"}},
|
||||
"pkg_b": {CONF_WIFI: {CONF_SSID: "test2"}},
|
||||
},
|
||||
}
|
||||
|
||||
call_count = 0
|
||||
|
||||
def failing_callback(package_config: dict, context: object) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
return package_config
|
||||
raise cv.Invalid("nested error")
|
||||
|
||||
with (
|
||||
caplog.at_level(logging.WARNING),
|
||||
pytest.raises(cv.Invalid, match="nested error"),
|
||||
):
|
||||
_walk_packages(config, failing_callback, validate_deprecated=False)
|
||||
|
||||
assert "deprecated" not in caplog.text.lower()
|
||||
|
||||
|
||||
def test_error_on_first_declared_package_still_detected() -> None:
|
||||
"""When the first declared package errors, it's the last processed in reverse.
|
||||
|
||||
All other entries are already resolved to dicts, but the failing entry
|
||||
retains its original IncludeFile value since assignment was skipped.
|
||||
"""
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"first_pkg": MagicMock(spec=IncludeFile),
|
||||
"second_pkg": MagicMock(spec=IncludeFile),
|
||||
"third_pkg": MagicMock(spec=IncludeFile),
|
||||
},
|
||||
}
|
||||
|
||||
call_count = 0
|
||||
|
||||
def fail_on_last(package_config: dict, context: object) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
# Reverse iteration: third_pkg (1), second_pkg (2), first_pkg (3)
|
||||
if call_count < 3:
|
||||
return {CONF_WIFI: {CONF_SSID: "test"}}
|
||||
raise cv.Invalid("error in first_pkg")
|
||||
|
||||
with pytest.raises(cv.Invalid, match="error in first_pkg"):
|
||||
_walk_packages(config, fail_on_last)
|
||||
|
||||
|
||||
def test_deprecated_single_package_fallback_still_works(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""The deprecated single-package form still falls back at the top level.
|
||||
|
||||
When a dict's values are plain config fragments (not package definitions)
|
||||
and the callback fails, the deprecated fallback wraps the dict in a list
|
||||
and retries with a deprecation warning.
|
||||
"""
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
CONF_WIFI: {CONF_SSID: "test", CONF_PASSWORD: "secret"},
|
||||
},
|
||||
}
|
||||
|
||||
attempt = 0
|
||||
|
||||
def fail_then_succeed(package_config: dict, context: object) -> dict:
|
||||
nonlocal attempt
|
||||
attempt += 1
|
||||
if attempt == 1:
|
||||
# First attempt: treating as named dict fails
|
||||
raise cv.Invalid("not a valid package")
|
||||
# Second attempt: after fallback wraps as list, succeeds
|
||||
return package_config
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_walk_packages(config, fail_then_succeed)
|
||||
|
||||
assert "deprecated" in caplog.text.lower()
|
||||
|
||||
|
||||
def test_merge_packages_invalid_nested_type_raises() -> None:
|
||||
"""Invalid nested packages type during merge raises cv.Invalid."""
|
||||
config = {
|
||||
|
||||
@@ -50,13 +50,6 @@ button:
|
||||
- platform: template
|
||||
name: Canbus Actions
|
||||
on_press:
|
||||
- canbus.send:
|
||||
can_id: 0x601
|
||||
data: [0, 1, 2]
|
||||
- canbus.send:
|
||||
can_id: 0x1FFFFFFF
|
||||
use_extended_id: true
|
||||
data: [0, 1, 2]
|
||||
- canbus.send: "abc"
|
||||
- canbus.send: [0, 1, 2]
|
||||
- canbus.send: !lambda return {0, 1, 2};
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
// --- format_hex_to() ---
|
||||
|
||||
TEST(FormatHexTo, Basic) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF};
|
||||
char buffer[7]; // 3 * 2 + 1
|
||||
format_hex_to(buffer, data, 3);
|
||||
EXPECT_STREQ(buffer, "abcdef");
|
||||
}
|
||||
|
||||
TEST(FormatHexTo, SingleByte) {
|
||||
const uint8_t data[] = {0x0F};
|
||||
char buffer[3];
|
||||
format_hex_to(buffer, data, 1);
|
||||
EXPECT_STREQ(buffer, "0f");
|
||||
}
|
||||
|
||||
TEST(FormatHexTo, ZeroLength) {
|
||||
char buffer[4] = "xxx";
|
||||
format_hex_to(buffer, static_cast<size_t>(sizeof(buffer)), static_cast<const uint8_t *>(nullptr), 0);
|
||||
EXPECT_STREQ(buffer, "");
|
||||
}
|
||||
|
||||
TEST(FormatHexTo, ZeroBufferSize) {
|
||||
char buffer[4] = "xxx";
|
||||
const uint8_t data[] = {0xAB};
|
||||
format_hex_to(buffer, static_cast<size_t>(0), data, 1);
|
||||
// Should not crash, buffer unchanged
|
||||
EXPECT_EQ(buffer[0], 'x');
|
||||
}
|
||||
|
||||
TEST(FormatHexTo, BufferTooSmall) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF};
|
||||
char buffer[5]; // only room for 2 bytes
|
||||
format_hex_to(buffer, data, 3);
|
||||
EXPECT_STREQ(buffer, "abcd");
|
||||
}
|
||||
|
||||
TEST(FormatHexTo, MacAddress) {
|
||||
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
|
||||
char buffer[13];
|
||||
format_hex_to(buffer, mac, 6);
|
||||
EXPECT_STREQ(buffer, "aabbccddeeff");
|
||||
}
|
||||
|
||||
// --- format_hex_pretty_to() ---
|
||||
|
||||
TEST(FormatHexPrettyTo, BasicColon) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF};
|
||||
char buffer[9]; // 3 * 3
|
||||
format_hex_pretty_to(buffer, data, 3);
|
||||
EXPECT_STREQ(buffer, "AB:CD:EF");
|
||||
}
|
||||
|
||||
TEST(FormatHexPrettyTo, SingleByte) {
|
||||
const uint8_t data[] = {0x0F};
|
||||
char buffer[3];
|
||||
format_hex_pretty_to(buffer, data, 1);
|
||||
EXPECT_STREQ(buffer, "0F");
|
||||
}
|
||||
|
||||
TEST(FormatHexPrettyTo, ZeroLength) {
|
||||
char buffer[4] = "xxx";
|
||||
format_hex_pretty_to(buffer, static_cast<size_t>(sizeof(buffer)), static_cast<const uint8_t *>(nullptr), 0);
|
||||
EXPECT_STREQ(buffer, "");
|
||||
}
|
||||
|
||||
TEST(FormatHexPrettyTo, ZeroBufferSize) {
|
||||
char buffer[4] = "xxx";
|
||||
const uint8_t data[] = {0xAB};
|
||||
format_hex_pretty_to(buffer, static_cast<size_t>(0), data, 1);
|
||||
EXPECT_EQ(buffer[0], 'x');
|
||||
}
|
||||
|
||||
TEST(FormatHexPrettyTo, CustomSeparator) {
|
||||
const uint8_t data[] = {0xAA, 0xBB, 0xCC};
|
||||
char buffer[9];
|
||||
format_hex_pretty_to(buffer, data, 3, '-');
|
||||
EXPECT_STREQ(buffer, "AA-BB-CC");
|
||||
}
|
||||
|
||||
// --- format_mac_addr_upper() ---
|
||||
|
||||
TEST(FormatMacAddrUpper, Basic) {
|
||||
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(mac, buffer);
|
||||
EXPECT_STREQ(buffer, "AA:BB:CC:DD:EE:FF");
|
||||
}
|
||||
|
||||
TEST(FormatMacAddrUpper, AllZeros) {
|
||||
const uint8_t mac[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(mac, buffer);
|
||||
EXPECT_STREQ(buffer, "00:00:00:00:00:00");
|
||||
}
|
||||
|
||||
// --- format_hex_char() ---
|
||||
|
||||
TEST(FormatHexChar, LowercaseDigits) {
|
||||
EXPECT_EQ(format_hex_char(0), '0');
|
||||
EXPECT_EQ(format_hex_char(9), '9');
|
||||
EXPECT_EQ(format_hex_char(10), 'a');
|
||||
EXPECT_EQ(format_hex_char(15), 'f');
|
||||
}
|
||||
|
||||
TEST(FormatHexChar, UppercaseDigits) {
|
||||
EXPECT_EQ(format_hex_pretty_char(0), '0');
|
||||
EXPECT_EQ(format_hex_pretty_char(9), '9');
|
||||
EXPECT_EQ(format_hex_pretty_char(10), 'A');
|
||||
EXPECT_EQ(format_hex_pretty_char(15), 'F');
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -1,77 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
// --- uint32_to_str_unchecked() (internal, raw pointer) ---
|
||||
|
||||
TEST(Uint32ToStr, InternalZero) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
char *end = uint32_to_str_unchecked(buf, 0);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "0");
|
||||
EXPECT_EQ(end - buf, 1);
|
||||
}
|
||||
|
||||
TEST(Uint32ToStr, InternalSingleDigit) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
char *end = uint32_to_str_unchecked(buf, 7);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "7");
|
||||
}
|
||||
|
||||
TEST(Uint32ToStr, InternalMultiDigit) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
char *end = uint32_to_str_unchecked(buf, 12345);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "12345");
|
||||
EXPECT_EQ(end - buf, 5);
|
||||
}
|
||||
|
||||
TEST(Uint32ToStr, InternalMaxValue) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
char *end = uint32_to_str_unchecked(buf, 4294967295u);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "4294967295");
|
||||
EXPECT_EQ(end - buf, 10);
|
||||
}
|
||||
|
||||
TEST(Uint32ToStr, InternalPowersOfTen) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
char *end;
|
||||
|
||||
end = uint32_to_str_unchecked(buf, 10);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "10");
|
||||
|
||||
end = uint32_to_str_unchecked(buf, 100);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "100");
|
||||
|
||||
end = uint32_to_str_unchecked(buf, 1000000);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "1000000");
|
||||
}
|
||||
|
||||
// --- uint32_to_str() (public, span API) ---
|
||||
|
||||
TEST(Uint32ToStr, SpanZero) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
EXPECT_EQ(uint32_to_str(buf, 0), 1u);
|
||||
EXPECT_STREQ(buf, "0");
|
||||
}
|
||||
|
||||
TEST(Uint32ToStr, SpanMultiDigit) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
EXPECT_EQ(uint32_to_str(buf, 12345), 5u);
|
||||
EXPECT_STREQ(buf, "12345");
|
||||
}
|
||||
|
||||
TEST(Uint32ToStr, SpanMaxValue) {
|
||||
char buf[UINT32_MAX_STR_SIZE];
|
||||
EXPECT_EQ(uint32_to_str(buf, 4294967295u), 10u);
|
||||
EXPECT_STREQ(buf, "4294967295");
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -4,14 +4,6 @@ esphome:
|
||||
- globals.set:
|
||||
id: glob_int
|
||||
value: "10"
|
||||
# Set a float global with an integer literal - must emit the correct
|
||||
# return type so TemplatableFn stores a direct function pointer.
|
||||
- globals.set:
|
||||
id: glob_float
|
||||
value: "102"
|
||||
- globals.set:
|
||||
id: glob_float
|
||||
value: !lambda "return 42;"
|
||||
|
||||
globals:
|
||||
- id: glob_int
|
||||
|
||||
@@ -20,7 +20,6 @@ lvgl:
|
||||
- id: lvgl_0
|
||||
default_font: space16
|
||||
displays: sdl0
|
||||
rotation: 180
|
||||
top_layer:
|
||||
|
||||
- id: lvgl_1
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
esphome:
|
||||
name: status-flags-test
|
||||
|
||||
host:
|
||||
api:
|
||||
actions:
|
||||
# Warning flag services for sensor_a
|
||||
- action: set_warning_a
|
||||
then:
|
||||
- lambda: "id(sensor_a)->status_set_warning();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
- action: clear_warning_a
|
||||
then:
|
||||
- lambda: "id(sensor_a)->status_clear_warning();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
|
||||
# Warning flag services for sensor_b
|
||||
- action: set_warning_b
|
||||
then:
|
||||
- lambda: "id(sensor_b)->status_set_warning();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
- action: clear_warning_b
|
||||
then:
|
||||
- lambda: "id(sensor_b)->status_clear_warning();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
|
||||
# Error flag services for sensor_a
|
||||
- action: set_error_a
|
||||
then:
|
||||
- lambda: "id(sensor_a)->status_set_error();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
- action: clear_error_a
|
||||
then:
|
||||
- lambda: "id(sensor_a)->status_clear_error();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
|
||||
# Error flag services for sensor_b
|
||||
- action: set_error_b
|
||||
then:
|
||||
- lambda: "id(sensor_b)->status_set_error();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
- action: clear_error_b
|
||||
then:
|
||||
- lambda: "id(sensor_b)->status_clear_error();"
|
||||
- component.update: app_warning_bit
|
||||
- component.update: app_error_bit
|
||||
|
||||
# Snapshot of the status_led_light's output state for observation.
|
||||
- action: snapshot_led
|
||||
then:
|
||||
- component.update: status_led_writes
|
||||
- component.update: status_led_last_state
|
||||
|
||||
logger:
|
||||
|
||||
# Tracks each write to the fake status_led output.
|
||||
globals:
|
||||
- id: status_led_write_count
|
||||
type: uint32_t
|
||||
restore_value: no
|
||||
initial_value: "0"
|
||||
- id: status_led_last_write
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: "false"
|
||||
|
||||
# Fake binary output — status_led_light writes to this instead of a pin.
|
||||
# Every write bumps a counter and records the last value, both of which
|
||||
# are exposed below so the test can verify status_led_light's loop is
|
||||
# actually reading App.get_app_state() and responding.
|
||||
output:
|
||||
- platform: template
|
||||
id: fake_status_led
|
||||
type: binary
|
||||
write_action:
|
||||
- globals.set:
|
||||
id: status_led_write_count
|
||||
value: !lambda "return id(status_led_write_count) + 1;"
|
||||
- globals.set:
|
||||
id: status_led_last_write
|
||||
value: !lambda "return state;"
|
||||
|
||||
# Actual status_led_light component under test.
|
||||
light:
|
||||
- platform: status_led
|
||||
name: Status LED
|
||||
id: status_led_light_id
|
||||
output: fake_status_led
|
||||
|
||||
sensor:
|
||||
# Two components that the test will toggle warning/error flags on.
|
||||
- platform: template
|
||||
name: Sensor A
|
||||
id: sensor_a
|
||||
update_interval: 24h
|
||||
lambda: return 1.0;
|
||||
- platform: template
|
||||
name: Sensor B
|
||||
id: sensor_b
|
||||
update_interval: 24h
|
||||
lambda: return 2.0;
|
||||
|
||||
# Expose App.app_state_'s STATUS_LED_WARNING / STATUS_LED_ERROR bits
|
||||
# as 0.0 / 1.0. force_update ensures every manual component.update
|
||||
# publishes even if the value is unchanged.
|
||||
- platform: template
|
||||
name: App Warning Bit
|
||||
id: app_warning_bit
|
||||
update_interval: 24h
|
||||
force_update: true
|
||||
lambda: |-
|
||||
return (App.get_app_state() & STATUS_LED_WARNING) != 0 ? 1.0 : 0.0;
|
||||
- platform: template
|
||||
name: App Error Bit
|
||||
id: app_error_bit
|
||||
update_interval: 24h
|
||||
force_update: true
|
||||
lambda: |-
|
||||
return (App.get_app_state() & STATUS_LED_ERROR) != 0 ? 1.0 : 0.0;
|
||||
|
||||
# Observables for the fake status_led output.
|
||||
- platform: template
|
||||
name: Status LED Writes
|
||||
id: status_led_writes
|
||||
update_interval: 24h
|
||||
force_update: true
|
||||
lambda: return id(status_led_write_count);
|
||||
- platform: template
|
||||
name: Status LED Last State
|
||||
id: status_led_last_state
|
||||
update_interval: 24h
|
||||
force_update: true
|
||||
lambda: |-
|
||||
return id(status_led_last_write) ? 1.0 : 0.0;
|
||||
@@ -1,209 +0,0 @@
|
||||
"""Integration tests for Component::status_set/clear_warning/error propagation.
|
||||
|
||||
Verifies that toggling STATUS_LED_WARNING / STATUS_LED_ERROR on individual
|
||||
components correctly updates the app-wide bits on Application::app_state_,
|
||||
AND that the status_led_light component actually responds to those bits
|
||||
by writing to its output (the full chain from component.status_set_warning
|
||||
→ App.app_state_ → status_led_light.loop() reading get_app_state()).
|
||||
|
||||
Exercises the multi-component OR semantics (the app bit stays set while
|
||||
any component still has the flag, and only clears when the last component
|
||||
clears its bit), the independence of warning and error, and the actual
|
||||
status_led_light read of the bits via a fake template output that counts
|
||||
writes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, SensorTracker, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Time to let the host-mode main loop run so status_led_light.loop() can
|
||||
# execute enough iterations to produce measurable write-count changes on
|
||||
# the fake template output. 300 ms is well above the minimum needed.
|
||||
STATUS_LED_SETTLE_S = 0.3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_flags(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, services = await client.list_entities_services()
|
||||
|
||||
# Map every custom API service by name for the test to execute.
|
||||
svc = {s.name: s for s in services}
|
||||
for name in (
|
||||
"set_warning_a",
|
||||
"clear_warning_a",
|
||||
"set_warning_b",
|
||||
"clear_warning_b",
|
||||
"set_error_a",
|
||||
"clear_error_a",
|
||||
"set_error_b",
|
||||
"clear_error_b",
|
||||
"snapshot_led",
|
||||
):
|
||||
assert name in svc, f"service {name} not registered"
|
||||
|
||||
# Track every sensor we care about. SensorTracker gives us
|
||||
# expect(value) / expect_any() futures that resolve when a
|
||||
# matching state arrives; much simpler than manual bookkeeping.
|
||||
tracker = SensorTracker(
|
||||
[
|
||||
"app_warning_bit",
|
||||
"app_error_bit",
|
||||
"status_led_writes",
|
||||
"status_led_last_state",
|
||||
]
|
||||
)
|
||||
tracker.key_to_sensor.update(
|
||||
build_key_to_entity_mapping(entities, list(tracker.sensor_states.keys()))
|
||||
)
|
||||
|
||||
# Swallow initial state broadcasts so the test only reacts to
|
||||
# state changes triggered by our service calls.
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(tracker.on_state))
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
async def call(name: str) -> None:
|
||||
await client.execute_service(svc[name], {})
|
||||
|
||||
async def call_and_expect_bits(
|
||||
service_name: str, *, warning: float, error: float
|
||||
) -> None:
|
||||
"""Execute a service and wait for both app bit sensors to match.
|
||||
|
||||
Each bit-toggling service calls component.update on both
|
||||
app_warning_bit and app_error_bit, so both sensors publish.
|
||||
"""
|
||||
futures = tracker.expect_all(
|
||||
{"app_warning_bit": warning, "app_error_bit": error}
|
||||
)
|
||||
await call(service_name)
|
||||
await tracker.await_all(futures)
|
||||
|
||||
async def snapshot_led_writes() -> int:
|
||||
"""Trigger a publish of the fake status_led output counter and return it."""
|
||||
future = tracker.expect_any("status_led_writes")
|
||||
await call("snapshot_led")
|
||||
await tracker.await_change(future, "status_led_writes")
|
||||
return int(tracker.sensor_states["status_led_writes"][-1])
|
||||
|
||||
# ---- Baseline: everything clean ----
|
||||
await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0)
|
||||
|
||||
# ================================================================
|
||||
# Part 1 — STATUS_LED_WARNING propagation to App.app_state_
|
||||
# ================================================================
|
||||
|
||||
# Single component set/clear
|
||||
await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0)
|
||||
|
||||
# Multi-component OR: both set, clear A, bit stays (B still has it), clear B, gone
|
||||
await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("set_warning_b", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("clear_warning_a", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("clear_warning_b", warning=0.0, error=0.0)
|
||||
|
||||
# Opposite clear order
|
||||
await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("set_warning_b", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("clear_warning_b", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0)
|
||||
|
||||
# ================================================================
|
||||
# Part 2 — STATUS_LED_ERROR propagation (same scenarios)
|
||||
# ================================================================
|
||||
|
||||
await call_and_expect_bits("set_error_a", warning=0.0, error=1.0)
|
||||
await call_and_expect_bits("clear_error_a", warning=0.0, error=0.0)
|
||||
|
||||
await call_and_expect_bits("set_error_a", warning=0.0, error=1.0)
|
||||
await call_and_expect_bits("set_error_b", warning=0.0, error=1.0)
|
||||
await call_and_expect_bits("clear_error_a", warning=0.0, error=1.0)
|
||||
await call_and_expect_bits("clear_error_b", warning=0.0, error=0.0)
|
||||
|
||||
# ================================================================
|
||||
# Part 3 — warning and error are independent
|
||||
# ================================================================
|
||||
|
||||
await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0)
|
||||
await call_and_expect_bits("set_error_b", warning=1.0, error=1.0)
|
||||
await call_and_expect_bits("clear_warning_a", warning=0.0, error=1.0)
|
||||
await call_and_expect_bits("clear_error_b", warning=0.0, error=0.0)
|
||||
|
||||
# ================================================================
|
||||
# Part 4 — status_led_light actually reads App.app_state_
|
||||
# ================================================================
|
||||
# The fake status_led_light output increments status_led_write_count
|
||||
# on every write. status_led_light::loop() writes its output on every
|
||||
# iteration while an error/warning bit is set, so after holding a
|
||||
# warning for ~300 ms we should see the counter move significantly.
|
||||
# This is the end-to-end proof that the bits we set above actually
|
||||
# reach status_led_light and drive its behavior.
|
||||
|
||||
count_before_warning = await snapshot_led_writes()
|
||||
await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0)
|
||||
# Let status_led_light's loop run long enough to toggle the pin
|
||||
# several times (it reads get_app_state() every main loop iteration).
|
||||
await asyncio.sleep(STATUS_LED_SETTLE_S)
|
||||
count_after_warning = await snapshot_led_writes()
|
||||
assert count_after_warning > count_before_warning, (
|
||||
"status_led_light did not respond to STATUS_LED_WARNING being set: "
|
||||
f"write count stayed at {count_before_warning} → {count_after_warning}. "
|
||||
"The full chain Component::status_set_warning → App.app_state_ → "
|
||||
"status_led_light::loop reading get_app_state() is broken."
|
||||
)
|
||||
await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0)
|
||||
|
||||
# Same check for ERROR
|
||||
count_before_error = await snapshot_led_writes()
|
||||
await call_and_expect_bits("set_error_a", warning=0.0, error=1.0)
|
||||
await asyncio.sleep(STATUS_LED_SETTLE_S)
|
||||
count_after_error = await snapshot_led_writes()
|
||||
assert count_after_error > count_before_error, (
|
||||
"status_led_light did not respond to STATUS_LED_ERROR being set: "
|
||||
f"write count stayed at {count_before_error} → {count_after_error}. "
|
||||
)
|
||||
await call_and_expect_bits("clear_error_a", warning=0.0, error=0.0)
|
||||
|
||||
# ---- Set → clear → re-set round-trip ----
|
||||
# After clearing, status_led_light stops writing (steady state).
|
||||
# Re-setting the flag must make it resume. This guards against a
|
||||
# future idle optimization (e.g. #15642) where status_led disables
|
||||
# its own loop when idle: if the re-enable path were broken, the
|
||||
# second set would not produce writes.
|
||||
#
|
||||
# Snapshot AFTER the clear to avoid counting writes that were still
|
||||
# in-flight from the error-set phase.
|
||||
count_after_clear = await snapshot_led_writes()
|
||||
await asyncio.sleep(STATUS_LED_SETTLE_S)
|
||||
count_after_idle = await snapshot_led_writes()
|
||||
assert count_after_idle - count_after_clear <= 5, (
|
||||
"status_led_light kept writing after warning/error was cleared: "
|
||||
f"count grew from {count_after_clear} to {count_after_idle}. "
|
||||
"Expected it to stop writing once all status bits were clear."
|
||||
)
|
||||
# Re-set warning — writes must resume.
|
||||
await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0)
|
||||
await asyncio.sleep(STATUS_LED_SETTLE_S)
|
||||
count_after_reset = await snapshot_led_writes()
|
||||
assert count_after_reset > count_after_idle + 5, (
|
||||
"status_led_light did not resume writing after re-setting "
|
||||
f"STATUS_LED_WARNING: count went from {count_after_idle} to "
|
||||
f"{count_after_reset}. If an idle optimization disabled the "
|
||||
"loop, the re-enable path may be broken."
|
||||
)
|
||||
await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0)
|
||||
@@ -84,9 +84,9 @@ def mock_decode_pc() -> Generator[Mock, None, None]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_run_external_process() -> Generator[Mock, None, None]:
|
||||
"""Mock run_external_process for platformio_api."""
|
||||
with patch("esphome.platformio_api.run_external_process") as mock:
|
||||
def mock_run_external_command() -> Generator[Mock, None, None]:
|
||||
"""Mock run_external_command for platformio_api."""
|
||||
with patch("esphome.platformio_api.run_external_command") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
wifi:
|
||||
password: pkg_password
|
||||
ssid: main_ssid
|
||||
@@ -1,4 +0,0 @@
|
||||
packages: !include 13-packages_list.yaml
|
||||
|
||||
wifi:
|
||||
ssid: main_ssid
|
||||
@@ -1,2 +0,0 @@
|
||||
- wifi:
|
||||
password: pkg_password
|
||||
@@ -1,3 +0,0 @@
|
||||
wifi:
|
||||
password: pkg_password
|
||||
ssid: main_ssid
|
||||
@@ -1,4 +0,0 @@
|
||||
packages: !include 14-packages_dict.yaml
|
||||
|
||||
wifi:
|
||||
ssid: main_ssid
|
||||
@@ -1,3 +0,0 @@
|
||||
network:
|
||||
wifi:
|
||||
password: pkg_password
|
||||
@@ -1231,48 +1231,6 @@ def test_upload_using_esptool_path_conversion(
|
||||
assert partitions_path.endswith("partitions.bin")
|
||||
|
||||
|
||||
def test_upload_using_esptool_skips_missing_extra_flash_images(
|
||||
tmp_path: Path,
|
||||
mock_run_external_command_main: Mock,
|
||||
mock_get_idedata: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A non-existent path in extra_flash_images must be filtered out with a
|
||||
warning, and must not appear in the esptool command line. Only the valid
|
||||
images are flashed. Regression test for
|
||||
https://github.com/esphome/esphome/issues/15634.
|
||||
"""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test")
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32}
|
||||
|
||||
missing_path = tmp_path / "variants" / "tasmota" / "tinyuf2.bin"
|
||||
|
||||
mock_idedata = MagicMock(spec=platformio_api.IDEData)
|
||||
mock_idedata.firmware_bin_path = tmp_path / "firmware.bin"
|
||||
mock_idedata.extra_flash_images = [
|
||||
platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"),
|
||||
platformio_api.FlashImage(path=missing_path, offset="0x2d0000"),
|
||||
]
|
||||
mock_get_idedata.return_value = mock_idedata
|
||||
|
||||
(tmp_path / "firmware.bin").touch()
|
||||
(tmp_path / "bootloader.bin").touch()
|
||||
# Intentionally do NOT create missing_path
|
||||
|
||||
config = {CONF_ESPHOME: {"platformio_options": {}}}
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.__main__"):
|
||||
result = upload_using_esptool(config, "/dev/ttyUSB0", None, None)
|
||||
|
||||
assert result == 0
|
||||
assert "Skipping missing flash image" in caplog.text
|
||||
assert str(missing_path) in caplog.text
|
||||
|
||||
cmd_list = list(mock_run_external_command_main.call_args[0][1:])
|
||||
assert str(missing_path) not in cmd_list
|
||||
assert "0x2d0000" not in cmd_list
|
||||
|
||||
|
||||
def test_upload_using_esptool_with_file_path(
|
||||
tmp_path: Path,
|
||||
mock_run_external_command_main: Mock,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Tests for platformio_api.py path functions."""
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -11,7 +10,7 @@ from unittest.mock import MagicMock, Mock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import platformio_api, platformio_runner
|
||||
from esphome import platformio_api
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
|
||||
@@ -282,13 +281,13 @@ def test_run_idedata_raises_on_invalid_json(
|
||||
|
||||
|
||||
def test_run_platformio_cli_sets_environment_variables(
|
||||
setup_core: Path, mock_run_external_process: Mock
|
||||
setup_core: Path, mock_run_external_command: Mock
|
||||
) -> None:
|
||||
"""Test run_platformio_cli sets correct environment variables."""
|
||||
CORE.build_path = str(setup_core / "build" / "test")
|
||||
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
mock_run_external_process.return_value = 0
|
||||
mock_run_external_command.return_value = 0
|
||||
platformio_api.run_platformio_cli("test", "arg")
|
||||
|
||||
# Check environment variables were set
|
||||
@@ -301,12 +300,10 @@ def test_run_platformio_cli_sets_environment_variables(
|
||||
assert "PLATFORMIO_LIBDEPS_DIR" in os.environ
|
||||
assert "PYTHONWARNINGS" in os.environ
|
||||
|
||||
# Check command was called correctly — runs PlatformIO as a subprocess
|
||||
# via the esphome.platformio_runner entry point.
|
||||
mock_run_external_process.assert_called_once()
|
||||
args = mock_run_external_process.call_args[0]
|
||||
assert "-m" in args
|
||||
assert "esphome.platformio_runner" in args
|
||||
# Check command was called correctly
|
||||
mock_run_external_command.assert_called_once()
|
||||
args = mock_run_external_command.call_args[0]
|
||||
assert "platformio" in args
|
||||
assert "test" in args
|
||||
assert "arg" in args
|
||||
|
||||
@@ -447,7 +444,7 @@ def test_patch_structhash(setup_core: Path) -> None:
|
||||
},
|
||||
):
|
||||
# Call patch_structhash
|
||||
platformio_runner.patch_structhash()
|
||||
platformio_api.patch_structhash()
|
||||
|
||||
# Verify both modules had clean_build_dir patched
|
||||
# Check that clean_build_dir was set on both modules
|
||||
@@ -499,7 +496,7 @@ def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None:
|
||||
},
|
||||
):
|
||||
# Call patch_structhash to install the patched function
|
||||
platformio_runner.patch_structhash()
|
||||
platformio_api.patch_structhash()
|
||||
|
||||
# Call the patched function
|
||||
mock_helpers.clean_build_dir(str(build_dir), [])
|
||||
@@ -549,7 +546,7 @@ def test_patched_clean_build_dir_keeps_updated(setup_core: Path) -> None:
|
||||
},
|
||||
):
|
||||
# Call patch_structhash to install the patched function
|
||||
platformio_runner.patch_structhash()
|
||||
platformio_api.patch_structhash()
|
||||
|
||||
# Call the patched function
|
||||
mock_helpers.clean_build_dir(str(build_dir), [])
|
||||
@@ -597,7 +594,7 @@ def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None:
|
||||
},
|
||||
):
|
||||
# Call patch_structhash to install the patched function
|
||||
platformio_runner.patch_structhash()
|
||||
platformio_api.patch_structhash()
|
||||
|
||||
# Call the patched function
|
||||
mock_helpers.clean_build_dir(str(build_dir), [])
|
||||
@@ -722,7 +719,7 @@ def test_patch_file_downloader_succeeds_first_try() -> None:
|
||||
),
|
||||
},
|
||||
):
|
||||
platformio_runner.patch_file_downloader()
|
||||
platformio_api.patch_file_downloader()
|
||||
|
||||
from platformio.package.download import FileDownloader
|
||||
|
||||
@@ -761,7 +758,7 @@ def test_patch_file_downloader_retries_on_failure() -> None:
|
||||
),
|
||||
patch("time.sleep") as mock_sleep,
|
||||
):
|
||||
platformio_runner.patch_file_downloader()
|
||||
platformio_api.patch_file_downloader()
|
||||
|
||||
from platformio.package.download import FileDownloader
|
||||
|
||||
@@ -802,7 +799,7 @@ def test_patch_file_downloader_raises_after_max_retries() -> None:
|
||||
),
|
||||
patch("time.sleep") as mock_sleep,
|
||||
):
|
||||
platformio_runner.patch_file_downloader()
|
||||
platformio_api.patch_file_downloader()
|
||||
|
||||
from platformio.package.download import FileDownloader
|
||||
|
||||
@@ -850,7 +847,7 @@ def test_patch_file_downloader_closes_session_and_response_between_retries() ->
|
||||
),
|
||||
patch("time.sleep"),
|
||||
):
|
||||
platformio_runner.patch_file_downloader()
|
||||
platformio_api.patch_file_downloader()
|
||||
|
||||
from platformio.package.download import FileDownloader
|
||||
|
||||
@@ -885,9 +882,9 @@ def test_patch_file_downloader_idempotent() -> None:
|
||||
},
|
||||
):
|
||||
# Patch multiple times
|
||||
platformio_runner.patch_file_downloader()
|
||||
platformio_runner.patch_file_downloader()
|
||||
platformio_runner.patch_file_downloader()
|
||||
platformio_api.patch_file_downloader()
|
||||
platformio_api.patch_file_downloader()
|
||||
platformio_api.patch_file_downloader()
|
||||
|
||||
from platformio.package.download import FileDownloader
|
||||
|
||||
@@ -898,18 +895,19 @@ def test_patch_file_downloader_idempotent() -> None:
|
||||
assert call_count == 1
|
||||
|
||||
|
||||
def _filter_through_redirect(line: str) -> str:
|
||||
"""Write a line through RedirectText with FILTER_PLATFORMIO_LINES and return what passes."""
|
||||
import io
|
||||
|
||||
from esphome.util import RedirectText
|
||||
|
||||
captured = io.StringIO()
|
||||
redirect = RedirectText(
|
||||
captured, filter_lines=platformio_runner.FILTER_PLATFORMIO_LINES
|
||||
def test_platformio_log_filter_allows_non_platformio_messages() -> None:
|
||||
"""Test that non-platformio logger messages are allowed through."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name="esphome.core",
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg="Some esphome message",
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
redirect.write(line + "\n")
|
||||
return captured.getvalue()
|
||||
assert log_filter.filter(record) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -932,9 +930,19 @@ def _filter_through_redirect(line: str) -> str:
|
||||
"Memory Usage -> https://bit.ly/pio-memory-usage",
|
||||
],
|
||||
)
|
||||
def test_filter_platformio_lines_blocks_noisy_messages(msg: str) -> None:
|
||||
"""Test that noisy platformio output lines are filtered out by RedirectText."""
|
||||
assert _filter_through_redirect(msg) == ""
|
||||
def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None:
|
||||
"""Test that noisy platformio messages are filtered out."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name="platformio.builder",
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg=msg,
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
assert log_filter.filter(record) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -946,6 +954,39 @@ def test_filter_platformio_lines_blocks_noisy_messages(msg: str) -> None:
|
||||
"warning: unused variable",
|
||||
],
|
||||
)
|
||||
def test_filter_platformio_lines_allows_other_messages(msg: str) -> None:
|
||||
"""Test that non-noisy platformio output lines pass through RedirectText."""
|
||||
assert _filter_through_redirect(msg) == msg + "\n"
|
||||
def test_platformio_log_filter_allows_other_platformio_messages(msg: str) -> None:
|
||||
"""Test that non-noisy platformio messages are allowed through."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name="platformio.builder",
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg=msg,
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
assert log_filter.filter(record) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"logger_name",
|
||||
[
|
||||
"PLATFORMIO.builder",
|
||||
"PlatformIO.core",
|
||||
"platformio.run",
|
||||
],
|
||||
)
|
||||
def test_platformio_log_filter_case_insensitive_logger_name(logger_name: str) -> None:
|
||||
"""Test that platformio logger name matching is case insensitive."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name=logger_name,
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg="Found 5 compatible libraries",
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
assert log_filter.filter(record) is False
|
||||
|
||||
Reference in New Issue
Block a user