mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.8.0b3
|
||||
PROJECT_NUMBER = 2026.8.0b4
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.9.6
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.11.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -5,11 +5,11 @@ bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
|
||||
on this component and contain no SDK calls of their own.
|
||||
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253
|
||||
(BLE 5.2), and any future BLE-5.x SoC. Capability is detected at compile time,
|
||||
not by a chip list: the C++ guards on `__has_include("ble_api.h")` — the Beken
|
||||
BLE 5.x public API header, which the LibreTiny beken-72xx builder ships only
|
||||
for BLE-5.x SoCs. BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) fail
|
||||
with a clear #error.
|
||||
(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in
|
||||
to_code; unknown families are capability-checked at compile time via
|
||||
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
|
||||
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
|
||||
fails with a clear #error.
|
||||
|
||||
No framework patch is needed: the LibreTiny beken-72xx builder already compiles
|
||||
and links the BLE 5.x stack (CFG_SUPPORT_BLE=1 + CFG_BLE_VERSION=BLE_VERSION_5_x;
|
||||
@@ -21,9 +21,16 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import libretiny
|
||||
from esphome.components.libretiny.const import FAMILY_BK7231N, FAMILY_BK7238
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
FAMILY_BK7231T,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_BK7251,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["bk72xx"]
|
||||
@@ -50,7 +57,32 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
request_scan_listener_slot = cg.slot_counter("BK72XX_BLE_SCAN_LISTENER_COUNT")
|
||||
|
||||
|
||||
def _unsupported_family_message(family: str) -> str | None:
|
||||
if family in (FAMILY_BK7231T, FAMILY_BK7251):
|
||||
return (
|
||||
f"bk72xx_ble does not support {family}: this SoC has the Beken BLE 4.2 "
|
||||
"stack; a BLE 5.x SoC such as BK7231N or BK7238 is required"
|
||||
)
|
||||
if family == FAMILY_BK7231Q:
|
||||
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
|
||||
return None
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
# Warn only: a hard error here would break the validate-only CI fixtures,
|
||||
# which run on a BLE 4.2 board. The hard error is raised at codegen.
|
||||
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
|
||||
_LOGGER.warning("%s (this configuration cannot compile)", msg)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
|
||||
raise EsphomeError(msg)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#ifdef USE_BK72XX_BLE
|
||||
|
||||
// Same SDK gate as bk72xx_ble.cpp (which carries the explanatory #error).
|
||||
#if !defined(CLANG_TIDY) && __has_include("ble_api.h")
|
||||
#if !defined(CLANG_TIDY) && __has_include("ble_api.h") && __has_include("app_ble.h")
|
||||
|
||||
extern "C" {
|
||||
#include "app_ble.h" // app_ble_env, app_ble_run, app_ble_reset, actv_state_t,
|
||||
@@ -115,5 +115,5 @@ BdkOpResult bdk_scan_release(uint8_t activity_idx, bool created, int *err_out) {
|
||||
|
||||
} // namespace esphome::bk72xx_ble
|
||||
|
||||
#endif // !CLANG_TIDY && ble_api.h
|
||||
#endif // !CLANG_TIDY && ble_api.h && app_ble.h
|
||||
#endif // USE_BK72XX_BLE
|
||||
|
||||
@@ -34,22 +34,26 @@
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SDK-capability gate (not a chip allowlist).
|
||||
// This component drives the Beken BLE *5.x* controller via its public API,
|
||||
// `ble_api.h`, which the LibreTiny beken-72xx builder ships only for the
|
||||
// BLE-5.x SoCs (it selects the `ble_pub` 5.x stack from CFG_BLE_VERSION; the
|
||||
// 4.2 SoCs build a different, older API with no ble_api.h). Gate on the header
|
||||
// itself so any BLE-5.x Beken chip — present or future — is supported without a
|
||||
// hard-coded list, and a non-5.x build fails here with a clear message instead
|
||||
// of a cryptic "ble_api.h: No such file or directory".
|
||||
// This component drives the Beken BLE *5.x* controller. `ble_api.h` cannot be
|
||||
// the probe: it ships for every SoC (driver/include) and merely switches on
|
||||
// CFG_BLE_VERSION internally. `app_ble.h` is on the include path only when the
|
||||
// LibreTiny beken-72xx builder selects a 5.x stack, so gating on it supports
|
||||
// any BLE-5.x chip — present or future — without a hard-coded list, and a
|
||||
// non-5.x build fails here with a clear message instead of a cryptic
|
||||
// "app_ble.h: No such file or directory".
|
||||
// ---------------------------------------------------------------------------
|
||||
#if defined(CLANG_TIDY)
|
||||
// The clang-tidy environment does not carry the full Beken BDK BLE 5.x API
|
||||
// (its ble_api.h variant lacks parts of the 5.x surface), so there is nothing
|
||||
// accurate to analyze the SDK calls against — skip the file under analysis.
|
||||
#define BK72XX_BLE_NO_SDK
|
||||
#elif !__has_include("ble_api.h")
|
||||
#elif !__has_include("ble_api.h") || !__has_include("app_ble.h")
|
||||
// Also skip the SDK body: #error does not stop the preprocessor, and on a 4.2
|
||||
// SoC ble_api.h exists, so without the guard the 5.x symbols would fail one by
|
||||
// one and bury this message.
|
||||
#define BK72XX_BLE_NO_SDK
|
||||
#error \
|
||||
"bk72xx_ble requires a BLE 5.x Beken SDK (ble_api.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported."
|
||||
"bk72xx_ble requires a BLE 5.x Beken SDK (app_ble.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported."
|
||||
#endif
|
||||
|
||||
#ifndef BK72XX_BLE_NO_SDK
|
||||
|
||||
@@ -360,17 +360,6 @@ static bool has_fault_addr() {
|
||||
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
|
||||
}
|
||||
|
||||
// Append both cores' backtrace addresses to buf; returns the new position.
|
||||
static int append_all_backtraces(char *buf, int size, int pos) {
|
||||
pos = append_addrs_to_hint(buf, size, pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
|
||||
s_raw_crash_data.reg_frame_count);
|
||||
#if SOC_CPU_CORES_NUM > 1
|
||||
pos = append_addrs_to_hint(buf, size, pos, s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
|
||||
s_raw_crash_data.other_reg_frame_count);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
|
||||
// The record was captured by a different firmware build (it survives soft
|
||||
// resets, including the OTA reboot), so symbolizing its addresses against the
|
||||
// current ELF would produce misleading symbols. Print them with lowercase
|
||||
@@ -443,11 +432,23 @@ void crash_handler_log() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Build addr2line hint with all captured addresses for easy copy-paste
|
||||
// Build addr2line hints for easy copy-paste. One line per core: the two
|
||||
// backtraces are separate stacks, and a combined list decodes as one
|
||||
// impossible call chain (and can overflow the buffer, dropping addresses).
|
||||
static const char *const ADDR2LINE_CMD = "addr2line -pfiaC -e firmware.elf";
|
||||
char hint[256];
|
||||
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc);
|
||||
append_all_backtraces(hint, sizeof(hint), pos);
|
||||
int pos = snprintf(hint, sizeof(hint), "Use: %s 0x%08" PRIX32, ADDR2LINE_CMD, s_raw_crash_data.pc);
|
||||
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
|
||||
s_raw_crash_data.reg_frame_count);
|
||||
ESP_LOGE(TAG, "%s", hint);
|
||||
#if SOC_CPU_CORES_NUM > 1
|
||||
if (s_raw_crash_data.other_backtrace_count > 0) {
|
||||
pos = snprintf(hint, sizeof(hint), "Other core: %s", ADDR2LINE_CMD);
|
||||
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
|
||||
s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
|
||||
ESP_LOGE(TAG, "%s", hint);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::esp32
|
||||
|
||||
@@ -220,7 +220,7 @@ void RotaryEncoderSensor::loop() {
|
||||
}
|
||||
|
||||
if (this->pin_i_ != nullptr && this->pin_i_->digital_read()) {
|
||||
this->store_.counter = 0;
|
||||
this->store_.counter = std::clamp<int32_t>(0, this->store_.min_value, this->store_.max_value);
|
||||
}
|
||||
int counter = this->store_.counter;
|
||||
if (this->store_.last_read != counter || this->publish_initial_value_) {
|
||||
|
||||
@@ -283,8 +283,11 @@ DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
|
||||
void DeltaFilter::set_baseline(float (*fn)(float)) { this->baseline_ = fn; }
|
||||
|
||||
optional<float> DeltaFilter::new_value(float value) {
|
||||
// Always yield the first value.
|
||||
if (std::isnan(this->last_value_)) {
|
||||
const bool no_value = std::isnan(value);
|
||||
const bool no_reference = std::isnan(this->last_value_);
|
||||
if (no_value && no_reference)
|
||||
return {};
|
||||
if (no_value || no_reference) {
|
||||
this->last_value_ = value;
|
||||
return value;
|
||||
}
|
||||
@@ -293,8 +296,7 @@ optional<float> DeltaFilter::new_value(float value) {
|
||||
float min = fabsf(this->min_a0_ + ref * this->min_a1_);
|
||||
float max = fabsf(this->max_a0_ + ref * this->max_a1_);
|
||||
float delta = fabsf(value - ref);
|
||||
// if there is no reference, e.g. for the first value, just accept this one,
|
||||
// otherwise accept only if within range.
|
||||
// accept only if within range
|
||||
if (delta > min && delta <= max) {
|
||||
this->last_value_ = value;
|
||||
return value;
|
||||
|
||||
@@ -307,6 +307,11 @@ void ZigbeeComponent::setup() {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_ZB_ZCZR
|
||||
ezb_bdb_set_router_rejoin_required(true);
|
||||
#endif
|
||||
|
||||
ezb_aps_secur_enable_distributed_security(false);
|
||||
ezb_nwk_set_min_join_lqi(32);
|
||||
if (ezb_app_signal_add_handler(ZigbeeComponent::app_signal_handler) != ESP_OK) {
|
||||
|
||||
@@ -285,7 +285,7 @@ async def attributes_to_code(
|
||||
async def esp32_to_code(config: ConfigType) -> "MockObj":
|
||||
add_idf_component(
|
||||
name="espressif/esp-zigbee-lib",
|
||||
ref="2.0.3",
|
||||
ref="2.0.4",
|
||||
)
|
||||
|
||||
# add sdkconfigs later so they can overwrite esp32 defaults
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.8.0b3"
|
||||
__version__ = "2026.8.0b4"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -15,6 +15,13 @@ inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() {
|
||||
// Set the wake-requested flag BEFORE esp_schedule so the consumer is
|
||||
// guaranteed to see it on its next gate check.
|
||||
wake_request_set();
|
||||
// Skip the post when a wake was already signalled and not yet consumed by
|
||||
// wakeable_delay(): esp_schedule() -> ets_post() can enter SDK WiFi pm code,
|
||||
// which must not be poked per-byte from the software serial RX ISR (see
|
||||
// esphome#18409). The flag can stay latched while the loop is awake, which
|
||||
// is intentional; posts are only needed to cut a suspend short.
|
||||
if (g_main_loop_woke)
|
||||
return;
|
||||
g_main_loop_woke = true;
|
||||
esp_schedule();
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ dependencies:
|
||||
rules:
|
||||
- if: "target in [esp32, esp32p4]"
|
||||
espressif/esp-zigbee-lib:
|
||||
version: 2.0.3
|
||||
version: 2.0.4
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32c5, esp32c6]"
|
||||
espressif/lan87xx:
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -234,6 +235,35 @@ def _check_platformio_python_stamp(config: "ProjectConfig") -> None:
|
||||
_write_pio_stamp_python(stamp_file, current)
|
||||
|
||||
|
||||
def _ccache_usable() -> bool:
|
||||
"""Return True when the ``ccache`` on PATH actually runs.
|
||||
|
||||
``shutil.which`` proves existence, not runnability: on Windows it also
|
||||
matches ``.bat``/``.cmd`` wrappers and stale package-manager shims whose
|
||||
target is gone. Wrapping compiles around such a find fails every compile
|
||||
step with an opaque OS error, so probe once and fall back to compiling
|
||||
without ccache when the probe fails.
|
||||
"""
|
||||
ccache = shutil.which("ccache")
|
||||
if ccache is None:
|
||||
return False
|
||||
try:
|
||||
subprocess.run(
|
||||
[ccache, "--version"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=15,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
_LOGGER.warning(
|
||||
"Ignoring ccache at %s because it failed to run; compiling without ccache",
|
||||
ccache,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _ccache_env() -> dict[str, str]:
|
||||
"""Return ccache settings for PlatformIO builds.
|
||||
|
||||
@@ -266,7 +296,7 @@ def _ccache_env() -> dict[str, str]:
|
||||
if "ESPHOME_CCACHE_ENABLE" in os.environ:
|
||||
enabled = get_bool_env("ESPHOME_CCACHE_ENABLE")
|
||||
else:
|
||||
enabled = shutil.which("ccache") is not None
|
||||
enabled = _ccache_usable()
|
||||
env = {"ESPHOME_CCACHE_ENABLE": "1" if enabled else "0"}
|
||||
if not enabled:
|
||||
return env
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: bk-family-gate-n
|
||||
|
||||
bk72xx:
|
||||
board: cb2s
|
||||
|
||||
bk72xx_ble:
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: bk-family-gate-q
|
||||
|
||||
bk72xx:
|
||||
board: wa2
|
||||
|
||||
bk72xx_ble:
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: bk-family-gate-t
|
||||
|
||||
bk72xx:
|
||||
board: generic-bk7231t-qfn32-tuya
|
||||
|
||||
bk72xx_ble:
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: bk-family-gate-7252
|
||||
|
||||
bk72xx:
|
||||
board: generic-bk7252
|
||||
|
||||
bk72xx_ble:
|
||||
@@ -0,0 +1,40 @@
|
||||
"""The non-5.x family rejection lives in to_code (config validation must stay
|
||||
family-agnostic for the validate-only CI fixtures), so codegen is the only
|
||||
place it can be pinned."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "match"),
|
||||
[
|
||||
("test_bk7231t.yaml", "BK7231T.*BLE 4.2"),
|
||||
("test_bk7252.yaml", "BK7251.*BLE 4.2"),
|
||||
("test_bk7231q.yaml", "BK7231Q.*no BLE"),
|
||||
],
|
||||
)
|
||||
def test_unsupported_family_rejected(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
match: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with pytest.raises(EsphomeError, match=match):
|
||||
generate_main(component_config_path(config_file))
|
||||
# Validation itself must not fail (CI validate fixtures run on a BLE 4.2
|
||||
# board), but it warns before codegen raises.
|
||||
assert "cannot compile" in caplog.text
|
||||
|
||||
|
||||
def test_ble5_family_generates(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("test_bk7231n.yaml"))
|
||||
assert "bk72xx_ble::BK72xxBLE" in main_cpp
|
||||
@@ -2,6 +2,6 @@ esphome:
|
||||
name: slotcount-controller
|
||||
|
||||
bk72xx:
|
||||
board: generic-bk7252
|
||||
board: cb2s
|
||||
|
||||
bk72xx_ble:
|
||||
|
||||
@@ -2,6 +2,6 @@ esphome:
|
||||
name: slotcount-tracker
|
||||
|
||||
bk72xx:
|
||||
board: generic-bk7252
|
||||
board: cb2s
|
||||
|
||||
bk72xx_ble_tracker:
|
||||
|
||||
@@ -33,6 +33,11 @@ sensor:
|
||||
id: source_sensor_5
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 6"
|
||||
id: source_sensor_6
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_1
|
||||
name: "Filter Min"
|
||||
@@ -81,6 +86,13 @@ sensor:
|
||||
filters:
|
||||
- delta: 50%
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_6
|
||||
name: "Filter NaN"
|
||||
id: filter_nan
|
||||
filters:
|
||||
- delta: 0
|
||||
|
||||
script:
|
||||
- id: test_filter_min
|
||||
then:
|
||||
@@ -188,6 +200,24 @@ script:
|
||||
id: source_sensor_5
|
||||
state: 250.0 # Passes (delta=90 > 80)
|
||||
|
||||
- id: test_filter_nan
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: 1.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: !lambda "return NAN;"
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: !lambda "return NAN;" # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: 2.0
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Filter Min"
|
||||
@@ -218,3 +248,9 @@ button:
|
||||
id: btn_filter_percentage
|
||||
on_press:
|
||||
- script.execute: test_filter_percentage
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter NaN"
|
||||
id: btn_filter_nan
|
||||
on_press:
|
||||
- script.execute: test_filter_nan
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityState, SensorState
|
||||
import pytest
|
||||
@@ -25,6 +26,7 @@ async def test_sensor_filters_delta(
|
||||
"filter_baseline_max": [],
|
||||
"filter_zero_delta": [],
|
||||
"filter_percentage": [],
|
||||
"filter_nan": [],
|
||||
}
|
||||
|
||||
filter_min_done = loop.create_future()
|
||||
@@ -32,16 +34,23 @@ async def test_sensor_filters_delta(
|
||||
filter_baseline_max_done = loop.create_future()
|
||||
filter_zero_delta_done = loop.create_future()
|
||||
filter_percentage_done = loop.create_future()
|
||||
filter_nan_done = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, SensorState) or state.missing_state:
|
||||
if not isinstance(state, SensorState):
|
||||
return
|
||||
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
if sensor_name not in sensor_values:
|
||||
return
|
||||
|
||||
sensor_values[sensor_name].append(state.state)
|
||||
if state.missing_state:
|
||||
# Only the NaN test is interested in unavailable states
|
||||
if sensor_name != "filter_nan":
|
||||
return
|
||||
sensor_values[sensor_name].append(math.nan)
|
||||
else:
|
||||
sensor_values[sensor_name].append(state.state)
|
||||
|
||||
# Check completion conditions
|
||||
if (
|
||||
@@ -74,6 +83,12 @@ async def test_sensor_filters_delta(
|
||||
and not filter_percentage_done.done()
|
||||
):
|
||||
filter_percentage_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_nan"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_nan_done.done()
|
||||
):
|
||||
filter_nan_done.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
@@ -89,6 +104,7 @@ async def test_sensor_filters_delta(
|
||||
"filter_baseline_max": "Filter Baseline Max",
|
||||
"filter_zero_delta": "Filter Zero Delta",
|
||||
"filter_percentage": "Filter Percentage",
|
||||
"filter_nan": "Filter NaN",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -108,13 +124,14 @@ async def test_sensor_filters_delta(
|
||||
"Test Filter Baseline Max": "filter_baseline_max",
|
||||
"Test Filter Zero Delta": "filter_zero_delta",
|
||||
"Test Filter Percentage": "filter_percentage",
|
||||
"Test Filter NaN": "filter_nan",
|
||||
}
|
||||
buttons = {}
|
||||
for entity in entities:
|
||||
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
|
||||
buttons[button_name_map[entity.name]] = entity.key
|
||||
|
||||
assert len(buttons) == 5, f"Expected 5 buttons, found {len(buttons)}"
|
||||
assert len(buttons) == 6, f"Expected 6 buttons, found {len(buttons)}"
|
||||
|
||||
# Test 1: Min
|
||||
sensor_values["filter_min"].clear()
|
||||
@@ -186,3 +203,18 @@ async def test_sensor_filters_delta(
|
||||
assert sensor_values["filter_percentage"] == pytest.approx(expected), (
|
||||
f"Test 5 failed: expected {expected}, got {sensor_values['filter_percentage']}"
|
||||
)
|
||||
|
||||
# Test 6: NaN passes through once, then is suppressed
|
||||
sensor_values["filter_nan"].clear()
|
||||
client.button_command(buttons["filter_nan"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_nan_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 6 timed out. Values: {sensor_values['filter_nan']}")
|
||||
|
||||
values = sensor_values["filter_nan"]
|
||||
assert values[0] == pytest.approx(1.0), f"Test 6 failed: got {values}"
|
||||
assert math.isnan(values[1]), (
|
||||
f"Test 6 failed: NaN not passed through, got {values}"
|
||||
)
|
||||
assert values[2] == pytest.approx(2.0), f"Test 6 failed: got {values}"
|
||||
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from types import SimpleNamespace
|
||||
@@ -431,6 +432,7 @@ def test_ccache_env_enabled_by_default(setup_core: Path) -> None:
|
||||
with (
|
||||
patch.dict(os.environ, {}, clear=True),
|
||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||
patch.object(toolchain.subprocess, "run"),
|
||||
):
|
||||
env = toolchain._ccache_env()
|
||||
|
||||
@@ -457,6 +459,44 @@ def test_ccache_env_disabled_without_binary(setup_core: Path) -> None:
|
||||
assert env == {"ESPHOME_CCACHE_ENABLE": "0"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"probe_error",
|
||||
[
|
||||
pytest.param(OSError("not runnable"), id="oserror"),
|
||||
pytest.param(subprocess.CalledProcessError(1, "ccache"), id="nonzero-exit"),
|
||||
pytest.param(subprocess.TimeoutExpired("ccache", 15), id="timeout"),
|
||||
],
|
||||
)
|
||||
def test_ccache_env_disabled_when_probe_fails(
|
||||
setup_core: Path, probe_error: Exception
|
||||
) -> None:
|
||||
"""A ccache that resolves on PATH but fails to run stays disabled."""
|
||||
CORE.build_path = setup_core / "build" / "test"
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {}, clear=True),
|
||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||
patch.object(toolchain.subprocess, "run", side_effect=probe_error),
|
||||
):
|
||||
env = toolchain._ccache_env()
|
||||
|
||||
assert env == {"ESPHOME_CCACHE_ENABLE": "0"}
|
||||
|
||||
|
||||
def test_ccache_env_forced_on_skips_probe(setup_core: Path) -> None:
|
||||
"""An explicit ESPHOME_CCACHE_ENABLE=1 does not probe the binary."""
|
||||
CORE.build_path = setup_core / "build" / "test"
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {"ESPHOME_CCACHE_ENABLE": "1"}, clear=True),
|
||||
patch.object(toolchain.subprocess, "run") as mock_probe,
|
||||
):
|
||||
env = toolchain._ccache_env()
|
||||
|
||||
assert env["ESPHOME_CCACHE_ENABLE"] == "1"
|
||||
mock_probe.assert_not_called()
|
||||
|
||||
|
||||
def test_ccache_env_opt_out(setup_core: Path) -> None:
|
||||
"""ESPHOME_CCACHE_ENABLE=0 disables ccache even with the binary present."""
|
||||
CORE.build_path = setup_core / "build" / "test"
|
||||
@@ -496,6 +536,7 @@ def test_ccache_env_respects_user_values_and_refreshes_basedir(
|
||||
with (
|
||||
patch.dict(os.environ, user_env, clear=True),
|
||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||
patch.object(toolchain.subprocess, "run"),
|
||||
):
|
||||
env = toolchain._ccache_env()
|
||||
|
||||
@@ -514,6 +555,7 @@ def test_run_platformio_cli_passes_ccache_env_to_subprocess_only(
|
||||
with (
|
||||
patch.dict(os.environ, {}, clear=False),
|
||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||
patch.object(toolchain.subprocess, "run"),
|
||||
):
|
||||
os.environ.pop("ESPHOME_CCACHE_ENABLE", None)
|
||||
mock_run_external_process.return_value = 0
|
||||
@@ -533,6 +575,7 @@ def test_ccache_env_requires_build_path(setup_core: Path) -> None:
|
||||
with (
|
||||
patch.dict(os.environ, {}, clear=True),
|
||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||
patch.object(toolchain.subprocess, "run"),
|
||||
pytest.raises(ValueError, match="CORE.build_path must be set"),
|
||||
):
|
||||
toolchain._ccache_env()
|
||||
@@ -544,7 +587,10 @@ def test_run_platformio_cli_merges_caller_env(
|
||||
"""A caller-supplied env is the base and gains the ccache settings."""
|
||||
CORE.build_path = str(setup_core / "build" / "test")
|
||||
|
||||
with patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"):
|
||||
with (
|
||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||
patch.object(toolchain.subprocess, "run"),
|
||||
):
|
||||
mock_run_external_process.return_value = 0
|
||||
toolchain.run_platformio_cli(
|
||||
"test", env={"CUSTOM_VAR": "1", "ESPHOME_CCACHE_ENABLE": "0"}
|
||||
|
||||
Reference in New Issue
Block a user